Initial commit

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

14
.editorconfig Normal file
View File

@@ -0,0 +1,14 @@
# Comma-separated list of rules to disable (Since 0.34.0)
# Note that rules in any ruleset other than the standard ruleset will need to be prefixed
# by the ruleset identifier.
disabled_rules=import-ordering,no-wildcard-imports
# Defines the imports layout. The layout can be composed by the following symbols:
# "*" - wildcard. There must be at least one entry of a single wildcard to match all other imports. Matches anything after a specified symbol/import as well.
# "|" - blank line. Supports only single blank lines between imports. No blank line is allowed in the beginning or end of the layout.
# "^" - alias import, e.g. "^android.*" will match all android alias imports, "^" will match all other alias imports.
# import paths - these can be full paths, e.g. "java.util.List.*" as well as wildcard paths, e.g. "kotlin.**"
# Examples (we use ij_kotlin_imports_layout to set an imports layout for both ktlint and IDEA via a single property):
ij_kotlin_imports_layout=* # alphabetical with capital letters before lower case letters (e.g. Z before a), no blank lines
ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ # default IntelliJ IDEA style, same as alphabetical, but with "java", "javax", "kotlin" and alias imports in the end of the imports list
ij_kotlin_imports_layout=android.**,|,^org.junit.**,kotlin.io.Closeable.*,|,*,^ # custom imports layout

83
.gitignore vendored Normal file
View File

@@ -0,0 +1,83 @@
# Built application files
*.apk
*.ap_
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
.cargo/
bin/
gen/
generated/
out/
target/
jniLibs/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
.idea/assetWizardSettings.xml
.idea/caches
.idea/compiler.xml
.idea/deploymentTargetDropDown.xml
.idea/dictionaries
.idea/gradle.xml
.idea/libraries
.idea/misc.xml
.idea/modules.xml
.idea/tasks.xml
.idea/vcs.xml
.idea/workspace.xml
*.iml
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Google Services (e.g. APIs or Firebase)
google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Rust / Cargo
fraget/
# other
DecompileChecker.kt
backup-dbs/
*.db
.DS_Store

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
zcash-android-sdk

134
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,134 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

21
.idea/runConfigurations/detektAll.xml generated Normal file
View File

@@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="detektAll" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="detektAll" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,53 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name=":darkside-test-lib:connectedAndroidTest" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="zcash-android-sdk.darkside-test-lib" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="2147483645" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="api-9130115880275692386-873230" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name=":sdk-lib:test" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":sdk-lib:test" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,53 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name=":sdk-lib:connectedAndroidTest" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
<module name="zcash-android-sdk.sdk-lib" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="2147483645" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="api-9130115880275692386-873230" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

23
.run/assemble.run.xml Normal file
View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="assemble" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="assemble" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="assembleAndroidTest" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="assembleAndroidTest assembleDebug assembleZcashmainnetDebug assembleZcashtestnetDebug" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

23
.run/clean.run.xml Normal file
View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="clean" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="clean" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

60
.run/demo-app.run.xml Normal file
View File

@@ -0,0 +1,60 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="demo-app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="zcash-android-sdk.demo-app" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="dependencyUpdates" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="dependencyUpdates" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

21
.run/ktlint.run.xml Normal file
View File

@@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="ktlintFormat" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="ktlintFormat" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

23
.run/lint.run.xml Normal file
View File

@@ -0,0 +1,23 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="lint" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="lint" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,28 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="publishToMavenLocal" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED" value="false" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="publishToMavenLocal" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

196
CHANGELOG.md Normal file
View File

@@ -0,0 +1,196 @@
Change Log
==========
Upcoming
------------------------------------
- Added `Zatoshi` typesafe object to represent amounts instead.
Version 1.6.0-beta01
------------------------------------
- Updated checkpoints for Mainnet and Testnet
- Fix: SDK can now be used on Intel x86_64 emulators
- Prevent R8 warnings for apps consuming the SDK
Version 1.5.0-beta01
------------------------------------
- New: Transactions can be created after NU5 activation.
- New: Support for receiving v5 transactions.
- Known issues: The SDK will not run on Intel 64-bit API 31+ emulators. Workarounds include: testing on a physical device, using an older 32-bit API version Intel emulator, or using an ARM emulator.
Version 1.4.0-beta01
------------------------------------
- Main entrypoint to the SDK has changed. See [MIGRATIONS.md](MIGRATIONS.md)
- The minimum version of Android supported is now API 19
- Updated checkpoints for Mainnet and Testnet
- Internal bugfixes around concurrent access to resources, which could cause transient failures and data corruption
- Added ProGuard rules so that SDK clients can use R8 to shrink their apps
- Updated dependencies, including Kotlin 1.6.21, Coroutines 1.6.1, GRPC 1.46.0, Okio 3.1.0, NDK 23
- Known issues: The SDK will not run on Intel 64-bit API 31+ emulators. Workarounds include: testing on a physical device, using an older 32-bit API version Intel emulator, or using an ARM emulator.
Version 1.3.0-beta20
------------------------------------
- New: Updated checkpoints for Mainnet and Testnet
Version 1.3.0-beta19
------------------------------------
- New: Updated checkpoints for Mainnet and Testnet
- Fix: Repackaged internal classes to a new `internal` package name
- Fix: Testnet checkpoints have been corrected
- Updated dependencies
Version 1.3.0-beta18
------------------------------------
- Fix: Corrected logic when calculating birthdates for wallets with zero received notes.
Version 1.3.0-beta17
------------------------------------
- Fix: Autoshielding confirmation count error so funds are available after 10 confirmations.
- New: Allow developers to enable Rust logs.
- New: Accept GZIP compression from lightwalletd.
- New: Reduce the UTXO retry time.
Version 1.3.0-beta16
------------------------------------
- Fix: Gracefully handle failures while fetching UTXOs.
- New: Expose StateFlows for balances.
- New: Make it easier to subscribe to transactions.
- New: Cleanup default logs.
- New: Convenience functions for WalletBalance objects.
Version 1.3.0-beta15
------------------------------------
- Fix: Increase reconnection attempts on failed app restart.
- New: Updated checkpoints for testnet and mainnet.
Version 1.3.0-beta14
------------------------------------
- New: Add separate flows for sapling, orchard and tranparent balances.
- Fix: Continue troubleshooting and fixing server disconnects.
- Updated dependencies.
Version 1.3.0-beta12
------------------------------------
- New: Expose network height as StateFlow.
- Fix: Reconnect to lightwalletd when a service exception occurs.
Version 1.3.0-beta11
------------------------------------
- Fix: Remove unused flag that was breaking new wallet creation for some wallets.
Version 1.3.0-beta10
------------------------------------
- Fix: Make it safe to call the new prepare function more than once.
Version 1.3.0-beta09
------------------------------------
- New: Add quick rewind feature, which makes it easy to rescan blocks after an upgrade.
- Fix: Repair complex data migration bug that caused crashes on upgrades.
Version 1.3.0-beta08
------------------------------------
- Fix: Disable librustzcash logs by default.
Version 1.3.0-beta07
------------------------------------
- Fix: Address issues with key migration, allowing wallets to reset viewing keys, when needed.
Version 1.3.0-beta06
------------------------------------
- Fix: Repair publishing so that AARs work on Windows machines [issue #222].
- Fix: Incorrect BranchId on 32-bit devics [issue #224].
- Fix: Rescan should not go beyond the wallet checkpoint.
- New: Drop Android Jetifier since it is no longer used.
- Updated checkpoints, improved tests (added Test Suites) and better error messages.
Version 1.3.0-beta05
------------------------------------
- Major: Consolidate product flavors into one library for the SDK instead of two.
- Major: Integrates with latest Librustzcash including full Data Access API support.
- Major: Move off of JCenter and onto Maven Central.
- New: Adds Ktlint [Credit: @nighthawk24]
- Fix: Added SaplingParamTool and ability to clear param files from cache [Credit: @herou]
- New: Added responsible disclosure document for vulnerabilities [Credit: @zebambam]
- New: UnifiedViewingKey concept.
- New: Adds support for autoshielding, including database migrations.
- New: Adds basic support for UTXOs, including refresh during scan.
- New: Support the ability to wipe all sqlite data and rebuild from keys.
- New: Switches to ZOMG lightwalletd instances.
- Fix: Only notify subscribers when a new block is detected.
- New: Add scan metrics and callbacks for apps to measure performance.
- Fix: Improve error handling and surface critical Initialization errors.
- New: Adds cleanup and removal of failed transactions.
- New: Improved logic for determining the wallet birthday.
- New: Add the ability to rewind and rescan blocks.
- New: Better safeguards against testnet v mainnet data contamination.
- New: Improved troubleshooting of ungraceful shutdowns.
- Docs: Update README to draw attention to the demo app.
- New: Expose transaction count.
- New: Derive sapling activation height from the active network.
- New: Latest checkpoints for mainnet and testnet.
Version 1.2.1-beta04
------------------------------------
- New: Updated to latest versions of grpc, grpc-okhttp and protoc
- Fix: Addresses root issue of Android 11 crash on SSL sockets
Version 1.2.1-beta03
------------------------------------
- New: Implements ZIP-313, reducing the default fee from 10,000 to 1,000 zats.
- Fix: 80% reduction in build warnings from 90 -> 18 and improved docs [Credit: @herou].
Version 1.2.1-beta02
------------------------------------
- New: Improve birthday configuration and config functions.
- Fix: Broken layout in demo app transaction list.
Version 1.2.1-beta01
------------------------------------
- New: Added latest checkpoints for testnet and mainnet.
- New: Added display name for Canopy.
- New: Update to the latest lightwalletd service definition.
- Fix: Convert Initializer.Builder to Initializer.Config to simplify the constructors.
Version 1.2.0-beta01
------------------------------------
- New: Added ability to erase initializer data.
- Fix: Updated to latest librustzcash, fixing send functionality on Canopy.
Version 1.1.0-beta10
------------------------------------
- New: Modified visibility on a few things to facilitate partner integrations.
Version 1.1.0-beta08
------------------------------------
- Fix: Publishing has been corrected by jcenter's support team.
- New: Minor improvement to initializer
Version 1.1.0-beta05
------------------------------------
- New: Synchronizer can now be started with just a viewing key.
- New: Initializer improvements.
- 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
------------------------------------
- New: Add support for canopy on testnet.
- New: Change the default lightwalletd server.
- New: Add lightwalletd service for fetching t-addr transactions.
- New: prove the concept of local RPC via protobufs.
- New: Iterate on the demo app.
- New: Added new checkpoints.
- Fix: Minor enhancements.
Version 1.1.0-beta03
------------------------------------
- New: Add robust support for transaction cancellation.
- New: Update to latest version of librustzcash.
- New: Expand test support.
- New: Improve and simplify intialization.
- New: Flag when rust is running in debug mode, causing a 10X slow down.
- New: Contributing guidelines.
- Fix: Minor cleanup and improvements.

191
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,191 @@
# Contributing Guidelines
This document contains information and guidelines about contributing to this project.
Please read it before you start participating.
**Topics**
* [Asking Questions](#asking-questions)
* [Reporting Security Issues](#reporting-security-issues)
* [Reporting Non Security Issues](#reporting-other-issues)
* [Commit Messages](#commit-messages)
* [Developers Certificate of Origin](#developers-certificate-of-origin)
## Asking Questions
Questions are welcome! We encourage you to ask questions through GitHub issues.
Before doing so, please check that the project issues database doesn't already
include an answer to your question. Then open a new Issue and use the "Question"
label.
## Reporting Security Issues
If you have discovered an issue with this code that could present a security hazard or wish to discuss a sensitive issue with our security team, please contact security@z.cash [security.asc](https://z.cash/gpg-pubkeys/security.asc). Key fingerprint = AF85 0445 546C 18B7 86F9 2C62 88FB 8B86 D8B5 A68C
## Reporting Non Security Issues
A great way to contribute to the project
is to send a detailed issue when you encounter a problem.
We always appreciate a well-written, thorough bug report.
Check that the project issues database
doesn't already include that problem or suggestion before submitting an issue.
If you find a match, add a quick "+1" or "I have this problem too."
Doing this helps prioritize the most common problems and requests.
When reporting issues, please include the following:
* The Android API you're using
* The device you're targeting
* The full output of any stack trace or compiler error
* A code snippet that reproduces the described behavior, if applicable
* Any other details that would be useful in understanding the problem
This information will help us review and fix your issue faster.
## Pull Requests
We **love** pull requests!
All contributions _will_ be licensed under the MIT license.
Code/comments should adhere to the following rules:
* Every Pull request must have an Issue associated to it. PRs with not
associated with an Issue will be closed
* Code build and Code Lint must pass.
* Names should be descriptive and concise.
* Although they are not mandatory, PRs that include significant testing will be
prioritized.
* All enhancements and bug fixes need to be documented in the CHANGELOG.
* When writing comments, use properly constructed sentences, including
punctuation.
* When documenting APIs and/or source code, don't make assumptions or make
implications about race, gender, religion, political orientation or anything
else that isn't relevant to the project.
* Remember that source code usually gets written once and read often: ensure
the reader doesn't have to make guesses. Make sure that the purpose and inner
logic are either obvious to a reasonably skilled professional, or add a
comment that explains it.
## Commit Messages
Commit history is an important part of the project's documentation.
Besides its obvious testimonial value, commits represent a point in time
in the project's lifetime in a given context. A good record of the changes that
occurred during the project's life helps to guarantee that it can outlive its
stakeholders no matter how foundational or crucial these individuals (or
groups) were. As any reading material, it is best appreciated and comprehended
when there's a visible structure that readers can follow and reason about.
For that we've defined a structure for commit messages that all contributors must
follow to maintain coherence on the project's commit log. The proposed format
has been inspired by [this great article](https://cbea.ms/git-commit/)
### Preparing to contribute to the project
The first thing you should look for is an existing issue. It is possible
that the contribution you are planning to work on was already discussed
by other users and/or contributors in the past. If not present, file an
issue following the criteria described in the preceeding sections.
Every contribution must reference an existing Issue. This issue is important
since it will be directly referenced in the title of your commit.
Although we prefer small PR's. We encourage our contributors to use Squash
commits extensively. Maintainers prefer avoiding _merge commits_ when possible.
It is very much likely that _if accepted_, your contribution will be _squash merged_.
When squashing commits, use your best judgement. In some situations, a refactoring may
be done before actual behavior changes are implemented. It is reasonable to keep such
a refactoring as a separate commit as it both makes review easier and allows for
these refactoring commit SHAs to be added to `.git-blame-ignore-revs`.
### Structuring a PR Commit
#### Commit Title
The first line of your commit message constitutes its _title_. Maintainers will
use commit titles to create release notes. Your contribution will be featured
in a public release of the project. Think of it as a newspaper headline. It
should be descriptive and provide the reader a broad idea of what the commit is
about. You can use a related github issue if it matches this criterion.
**Preferred title format**
`[#{issue_number}] {self_descriptive_title}`
Example
`[#258] - User can take the backup test successfully more than once`
optionally you can append the PR # between parenthesis.
#### Commit message's body
Use the body of the commit to bring more context to the change. Usually the bulk
of the problem might be explained in the GitHub Issue. It's a good long term strategy
not to rely on such elements. If the project were to change its hosting, much of the
associated "Issues" and "pull requests" will be lost, yet the commit history will
probably be preserved and the context will also be.
If there are followup issues for this commit, consider referencing those as well.
**Use the tools on your favor!**
When opening a Pull Request, GitHub will take the title of your commit as the PR's
title and the body of your PR its description. Having a proper structure on your
commit will make your day shorter.
### Example:
````
commit [some_hash]
Author: You <you@somedomain.io>
Date: some date
[#258] User can take the backup test successfully more than once (#282)
Closes #258
this checks that when the user taps the finished button on the phrase displayed it has definitely not passed the test before going to the recovery flow.
Note: this should actually go to the next or previous screen according to the context that takes the user to the phrase display screen from that context.
Add //TODO comment with the permanent fix for the problem
````
When you open a PR with a commit like this one the first line will land on the GUI's title field,
and the body will be added as the description of the PR.
Adding the text `Closes #{issue_number}` will tell GitHub to close the issue when the PR is merged.
Let the machines do their work.
## Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
- (a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
- (b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
- (c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
- (d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
This contribution guide is inspired on great projects like [AlamoFire](https://github.com/Alamofire/Foundation/blob/master/CONTRIBUTING.md) and [CocoaPods](https://github.com/CocoaPods/CocoaPods/blob/master/CONTRIBUTING.md)

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017-2021 Electric Coin Company
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

53
MIGRATIONS.md Normal file
View File

@@ -0,0 +1,53 @@
Troubleshooting Migrations
==========
Upcoming Migration to Version 1.7 from 1.6
--------------------------------------
Various APIs used `Long` value to represent Zatoshi currency amounts. Those APIs now use a typesafe `Zatoshi` class. When passing amounts, simply wrap Long values with the Zatoshi constructor `Zatoshi(Long)`. When receiving values, simply unwrap Long values with `Zatoshi.value`.
`WalletBalance` no longer has uninitialized default values. This means that `Synchronizer` fields that expose a WalletBalance now use `null` to signal an uninitialized value. Specifically this means `Synchronizer.orchardBalances`, `Synchronzier.saplingBalances`, and `Synchronizer.transparentBalances` have nullable values now.
`ZcashSdk.ZATOSHI_PER_ZEC` has been moved to `Zatoshi.ZATOSHI_PER_ZEC`.
`ZcashSdk.MINERS_FEE_ZATOSHI` has been renamed to `ZcashSdk.MINERS_FEE` and the type has changed from `Long` to `Zatoshi`.
Upcoming Migrating to Version 1.4.* from 1.3.*
--------------------------------------
The main entrypoint to the SDK has changed.
Previously, a Synchronizer was initialized with `Synchronizer(initializer)` and now it is initialized with `Synchronizer.new(initializer)` which is also now a suspending function. Helper methods `Synchronizer.newBlocking()` and `Initializer.newBlocking()` can be used to ease the transition.
For clients needing more complex initialization, the previous default method arguments for `Synchronizer()` were moved to `DefaultSynchronizerFactory`.
The minimum Android version supported is now API 19.
Migrating to Version 1.3.0-beta18 from 1.3.0-beta19
--------------------------------------
Various APIs that have always been considered private have been moved into a new package called `internal`. While this should not be a breaking change, clients that might have relied on these internal classes should stop doing so. If necessary, these calls can be migrated by changing the import to the new `internal` package name.
A number of methods have been converted to suspending functions, because they were performing slow or blocking calls (e.g. disk IO) internally. This is a breaking change.
Migrating to Version 1.3.* from 1.2.*
--------------------------------------
The biggest breaking changes in 1.3 that inspired incrementing the minor version number was simplifying down to one "network aware" library rather than two separate libraries, each dedicated to either testnet or mainnet. This greatly simplifies the gradle configuration and has lots of other benefits. Wallets can now set a network with code similar to the following:
```kotlin
// Simple example
val network: ZcashNetwork = if (testMode) ZcashNetwork.Testnet else ZcashNetwork.Mainnet
// Dependency Injection example
@Provides @Singleton fun provideNetwork(): ZcashNetwork = ZcashNetwork.Mainnet
```
1.3 also adds a runtime check for wallets that are accessing properties before the synchronizer has started. By introducing a `prepare` step, we are now able to catch these errors proactively rather than allowing them to turn into subtle bugs that only surface later. We found this when code was accessing properties before database migrations completed, causing undefined results. Developers do not need to make any changes to enable these checks, they happen automatically and result in detailed error messages.
| Error | Issue | Fix |
| ------------------------------- | ----------------------------------- | ------------------------ |
| No value passed for parameter 'network' | Many functions are now network-aware | pass an instance of ZcashNetwork, which is typically set during initialization |
| Unresolved reference: validate | The `validate` package was removed | instead of `cash.z.ecc.android.sdk.validate.AddressType`<br/>import `cash.z.ecc.android.sdk.type.AddressType` |
| Unresolved reference: WalletBalance | WalletBalance was moved out of `CompactBlockProcessor` and up to the `type` package | instead of `cash.z.ecc.android.sdk.CompactBlockProcessor.WalletBalance`<br/>import `cash.z.ecc.android.sdk.type.WalletBalance` |
| Unresolved reference: server | This was replaced by `setNetwork` | instead of `config.server(host, port)`<br/>use `config.setNetwork(network, host, port)` |
| Unresolved reference: balances | 3 types of balances are now exposed | change `balances` to `saplingBalances` |
| Unresolved reference: latestBalance | There are now multiple balance types so this convenience function was removed in favor of forcing wallets to think about which balances they want to show. | In most cases, just use `synchronizer.saplingBalances.value` directly, instead |
| Type mismatch: inferred type is String but ZcashNetwork was expected | This function is now network aware | use `Initializer.erase(context, network, alias)` |
| Type mismatch: inferred type is Int? but ZcashNetwork was expected | This function is now network aware | use `WalletBirthdayTool.loadNearest(context, network, height)` instead |
| None of the following functions can be called with the arguments supplied: <br/>public open fun deriveShieldedAddress(seed: ByteArray, network: ZcashNetwork, accountIndex: Int = ...): String defined in cash.z.ecc.android.sdk.tool.DerivationTool.Companion<br/>public open fun deriveShieldedAddress(viewingKey: String, network: ZcashNetwork): String defined in cash.z.ecc.android.sdk.tool.DerivationTool.Companion | This function is now network aware | use `deriveShieldedAddress(seed, network)`|

226
README.md Normal file
View File

@@ -0,0 +1,226 @@
[![license](https://img.shields.io/github/license/zcash/zcash-android-wallet-sdk.svg?maxAge=2592000&style=plastic)](https://github.com/zcash/zcash-android-wallet-sdk/blob/master/LICENSE)
![Maven Central](https://img.shields.io/maven-central/v/cash.z.ecc.android/zcash-android-sdk?color=success&style=plastic)
This is a beta build and is currently under active development. Please be advised of the following:
- This code currently is not audited by an external security auditor, use it at your own risk
- The code **has not been subjected to thorough review** by engineers at the Electric Coin Company
- We **are actively changing** the codebase and adding features where/when needed
🔒 Security Warnings
- The Zcash Android Wallet SDK is experimental and a work in progress. Use it at your own risk.
- Developers using this SDK must familiarize themselves with the current [threat
model](https://zcash.readthedocs.io/en/latest/rtd_pages/wallet_threat_model.html), especially the known weaknesses described there.
---
# Zcash Android SDK
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.
## Contents
- [Requirements](#requirements)
- [Structure](#structure)
- [Overview](#overview)
- [Components](#components)
- [Quickstart](#quickstart)
- [Examples](#examples)
- [Compiling Sources](#compiling-sources)
- [Versioning](#versioning)
- [Examples](#examples)
## Requirements
This SDK is designed to work with [lightwalletd](https://github.com/zcash-hackworks/lightwalletd)
## Structure
From an app developer's perspective, this SDK will encapsulate the most complex aspects of using Zcash, freeing the developer to focus on UI and UX, rather than scanning blockchains and building commitment trees! Internally, the SDK is structured as follows:
![SDK Diagram](assets/sdk_diagram_final.png?raw=true "SDK Diagram")
Thankfully, the only thing an app developer has to be concerned with is the following:
![SDK Diagram Developer Perspective](assets/sdk_dev_pov_final.png?raw=true "SDK Diagram Dev PoV")
[Back to contents](#contents)
## Overview
At a high level, this SDK simply helps native Android codebases connect to Zcash's Rust crypto libraries without needing to know Rust or be a Cryptographer. Think of it as welding. The SDK takes separate things and tightly bonds them together such that each can remain as idiomatic as possible. Its goal is to make it easy for an app to incorporate shielded transactions while remaining a good citizen on mobile devices.
Given all the moving parts, making things easy requires coordination. The [Synchronizer](docs/-synchronizer/README.md) provides that layer of abstraction so that the primary steps to make use of this SDK are simply:
1. Start the [Synchronizer](docs/-synchronizer/README.md)
2. Subscribe to wallet data
The [Synchronizer](docs/-synchronizer/README.md) takes care of
- Connecting to the light wallet server
- Downloading the latest compact blocks in a privacy-sensitive way
- Scanning and trial decrypting those blocks for shielded transactions related to the wallet
- Processing those related transactions into useful data for the UI
- Sending payments to a full node through [lightwalletd](https://github.com/zcash/lightwalletd)
- Monitoring sent payments for status updates
To accomplish this, these responsibilities of the SDK are divided into separate components. Each component is coordinated by the [Synchronizer](docs/-synchronizer/README.md), which is the thread that ties it all together.
#### Components
| Component | Summary |
| :----------------------------- | :---------------------------------------------------------------------------------------- |
| **LightWalletService** | Service used for requesting compact blocks |
| **CompactBlockStore** | Stores compact blocks that have been downloaded from the `LightWalletService` |
| **CompactBlockProcessor** | Validates and scans the compact blocks in the `CompactBlockStore` for transaction details |
| **OutboundTransactionManager** | Creates, Submits and manages transactions for spending funds |
| **Initializer** | Responsible for all setup that must happen before synchronization can begin. Loads the rust library and helps initialize databases. |
| **DerivationTool**, **BirthdayTool** | Utilities for deriving keys, addresses and loading wallet checkpoints, called "birthdays." |
| **RustBackend** | Wraps and simplifies the rust library and exposes its functionality to the Kotlin SDK |
[Back to contents](#contents)
## Quickstart
Add flavors for testnet v mainnet. Since `productFlavors` cannot start with the word 'test' we recommend:
build.gradle:
```groovy
flavorDimensions 'network'
productFlavors {
// would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test"
zcashtestnet {
dimension 'network'
matchingFallbacks = ['zcashtestnet', 'debug']
}
zcashmainnet {
dimension 'network'
matchingFallbacks = ['zcashmainnet', 'release']
}
}
```
build.gradle.kts
```kotlin
flavorDimensions.add("network")
productFlavors {
// would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test"
create("zcashtestnet") {
dimension = "network"
matchingFallbacks.addAll(listOf("zcashtestnet", "debug"))
}
create("zcashmainnet") {
dimension = "network"
matchingFallbacks.addAll(listOf("zcashmainnet", "release"))
}
}
```
Add the SDK dependency:
```kotlin
implementation("cash.z.ecc.android:zcash-android-sdk:1.4.0-beta01")
```
Start the [Synchronizer](docs/-synchronizer/README.md)
```kotlin
synchronizer.start(this)
```
Get the wallet's address
```kotlin
synchronizer.getAddress()
// or alternatively
DerivationTool.deriveShieldedAddress(viewingKey)
```
Send funds to another address
```kotlin
synchronizer.sendToAddress(spendingKey, zatoshi, address, memo)
```
[Back to contents](#contents)
## Examples
Full working examples can be found in the [demo app](demo-app), covering all major functionality of the SDK. Each demo strives to be self-contained so that a developer can understand everything required for it to work. Testnet builds of the demo app will soon be available to [download as github releases](https://github.com/zcash/zcash-android-wallet-sdk/releases).
### Demos
Menu Item|Related Code|Description
:-----|:-----|:-----
Get Private Key|[GetPrivateKeyFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt)|Given a seed, display its viewing key and spending key
Get Address|[GetAddressFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt)|Given a seed, display its z-addr
Get Balance|[GetBalanceFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt)|Display the balance
Get Latest Height|[GetLatestHeightFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt)|Given a lightwalletd server, retrieve the latest block height
Get Block|[GetBlockFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt)|Given a lightwalletd server, retrieve a compact block
Get Block Range|[GetBlockRangeFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt)|Given a lightwalletd server, retrieve a range of compact blocks
List Transactions|[ListTransactionsFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt)|Given a seed, list all related shielded transactions
Send|[SendFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt)|Send and monitor a transaction, the most complex demo
[Back to contents](#contents)
## Compiling Sources
:warning: Compilation is not required unless you plan to submit a patch or fork the code. Instead, it is recommended to simply add the SDK dependencies via Gradle.
In the event that you *do* want to compile the SDK from sources, please see [Setup.md](docs/Setup.md).
[Back to contents](#contents)
## Versioning
This project follows [semantic versioning](https://semver.org/) with pre-release versions. An example of a valid version number is `1.0.4-alpha11` denoting the `11th` iteration of the `alpha` pre-release of version `1.0.4`. Stable releases, such as `1.0.4` will not contain any pre-release identifiers. Pre-releases include the following, in order of stability: `alpha`, `beta`, `rc`. Version codes offer a numeric representation of the build name that always increases. The first six significant digits represent the major, minor and patch number (two digits each) and the last 3 significant digits represent the pre-release identifier. The first digit of the identifier signals the build type. Lastly, each new build has a higher version code than all previous builds. The following table breaks this down:
#### Build Types
| Type | Purpose | Stability | Audience | Identifier | Example Version |
| :---- | :--------- | :---------- | :-------- | :------- | :--- |
| **alpha** | **Sandbox.** For developers to verify behavior and try features. Things seen here might never go to production. Most bugs here can be ignored.| Unstable: Expect bugs | Internal developers | 0XX | 1.2.3-alpha04 (10203004) |
| **beta** | **Hand-off.** For developers to present finished features. Bugs found here should be reported and immediately addressed, if they relate to recent changes. | Unstable: Report bugs | Internal stakeholders | 2XX | 1.2.3-beta04 (10203204) |
| **release candidate** | **Hardening.** Final testing for an app release that we believe is ready to go live. The focus here is regression testing to ensure that new changes have not introduced instability in areas that were previously working. | Stable: Hunt for bugs | External testers | 4XX | 1.2.3-rc04 (10203404) |
| **production** | **Delivery.** Deliver new features to end-users. Any bugs found here need to be prioritized. Some will require immediate attention but most can be worked into a future release. | Stable: Prioritize bugs | Public | 8XX | 1.2.3 (10203800) |
[Back to contents](#contents)
## Examples
A primitive example to exercise the SDK exists in this repo, under [Demo App](demo-app).
There's also a more comprehensive [Sample Wallet](https://github.com/zcash/zcash-android-wallet).
[Back to contents](#contents)
## Checkpoints
To improve the speed of syncing with the Zcash network, the SDK contains a series of embedded checkpoints. These should be updated periodically, as new transactions are added to the network. Checkpoints are stored under the [assets](sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint) directory as JSON files. Checkpoints for both mainnet and testnet are bundled into the SDK.
To update the checkpoints, see [Checkmate](https://github.com/zcash-hackworks/checkmate).
We generally recommend adding new checkpoints every few weeks. By convention, checkpoints are added in block increments of 10,000 which provides a reasonable tradeoff in terms of number of checkpoints versus performance.
There are two special checkpoints, one for sapling activation and another for orchard activation. These are mentioned because they don't follow the "round 10,000" rule.
* Sapling activation
* Mainnet: 419200
* Testnet: 280000
* Orchard activation
* Mainnet: 1687104
* Testnet: 1842420
## Publishing
Publishing instructions for maintainers of this repository can be found in [PUBLISHING.md](PUBLISHING.md)
[Back to contents](#contents)
# Known Issues
1. During builds, a warning will be printed that says "Unable to detect AGP versions for included builds. All projects in the build should use the same AGP version." This can be safely ignored. The version under build-conventions is the same as the version used elsewhere in the application.
1. Android Studio will warn about the Gradle checksum. This is a [known issue](https://github.com/gradle/gradle/issues/9361) and can be safely ignored.

BIN
assets/build-variants.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
assets/ndk-window.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

BIN
assets/sdk-manager-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

View File

@@ -0,0 +1,34 @@
import org.jetbrains.kotlin.konan.properties.loadProperties
plugins {
`kotlin-dsl`
}
buildscript {
dependencyLocking {
lockAllConfigurations()
}
}
dependencyLocking {
lockAllConfigurations()
}
// Per conversation in the KotlinLang Slack, Gradle uses Java 8 compatibility internally
// for all build scripts.
// https://kotlinlang.slack.com/archives/C19FD9681/p1636632870122900?thread_ts=1636572288.117000&cid=C19FD9681
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
val rootProperties = getRootProperties()
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${rootProperties.getProperty("KOTLIN_VERSION")}")
implementation("com.android.tools.build:gradle:${rootProperties.getProperty("ANDROID_GRADLE_PLUGIN_VERSION")}")
implementation("wtf.emulator:gradle-plugin:${rootProperties.getProperty("EMULATOR_WTF_GRADLE_PLUGIN_VERSION")}")
}
// A slightly gross way to use the root gradle.properties as the single source of truth for version numbers
fun getRootProperties() = loadProperties(File(project.projectDir.parentFile, "gradle.properties").path)

View File

@@ -0,0 +1,45 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.github.gundy:semver4j:0.16.4=classpath
com.google.code.findbugs:jsr305:3.0.2=classpath
com.google.code.gson:gson:2.8.6=classpath
com.google.errorprone:error_prone_annotations:2.3.4=classpath
com.google.guava:failureaccess:1.0.1=classpath
com.google.guava:guava:29.0-jre=classpath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath
com.google.j2objc:j2objc-annotations:1.3=classpath
de.undercouch:gradle-download-task:4.1.1=classpath
org.checkerframework:checker-qual:2.11.1=classpath
org.gradle.kotlin.kotlin-dsl:org.gradle.kotlin.kotlin-dsl.gradle.plugin:2.1.7=classpath
org.gradle.kotlin:gradle-kotlin-dsl-plugins:2.1.7=classpath
org.jetbrains.intellij.deps:trove4j:1.0.20181211=classpath
org.jetbrains.kotlin:kotlin-android-extensions:1.5.31=classpath
org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.5.31=classpath
org.jetbrains.kotlin:kotlin-build-common:1.5.31=classpath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.31=classpath
org.jetbrains.kotlin:kotlin-compiler-runner:1.5.31=classpath
org.jetbrains.kotlin:kotlin-daemon-client:1.5.31=classpath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.31=classpath
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.31=classpath
org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.5.31=classpath
org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31=classpath
org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.5.31=classpath
org.jetbrains.kotlin:kotlin-native-utils:1.5.31=classpath
org.jetbrains.kotlin:kotlin-project-model:1.5.31=classpath
org.jetbrains.kotlin:kotlin-sam-with-receiver:1.5.31=classpath
org.jetbrains.kotlin:kotlin-scripting-common:1.5.31=classpath
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.5.31=classpath
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.5.31=classpath
org.jetbrains.kotlin:kotlin-scripting-jvm:1.5.31=classpath
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=classpath
org.jetbrains.kotlin:kotlin-stdlib:1.5.31=classpath
org.jetbrains.kotlin:kotlin-tooling-metadata:1.5.31=classpath
org.jetbrains.kotlin:kotlin-util-io:1.5.31=classpath
org.jetbrains.kotlin:kotlin-util-klib:1.5.31=classpath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=classpath
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0=classpath
org.jetbrains:annotations:13.0=classpath
empty=

View File

@@ -0,0 +1,182 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
androidx.databinding:databinding-common:7.2.1=runtimeClasspath
androidx.databinding:databinding-compiler-common:7.2.1=runtimeClasspath
com.android.databinding:baseLibrary:7.2.1=runtimeClasspath
com.android.tools.analytics-library:crash:30.2.1=runtimeClasspath
com.android.tools.analytics-library:protos:30.2.1=runtimeClasspath
com.android.tools.analytics-library:shared:30.2.1=runtimeClasspath
com.android.tools.analytics-library:tracker:30.2.1=runtimeClasspath
com.android.tools.build.jetifier:jetifier-core:1.0.0-beta09=runtimeClasspath
com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta09=runtimeClasspath
com.android.tools.build:aapt2-proto:7.2.1-7984345=runtimeClasspath
com.android.tools.build:aaptcompiler:7.2.1=runtimeClasspath
com.android.tools.build:apksig:7.2.1=compileClasspath,runtimeClasspath
com.android.tools.build:apkzlib:7.2.1=compileClasspath,runtimeClasspath
com.android.tools.build:builder-model:7.2.1=compileClasspath,runtimeClasspath
com.android.tools.build:builder-test-api:7.2.1=runtimeClasspath
com.android.tools.build:builder:7.2.1=compileClasspath,runtimeClasspath
com.android.tools.build:bundletool:1.8.2=runtimeClasspath
com.android.tools.build:gradle-api:7.2.1=compileClasspath,runtimeClasspath
com.android.tools.build:gradle:7.2.1=compileClasspath,runtimeClasspath
com.android.tools.build:manifest-merger:30.2.1=compileClasspath,runtimeClasspath
com.android.tools.build:transform-api:2.0.0-deprecated-use-gradle-api=runtimeClasspath
com.android.tools.ddms:ddmlib:30.2.1=runtimeClasspath
com.android.tools.layoutlib:layoutlib-api:30.2.1=runtimeClasspath
com.android.tools.lint:lint-model:30.2.1=runtimeClasspath
com.android.tools.lint:lint-typedef-remover:30.2.1=runtimeClasspath
com.android.tools.utp:android-device-provider-ddmlib-proto:30.2.1=runtimeClasspath
com.android.tools.utp:android-device-provider-gradle-proto:30.2.1=runtimeClasspath
com.android.tools.utp:android-test-plugin-host-additional-test-output-proto:30.2.1=runtimeClasspath
com.android.tools.utp:android-test-plugin-host-coverage-proto:30.2.1=runtimeClasspath
com.android.tools.utp:android-test-plugin-host-retention-proto:30.2.1=runtimeClasspath
com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.2.1=runtimeClasspath
com.android.tools:annotations:30.2.1=runtimeClasspath
com.android.tools:common:30.2.1=runtimeClasspath
com.android.tools:dvlib:30.2.1=runtimeClasspath
com.android.tools:repository:30.2.1=runtimeClasspath
com.android.tools:sdk-common:30.2.1=runtimeClasspath
com.android.tools:sdklib:30.2.1=runtimeClasspath
com.android:signflinger:7.2.1=runtimeClasspath
com.android:zipflinger:7.2.1=compileClasspath,runtimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.11.1=runtimeClasspath
com.fasterxml.jackson.core:jackson-core:2.11.1=runtimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.11.1=runtimeClasspath
com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.1=runtimeClasspath
com.fasterxml.jackson.module:jackson-module-jaxb-annotations:2.11.1=runtimeClasspath
com.fasterxml.jackson.module:jackson-module-kotlin:2.11.1=runtimeClasspath
com.fasterxml.woodstox:woodstox-core:6.2.1=runtimeClasspath
com.github.gundy:semver4j:0.16.4=runtimeClasspath
com.google.android:annotations:4.1.1.4=runtimeClasspath
com.google.api.grpc:proto-google-common-protos:1.12.0=runtimeClasspath
com.google.auto.value:auto-value-annotations:1.6.2=runtimeClasspath
com.google.code.findbugs:jsr305:3.0.2=runtimeClasspath
com.google.code.gson:gson:2.8.9=runtimeClasspath
com.google.crypto.tink:tink:1.3.0-rc2=runtimeClasspath
com.google.dagger:dagger:2.28.3=runtimeClasspath
com.google.errorprone:error_prone_annotations:2.3.4=runtimeClasspath
com.google.flatbuffers:flatbuffers-java:1.12.0=runtimeClasspath
com.google.guava:failureaccess:1.0.1=runtimeClasspath
com.google.guava:guava:30.1-jre=runtimeClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=runtimeClasspath
com.google.j2objc:j2objc-annotations:1.3=runtimeClasspath
com.google.jimfs:jimfs:1.1=runtimeClasspath
com.google.protobuf:protobuf-java-util:3.10.0=runtimeClasspath
com.google.protobuf:protobuf-java:3.10.0=runtimeClasspath
com.google.testing.platform:core-proto:0.0.8-alpha07=runtimeClasspath
com.googlecode.json-simple:json-simple:1.1=runtimeClasspath
com.googlecode.juniversalchardet:juniversalchardet:1.0.3=runtimeClasspath
com.squareup:javapoet:1.10.0=runtimeClasspath
com.squareup:javawriter:2.5.0=compileClasspath,runtimeClasspath
com.sun.activation:javax.activation:1.2.0=runtimeClasspath
com.sun.istack:istack-commons-runtime:3.0.8=runtimeClasspath
com.sun.xml.fastinfoset:FastInfoset:1.2.16=runtimeClasspath
com.vdurmont:semver4j:3.1.0=runtimeClasspath
commons-codec:commons-codec:1.11=runtimeClasspath
commons-io:commons-io:2.4=runtimeClasspath
commons-logging:commons-logging:1.2=runtimeClasspath
de.undercouch:gradle-download-task:4.1.1=runtimeClasspath
io.grpc:grpc-api:1.21.1=runtimeClasspath
io.grpc:grpc-context:1.21.1=runtimeClasspath
io.grpc:grpc-core:1.21.1=runtimeClasspath
io.grpc:grpc-netty:1.21.1=runtimeClasspath
io.grpc:grpc-protobuf-lite:1.21.1=runtimeClasspath
io.grpc:grpc-protobuf:1.21.1=runtimeClasspath
io.grpc:grpc-stub:1.21.1=runtimeClasspath
io.netty:netty-buffer:4.1.34.Final=runtimeClasspath
io.netty:netty-codec-http2:4.1.34.Final=runtimeClasspath
io.netty:netty-codec-http:4.1.34.Final=runtimeClasspath
io.netty:netty-codec-socks:4.1.34.Final=runtimeClasspath
io.netty:netty-codec:4.1.34.Final=runtimeClasspath
io.netty:netty-common:4.1.34.Final=runtimeClasspath
io.netty:netty-handler-proxy:4.1.34.Final=runtimeClasspath
io.netty:netty-handler:4.1.34.Final=runtimeClasspath
io.netty:netty-resolver:4.1.34.Final=runtimeClasspath
io.netty:netty-transport:4.1.34.Final=runtimeClasspath
io.opencensus:opencensus-api:0.21.0=runtimeClasspath
io.opencensus:opencensus-contrib-grpc-metrics:0.21.0=runtimeClasspath
it.unimi.dsi:fastutil:8.4.0=runtimeClasspath
jakarta.activation:jakarta.activation-api:1.2.1=runtimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=runtimeClasspath
javax.inject:javax.inject:1=runtimeClasspath
net.java.dev.jna:jna-platform:5.6.0=runtimeClasspath
net.java.dev.jna:jna:5.6.0=runtimeClasspath
net.sf.jopt-simple:jopt-simple:4.9=runtimeClasspath
net.sf.kxml:kxml2:2.3.0=runtimeClasspath
org.apache.commons:commons-compress:1.20=runtimeClasspath
org.apache.httpcomponents:httpclient:4.5.9=runtimeClasspath
org.apache.httpcomponents:httpcore:4.4.11=runtimeClasspath
org.apache.httpcomponents:httpmime:4.5.6=runtimeClasspath
org.bitbucket.b_c:jose4j:0.7.0=runtimeClasspath
org.bouncycastle:bcpkix-jdk15on:1.56=runtimeClasspath
org.bouncycastle:bcprov-jdk15on:1.56=runtimeClasspath
org.checkerframework:checker-qual:3.5.0=runtimeClasspath
org.codehaus.mojo:animal-sniffer-annotations:1.17=runtimeClasspath
org.codehaus.woodstox:stax2-api:4.2.1=runtimeClasspath
org.glassfish.jaxb:jaxb-runtime:2.3.2=runtimeClasspath
org.glassfish.jaxb:txw2:2.3.2=runtimeClasspath
org.jdom:jdom2:2.0.6=runtimeClasspath
org.jetbrains.dokka:dokka-core:1.4.32=runtimeClasspath
org.jetbrains.intellij.deps:trove4j:1.0.20181211=kotlinCompilerClasspath
org.jetbrains.intellij.deps:trove4j:1.0.20200330=runtimeClasspath
org.jetbrains.kotlin:kotlin-android-extensions:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-build-common:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.31=kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-daemon-client:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.31=kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.6.21=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.6.21=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-native-utils:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-native-utils:1.6.21=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-project-model:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-project-model:1.6.21=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-reflect:1.5.31=compileClasspath,kotlinCompilerClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-sam-with-receiver:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-script-runtime:1.5.31=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-common:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-common:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-scripting-jvm:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-jvm:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,runtimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.0=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.0=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.5.31=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,runtimeClasspath
org.jetbrains.kotlin:kotlin-tooling-metadata:1.6.21=runtimeClasspath
org.jetbrains.kotlin:kotlin-util-io:1.5.31=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-util-io:1.6.21=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-util-klib:1.6.21=runtimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=kotlinCompilerPluginClasspathMain,runtimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1=runtimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0=kotlinCompilerPluginClasspathMain
org.jetbrains:annotations:13.0=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,runtimeClasspath
org.jetbrains:markdown-jvm:0.2.1=runtimeClasspath
org.jetbrains:markdown:0.2.1=runtimeClasspath
org.json:json:20180813=runtimeClasspath
org.jsoup:jsoup:1.13.1=runtimeClasspath
org.jvnet.staxex:stax-ex:1.8.1=runtimeClasspath
org.ow2.asm:asm-analysis:9.1=runtimeClasspath
org.ow2.asm:asm-commons:9.1=runtimeClasspath
org.ow2.asm:asm-tree:9.1=runtimeClasspath
org.ow2.asm:asm-util:9.1=runtimeClasspath
org.ow2.asm:asm:9.1=compileClasspath,runtimeClasspath
org.slf4j:slf4j-api:1.7.30=runtimeClasspath
org.tensorflow:tensorflow-lite-metadata:0.1.0-rc2=runtimeClasspath
wtf.emulator:gradle-plugin:0.0.10=compileClasspath,runtimeClasspath
xerces:xercesImpl:2.12.0=runtimeClasspath
xml-apis:xml-apis:1.4.01=runtimeClasspath
empty=annotationProcessor

View File

@@ -0,0 +1,15 @@
pluginManagement {
repositories {
gradlePluginPortal()
}
}
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}
rootProject.name = "build-conventions"

View File

@@ -0,0 +1,102 @@
import com.android.build.api.dsl.CommonExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
pluginManager.withPlugin("com.android.application") {
project.the<com.android.build.gradle.AppExtension>().apply {
configureBaseExtension()
defaultConfig {
minSdk = project.property("ANDROID_MIN_SDK_VERSION").toString().toInt()
targetSdk = project.property("ANDROID_TARGET_SDK_VERSION").toString().toInt()
// en_XA and ar_XB are pseudolocales for debugging.
// The rest of the locales provides an explicit list of the languages to keep in the
// final app. Doing this will strip out additional locales from libraries like
// Google Play Services and Firebase, which add unnecessary bloat.
resourceConfigurations.addAll(listOf("en", "en-rUS", "en-rGB", "en-rAU", "en_XA", "ar_XB"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
}
}
}
pluginManager.withPlugin("com.android.library") {
project.the<com.android.build.gradle.LibraryExtension>().apply {
configureBaseExtension()
defaultConfig {
minSdk = project.property("ANDROID_MIN_SDK_VERSION").toString().toInt()
targetSdk = project.property("ANDROID_TARGET_SDK_VERSION").toString().toInt()
// The last two are for support of pseudolocales in debug builds.
// If we add other localizations, they should be included in this list.
// By explicitly setting supported locales, we strip out unused localizations from third party
// libraries (e.g. play services)
resourceConfigurations.addAll(listOf("en", "en-rUS", "en-rGB", "en-rAU", "en_XA", "ar_XB"))
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("proguard-consumer.txt")
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
}
testCoverage {
jacocoVersion = project.property("JACOCO_VERSION").toString()
}
}
}
fun com.android.build.gradle.BaseExtension.configureBaseExtension() {
compileSdkVersion(project.property("ANDROID_COMPILE_SDK_VERSION").toString().toInt())
ndkVersion = project.property("ANDROID_NDK_VERSION").toString()
compileOptions {
val javaVersion = JavaVersion.toVersion(project.property("ANDROID_JVM_TARGET").toString())
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
buildTypes {
getByName("debug").apply {
isTestCoverageEnabled = project.property("IS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED")
.toString().toBoolean()
}
}
signingConfigs {
val debugKeystorePath = project.property("ZCASH_DEBUG_KEYSTORE_PATH").toString()
val isExplicitDebugSigningEnabled = !debugKeystorePath.isNullOrBlank()
if (isExplicitDebugSigningEnabled) {
// If this block doesn't execute, the output will still be signed with the default keystore
getByName("debug").apply {
storeFile = File(debugKeystorePath)
}
}
}
testOptions {
animationsDisabled = true
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
execution = "ANDROIDX_TEST_ORCHESTRATOR"
}
}
if (this is CommonExtension<*, *, *, *>) {
kotlinOptions {
jvmTarget = project.property("ANDROID_JVM_TARGET").toString()
allWarningsAsErrors = project.property("ZCASH_IS_TREAT_WARNINGS_AS_ERRORS").toString().toBoolean()
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" +
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi" + "-opt-in=kotlinx.coroutines.FlowPreview"
}
}
}
fun CommonExtension<*, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
(this as ExtensionAware).extensions.configure("kotlinOptions", block)
}

View File

@@ -0,0 +1,15 @@
//dependencyLocking {
// lockAllConfigurations()
//}
tasks {
register("resolveAll") {
doLast {
configurations.filter {
// Add any custom filtering on the configurations to be resolved
it.isCanBeResolved
}.forEach { it.resolve() }
}
}
}

View File

@@ -0,0 +1,33 @@
// Emulator WTF has min and max values that might differ from our project's
// These are determined by `ew-cli --models`
@Suppress("MagicNumber", "PropertyName", "VariableNaming")
val EMULATOR_WTF_MIN_SDK = 23
@Suppress("MagicNumber", "PropertyName", "VariableNaming")
val EMULATOR_WTF_MAX_SDK = 31
pluginManager.withPlugin("wtf.emulator.gradle") {
project.the<wtf.emulator.EwExtension>().apply {
val tokenString = project.properties["ZCASH_EMULATOR_WTF_API_KEY"].toString()
if (tokenString.isNotEmpty()) {
token.set(tokenString)
}
val libraryMinSdkVersion = run {
val buildMinSdk = project.properties["ANDROID_MIN_SDK_VERSION"].toString().toInt()
buildMinSdk.coerceAtLeast(EMULATOR_WTF_MIN_SDK).toString()
}
val targetSdkVersion = run {
val buildTargetSdk = project.properties["ANDROID_TARGET_SDK_VERSION"].toString().toInt()
buildTargetSdk.coerceAtMost(EMULATOR_WTF_MAX_SDK).toString()
}
devices.set(
listOf(
mapOf("model" to "Pixel2", "version" to libraryMinSdkVersion),
mapOf("model" to "Pixel2", "version" to targetSdkVersion)
)
)
}
}

View File

@@ -0,0 +1,41 @@
plugins {
id("java")
}
val ktlint by configurations.creating
dependencies {
ktlint("com.pinterest:ktlint:${project.property("KTLINT_VERSION")}") {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named<Bundling>(Bundling.EXTERNAL))
}
}
}
tasks {
val editorConfigFile = rootProject.file(".editorconfig")
val ktlintArgs = listOf("**/src/**/*.kt", "!**/build/**.kt", "--editorconfig=$editorConfigFile")
register("ktlint", org.gradle.api.tasks.JavaExec::class) {
description = "Check code style with ktlint"
classpath = ktlint
mainClass.set("com.pinterest.ktlint.Main")
args = ktlintArgs
}
register("ktlintFormat", org.gradle.api.tasks.JavaExec::class) {
// https://github.com/pinterest/ktlint/issues/1195#issuecomment-1009027802
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
description = "Apply code style formatting with ktlint"
classpath = ktlint
mainClass.set("com.pinterest.ktlint.Main")
args = listOf("-F") + ktlintArgs
}
}
java {
val javaVersion = JavaVersion.toVersion(project.property("ANDROID_JVM_TARGET").toString())
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}

View File

@@ -0,0 +1,47 @@
import java.util.concurrent.TimeUnit
if (isRosetta()) {
logger.warn("This Gradle invocation is running under Rosetta. Use an ARM (aarch64) JDK to " +
"improve performance. One can be downloaded from https://adoptium.net/temurin/releases")
}
@Suppress("MagicNumber")
private val maxTimeoutMillis = 5000L
/**
* This method is safe to call from any operating system or CPU architecture.
*
* @return True if the application is running under Rosetta.
*/
fun isRosetta(): Boolean {
if (System.getProperty("os.name").toLowerCase(java.util.Locale.ROOT).startsWith("mac")) {
// Counterintuitive, but running under Rosetta is reported as Intel64 to the JVM
if (!System.getProperty("os.arch").toLowerCase(java.util.Locale.ROOT).contains("aarch64")) {
val outputValue = Runtime.getRuntime()
.exec("sysctl -in sysctl.proc_translated")
.scanOutputLine()
?.toIntOrNull()
if (1 == outputValue) {
return true
}
}
}
return false
}
fun Process.scanOutputLine(): String? {
var outputString = ""
inputStream.use { inputStream ->
java.util.Scanner(inputStream).useDelimiter("\\A").use { scanner ->
while (scanner.hasNext()) {
outputString = scanner.next()
}
}
}
waitFor(maxTimeoutMillis, TimeUnit.MILLISECONDS)
return outputString.trim()
}

105
build.gradle.kts Normal file
View File

@@ -0,0 +1,105 @@
buildscript {
repositories {
google()
gradlePluginPortal()
}
dependencies {
classpath(kotlin("gradle-plugin", version = libs.versions.kotlin.get()))
classpath(libs.gradle.plugin.rust)
classpath(libs.gradle.plugin.navigation)
}
}
plugins {
id("com.github.ben-manes.versions")
id("com.osacky.fulladle")
id("io.gitlab.arturbosch.detekt")
id("zcash-sdk.ktlint-conventions")
id("zcash-sdk.rosetta-conventions")
}
tasks {
register("detektAll", io.gitlab.arturbosch.detekt.Detekt::class) {
parallel = true
setSource(files(projectDir))
include("**/*.kt")
include("**/*.kts")
exclude("**/resources/**")
exclude("**/build/**")
exclude("**/commonTest/**")
exclude("**/jvmTest/**")
exclude("**/androidTest/**")
config.setFrom(files("${rootProject.projectDir}/tools/detekt.yml"))
baseline.set(file("$rootDir/tools/detekt-baseline.xml"))
buildUponDefaultConfig = true
}
withType<com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask> {
gradleReleaseChannel = "current"
resolutionStrategy {
componentSelection {
all {
if (isNonStable(candidate.version) && !isNonStable(currentVersion)) {
reject("Unstable")
}
}
}
}
}
}
val unstableKeywords = listOf("alpha", "beta", "rc", "m", "ea", "build")
fun isNonStable(version: String): Boolean {
val versionLowerCase = version.toLowerCase()
return unstableKeywords.any { versionLowerCase.contains(it) }
}
fladle {
// Firebase Test Lab has min and max values that might differ from our project's
// These are determined by `gcloud firebase test android models list`
@Suppress("MagicNumber", "PropertyName", "VariableNaming")
val FIREBASE_TEST_LAB_MIN_API = 23
@Suppress("MagicNumber", "PropertyName", "VariableNaming")
val FIREBASE_TEST_LAB_MAX_API = 30
val minSdkVersion = run {
val buildMinSdk = project.properties["ANDROID_MIN_SDK_VERSION"].toString().toInt()
buildMinSdk.coerceAtLeast(FIREBASE_TEST_LAB_MIN_API).toString()
}
val targetSdkVersion = run {
val buildTargetSdk = project.properties["ANDROID_TARGET_SDK_VERSION"].toString().toInt()
buildTargetSdk.coerceAtMost(FIREBASE_TEST_LAB_MAX_API).toString()
}
val firebaseTestLabKeyPath = project.properties["ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH"].toString()
val firebaseProject = project.properties["ZCASH_FIREBASE_TEST_LAB_PROJECT"].toString()
if (firebaseTestLabKeyPath.isNotEmpty()) {
serviceAccountCredentials.set(File(firebaseTestLabKeyPath))
} else if (firebaseProject.isNotEmpty()) {
projectId.set(firebaseProject)
}
devices.addAll(
mapOf("model" to "Pixel2", "version" to minSdkVersion),
mapOf("model" to "Pixel2", "version" to targetSdkVersion)
)
@Suppress("MagicNumber")
flakyTestAttempts.set(2)
flankVersion.set(libs.versions.flank.get())
filesToDownload.set(listOf(
"*/matrix_*/*test_results_merged\\.xml",
"*/matrix_*/*/artifacts/sdcard/googletest/test_outputfiles/*\\.png"
))
directoriesToPull.set(listOf(
"/sdcard/googletest/test_outputfiles"
))
}

View File

@@ -0,0 +1,27 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("zcash-sdk.android-conventions")
id("kotlin-kapt")
}
android {
defaultConfig {
//targetSdk = 30 //Integer.parseInt(project.property("targetSdkVersion"))
multiDexEnabled = true
}
}
dependencies {
implementation(projects.sdkLib)
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.multidex)
implementation(libs.bundles.grpc)
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.zcashwalletplgn)
androidTestImplementation(libs.bip39)
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cash.z.ecc.android.sdk.darkside">
<!-- For code coverage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:name="androidx.multidex.MultiDexApplication" />
</manifest>

View File

@@ -0,0 +1,88 @@
package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.internal.twigTask
// import cash.z.ecc.android.sdk.darkside.test.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()
// private val secondAddress = "zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx"
// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord.enterTheDarkside()
// sithLord.chainMaker.makeSimpleChain()
// sithLord.startSync(classScope).await()
// }
// }
// }

View File

@@ -0,0 +1,75 @@
package cash.z.ecc.android.sdk.darkside
// import cash.z.ecc.android.sdk.SdkSynchronizer
// import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.internal.twig
// import cash.z.ecc.android.sdk.darkside.test.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()
// private val secondAddress = "zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx"
// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord.enterTheDarkside()
// sithLord.chainMaker.simpleChain()
// sithLord.startSync(classScope).await()
// }
// }
// }

View File

@@ -0,0 +1,196 @@
package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.internal.twig
// import cash.z.ecc.android.sdk.internal.twigTask
// import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
// import cash.z.ecc.android.sdk.darkside.test.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(, "MultiRecipientInRust")
//
// private val randomPhrases = listOf(
// "profit save black expose rude feature early rocket alter borrow finish october few duty flush kick spell bean burden enforce bitter theme silent uphold",
// "unit ice dial annual duty feature smoke expose hard joy globe just accuse inner fog cash neutral forum strategy crash subject hurdle lecture sand",
// "average talent frozen work brand output major soldier witness keen brown bind indicate burden furnace long crime joke inhale chronic ordinary renew boat flame",
// "echo viable panic unaware stay magnet cake museum yellow abandon mountain height lunch advance tongue market bamboo cushion okay morning minute icon obtain december",
// "renew enlist travel stand trust execute decade surge follow push student school focus woman ripple movie that bitter plug same index wife spread differ"
// )
//
// private val viewingKeys = listOf(
// "zxviews1qws7ryw7qqqqpqq77dmhl9tufzdsgy8hcjq8kxjtgkfwwgqn4a26ahmhmjqueptd2pmq3f73pm8uaa25aze5032qw4dppkx4l625xcjcm94d5e65fcq4j2uptnjuqpyu2rvud88dtjwseglgzfe5l4te2xw62yq4tv62d2f6kl4706c6dmfxg2cmsdlzlt9ykpvacaterq4alljr3efke7k46xcrg4pxc02ezj0txwqjjve23nqqp7t5n5qat4d8569krxgkcd852uqg2t2vn",
// "zxviews1qdtp7dwfqqqqpqq3zxegnzc6qtacjp4m6qhyz7typdw9h9smra3rn322dkhyfg8kktk66k7zaj9tt5j6e58enx89pwry4rxwmcuzqyxlsap965r5gxpt604chmjyuhder6xwu3tx0h608as5sgxapqdqa6v6hy6qzh9fft0ns3cj9f8zrhu0ukzf9gn2arr02kzdct0jh5ee3zjch3xscjv34pzkgpueuq0pyl706alssuchqu4jmjm22fcq3htlwxt3f3hdytne7mgscrz5m",
// "zxviews1qvfmgpzjqqqqpqqnpl2s9n774mrv72zsuw73km9x6ax2s26d0d0ua20nuxvkexa4lq5fsc6psl8csspyqrlwfeuele5crlwpyjufgkzyy6ffw8hc52hn04jzru6mntms8c2cm255gu200zx4pmz06k3s90jatwehazl465tf6uyj6whwarpcca9exzr7wzltelq5tusn3x3jchjyk6cj09xyctjzykp902w4x23zdsf46d3fn9rtkgm0rmek296c5nhuzf99a2x6umqr804k9",
// "zxviews1qv85jn3hqqqqpq9jam3g232ylvvhy8e5vdhp0x9zjppr49sw6awwrm3a3d8l9j9es2ed9h29r6ta5tzt53j2y0ex84lzns0thp7n9wzutjapq29chfewqz34q5g6545f8jf0e69jcg9eyv66s8pt3y5dwxg9nrezz8q9j9fwxryeleayay6m09zpt0dem8hkazlw5jk6gedrakp9z7wzq2ptf6aqkft6z02mtrnq4a5pguwp4m8xkh52wz0r3naeycnqllnvsn8ag5q73pqgd",
// "zxviews1qwhel8pxqqqqpqxjl3cqu2z8hu0tqdd5qchkrdtsjuce9egdqlpu7eff2rn3gknm0msw7ug6qp4ynppscvv6hfm2nkf42lhz8la5et3zsej84xafcn0xdd9ms452hfjp4tljshtffscsl68wgdv3j5nnelxsdcle5rnwkuz6lvvpqs7s2x0cnhemhnwzhx5ccakfgxfym0w8dxglq4h6pwukf2az6lcm38346qc5s9rgx6s988fr0kxnqg0c6g6zlxa2wpc7jh0gz7q4ysx0l"
// )
// private val spendingKeys = listOf(
// "secret-extended-key-main1qws7ryw7qqqqpqq77dmhl9tufzdsgy8hcjq8kxjtgkfwwgqn4a26ahmhmjqueptd2pt49qhm63lt8v93tlqzw7psmkvqqfm6xdnc2qwkflfcenqs7s4sj2yn0c75n982wjrf5k5h37vt3wxwr3pqnjk426lltctrms2uqmqgkl4706c6dmfxg2cmsdlzlt9ykpvacaterq4alljr3efke7k46xcrg4pxc02ezj0txwqjjve23nqqp7t5n5qat4d8569krxgkcd852uqxj5ljt",
// "secret-extended-key-main1qdtp7dwfqqqqpqq3zxegnzc6qtacjp4m6qhyz7typdw9h9smra3rn322dkhyfg8kk26p0fcjuklryw0ed6falf6c7dwqehleca0xf6m6tlnv5zdjx7lqs4xmseqjz0fvk273aczatxxjaqmy3kv8wtzcc6pf6qtrjy5g2mqgs3cj9f8zrhu0ukzf9gn2arr02kzdct0jh5ee3zjch3xscjv34pzkgpueuq0pyl706alssuchqu4jmjm22fcq3htlwxt3f3hdytne7mgacmaq6",
// "secret-extended-key-main1qvfmgpzjqqqqpqqnpl2s9n774mrv72zsuw73km9x6ax2s26d0d0ua20nuxvkexa4lzc4n8a3zfvyn2qns37fx00avdtjewghmxz5nc2ey738nrpu4pqqnwysmcls5yek94lf03d5jtsa25nmuln4xjvu6e4g0yrr6xesp9cr6uyj6whwarpcca9exzr7wzltelq5tusn3x3jchjyk6cj09xyctjzykp902w4x23zdsf46d3fn9rtkgm0rmek296c5nhuzf99a2x6umqvf4man",
// "secret-extended-key-main1qv85jn3hqqqqpq9jam3g232ylvvhy8e5vdhp0x9zjppr49sw6awwrm3a3d8l9j9estq9a548lguf0n9fsjs7c96uaymhysuzeek5eg8un0fk8umxszxstm0xfq77x68yjk4t4j7h2xqqjf8nmkx0va3cphnhxpvd0l5dhzgyxryeleayay6m09zpt0dem8hkazlw5jk6gedrakp9z7wzq2ptf6aqkft6z02mtrnq4a5pguwp4m8xkh52wz0r3naeycnqllnvsn8ag5qru36vk",
// "secret-extended-key-main1qwhel8pxqqqqpqxjl3cqu2z8hu0tqdd5qchkrdtsjuce9egdqlpu7eff2rn3gknm0mdwr9358t3dlcf47vakdwewxy64k7ds7y3k455rfch7s2x8mfesjsxptyfvc9heme3zj08wwdk4l9mwce92lvrl797wmmddt65ygwcqlvvpqs7s2x0cnhemhnwzhx5ccakfgxfym0w8dxglq4h6pwukf2az6lcm38346qc5s9rgx6s988fr0kxnqg0c6g6zlxa2wpc7jh0gz7qx7zl33"
// )
// private val addresses = listOf(
// "zs1d8lenyz7uznnna6ttmj6rk9l266989f78c3d79f0r6r28hn0gc9fzdktrdnngpcj8wr2cd4zcq2",
// "zs13x79khp5z0ydgnfue8p88fjnrjxtnz0gwxyef525gd77p72nqh7zr447n6klgr5yexzp64nc7hf",
// "zs1jgvqpsyzs90hlqz85qry3zv52keejgx0f4pnljes8h4zs96zcxldu9llc03dvhkp6ds67l4s0d5",
// "zs1lr428hhedq3yk8n2wr378e6ua3u3r4ma5a8dqmf3r64y96vww5vh6327jfudtyt7v3eqw22c2t6",
// "zs1hy7mdwl6y0hwxts6a5lca2xzlr0p8v5tkvvz7jfa4d04lx5uedg6ya8fmthywujacx0acvfn837"
// )
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord.enterTheDarkside()
// sithLord.chainMaker.makeSimpleChain()
// sithLord.startSync(classScope).await()
// }
// }
// }

View File

@@ -0,0 +1,96 @@
package cash.z.ecc.android.sdk.darkside // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import org.junit.Before
// import org.junit.BeforeClass
// import org.junit.Test
//
// class OutboundTransactionsTest : ScopedTest() {
//
// @Before
// fun beforeEachTest() {
// testCoordinator.clearUnminedTransactions()
// }
//
// @Test
// fun testSendIncrementsTransaction() {
// validator.validateTransactionCount(initialTxCount)
// testCoordinator.sendTransaction(txAmount).awaitSync()
// validator.validatTransactionCount(initialTxCount + 1)
// }
//
// @Test
// fun testSendReducesBalance() {
// validator.validateBalance(initialBalance)
// testCoordinator.sendTransaction(txAmount).awaitSync()
// validator.validateBalanceLessThan(initialBalance)
// }
//
// @Test
// fun testTransactionPending() {
// testCoordinator.sendTransaction(txAmount).awaitSync()
// validator.validateTransactionPending(testCoordinator.lastTransactionId)
// }
//
// @Test
// fun testTransactionConfirmations_1() {
// testCoordinator.sendTransaction(txAmount).generateNextBlock().awaitSync()
// validator.validateConfirmations(testCoordinator.lastTransactionId, 1)
// validator.validateBalanceLessThan(initialBalance - txAmount)
// }
//
// @Test
// fun testTransactionConfirmations_9() {
// testCoordinator.sendTransaction(txAmount).generateNextBlock().advanceBlocksBy(8).awaitSync()
// validator.validateConfirmations(testCoordinator.lastTransactionId, 9)
// validator.validateBalanceLessThan(initialBalance - txAmount)
// }
//
// @Test
// fun testTransactionConfirmations_10() {
// testCoordinator.sendTransaction(txAmount).generateNextBlock().advanceBlocksBy(9).awaitSync()
// validator.validateConfirmations(testCoordinator.lastTransactionId, 10)
// validator.validateBalance(initialBalance - txAmount)
// }
//
// @Test
// fun testTransactionExpiration() {
// validator.validateBalance(initialBalance)
//
// // pending initially
// testCoordinator.sendTransaction(txAmount).awaitSync()
// val id = testCoordinator.lastTransactionId
// validator.validateTransactionPending(id)
//
// // still pending after 9 blocks
// testCoordinator.advanceBlocksBy(9).awaitSync()
// validator.validateTransactionPending(id)
// validator.validateBalanceLessThan(initialBalance)
//
// // expired after 10 blocks
// testCoordinator.advanceBlocksBy(1).awaitSync()
// validator.validateTransactionExpired(id)
//
// validator.validateBalance(initialBalance)
// }
//
//
//
// companion object {
// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
// private const val initialBalance = 1.234
// private const val txAmount = 1.1
// private const val initialTxCount = 3
// private val testCoordinator = DarksideTestCoordinator()
// private val validator = testCoordinator.validator
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// testCoordinator
// .enterTheDarkside()
// .resetBlocks(blocksUrl)
// }
// }
// }

View File

@@ -0,0 +1,25 @@
package cash.z.ecc.android.sdk.darkside
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTest
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
/**
* Integration test to run in order to catch any regressions in transparent behavior.
*/
@RunWith(AndroidJUnit4::class)
class TransparentIntegrationTest : DarksideTest() {
@Before
fun setup() = runOnce {
sithLord.await()
}
@Test
@Ignore("This test is broken")
fun sanityTest() {
validator.validateTxCount(5)
}
}

View File

@@ -0,0 +1,102 @@
package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.internal.twig
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class InboundTxTests : ScopedTest() {
@Test
fun testTargetBlock_downloaded() {
validator.validateMinHeightDownloaded(firstBlock)
}
@Test
fun testTargetBlock_scanned() {
validator.validateMinHeightScanned(targetTxBlock - 1)
}
@Test
fun testLatestHeight() {
validator.validateLatestHeight(targetTxBlock - 1)
}
@Test
fun testTxCountInitial() {
validator.validateTxCount(0)
}
@Test
fun testTxCountAfter() {
twig("ADDING TRANSACTIONS!!!")
// add 2 transactions to block 663188 and 'mine' that block
addTransactions(targetTxBlock, tx663174, tx663188)
sithLord.await(timeout = 30_000L, targetHeight = targetTxBlock)
validator.validateTxCount(2)
}
private fun addTransactions(targetHeight: Int, vararg txs: String) {
val overwriteBlockCount = 5
chainMaker
// .stageEmptyBlocks(targetHeight, overwriteBlockCount)
.stageTransactions(targetHeight, *txs)
.applyTipHeight(targetHeight)
}
companion object {
private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val tx663174 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt"
private const val tx663188 = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt"
private const val txIndexReorg = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-index-reorg/t1.txt"
private val txSend = arrayOf(
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/c9e35e6ff444b071d63bf9bab6480409d6361760445c8a28d24179adb35c2495.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/72a29d7db511025da969418880b749f7fc0fc910cdb06f52193b5fa5c0401d9d.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/34e507cab780546f980176f3ff2695cd404917508c7e5ee18cc1d2ff3858cb08.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/6edf869063eccff3345676b0fed9f1aa6988fb2524e3d9ca7420a13cfadcd76c.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/de97394ae220c28a33ba78b944e82dabec8cb404a4407650b134b3d5950358c0.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/4eaa902279f8380914baf5bcc470d8b7c11d84fda809f67f517a7cb48912b87b.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/73c5edf8ffba774d99155121ccf07e67fbcf14284458f7e732751fea60d3bcbc.txt"
)
private val txRecv = arrayOf(
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/8f064d23c66dc36e32445e5f3b50e0f32ac3ddb78cff21fb521eb6c19c07c99a.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/d2e7be14bbb308f9d4d68de424d622cbf774226d01cd63cc6f155fafd5cd212c.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e6566be3a4f9a80035dab8e1d97e40832a639e3ea938fb7972ea2f8482ff51ce.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/e9527891b5d43d1ac72f2c0a3ac18a33dc5a0529aec04fa600616ed35f8123f8.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/75f2cdd2ff6a94535326abb5d9e663d53cbfa5f31ebb24b4d7e420e9440d41a2.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/7690c8ec740c1be3c50e2aedae8bf907ac81141ae8b6a134c1811706c73f49a6.txt",
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt"
)
private const val firstBlock = 663150
private const val targetTxBlock = 663188
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
private val chainMaker = sithLord.chainMaker
@BeforeClass
@JvmStatic
fun startAllTests() {
sithLord.enterTheDarkside()
chainMaker
.resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = targetTxBlock)
.stageEmptyBlocks(firstBlock + 1, 100)
.applyTipHeight(targetTxBlock - 1)
sithLord.synchronizer.start(classScope)
sithLord.await()
}
}
}

View File

@@ -0,0 +1,53 @@
package cash.z.ecc.android.sdk.darkside.reorgs // package cash.z.ecc.android.sdk.integration
//
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
// import org.junit.Assert.assertFalse
// import org.junit.Assert.assertTrue
// import org.junit.BeforeClass
// import org.junit.Test
//
// class ReorgBasicTest : ScopedTest() {
//
// private var callbackTriggered = false
//
// @Test
// fun testReorgChangesBlockHash() {
// testCoordinator.resetBlocks(blocksUrl)
// validator.validateBlockHash(targetHeight, targetHash)
// testCoordinator.updateBlocks(reorgUrl)
// validator.validateBlockHash(targetHeight, reorgHash)
// }
//
// @Test
// fun testReorgTriggersCallback() {
// callbackTriggered = false
// testCoordinator.resetBlocks(blocksUrl)
// testCoordinator.synchronizer.registerReorgListener(reorgCallback)
// assertFalse(callbackTriggered)
//
// testCoordinator.updateBlocks(reorgUrl).awaitSync()
// assertTrue(callbackTriggered)
// testCoordinator.synchronizer.unregisterReorgListener()
// }
//
// fun reorgCallback() {
// callbackTriggered = true
// }
//
// companion object {
// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
// private const val reorgUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"
// private const val targetHeight = 663250
// private const val targetHash = "tbd"
// private const val reorgHash = "tbd"
// private val testCoordinator = DarksideTestCoordinator()
// private val validator = testCoordinator.validator
//
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// testCoordinator.enterTheDarkside()
// }
// }
// }

View File

@@ -0,0 +1,239 @@
package cash.z.ecc.android.sdk.darkside.reorgs // package cash.z.ecc.android.sdk.integration
//
// import androidx.test.platform.app.InstrumentationRegistry
// import cash.z.ecc.android.sdk.Initializer
// import cash.z.ecc.android.sdk.SdkSynchronizer
// import cash.z.ecc.android.sdk.Synchronizer
// import cash.z.ecc.android.sdk.test.ScopedTest
// import cash.z.ecc.android.sdk.ext.import
// import cash.z.ecc.android.sdk.internal.twig
// import cash.z.ecc.android.sdk.darkside.test.DarksideApi
// import io.grpc.StatusRuntimeException
// import kotlinx.coroutines.delay
// import kotlinx.coroutines.flow.filter
// import kotlinx.coroutines.flow.first
// import kotlinx.coroutines.flow.onEach
// import kotlinx.coroutines.runBlocking
// import org.junit.Assert.*
// import org.junit.Before
// import org.junit.BeforeClass
// import org.junit.Test
//
// class ReorgHandlingTest : ScopedTest() {
//
// @Before
// fun setup() {
// timeout(30_000L) {
// synchronizer.awaitSync()
// }
// }
//
// @Test
// fun testBeforeReorg_minHeight() = timeout(30_000L) {
// // validate that we are synced, at least to the birthday height
// synchronizer.validateMinSyncHeight(birthdayHeight)
// }
//
// @Test
// fun testBeforeReorg_maxHeight() = timeout(30_000L) {
// // validate that we are not synced beyond the target height
// synchronizer.validateMaxSyncHeight(targetHeight)
// }
//
// @Test
// fun testBeforeReorg_latestBlockHash() = timeout(30_000L) {
// val latestBlock = getBlock(targetHeight)
// assertEquals("foo", latestBlock.header.toStringUtf8())
// }
//
// @Test
// fun testAfterSmallReorg_callbackTriggered() = timeout(30_000L) {
// hadReorg = false
// triggerSmallReorg()
// assertTrue(hadReorg)
// }
//
// @Test
// fun testAfterSmallReorg_callbackTriggered() = timeout(30_000L) {
// hadReorg = false
// triggerSmallReorg()
// assertTrue(hadReorg)
// }
// // @Test
// // fun testSync_100Blocks()= timeout(10_000L) {
// // // validate that we are synced below the target height, at first
// // synchronizer.validateMaxSyncHeight(targetHeight - 1)
// // // then trigger and await more blocks
// // synchronizer.awaitHeight(targetHeight)
// // // validate that we are above the target height afterward
// // synchronizer.validateMinSyncHeight(targetHeight)
// // }
//
// private fun Synchronizer.awaitSync() = runBlocking<Unit> {
// twig("*** Waiting for sync ***")
// status.onEach {
// twig("got processor status $it")
// assertTrue("Error: Cannot complete test because the server is disconnected.", it != Synchronizer.Status.DISCONNECTED)
// delay(1000)
// }.filter { it == Synchronizer.Status.SYNCED }.first()
// twig("*** Done waiting for sync! ***")
// }
//
// private fun Synchronizer.awaitHeight(height: Int) = runBlocking<Unit> {
// twig("*** Waiting for block $height ***")
// // processorInfo.first { it.lastScannedHeight >= height }
// processorInfo.onEach {
// twig("got processor info $it")
// delay(1000)
// }.first { it.lastScannedHeight >= height }
// twig("*** Done waiting for block $height! ***")
// }
//
// private fun Synchronizer.validateMinSyncHeight(minHeight: Int) = runBlocking<Unit> {
// val info = processorInfo.first()
// val lastDownloadedHeight = info.lastDownloadedHeight
// assertTrue("Expected to be synced beyond $minHeight but the last downloaded block was" +
// " $lastDownloadedHeight details: $info", lastDownloadedHeight >= minHeight)
// }
//
// private fun Synchronizer.validateMaxSyncHeight(maxHeight: Int) = runBlocking<Unit> {
// val lastDownloadedHeight = processorInfo.first().lastScannedHeight
// assertTrue("Did not expect to be synced beyond $maxHeight but we are synced to" +
// " $lastDownloadedHeight", lastDownloadedHeight <= maxHeight)
// }
//
// private fun getBlock(height: Int) =
// lightwalletd.getBlockRange(height..height).first()
//
// private val lightwalletd
// get() = (synchronizer as SdkSynchronizer).processor.downloader.lightwalletService
//
// companion object {
// private const val port = 9067
// private const val birthdayHeight = 663150
// private const val targetHeight = 663200
// private const val seedPhrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
// private val context = InstrumentationRegistry.getInstrumentation().context
// private val initializer = Initializer(context, host, port, "ReorgHandlingTests")
// private lateinit var synchronizer: Synchronizer
// private lateinit var sithLord: DarksideApi
//
// @BeforeClass
// @JvmStatic
// fun startOnce() {
//
// sithLord = DarksideApi(context, host, port)
// enterTheDarkside()
//
// // don't start until after we enter the darkside (otherwise the we find no blocks to begin with and sleep for an interval)
// synchronizer.start(classScope)
// }
//
// private fun enterTheDarkside() = runBlocking<Unit> {
// // verify that we are on the darkside
// try {
// twig("entering the darkside")
// var info = synchronizer.getServerInfo()
// assertTrue(
// "Error: not on the darkside",
// info.chainName.contains("darkside")
// or info.vendor.toLowerCase().contains("darkside", true)
// )
// twig("initiating the darkside")
// sithLord.initiate(birthdayHeight + 10)
// info = synchronizer.getServerInfo()
// assertTrue(
// "Error: server not configured for the darkside. Expected initial height of" +
// " $birthdayHeight but found ${info.blockHeight}", birthdayHeight <= info.blockHeight)
// twig("darkside initiation complete!")
// } catch (error: StatusRuntimeException) {
// fail("Error while fetching server status. Testing cannot begin due to:" +
// " ${error.message}. Verify that the server is running")
// }
// }
// }
// /*
//
// beginning to process new blocks (with lower bound: 663050)...
// downloading blocks in range 663202..663202
// found 1 missing blocks, downloading in 1 batches of 100...
// downloaded 663202..663202 (batch 1 of 1) [663202..663202] | 10ms
// validating blocks in range 663202..663202 in db: /data/user/0/cash.z.ecc.android.sdk.test/databases/ReorgTest22_Cache.db
// offset = min(100, 10 * (1)) = 10
// lowerBound = max(663201 - 10, 663050) = 663191
// handling chain error at 663201 by rewinding to block 663191
// chain error detected at height: 663201. Rewinding to: 663191
// beginning to process new blocks (with lower bound: 663050)...
// downloading blocks in range 663192..663202
// found 11 missing blocks, downloading in 1 batches of 100...
// downloaded 663192..663202 (batch 1 of 1) [663192..663202] | 8ms
// validating blocks in range 663192..663202 in db: /data/user/0/cash.z.ecc.android.sdk.test/databases/ReorgTest22_Cache.db
// offset = min(100, 10 * (2)) = 20
// lowerBound = max(663191 - 20, 663050) = 663171
// handling chain error at 663191 by rewinding to block 663171
// chain error detected at height: 663191. Rewinding to: 663171
// beginning to process new blocks (with lower bound: 663050)...
// downloading blocks in range 663172..663202
// found 31 missing blocks, downloading in 1 batches of 100...
// downloaded 663172..663202 (batch 1 of 1) [663172..663202] | 15ms
// validating blocks in range 663172..663202 in db: /data/user/0/cash.z.ecc.android.sdk.test/databases/ReorgTest22_Cache.db
// scanning blocks for range 663172..663202 in batches
// batch scanned: 663202/663202
// batch scan complete!
// Successfully processed new blocks. Sleeping for 20000ms
//
// */
// //
// // @Test
// // fun testHeightChange() {
// // setTargetHeight(targetHeight)
// // synchronizer.validateSyncedTo(targetHeight)
// // }
// //
// // @Test
// // fun testSmallReorgSync() {
// // verifyReorgSync(smallReorgSize)
// // }
// //
// // @Test
// // fun testSmallReorgCallback() {
// // verifyReorgCallback(smallReorgSize)
// // }
// //
// // @Test
// // fun testLargeReorgSync() {
// // verifyReorgSync(largeReorgSize)
// // }
// //
// // @Test
// // fun testLargeReorgCallback() {
// // verifyReorgCallback(largeReorgSize)
// // }
// //
// //
// // //
// // // Helper Functions
// // //
// //
// // fun verifyReorgSync(reorgSize: Int) {
// // setTargetHeight(targetHeight)
// // synchronizer.validateSyncedTo(targetHeight)
// // getHash(targetHeight).let { initialHash ->
// // setReorgHeight(targetHeight - reorgSize)
// // synchronizer.validateSyncedTo(targetHeight)
// // assertNotEquals("Hash should change after a reorg", initialHash, getHash(targetHeight))
// // }
// // }
// //
// // fun verifyReorgCallback(reorgSize: Int) {
// // setTargetHeight(targetHeight)
// // synchronizer.validateSyncedTo(targetHeight)
// // getHash(targetHeight).let { initialHash ->
// // setReorgHeight(targetHeight - 10)
// // synchronizer.validateReorgCallback()
// // }
// // }
//
//
// }
//

View File

@@ -0,0 +1,46 @@
package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReorgSetupTest : ScopedTest() {
private val birthdayHeight = 663150
private val targetHeight = 663250
@Before
fun setup() {
sithLord.await()
}
@Test
fun testBeforeReorg_minHeight() = timeout(30_000L) {
// validate that we are synced, at least to the birthday height
validator.validateMinHeightDownloaded(birthdayHeight)
}
@Test
fun testBeforeReorg_maxHeight() = timeout(30_000L) {
// validate that we are not synced beyond the target height
validator.validateMaxHeightScanned(targetHeight)
}
companion object {
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
@BeforeClass
@JvmStatic
fun startOnce() {
sithLord.enterTheDarkside()
sithLord.synchronizer.start(classScope)
}
}
}

View File

@@ -0,0 +1,61 @@
package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.internal.twig
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReorgSmallTest : ScopedTest() {
private val targetHeight = 663250
private val hashBeforeReorg = "09ec0d5de30d290bc5a2318fbf6a2427a81c7db4790ce0e341a96aeac77108b9"
private val hashAfterReorg = "tbd"
@Before
fun setup() {
sithLord.await()
}
@Test
fun testBeforeReorg_latestBlockHash() = timeout(30_000L) {
validator.validateBlockHash(targetHeight, hashBeforeReorg)
}
@Test
fun testAfterReorg_callbackTriggered() = timeout(30_000L) {
hadReorg = false
// sithLord.triggerSmallReorg()
sithLord.await()
twig("checking whether a reorg happened (spoiler: ${if (hadReorg) "yep" else "nope"})")
assertTrue(hadReorg)
}
@Test
fun testAfterReorg_latestBlockHash() = timeout(30_000L) {
validator.validateBlockHash(targetHeight, hashAfterReorg)
}
companion object {
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
private var hadReorg = false
@BeforeClass
@JvmStatic
fun startOnce() {
sithLord.enterTheDarkside()
validator.onReorg { _, _ ->
hadReorg = true
}
sithLord.synchronizer.start(classScope)
}
}
}

View File

@@ -0,0 +1,75 @@
package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
import cash.z.ecc.android.sdk.darkside.test.SimpleMnemonics
import cash.z.ecc.android.sdk.ext.toHex
import org.junit.Assert.assertEquals
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SetupTest : ScopedTest() {
// @Test
// fun testFirstBlockExists() {
// validator.validateHasBlock(
// firstBlock
// )
// }
//
// @Test
// fun testLastBlockExists() {
// validator.validateHasBlock(
// lastBlock
// )
// }
//
// @Test
// fun testLastBlockHash() {
// validator.validateBlockHash(
// lastBlock,
// lastBlockHash
// )
// }
@Test
@Ignore("This test is broken")
fun tempTest() {
val phrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
val result = SimpleMnemonics().toSeed(phrase.toCharArray()).toHex()
assertEquals("abc", result)
}
@Test
@Ignore("This test is broken")
fun tempTest2() {
val s = SimpleMnemonics()
val ent = s.nextEntropy()
val phrase = s.nextMnemonic(ent)
assertEquals("a", "${ent.toHex()}|${String(phrase)}")
}
companion object {
private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val firstBlock = 663150
private const val lastBlock = 663200
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator
// @BeforeClass
// @JvmStatic
// fun startAllTests() {
// sithLord
// .enterTheDarkside()
// // TODO: fix this
// // .resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = lastBlock)
// .startSync(classScope)
// .await()
// }
}
}

View File

@@ -0,0 +1,35 @@
package cash.z.ecc.android.sdk.darkside.reproduce
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTest
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReproduceZ2TFailureTest : DarksideTest() {
@Before
fun setup() {
println("dBUG RUNNING")
}
@Test
@Ignore("This test is broken")
fun once() {
}
@Test
@Ignore("This test is broken")
fun twice() {
}
companion object {
@JvmStatic
@BeforeClass
fun beforeAll() {
println("dBUG BEFOERE IOT ALL")
}
}
}

View File

@@ -0,0 +1,177 @@
package cash.z.ecc.android.sdk.darkside.test
import android.content.Context
import cash.z.ecc.android.sdk.R
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.wallet.sdk.rpc.Darkside
import cash.z.wallet.sdk.rpc.Darkside.DarksideTransactionsURL
import cash.z.wallet.sdk.rpc.DarksideStreamerGrpc
import cash.z.wallet.sdk.rpc.Service
import io.grpc.ManagedChannel
import io.grpc.stub.StreamObserver
import java.lang.RuntimeException
import java.util.concurrent.TimeUnit
import kotlin.random.Random
class DarksideApi(
private val channel: ManagedChannel,
private val singleRequestTimeoutSec: Long = 10L
) {
constructor(
appContext: Context,
host: String,
port: Int = ZcashNetwork.Mainnet.defaultPort,
usePlainText: Boolean = appContext.resources.getBoolean(
R.bool.lightwalletd_allow_very_insecure_connections
)
) : this(
LightWalletGrpcService.createDefaultChannel(
appContext,
host,
port,
usePlainText
)
)
//
// Service APIs
//
fun reset(
saplingActivationHeight: Int = 419200,
branchId: String = "e9ff75a6", // Canopy,
chainName: String = "darkside${ZcashNetwork.Mainnet.networkName}"
) = apply {
twig("resetting darksidewalletd with saplingActivation=$saplingActivationHeight branchId=$branchId chainName=$chainName")
Darkside.DarksideMetaState.newBuilder()
.setBranchID(branchId)
.setChainName(chainName)
.setSaplingActivation(saplingActivationHeight)
.build().let { request ->
createStub().reset(request)
}
}
fun stageBlocks(url: String) = apply {
twig("staging blocks url=$url")
createStub().stageBlocks(url.toUrl())
}
fun stageTransactions(url: String, targetHeight: Int) = apply {
twig("staging transaction at height=$targetHeight from url=$url")
createStub().stageTransactions(
DarksideTransactionsURL.newBuilder().setHeight(targetHeight).setUrl(url).build()
)
}
fun stageEmptyBlocks(startHeight: Int, count: Int = 10, nonce: Int = Random.nextInt()) = apply {
twig("staging $count empty blocks starting at $startHeight with nonce $nonce")
createStub().stageBlocksCreate(
Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight).setCount(count).setNonce(nonce).build()
)
}
fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: Int) {
if (txs == null) {
twig("no transactions to stage")
return
}
twig("staging transaction at height=$tipHeight")
val response = EmptyResponse()
createStreamingStub().stageTransactionsStream(response).apply {
txs.forEach {
twig("stageTransactions: onNext calling!!!")
onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.toLong()).build()) // apply the tipHeight because the passed in txs might not know their destination height (if they were created via SendTransaction)
twig("stageTransactions: onNext called")
}
twig("stageTransactions: onCompleted calling!!!")
onCompleted()
twig("stageTransactions: onCompleted called")
}
response.await()
}
fun applyBlocks(tipHeight: Int) {
twig("applying blocks up to tipHeight=$tipHeight")
createStub().applyStaged(tipHeight.toHeight())
}
fun getSentTransactions(): MutableIterator<Service.RawTransaction>? {
twig("grabbing sent transactions...")
return createStub().getIncomingTransactions(Service.Empty.newBuilder().build())
}
// fun setMetaState(
// branchId: String = "2bb40e60", // Blossom,
// chainName: String = "darkside",
// saplingActivationHeight: Int = 419200
// ): DarksideApi = apply {
// createStub().setMetaState(
// Darkside.DarksideMetaState.newBuilder()
// .setBranchID(branchId)
// .setChainName(chainName)
// .setSaplingActivation(saplingActivationHeight)
// .build()
// )
// }
// fun setLatestHeight(latestHeight: Int) = setState(latestHeight, reorgHeight)
//
// fun setReorgHeight(reorgHeight: Int)
// = setState(latestHeight.coerceAtLeast(reorgHeight), reorgHeight)
//
// fun setState(latestHeight: Int = -1, reorgHeight: Int = latestHeight): DarksideApi {
// this.latestHeight = latestHeight
// this.reorgHeight = reorgHeight
// // TODO: change this service to accept ints as heights, like everywhere else
// createStub().darksideSetState(
// Darkside.DarksideState.newBuilder()
// .setLatestHeight(latestHeight.toLong())
// .setReorgHeight(reorgHeight.toLong())
// .build()
// )
// return this
// }
private fun createStub(): DarksideStreamerGrpc.DarksideStreamerBlockingStub =
DarksideStreamerGrpc
.newBlockingStub(channel)
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
private fun createStreamingStub(): DarksideStreamerGrpc.DarksideStreamerStub =
DarksideStreamerGrpc
.newStub(channel)
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
private fun String.toUrl() = Darkside.DarksideBlocksURL.newBuilder().setUrl(this).build()
private fun Int.toHeight() = Darkside.DarksideHeight.newBuilder().setHeight(this).build()
class EmptyResponse : StreamObserver<Service.Empty> {
var completed = false
var error: Throwable? = null
override fun onNext(value: Service.Empty?) {
twig("<><><><><><><><> EMPTY RESPONSE: ONNEXT CALLED!!!!")
}
override fun onError(t: Throwable?) {
twig("<><><><><><><><> EMPTY RESPONSE: ONERROR CALLED!!!!")
error = t
completed = true
}
override fun onCompleted() {
twig("<><><><><><><><> EMPTY RESPONSE: ONCOMPLETED CALLED!!!")
completed = true
}
fun await() {
while (!completed) {
twig("awaiting server response...")
Thread.sleep(20L)
}
if (error != null) throw RuntimeException("Server responded with an error: $error caused by ${error?.cause}")
}
}
}

View File

@@ -0,0 +1,18 @@
package cash.z.ecc.android.sdk.darkside.test
open class DarksideTest(name: String = javaClass.simpleName) : ScopedTest() {
val sithLord = DarksideTestCoordinator()
val validator = sithLord.validator
fun runOnce(block: () -> Unit) {
if (!ranOnce) {
sithLord.enterTheDarkside()
sithLord.synchronizer.start(classScope)
block()
ranOnce = true
}
}
companion object {
private var ranOnce = false
}
}

View File

@@ -0,0 +1,311 @@
package cash.z.ecc.android.sdk.darkside.test
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork
import io.grpc.StatusRuntimeException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
class DarksideTestCoordinator(val wallet: TestWallet) {
constructor(
alias: String = "DarksideTestCoordinator",
seedPhrase: String = DEFAULT_SEED_PHRASE,
startHeight: Int = DEFAULT_START_HEIGHT,
host: String = COMPUTER_LOCALHOST,
network: ZcashNetwork = ZcashNetwork.Mainnet,
port: Int = network.defaultPort
) : this(TestWallet(seedPhrase, alias, network, host, startHeight = startHeight, port = port))
private val targetHeight = 663250
private val context = InstrumentationRegistry.getInstrumentation().context
// dependencies: private
private lateinit var darkside: DarksideApi
// dependencies: public
val validator = DarksideTestValidator()
val chainMaker = DarksideChainMaker()
// wallet delegates
val synchronizer get() = wallet.synchronizer
val send get() = wallet::send
//
// High-level APIs
//
/**
* Setup dependencies, including the synchronizer and the darkside API connection
*/
fun enterTheDarkside(): DarksideTestCoordinator = runBlocking {
// verify that we are on the darkside
try {
twig("entering the darkside")
initiate()
synchronizer.getServerInfo().apply {
assertTrue(
"Error: not on the darkside",
vendor.contains("dark", true)
or chainName.contains("dark", true)
)
}
twig("darkside initiation complete!")
} catch (error: StatusRuntimeException) {
Assert.fail(
"Error while fetching server status. Testing cannot begin due to:" +
" ${error.message} Caused by: ${error.cause} Verify that the server is running!"
)
}
this@DarksideTestCoordinator
}
/**
* Setup the synchronizer and darksidewalletd with their initial state
*/
fun initiate() {
twig("*************** INITIALIZING TEST COORDINATOR (ONLY ONCE) ***********************")
val channel = synchronizer.channel
darkside = DarksideApi(channel)
darkside.reset()
}
// fun triggerSmallReorg() {
// darkside.setBlocksUrl(smallReorg)
// }
//
// fun triggerLargeReorg() {
// darkside.setBlocksUrl(largeReorg)
// }
// redo this as a call to wallet but add delay time to wallet join() function
/**
* Waits for, at most, the given amount of time for the synchronizer to download and scan blocks
* and reach a 'SYNCED' status.
*/
fun await(timeout: Long = 60_000L, targetHeight: Int = -1) = runBlocking {
ScopedTest.timeoutWith(this, timeout) {
twig("*** Waiting up to ${timeout / 1_000}s for sync ***")
synchronizer.status.onEach {
twig("got processor status $it")
if (it == Synchronizer.Status.DISCONNECTED) {
twig("waiting a bit before giving up on connection...")
} else if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("awaiting new blocks from server...")
}
}.map {
// whenever we're waiting for a target height, for simplicity, if we're sleeping,
// and in between polls, then consider it that we're not synced
if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("switching status to DOWNLOADING because we're still waiting for height $targetHeight")
Synchronizer.Status.DOWNLOADING
} else {
it
}
}.filter { it == Synchronizer.Status.SYNCED }.first()
twig("*** Done waiting for sync! ***")
}
}
// /**
// * Send a transaction and wait until it has been fully created and successfully submitted, which
// * takes about 10 seconds.
// */
// suspend fun createAndSubmitTx(
// zatoshi: Long,
// toAddress: String,
// memo: String = "",
// fromAccountIndex: Int = 0
// ) = coroutineScope {
//
// wallet.send(toAddress, memo, zatoshi, fromAccountIndex)
// }
fun stall(delay: Long = 5000L) = runBlocking {
twig("*** Stalling for ${delay}ms ***")
delay(delay)
}
//
// Validation
//
inner class DarksideTestValidator {
fun validateHasBlock(height: Int) {
runBlocking {
assertTrue((synchronizer as SdkSynchronizer).findBlockHashAsHex(height) != null)
assertTrue((synchronizer as SdkSynchronizer).findBlockHash(height)?.size ?: 0 > 0)
}
}
fun validateLatestHeight(height: Int) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val networkBlockHeight = info.networkBlockHeight
assertTrue(
"Expected latestHeight of $height but the server last reported a height of" +
" $networkBlockHeight! Full details: $info",
networkBlockHeight == height
)
}
fun validateMinHeightDownloaded(minHeight: Int) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val lastDownloadedHeight = info.lastDownloadedHeight
assertTrue(
"Expected to have at least downloaded $minHeight but the last downloaded block was" +
" $lastDownloadedHeight! Full details: $info",
lastDownloadedHeight >= minHeight
)
}
fun validateMinHeightScanned(minHeight: Int) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first()
val lastScannedHeight = info.lastScannedHeight
assertTrue(
"Expected to have at least scanned $minHeight but the last scanned block was" +
" $lastScannedHeight! Full details: $info",
lastScannedHeight >= minHeight
)
}
fun validateMaxHeightScanned(maxHeight: Int) = runBlocking<Unit> {
val lastDownloadedHeight = synchronizer.processorInfo.first().lastScannedHeight
assertTrue(
"Did not expect to be synced beyond $maxHeight but we are synced to" +
" $lastDownloadedHeight",
lastDownloadedHeight <= maxHeight
)
}
fun validateBlockHash(height: Int, expectedHash: String) {
val hash = runBlocking { (synchronizer as SdkSynchronizer).findBlockHashAsHex(height) }
assertEquals(expectedHash, hash)
}
fun onReorg(callback: (errorHeight: Int, rewindHeight: Int) -> Unit) {
synchronizer.onChainErrorHandler = callback
}
fun validateTxCount(count: Int) {
val txCount = runBlocking { (synchronizer as SdkSynchronizer).getTransactionCount() }
assertEquals("Expected $count transactions but found $txCount instead!", count, txCount)
}
fun validateMinBalance(available: Long = -1, total: Long = -1) {
val balance = synchronizer.saplingBalances.value
if (available > 0) {
assertTrue("invalid available balance. Expected a minimum of $available but found ${balance?.available}", available <= balance?.available?.value!!)
}
if (total > 0) {
assertTrue("invalid total balance. Expected a minimum of $total but found ${balance?.total}", total <= balance?.total?.value!!)
}
}
suspend fun validateBalance(available: Long = -1, total: Long = -1, accountIndex: Int = 0) {
val balance = (synchronizer as SdkSynchronizer).processor.getBalanceInfo(accountIndex)
if (available > 0) {
assertEquals("invalid available balance", available, balance.available)
}
if (total > 0) {
assertEquals("invalid total balance", total, balance.total)
}
}
}
//
// Chain Creations
//
inner class DarksideChainMaker {
var lastTipHeight = -1
/**
* Resets the darksidelightwalletd server, stages the blocks represented by the given URL, then
* applies those changes and waits for them to take effect.
*/
fun resetBlocks(
blocksUrl: String,
startHeight: Int = DEFAULT_START_HEIGHT,
tipHeight: Int = startHeight + 100
): DarksideChainMaker = apply {
darkside
.reset(startHeight)
.stageBlocks(blocksUrl)
applyTipHeight(tipHeight)
}
fun stageTransaction(url: String, targetHeight: Int): DarksideChainMaker = apply {
darkside.stageTransactions(url, targetHeight)
}
fun stageTransactions(targetHeight: Int, vararg urls: String): DarksideChainMaker = apply {
urls.forEach {
darkside.stageTransactions(it, targetHeight)
}
}
fun stageEmptyBlocks(startHeight: Int, count: Int = 10): DarksideChainMaker = apply {
darkside.stageEmptyBlocks(startHeight, count)
}
fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight + 1, 1)
fun applyTipHeight(tipHeight: Int): DarksideChainMaker = apply {
twig("applying tip height of $tipHeight")
darkside.applyBlocks(tipHeight)
lastTipHeight = tipHeight
}
/**
* Creates a chain with 100 blocks and a transaction in the middle.
*
* The chain starts at block 663150 and ends at block 663250
*/
fun makeSimpleChain() {
darkside
.reset(DEFAULT_START_HEIGHT)
.stageBlocks("https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/tx-incoming/blocks.txt")
applyTipHeight(DEFAULT_START_HEIGHT + 100)
}
fun advanceBy(numEmptyBlocks: Int) {
val nextBlock = lastTipHeight + 1
twig("adding $numEmptyBlocks empty blocks to the chain starting at $nextBlock")
darkside.stageEmptyBlocks(nextBlock, numEmptyBlocks)
applyTipHeight(nextBlock + numEmptyBlocks)
}
fun applyPendingTransactions(targetHeight: Int = lastTipHeight + 1) {
stageEmptyBlocks(lastTipHeight + 1, targetHeight - lastTipHeight)
darkside.stageTransactions(darkside.getSentTransactions()?.iterator(), targetHeight)
applyTipHeight(targetHeight)
}
}
companion object {
/**
* This is a special localhost value on the Android emulator, which allows it to contact
* the localhost of the computer running the emulator.
*/
const val COMPUTER_LOCALHOST = "10.0.2.2"
// Block URLS
private const val beforeReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"
private const val smallReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"
private const val largeReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt"
private const val DEFAULT_START_HEIGHT = 663150
private const val DEFAULT_SEED_PHRASE =
"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
}
}

View File

@@ -0,0 +1,57 @@
package cash.z.ecc.android.sdk.darkside.test
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.test.core.app.ApplicationProvider
import org.junit.Before
/**
* Subclass this to validate the environment for running Darkside tests.
*/
open class DarksideTestPrerequisites {
@Before
fun verifyEmulator() {
require(isProbablyEmulator(ApplicationProvider.getApplicationContext())) {
"Darkside tests are configured to only run on the Android Emulator. Please see https://github.com/zcash/zcash-android-wallet-sdk/blob/master/docs/tests/Darkside.md"
}
}
companion object {
private fun isProbablyEmulator(context: Context): Boolean {
if (isDebuggable(context)) {
// This is imperfect and could break in the future
if (null == Build.DEVICE ||
"generic" == Build.DEVICE || // $NON-NLS
("generic_x86" == Build.DEVICE) // $NON-NLS
) {
return true
}
}
return false
}
/**
* @return Whether the application running is debuggable. This is determined from the
* ApplicationInfo object (`BuildInfo` is useless for libraries.)
*/
private fun isDebuggable(context: Context): Boolean {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.PackageInfoFlags.of(0L)
)
} else {
@Suppress("Deprecation")
context.packageManager.getPackageInfo(context.packageName, 0)
}
// Normally shouldn't be null, but could be with a MockContext
return packageInfo.applicationInfo?.let {
0 != (it.flags and ApplicationInfo.FLAG_DEBUGGABLE)
} ?: false
}
}
}

View File

@@ -0,0 +1,91 @@
package cash.z.ecc.android.sdk.darkside.test
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.twig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import java.util.concurrent.TimeoutException
open class ScopedTest(val defaultTimeout: Long = 2000L) : DarksideTestPrerequisites() {
protected lateinit var testScope: CoroutineScope
// if an androidTest doesn't need a context, then maybe it should be a unit test instead?!
val context: Context = InstrumentationRegistry.getInstrumentation().context
@Before
fun start() {
twig("===================== TEST STARTED ==================================")
testScope = CoroutineScope(
Job(classScope.coroutineContext[Job]!!) + newFixedThreadPoolContext(
5,
this.javaClass.simpleName
)
)
}
@After
fun end() = runBlocking<Unit> {
twig("======================= TEST CANCELLING =============================")
testScope.cancel()
testScope.coroutineContext[Job]?.join()
twig("======================= TEST ENDED ==================================")
}
fun timeout(duration: Long, block: suspend () -> Unit) = timeoutWith(testScope, duration, block)
companion object {
@JvmStatic
lateinit var classScope: CoroutineScope
init {
Twig.plant(TroubleshootingTwig())
twig("================================================================ INIT")
}
@BeforeClass
@JvmStatic
fun createScope() {
twig("======================= CLASS STARTED ===============================")
classScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(2, this.javaClass.simpleName)
)
}
@AfterClass
@JvmStatic
fun destroyScope() = runBlocking<Unit> {
twig("======================= CLASS CANCELLING ============================")
classScope.cancel()
classScope.coroutineContext[Job]?.join()
twig("======================= CLASS ENDED =================================")
}
@JvmStatic
fun timeoutWith(scope: CoroutineScope, duration: Long, block: suspend () -> Unit) {
scope.launch {
delay(duration)
val message = "ERROR: Test timed out after ${duration}ms"
twig(message)
throw TimeoutException(message)
}.let { selfDestruction ->
scope.launch {
block()
selfDestruction.cancel()
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
package cash.z.ecc.android.sdk.darkside.test
import cash.z.android.plugin.MnemonicPlugin
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount
import cash.z.ecc.android.bip39.toEntropy
import cash.z.ecc.android.bip39.toSeed
import java.util.Locale
class SimpleMnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
}

View File

@@ -0,0 +1,175 @@
package cash.z.ecc.android.sdk.darkside.test
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.db.entity.isPending
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import java.util.concurrent.TimeoutException
/**
* A simple wallet that connects to testnet for integration testing. The intention is that it is
* easy to drive and nice to use.
*/
class TestWallet(
val seedPhrase: String,
val alias: String = "TestWallet",
val network: ZcashNetwork = ZcashNetwork.Testnet,
val host: String = network.defaultHost,
startHeight: Int? = null,
val port: Int = network.defaultPort
) {
constructor(
backup: Backups,
network: ZcashNetwork = ZcashNetwork.Testnet,
alias: String = "TestWallet"
) : this(
backup.seedPhrase,
network = network,
startHeight = if (network == ZcashNetwork.Mainnet) backup.mainnetBirthday else backup.testnetBirthday,
alias = alias
)
val walletScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(3, this.javaClass.simpleName)
)
// Although runBlocking isn't great, this usage is OK because this is only used within the
// automated tests
private val context = InstrumentationRegistry.getInstrumentation().context
private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed()
private val shieldedSpendingKey =
runBlocking { DerivationTool.deriveSpendingKeys(seed, network = network)[0] }
private val transparentSecretKey =
runBlocking { DerivationTool.deriveTransparentSecretKey(seed, network = network) }
val initializer = runBlocking {
Initializer.new(context) { config ->
runBlocking { config.importWallet(seed, startHeight, network, host, alias = alias) }
}
}
val synchronizer: SdkSynchronizer = runBlocking { Synchronizer.new(initializer) } as SdkSynchronizer
val service = (synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService)
val available get() = synchronizer.saplingBalances.value?.available
val shieldedAddress =
runBlocking { DerivationTool.deriveShieldedAddress(seed, network = network) }
val transparentAddress =
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
val birthdayHeight get() = synchronizer.latestBirthdayHeight
val networkName get() = synchronizer.network.networkName
val connectionInfo get() = service.connectionInfo.toString()
suspend fun transparentBalance(): WalletBalance {
synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight)
return synchronizer.getTransparentBalance(transparentAddress)
}
suspend fun sync(timeout: Long = -1): TestWallet {
val killSwitch = walletScope.launch {
if (timeout > 0) {
delay(timeout)
throw TimeoutException("Failed to sync wallet within ${timeout}ms")
}
}
if (!synchronizer.isStarted) {
twig("Starting sync")
synchronizer.start(walletScope)
} else {
twig("Awaiting next SYNCED status")
}
// block until synced
synchronizer.status.first { it == Synchronizer.Status.SYNCED }
killSwitch.cancel()
twig("Synced!")
return this
}
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() }
.collect {
twig("Updated transaction: $it")
}
Twig.clip("$alias sending")
return this
}
suspend fun rewindToHeight(height: Int): TestWallet {
synchronizer.rewindToNearestHeight(height, false)
return this
}
suspend fun shieldFunds(): TestWallet {
twig("checking $transparentAddress for transactions!")
synchronizer.refreshUtxos(transparentAddress, 935000).let { count ->
twig("FOUND $count new UTXOs")
}
synchronizer.getTransparentBalance(transparentAddress).let { walletBalance ->
twig("FOUND utxo balance of total: ${walletBalance.total} available: ${walletBalance.available}")
if (walletBalance.available.value > 0L) {
synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
.onCompletion { twig("done shielding funds") }
.catch { twig("Failed with $it") }
.collect()
}
}
return this
}
suspend fun join(timeout: Long? = null): TestWallet {
// block until stopped
twig("Staying alive until synchronizer is stopped!")
if (timeout != null) {
twig("Scheduling a stop in ${timeout}ms")
walletScope.launch {
delay(timeout)
synchronizer.stop()
}
}
synchronizer.status.first { it == Synchronizer.Status.STOPPED }
twig("Stopped!")
return this
}
companion object {
init {
Twig.enabled(true)
}
}
enum class Backups(val seedPhrase: String, val testnetBirthday: Int, val mainnetBirthday: Int) {
// TODO: get the proper birthday values for these wallets
DEFAULT("column rhythm acoustic gym cost fit keen maze fence seed mail medal shrimp tell relief clip cannon foster soldier shallow refuse lunar parrot banana", 1_355_928, 1_000_000),
SAMPLE_WALLET("input frown warm senior anxiety abuse yard prefer churn reject people glimpse govern glory crumble swallow verb laptop switch trophy inform friend permit purpose", 1_330_190, 1_000_000),
DEV_WALLET("still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread", 1_000_000, 991645),
ALICE("quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten", 1_330_190, 1_000_000),
BOB("canvas wine sugar acquire garment spy tongue odor hole cage year habit bullet make label human unit option top calm neutral try vocal arena", 1_330_190, 1_000_000),
;
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="cash.z.ecc.android.sdk.darkside">
<application android:name="androidx.multidex.MultiDexApplication" />
</manifest>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="lightwalletd_allow_very_insecure_connections">true</bool>
</resources>

76
demo-app/README.md Normal file
View File

@@ -0,0 +1,76 @@
# Android demo app
This is a demo app that exercises code in https://github.com/zcash/zcash-android-wallet-sdk, which has all the Android-related functionalities necessary to build a mobile Zcash shielded wallet.
It relies on [Lightwalletd](https://github.com/zcash/lightwalletd), a backend service that provides a bandwidth-efficient interface to the Zcash blockchain. There is an equivalent [iOS demo app](https://github.com/zcash/ZcashLightClientKit).
## Contents
- [Requirements](#requirements)
- [Installation](#installation)
- [Exploring the demo app](#exploring-the-demo-app)
- [Demos](#demos)
- [Getting started](#getting-started)
- [Resources](#resources)
## Requirements
The demo app is built in Kotlin, and targets API 21. The demo pulls the pre-built SDK from jcenter so, unlike the SDK, it does not require Rust or the NDK!
[Back to contents](#contents)
## Installation
In short, you simply will need to:
0. (pre-requisite) Install [Android Studio](https://developer.android.com/studio) and [setup an emulator](https://developer.android.com/studio/run/emulator#runningapp) or device
1. Clone this repo: https://github.com/zcash/zcash-android-wallet-sdk
2. Open the `demo-app` folder in Android Studio and [launch the app](https://developer.android.com/studio/run/emulator#runningapp)
(recommended build variant: `zcashmainnetDebug`)
[Back to contents](#contents)
## Exploring the demo app
After building the app, the emulator should launch with a basic app that exercises the SDK (see picture below).
To explore the app, click on each menu item, in order, and also look at the associated code.
![The android demo app, running in Android Studio](assets/demo-app.png?raw=true "Demo App with Android Studio")
The demo app is not trying to show what's possible, but to present how to accomplish the building blocks of wallet functionality in a simple way in code. It is comprised of the following self-contained demos. All data is reset between demos in order to keep the behavior repeatable and independent of state.
### Demos
Menu Item|Related Code|Description
:-----|:-----|:-----
Get Private Key|[GetPrivateKeyFragment.kt](app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt)|Given a seed, display its viewing key and spending key
Get Address|[GetAddressFragment.kt](app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt)|Given a seed, display its z-addr
Get Balance|[GetBalanceFragment.kt](app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt)|Display the balance
Get Latest Height|[GetLatestHeightFragment.kt](app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt)|Given a lightwalletd server, retrieve the latest block height
Get Block|[GetBlockFragment.kt](app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt)|Given a lightwalletd server, retrieve a compact block
Get Block Range|[GetBlockRangeFragment.kt](app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt)|Given a lightwalletd server, retrieve a range of compact blocks
List Transactions|[ListTransactionsFragment.kt](app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt)|Given a seed, list all related shielded transactions
Send|[SendFragment.kt](app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt)|Send and monitor a transaction, the most complex demo
[Back to contents](#contents)
## Getting started
Were assuming you already have a brilliant app idea, a vision for the apps UI, and know the ins and outs of the Android lifecycle. Well just stick to the Zcash app part of “getting started.”
Similarly, the best way to build a functioning Zcash shielded app is to implement the functionalities that are listed in the demo app, in roughly that order:
1. Generate and safely store your private key.
1. Get the associated address, and display it to the user on a receive screen. You may also want to generate a QR code from this address.
1. Make sure your app can talk to the lightwalletd server and check by asking for the latest height, and verify that its current with the Zcash network.
1. Try interacting with lightwalletd by fetching a block and processing it. Then try fetching a range of blocks, which is much more efficient.
1. Now that you have the blocks process them and list transactions that send to or are from that wallet, to calculate your balance.
1. With a current balance (and funds, of course), send a transaction and monitor its transaction status and update the UI with the results.
[Back to contents](#contents)
## Resources
You dont need to do it all on your own.
* Chat with the wallet team: [Zcash discord community channel, wallet](https://discord.gg/efFG7UJ)
* Discuss ideas with other community members: [Zcash forum](https://forum.zcashcommunity.com/)
* Get funded to build a Zcash app: [Zcash foundation grants program](https://grants.zfnd.org/)
* Follow Zcash-specific best practices: [Zcash wallet developer checklist](https://zcash.readthedocs.io/en/latest/rtd_pages/ux_wallet_checklist.html)
* Get more information and see FAQs about the wallet: [Shielded resources documentation](https://zcash.readthedocs.io/en/latest/rtd_pages/shielded_support.html)
[Back to contents](#contents)

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

123
demo-app/build.gradle.kts Normal file
View File

@@ -0,0 +1,123 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("zcash-sdk.android-conventions")
id("kotlin-parcelize")
id("androidx.navigation.safeargs")
id("com.osacky.fladle")
}
android {
defaultConfig {
applicationId = "cash.z.ecc.android.sdk.demoapp"
minSdk = 21 // Different from the SDK min
versionCode = 1
versionName = "1.0"
}
buildFeatures {
viewBinding = true
}
flavorDimensions.add("network")
productFlavors {
// would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test"
create("zcashtestnet") {
dimension = "network"
applicationId = "cash.z.ecc.android.sdk.demoapp.testnet"
matchingFallbacks.addAll(listOf("zcashtestnet", "debug"))
}
create("zcashmainnet") {
dimension = "network"
applicationId = "cash.z.ecc.android.sdk.demoapp.mainnet"
matchingFallbacks.addAll(listOf("zcashmainnet", "release"))
}
}
buildTypes {
getByName("release").apply {
isMinifyEnabled = project.property("IS_MINIFY_APP_ENABLED").toString().toBoolean()
proguardFiles.addAll(
listOf(
getDefaultProguardFile("proguard-android-optimize.txt"),
File("proguard-project.txt")
)
)
}
}
lint {
baseline = File("lint-baseline.xml")
}
}
dependencies {
// SDK
implementation(projects.sdkLib)
// sample mnemonic plugin
implementation(libs.zcashwalletplgn)
implementation(libs.bip39)
// Android
implementation(libs.androidx.core)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.navigation.ui)
implementation(libs.material)
androidTestImplementation(libs.bundles.androidx.test)
implementation(libs.bundles.grpc)
}
fladle {
// Firebase Test Lab has min and max values that might differ from our project's
// These are determined by `gcloud firebase test android models list`
@Suppress("MagicNumber", "PropertyName", "VariableNaming")
val FIREBASE_TEST_LAB_MIN_API = 23
@Suppress("MagicNumber", "PropertyName", "VariableNaming")
val FIREBASE_TEST_LAB_MAX_API = 30
val minSdkVersion = run {
val buildMinSdk =
project.properties["ANDROID_MIN_SDK_VERSION"].toString().toInt()
buildMinSdk.coerceAtLeast(FIREBASE_TEST_LAB_MIN_API).toString()
}
val targetSdkVersion = run {
val buildTargetSdk =
project.properties["ANDROID_TARGET_SDK_VERSION"].toString().toInt()
buildTargetSdk.coerceAtMost(FIREBASE_TEST_LAB_MAX_API).toString()
}
val firebaseTestLabKeyPath = project.properties["ZCASH_FIREBASE_TEST_LAB_API_KEY_PATH"].toString()
val firebaseProject = project.properties["ZCASH_FIREBASE_TEST_LAB_PROJECT"].toString()
if (firebaseTestLabKeyPath.isNotEmpty()) {
serviceAccountCredentials.set(File(firebaseTestLabKeyPath))
} else if (firebaseProject.isNotEmpty()) {
projectId.set(firebaseProject)
}
configs {
create("sanityConfig") {
clearPropertiesForSanityRobo()
debugApk.set(
project.provider {
"${buildDir}/outputs/apk/zcashmainnet/release/demo-app-zcashmainnet-release.apk"
}
)
testTimeout.set("5m")
devices.addAll(
mapOf("model" to "Pixel2", "version" to minSdkVersion),
mapOf("model" to "Pixel2", "version" to targetSdkVersion)
)
flankVersion.set(libs.versions.flank.get())
}
}
}

1320
demo-app/lint-baseline.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
# Allow for debuggable stacktraces
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,97 @@
package cash.z.ecc.android.sdk.demoapp
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import com.google.android.material.snackbar.Snackbar
abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
/**
* Since the lightwalletservice is not a component that apps typically use, directly, we provide
* this from one place. Everything that can be done with the service can/should be done with the
* synchronizer because it wraps the service.
*/
val lightwalletService get() = mainActivity()?.lightwalletService
// contains view information provided by the user
val sharedViewModel: SharedViewModel by activityViewModels()
lateinit var binding: T
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = inflateBinding(layoutInflater)
return binding.root
}
override fun onResume() {
super.onResume()
registerActionButtonListener()
}
override fun onPause() {
super.onPause()
unregisterActionButtonListener()
(activity as? MainActivity)?.hideKeyboard()
}
private fun registerActionButtonListener() {
(activity as? MainActivity)?.fabListener = this
}
private fun unregisterActionButtonListener() {
(activity as? MainActivity)?.apply {
if (fabListener === this@BaseDemoFragment) fabListener = null
}
}
/**
* Callback that gets invoked on the visible fragment whenever the floating action button is
* tapped. This provides a convenient placeholder for the developer to extend the
* behavior for a demo, for instance by copying the address to the clipboard, whenever the FAB
* is tapped on the address screen.
*/
open fun onActionButtonClicked() {
// Show a message so that it's easy for developers to find how to replace this behavior for
// each fragment. Simply override this [onActionButtonClicked] callback to add behavior to a
// demo. In other words, this function probably doesn't need to change because desired
// behavior should go in the child fragment, which overrides this.
Snackbar.make(requireView(), "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action") { /* auto-close */ }.show()
}
/**
* Convenience function to the given text to the clipboard.
*/
open fun copyToClipboard(text: String, description: String = "Copied to clipboard!") {
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)?.let { cm ->
cm.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text))
}
toast(description)
}
/**
* Convenience function to show a toast in the main activity.
*/
fun toast(message: String) {
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
}
/**
* Inflate the ViewBinding. Unfortunately, the `inflate` function is not part of the ViewBinding
* interface so the base class cannot take care of this behavior without some help.
*/
abstract fun inflateBinding(layoutInflater: LayoutInflater): T
}

View File

@@ -0,0 +1,161 @@
package cash.z.ecc.android.sdk.demoapp
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.getSystemService
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.type.ZcashNetwork
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView
class MainActivity :
AppCompatActivity(),
ClipboardManager.OnPrimaryClipChangedListener,
DrawerLayout.DrawerListener {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var clipboard: ClipboardManager
private var clipboardListener: ((String?) -> Unit)? = null
var fabListener: BaseDemoFragment<out ViewBinding>? = null
/**
* The service to use for all demos that interact directly with the service. Since gRPC channels
* are expensive to recreate, we set this up once per demo. A real app would hardly ever use
* this object because it would utilize the synchronizer, instead, which exposes APIs that
* automatically sync with the server.
*/
var lightwalletService: LightWalletService? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.addPrimaryClipChangedListener(this)
setContentView(R.layout.activity_main)
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
val fab: FloatingActionButton = findViewById(R.id.fab)
fab.setOnClickListener { view ->
onFabClicked(view)
}
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
val navView: NavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.nav_home, R.id.nav_address, R.id.nav_balance, R.id.nav_block, R.id.nav_private_key,
R.id.nav_latest_height, R.id.nav_block_range,
R.id.nav_transactions, R.id.nav_utxos, R.id.nav_send
),
drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
drawerLayout.addDrawerListener(this)
initService()
}
override fun onDestroy() {
super.onDestroy()
lightwalletService?.shutdown()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.action_settings) {
val navController = findNavController(R.id.nav_host_fragment)
navController.navigate(R.id.nav_home)
true
} else {
super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
//
// Private functions
//
private fun initService() {
if (lightwalletService != null) {
lightwalletService?.shutdown()
}
lightwalletService = LightWalletGrpcService(applicationContext, ZcashNetwork.fromResources(applicationContext))
}
private fun onFabClicked(view: View) {
fabListener?.onActionButtonClicked()
}
//
// Helpers
//
fun getClipboardText(): String? {
return with(clipboard) {
if (!hasPrimaryClip()) return null
return primaryClip!!.getItemAt(0)?.coerceToText(this@MainActivity)?.toString()
}
}
override fun onPrimaryClipChanged() {
clipboardListener?.invoke(getClipboardText())
}
fun setClipboardListener(block: (String?) -> Unit) {
clipboardListener = block
block(getClipboardText())
}
fun removeClipboardListener() {
clipboardListener = null
}
fun hideKeyboard() {
val windowToken = window.decorView.rootView.windowToken
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(windowToken, 0)
}
/* DrawerListener implementation */
override fun onDrawerStateChanged(newState: Int) {
}
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
}
override fun onDrawerClosed(drawerView: View) {
}
override fun onDrawerOpened(drawerView: View) {
hideKeyboard()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,154 @@
package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
/**
* List all transactions related to the given seed, since the given birthday. This begins by
* downloading any missing blocks and then validating and scanning their contents. Once scan is
* complete, the transactions are available in the database and can be accessed by any SQL tool.
* By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the
* database in a paged format that works natively with RecyclerViews.
*/
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
private lateinit var initializer: Initializer
private lateinit var synchronizer: Synchronizer
private lateinit var adapter: TransactionAdapter<ConfirmedTransaction>
private lateinit var address: String
private var status: Synchronizer.Status? = null
private val isSynced get() = status == Synchronizer.Status.SYNCED
/**
* Initialize the required values that would normally live outside the demo but are repeated
* here for completeness so that each demo file can serve as a standalone example.
*/
private fun setup() {
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
var seedPhrase = sharedViewModel.seedPhrase.value
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
// have the seed stored
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
initializer = runBlocking {
Initializer.new(requireApplicationContext()) {
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
}
}
address = runBlocking {
DerivationTool.deriveShieldedAddress(
seed,
ZcashNetwork.fromResources(requireApplicationContext())
)
}
synchronizer = Synchronizer.newBlocking(initializer)
}
private fun initTransactionUI() {
binding.recyclerTransactions.layoutManager =
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = TransactionAdapter()
binding.recyclerTransactions.adapter = adapter
}
private fun monitorChanges() {
// the lifecycleScope is used to stop everything when the fragment dies
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
}
//
// Change listeners
//
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
if (info.isScanning) binding.textInfo.text = "Scanning blocks...${info.scanProgress}%"
}
private fun onProgress(i: Int) {
if (i < 100) binding.textInfo.text = "Downloading blocks...$i%"
}
private fun onStatus(status: Synchronizer.Status) {
this.status = status
binding.textStatus.text = "Status: $status"
if (isSynced) onSyncComplete()
}
private fun onSyncComplete() {
binding.textInfo.visibility = View.INVISIBLE
}
private fun onTransactionsUpdated(transactions: List<ConfirmedTransaction>) {
twig("got a new paged list of transactions")
adapter.submitList(transactions)
// show message when there are no transactions
if (isSynced) {
binding.textInfo.apply {
if (transactions.isEmpty()) {
visibility = View.VISIBLE
text =
"No transactions found. Try to either change the seed words " +
"or send funds to this address (tap the FAB to copy it):\n\n $address"
} else {
visibility = View.INVISIBLE
text = ""
}
}
}
}
//
// Android Lifecycle overrides
//
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setup()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initTransactionUI()
}
override fun onResume() {
super.onResume()
// the lifecycleScope is used to dispose of the synchronizer when the fragment dies
synchronizer.start(lifecycleScope)
monitorChanges()
}
//
// Base Fragment overrides
//
override fun onActionButtonClicked() {
if (::address.isInitialized) copyToClipboard(address)
}
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListTransactionsBinding =
FragmentListTransactionsBinding.inflate(layoutInflater)
}

View File

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

View File

@@ -0,0 +1,41 @@
package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import java.text.SimpleDateFormat
import java.util.Locale
/**
* Simple view holder for displaying confirmed transactions in the recyclerview.
*/
class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
private val infoText = itemView.findViewById<TextView>(R.id.text_transaction_info)
private val timeText = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
private val icon = itemView.findViewById<ImageView>(R.id.image_transaction_type)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
fun bindTo(transaction: T?) {
val isInbound = transaction?.toAddress.isNullOrEmpty()
amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
timeText.text =
if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending"
else formatter.format(transaction.blockTimeInSeconds * 1000L)
infoText.text = getMemoString(transaction)
icon.rotation = if (isInbound) 0f else 180f
icon.rotation = if (isInbound) 0f else 180f
icon.setColorFilter(ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound))
}
private fun getMemoString(transaction: T?): String {
return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More