Initial commit
This commit is contained in:
14
.editorconfig
Normal file
14
.editorconfig
Normal 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
83
.gitignore
vendored
Normal 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
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
zcash-android-sdk
|
||||
134
.idea/codeStyles/Project.xml
generated
Normal file
134
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
21
.idea/runConfigurations/detektAll.xml
generated
Normal 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>
|
||||
53
.run/_darkside-test-lib_connectedAndroidTest.run.xml
Normal file
53
.run/_darkside-test-lib_connectedAndroidTest.run.xml
Normal 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>
|
||||
23
.run/_sdk-lib_test.run.xml
Normal file
23
.run/_sdk-lib_test.run.xml
Normal 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>
|
||||
53
.run/_sdkLib_connectedCheck.run.xml
Normal file
53
.run/_sdkLib_connectedCheck.run.xml
Normal 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
23
.run/assemble.run.xml
Normal 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>
|
||||
21
.run/assembleAndroidTest.run.xml
Normal file
21
.run/assembleAndroidTest.run.xml
Normal 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
23
.run/clean.run.xml
Normal 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
60
.run/demo-app.run.xml
Normal 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>
|
||||
23
.run/dependencyUpdates.run.xml
Normal file
23
.run/dependencyUpdates.run.xml
Normal 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
21
.run/ktlint.run.xml
Normal 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
23
.run/lint.run.xml
Normal 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>
|
||||
28
.run/publishToMavenLocal.run.xml
Normal file
28
.run/publishToMavenLocal.run.xml
Normal 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
196
CHANGELOG.md
Normal 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
191
CONTRIBUTING.md
Normal 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
21
LICENSE
Normal 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
53
MIGRATIONS.md
Normal 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
226
README.md
Normal file
@@ -0,0 +1,226 @@
|
||||
[](https://github.com/zcash/zcash-android-wallet-sdk/blob/master/LICENSE)
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
Thankfully, the only thing an app developer has to be concerned with is the following:
|
||||
|
||||

|
||||
|
||||
[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
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
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
BIN
assets/sdk-manager-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
assets/sdk_dev_pov_final.png
Normal file
BIN
assets/sdk_dev_pov_final.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
BIN
assets/sdk_diagram_final.png
Normal file
BIN
assets/sdk_diagram_final.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 445 KiB |
34
build-conventions/build.gradle.kts
Normal file
34
build-conventions/build.gradle.kts
Normal 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)
|
||||
45
build-conventions/buildscript-gradle.lockfile
Normal file
45
build-conventions/buildscript-gradle.lockfile
Normal 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=
|
||||
182
build-conventions/gradle.lockfile
Normal file
182
build-conventions/gradle.lockfile
Normal 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
|
||||
15
build-conventions/settings.gradle.kts
Normal file
15
build-conventions/settings.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "build-conventions"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
105
build.gradle.kts
Normal 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"
|
||||
))
|
||||
}
|
||||
27
darkside-test-lib/build.gradle.kts
Normal file
27
darkside-test-lib/build.gradle.kts
Normal 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)
|
||||
}
|
||||
10
darkside-test-lib/src/androidTest/AndroidManifest.xml
Normal file
10
darkside-test-lib/src/androidTest/AndroidManifest.xml
Normal 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>
|
||||
@@ -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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -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()
|
||||
// // }
|
||||
// // }
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
;
|
||||
}
|
||||
}
|
||||
7
darkside-test-lib/src/main/AndroidManifest.xml
Normal file
7
darkside-test-lib/src/main/AndroidManifest.xml
Normal 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>
|
||||
4
darkside-test-lib/src/main/res/values/bools.xml
Normal file
4
darkside-test-lib/src/main/res/values/bools.xml
Normal 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
76
demo-app/README.md
Normal 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 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
|
||||
We’re assuming you already have a brilliant app idea, a vision for the app’s UI, and know the ins and outs of the Android lifecycle. We’ll 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 it’s 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 don’t 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)
|
||||
BIN
demo-app/assets/demo-app.png
Normal file
BIN
demo-app/assets/demo-app.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
123
demo-app/build.gradle.kts
Normal file
123
demo-app/build.gradle.kts
Normal 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
1320
demo-app/lint-baseline.xml
Normal file
File diff suppressed because it is too large
Load Diff
3
demo-app/proguard-project.txt
Normal file
3
demo-app/proguard-project.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# Allow for debuggable stacktraces
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,169 @@
|
||||
package cash.z.wallet.sdk.sample.demoapp
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.db.entity.isFailure
|
||||
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
||||
import cash.z.ecc.android.sdk.ext.toHex
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Sample code to demonstrate key functionality without UI, inspired by:
|
||||
* https://github.com/EdgeApp/eosjs-node-cli/blob/paul/cleanup/app.js
|
||||
*/
|
||||
class SampleCodeTest {
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Seed derivation
|
||||
@Ignore("This test is not implemented")
|
||||
@Test
|
||||
fun createBip39Seed_fromSeedPhrase() {
|
||||
// TODO: log(seedPhrase.asRawEntropy().asBip39seed())
|
||||
}
|
||||
|
||||
@Ignore("This test is not implemented")
|
||||
@Test
|
||||
fun createRawEntropy() {
|
||||
// TODO: call: Mnemonic::from_phrase(seed_phrase, Language::English).unwrap().entropy()
|
||||
// log(seedPhrase.asRawEntropy())
|
||||
}
|
||||
|
||||
@Ignore("This test is not implemented")
|
||||
@Test
|
||||
fun createBip39Seed_fromRawEntropy() {
|
||||
// get the 64 byte bip39 entropy
|
||||
// TODO: call: bip39::Seed::new(&Mnemonic::from_entropy(&seed_bytes, Language::English).unwrap(), "")
|
||||
// log(rawEntropy.asBip39Seed())
|
||||
}
|
||||
|
||||
@Ignore("This test is not implemented")
|
||||
@Test
|
||||
fun deriveSeedPhraseFrom() {
|
||||
// TODO: let mnemonic = Mnemonic::from_entropy(entropy, Language::English).unwrap();
|
||||
// log(entropy.asSeedPhrase())
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Derive Extended Spending Key
|
||||
@Test fun deriveSpendingKey() {
|
||||
val spendingKeys = runBlocking {
|
||||
DerivationTool.deriveSpendingKeys(
|
||||
seed,
|
||||
ZcashNetwork.Mainnet
|
||||
)
|
||||
}
|
||||
assertEquals(1, spendingKeys.size)
|
||||
log("Spending Key: ${spendingKeys?.get(0)}")
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Get Address
|
||||
@Test fun getAddress() = runBlocking {
|
||||
val address = synchronizer.getAddress()
|
||||
assertFalse(address.isNullOrBlank())
|
||||
log("Address: $address")
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Derive address from Extended Full Viewing Key
|
||||
@Test fun getAddressFromViewingKey() {
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Query latest block height
|
||||
@Test fun getLatestBlockHeightTest() {
|
||||
val lightwalletService = LightWalletGrpcService(context, lightwalletdHost)
|
||||
log("Latest Block: ${lightwalletService.getLatestBlockHeight()}")
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Download compact block range
|
||||
@Test fun getBlockRange() {
|
||||
val blockRange = 500_000..500_009
|
||||
val lightwalletService = LightWalletGrpcService(context, lightwalletdHost)
|
||||
val blocks = lightwalletService.getBlockRange(blockRange)
|
||||
assertEquals(blockRange.count(), blocks.size)
|
||||
|
||||
blocks.forEachIndexed { i, block ->
|
||||
log("Block #$i: height:${block.height} hash:${block.hash.toByteArray().toHex()}")
|
||||
}
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Query account outgoing transactions
|
||||
@Test fun queryOutgoingTransactions() {
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Query account incoming transactions
|
||||
@Test fun queryIncomingTransactions() {
|
||||
}
|
||||
|
||||
// // ///////////////////////////////////////////////////
|
||||
// // Create a signed transaction (with memo)
|
||||
// @Test fun createTransaction() = runBlocking {
|
||||
// val rustBackend = RustBackend.init(context)
|
||||
// val repository = PagedTransactionRepository(context)
|
||||
// val encoder = WalletTransactionEncoder(rustBackend, repository)
|
||||
// val spendingKey = DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.Mainnet)[0]
|
||||
//
|
||||
// val amount = 0.123.convertZecToZatoshi()
|
||||
// val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw"
|
||||
// val memo = "Test Transaction".toByteArray()
|
||||
//
|
||||
// val encodedTx = encoder.createTransaction(spendingKey, amount, address, memo)
|
||||
// assertTrue(encodedTx.raw.isNotEmpty())
|
||||
// log("Transaction ID: ${encodedTx.txId.toHex()}")
|
||||
// }
|
||||
|
||||
// ///////////////////////////////////////////////////
|
||||
// Create a signed transaction (with memo) and broadcast
|
||||
@Test fun submitTransaction() = runBlocking {
|
||||
val amount = 0.123.convertZecToZatoshi()
|
||||
val address = "ztestsapling1tklsjr0wyw0d58f3p7wufvrj2cyfv6q6caumyueadq8qvqt8lda6v6tpx474rfru9y6u75u7qnw"
|
||||
val memo = "Test Transaction"
|
||||
val spendingKey = DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.Mainnet)[0]
|
||||
val transactionFlow = synchronizer.sendToAddress(spendingKey, amount, address, memo)
|
||||
transactionFlow.collect {
|
||||
log("pending transaction updated $it")
|
||||
assertTrue("Failed to send funds. See log for details.", !it?.isFailure())
|
||||
}
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////
|
||||
// Utility Functions
|
||||
// ////////////////////////////////////////////////////
|
||||
|
||||
companion object {
|
||||
private val seed = "Insert seed for testing".toByteArray()
|
||||
private val lightwalletdHost: String = ZcashNetwork.Mainnet.defaultHost
|
||||
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val synchronizer: Synchronizer = run {
|
||||
val initializer = runBlocking { Initializer.new(context) {} }
|
||||
Synchronizer.newBlocking(initializer)
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun init() {
|
||||
Twig.plant(TroubleshootingTwig())
|
||||
}
|
||||
|
||||
fun log(message: String?) = twig(message ?: "null")
|
||||
}
|
||||
}
|
||||
27
demo-app/src/main/AndroidManifest.xml
Normal file
27
demo-app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cash.z.ecc.android.sdk.demoapp">
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
18
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/App.kt
Normal file
18
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/App.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package cash.z.ecc.android.sdk.demoapp
|
||||
|
||||
import android.app.Application
|
||||
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
|
||||
class App : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictModeHelper.enableStrictMode()
|
||||
}
|
||||
|
||||
Twig.plant(TroubleshootingTwig())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package cash.z.ecc.android.sdk.demoapp
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
|
||||
|
||||
/**
|
||||
* Since the lightwalletservice is not a component that apps typically use, directly, we provide
|
||||
* this from one place. Everything that can be done with the service can/should be done with the
|
||||
* synchronizer because it wraps the service.
|
||||
*/
|
||||
val lightwalletService get() = mainActivity()?.lightwalletService
|
||||
|
||||
// contains view information provided by the user
|
||||
val sharedViewModel: SharedViewModel by activityViewModels()
|
||||
lateinit var binding: T
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = inflateBinding(layoutInflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
registerActionButtonListener()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
unregisterActionButtonListener()
|
||||
(activity as? MainActivity)?.hideKeyboard()
|
||||
}
|
||||
|
||||
private fun registerActionButtonListener() {
|
||||
(activity as? MainActivity)?.fabListener = this
|
||||
}
|
||||
|
||||
private fun unregisterActionButtonListener() {
|
||||
(activity as? MainActivity)?.apply {
|
||||
if (fabListener === this@BaseDemoFragment) fabListener = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback that gets invoked on the visible fragment whenever the floating action button is
|
||||
* tapped. This provides a convenient placeholder for the developer to extend the
|
||||
* behavior for a demo, for instance by copying the address to the clipboard, whenever the FAB
|
||||
* is tapped on the address screen.
|
||||
*/
|
||||
open fun onActionButtonClicked() {
|
||||
// Show a message so that it's easy for developers to find how to replace this behavior for
|
||||
// each fragment. Simply override this [onActionButtonClicked] callback to add behavior to a
|
||||
// demo. In other words, this function probably doesn't need to change because desired
|
||||
// behavior should go in the child fragment, which overrides this.
|
||||
Snackbar.make(requireView(), "Replace with your own action", Snackbar.LENGTH_LONG)
|
||||
.setAction("Action") { /* auto-close */ }.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to the given text to the clipboard.
|
||||
*/
|
||||
open fun copyToClipboard(text: String, description: String = "Copied to clipboard!") {
|
||||
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)?.let { cm ->
|
||||
cm.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text))
|
||||
}
|
||||
toast(description)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to show a toast in the main activity.
|
||||
*/
|
||||
fun toast(message: String) {
|
||||
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the ViewBinding. Unfortunately, the `inflate` function is not part of the ViewBinding
|
||||
* interface so the base class cannot take care of this behavior without some help.
|
||||
*/
|
||||
abstract fun inflateBinding(layoutInflater: LayoutInflater): T
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package cash.z.ecc.android.sdk.demoapp
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.navigateUp
|
||||
import androidx.navigation.ui.setupActionBarWithNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
|
||||
import cash.z.ecc.android.sdk.internal.service.LightWalletService
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
|
||||
class MainActivity :
|
||||
AppCompatActivity(),
|
||||
ClipboardManager.OnPrimaryClipChangedListener,
|
||||
DrawerLayout.DrawerListener {
|
||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
private lateinit var clipboard: ClipboardManager
|
||||
private var clipboardListener: ((String?) -> Unit)? = null
|
||||
var fabListener: BaseDemoFragment<out ViewBinding>? = null
|
||||
|
||||
/**
|
||||
* The service to use for all demos that interact directly with the service. Since gRPC channels
|
||||
* are expensive to recreate, we set this up once per demo. A real app would hardly ever use
|
||||
* this object because it would utilize the synchronizer, instead, which exposes APIs that
|
||||
* automatically sync with the server.
|
||||
*/
|
||||
var lightwalletService: LightWalletService? = null
|
||||
private set
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.addPrimaryClipChangedListener(this)
|
||||
setContentView(R.layout.activity_main)
|
||||
val toolbar: Toolbar = findViewById(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
val fab: FloatingActionButton = findViewById(R.id.fab)
|
||||
fab.setOnClickListener { view ->
|
||||
onFabClicked(view)
|
||||
}
|
||||
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
|
||||
val navView: NavigationView = findViewById(R.id.nav_view)
|
||||
val navController = findNavController(R.id.nav_host_fragment)
|
||||
// Passing each menu ID as a set of Ids because each
|
||||
// menu should be considered as top level destinations.
|
||||
appBarConfiguration = AppBarConfiguration(
|
||||
setOf(
|
||||
R.id.nav_home, R.id.nav_address, R.id.nav_balance, R.id.nav_block, R.id.nav_private_key,
|
||||
R.id.nav_latest_height, R.id.nav_block_range,
|
||||
R.id.nav_transactions, R.id.nav_utxos, R.id.nav_send
|
||||
),
|
||||
drawerLayout
|
||||
)
|
||||
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||
navView.setupWithNavController(navController)
|
||||
drawerLayout.addDrawerListener(this)
|
||||
|
||||
initService()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
lightwalletService?.shutdown()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
menuInflater.inflate(R.menu.main, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == R.id.action_settings) {
|
||||
val navController = findNavController(R.id.nav_host_fragment)
|
||||
navController.navigate(R.id.nav_home)
|
||||
true
|
||||
} else {
|
||||
super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val navController = findNavController(R.id.nav_host_fragment)
|
||||
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
||||
}
|
||||
|
||||
//
|
||||
// Private functions
|
||||
//
|
||||
|
||||
private fun initService() {
|
||||
if (lightwalletService != null) {
|
||||
lightwalletService?.shutdown()
|
||||
}
|
||||
lightwalletService = LightWalletGrpcService(applicationContext, ZcashNetwork.fromResources(applicationContext))
|
||||
}
|
||||
|
||||
private fun onFabClicked(view: View) {
|
||||
fabListener?.onActionButtonClicked()
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
fun getClipboardText(): String? {
|
||||
return with(clipboard) {
|
||||
if (!hasPrimaryClip()) return null
|
||||
return primaryClip!!.getItemAt(0)?.coerceToText(this@MainActivity)?.toString()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrimaryClipChanged() {
|
||||
clipboardListener?.invoke(getClipboardText())
|
||||
}
|
||||
|
||||
fun setClipboardListener(block: (String?) -> Unit) {
|
||||
clipboardListener = block
|
||||
block(getClipboardText())
|
||||
}
|
||||
|
||||
fun removeClipboardListener() {
|
||||
clipboardListener = null
|
||||
}
|
||||
|
||||
fun hideKeyboard() {
|
||||
val windowToken = window.decorView.rootView.windowToken
|
||||
getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(windowToken, 0)
|
||||
}
|
||||
|
||||
/* DrawerListener implementation */
|
||||
|
||||
override fun onDrawerStateChanged(newState: Int) {
|
||||
}
|
||||
|
||||
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
|
||||
}
|
||||
|
||||
override fun onDrawerClosed(drawerView: View) {
|
||||
}
|
||||
|
||||
override fun onDrawerOpened(drawerView: View) {
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cash.z.ecc.android.sdk.demoapp
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* Shared mutable state for the demo
|
||||
*/
|
||||
class SharedViewModel : ViewModel() {
|
||||
|
||||
private val _seedPhrase = MutableStateFlow(DemoConstants.initialSeedWords)
|
||||
|
||||
// publicly, this is read-only
|
||||
val seedPhrase: StateFlow<String> get() = _seedPhrase
|
||||
|
||||
fun updateSeedPhrase(newPhrase: String?): Boolean {
|
||||
return if (isValidSeedPhrase(newPhrase)) {
|
||||
_seedPhrase.value = newPhrase!!
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidSeedPhrase(phrase: String?): Boolean {
|
||||
if (phrase.isNullOrEmpty()) return false
|
||||
return try {
|
||||
Mnemonics.MnemonicCode(phrase).validate()
|
||||
true
|
||||
} catch (t: Throwable) { false }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package cash.z.ecc.android.sdk.demoapp
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.StrictMode
|
||||
|
||||
object StrictModeHelper {
|
||||
|
||||
fun enableStrictMode() {
|
||||
configureStrictMode()
|
||||
|
||||
// Workaround for Android bug
|
||||
// https://issuetracker.google.com/issues/36951662
|
||||
// Not needed if target O_MR1 and running on O_MR1
|
||||
// Don't really need to check target, because of Google Play enforcement on targetSdkVersion for app updates
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
|
||||
Handler(Looper.getMainLooper()).postAtFrontOfQueue { configureStrictMode() }
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun configureStrictMode() {
|
||||
StrictMode.enableDefaults()
|
||||
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy.Builder().apply {
|
||||
detectAll()
|
||||
penaltyLog()
|
||||
}.build()
|
||||
)
|
||||
|
||||
// Don't enable missing network tags, because those are noisy.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy.Builder().apply {
|
||||
detectActivityLeaks()
|
||||
detectCleartextNetwork()
|
||||
detectContentUriWithoutPermission()
|
||||
detectFileUriExposure()
|
||||
detectLeakedClosableObjects()
|
||||
detectLeakedRegistrationObjects()
|
||||
detectLeakedSqlLiteObjects()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// Disable because this is mostly flagging Android X and Play Services
|
||||
// builder.detectNonSdkApiUsage();
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
} else {
|
||||
StrictMode.setVmPolicy(
|
||||
StrictMode.VmPolicy.Builder().apply {
|
||||
detectAll()
|
||||
penaltyLog()
|
||||
}.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.getaddress
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Displays the address associated with the seed defined by the default config. To modify the seed
|
||||
* that is used, update the `DemoConfig.seedWords` value.
|
||||
*/
|
||||
class GetAddressFragment : BaseDemoFragment<FragmentGetAddressBinding>() {
|
||||
|
||||
private lateinit var viewingKey: UnifiedViewingKey
|
||||
private lateinit var seed: ByteArray
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
* here for completeness so that each demo file can serve as a standalone example.
|
||||
*/
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
val seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
// the derivation tool can be used for generating keys and addresses
|
||||
viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }
|
||||
}
|
||||
|
||||
private fun displayAddress() {
|
||||
// a full fledged app would just get the address from the synchronizer
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
|
||||
val zaddress = DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))
|
||||
val taddress = DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))
|
||||
binding.textInfo.text = "z-addr:\n$zaddress\n\n\nt-addr:\n$taddress"
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: show an example with the synchronizer
|
||||
|
||||
//
|
||||
// Android Lifecycle overrides
|
||||
//
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
displayAddress()
|
||||
}
|
||||
|
||||
//
|
||||
// Base Fragment overrides
|
||||
//
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
copyToClipboard(
|
||||
DerivationTool.deriveShieldedAddress(
|
||||
viewingKey.extfvk,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
),
|
||||
"Shielded address copied to clipboard!"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetAddressBinding =
|
||||
FragmentGetAddressBinding.inflate(layoutInflater)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.getbalance
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Displays the available balance && total balance associated with the seed defined by the default config.
|
||||
* comments.
|
||||
*/
|
||||
class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
||||
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBalanceBinding =
|
||||
FragmentGetBalanceBinding.inflate(layoutInflater)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setup()
|
||||
}
|
||||
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
val seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
// converting seed into viewingKey
|
||||
val viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }
|
||||
|
||||
// using the ViewingKey to initialize
|
||||
runBlocking {
|
||||
Initializer.new(requireApplicationContext(), null) {
|
||||
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
|
||||
it.importWallet(viewingKey, network = ZcashNetwork.fromResources(requireApplicationContext()))
|
||||
}
|
||||
}.let { initializer ->
|
||||
synchronizer = Synchronizer.newBlocking(initializer)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// the lifecycleScope is used to dispose of the synchronize when the fragment dies
|
||||
synchronizer.start(lifecycleScope)
|
||||
monitorChanges()
|
||||
}
|
||||
|
||||
private fun monitorChanges() {
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.saplingBalances.filterNotNull().collectWith(lifecycleScope, ::onBalance)
|
||||
}
|
||||
|
||||
private fun onBalance(balance: WalletBalance) {
|
||||
binding.textBalance.text = """
|
||||
Available balance: ${balance.available.convertZatoshiToZecString(12)}
|
||||
Total balance: ${balance.total.convertZatoshiToZecString(12)}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun onStatus(status: Synchronizer.Status) {
|
||||
binding.textStatus.text = "Status: $status"
|
||||
val balance = synchronizer.saplingBalances.value
|
||||
if (null == balance) {
|
||||
binding.textBalance.text = "Calculating balance..."
|
||||
} else {
|
||||
onBalance(balance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProgress(i: Int) {
|
||||
if (i < 100) {
|
||||
binding.textStatus.text = "Downloading blocks...$i%"
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
||||
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.getblock
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import cash.z.ecc.android.sdk.demoapp.util.toHtml
|
||||
import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime
|
||||
import cash.z.ecc.android.sdk.demoapp.util.withCommas
|
||||
import cash.z.ecc.android.sdk.ext.toHex
|
||||
|
||||
/**
|
||||
* Retrieves a compact block from the lightwalletd service and displays basic information about it.
|
||||
* This demonstrates the basic ability to connect to the server, request a compact block and parse
|
||||
* the response.
|
||||
*/
|
||||
class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
|
||||
|
||||
private fun setBlockHeight(blockHeight: Int) {
|
||||
val blocks =
|
||||
lightwalletService?.getBlockRange(blockHeight..blockHeight)
|
||||
val block = blocks?.firstOrNull()
|
||||
binding.textInfo.visibility = View.VISIBLE
|
||||
binding.textInfo.text = Html.fromHtml(
|
||||
"""
|
||||
<b>block height:</b> ${block?.height.withCommas()}
|
||||
<br/><b>block time:</b> ${block?.time.toRelativeTime(requireApplicationContext())}
|
||||
<br/><b>number of shielded TXs:</b> ${block?.vtxCount}
|
||||
<br/><b>hash:</b> ${block?.hash?.toByteArray()?.toHex()}
|
||||
<br/><b>prevHash:</b> ${block?.prevHash?.toByteArray()?.toHex()}
|
||||
${block?.vtxList.toHtml()}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
private fun onApply(_unused: View? = null) {
|
||||
try {
|
||||
setBlockHeight(binding.textBlockHeight.text.toString().toInt())
|
||||
} catch (t: Throwable) {
|
||||
toast("Error: $t")
|
||||
}
|
||||
mainActivity()?.hideKeyboard()
|
||||
}
|
||||
|
||||
private fun loadNext(offset: Int) {
|
||||
val nextBlockHeight = (binding.textBlockHeight.text.toString().toIntOrNull() ?: -1) + offset
|
||||
binding.textBlockHeight.setText(nextBlockHeight.toString())
|
||||
onApply()
|
||||
}
|
||||
|
||||
//
|
||||
// Android Lifecycle overrides
|
||||
//
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.buttonApply.setOnClickListener(::onApply)
|
||||
binding.buttonPrevious.setOnClickListener {
|
||||
loadNext(-1)
|
||||
}
|
||||
binding.buttonNext.setOnClickListener {
|
||||
loadNext(1)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Base Fragment overrides
|
||||
//
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockBinding =
|
||||
FragmentGetBlockBinding.inflate(layoutInflater)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.getblockrange
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockRangeBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime
|
||||
import cash.z.ecc.android.sdk.demoapp.util.withCommas
|
||||
|
||||
/**
|
||||
* Retrieves a range of compact block from the lightwalletd service and displays basic information
|
||||
* about them. This demonstrates the basic ability to connect to the server, request a range of
|
||||
* compact block and parse the response. This could be augmented to display metadata about certain
|
||||
* block ranges for instance, to find the block with the most shielded transactions in a range.
|
||||
*/
|
||||
class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
|
||||
|
||||
private fun setBlockRange(blockRange: IntRange) {
|
||||
val start = System.currentTimeMillis()
|
||||
val blocks =
|
||||
lightwalletService?.getBlockRange(blockRange)
|
||||
val fetchDelta = System.currentTimeMillis() - start
|
||||
|
||||
// Note: This is a demo so we won't worry about iterating efficiently over these blocks
|
||||
|
||||
binding.textInfo.text = Html.fromHtml(
|
||||
blocks?.run {
|
||||
val count = size
|
||||
val emptyCount = count { it.vtxCount == 0 }
|
||||
val maxTxs = maxByOrNull { it.vtxCount }
|
||||
val maxIns = maxByOrNull { block ->
|
||||
block.vtxList.maxOfOrNull { it.spendsCount } ?: -1
|
||||
}
|
||||
val maxInTx = maxIns?.vtxList?.maxByOrNull { it.spendsCount }
|
||||
val maxOuts = maxByOrNull { block ->
|
||||
block.vtxList.maxOfOrNull { it.outputsCount } ?: -1
|
||||
}
|
||||
val maxOutTx = maxOuts?.vtxList?.maxByOrNull { it.outputsCount }
|
||||
val txCount = sumBy { it.vtxCount }
|
||||
val outCount = sumBy { block -> block.vtxList.sumBy { it.outputsCount } }
|
||||
val inCount = sumBy { block -> block.vtxList.sumBy { it.spendsCount } }
|
||||
|
||||
val processTime = System.currentTimeMillis() - start - fetchDelta
|
||||
@Suppress("MaxLineLength")
|
||||
"""
|
||||
<b>total blocks:</b> ${count.withCommas()}
|
||||
<br/><b>fetch time:</b> ${if (fetchDelta > 1000) "%.2f sec".format(fetchDelta / 1000.0) else "%d ms".format(fetchDelta)}
|
||||
<br/><b>process time:</b> ${if (processTime > 1000) "%.2f sec".format(processTime / 1000.0) else "%d ms".format(processTime)}
|
||||
<br/><b>block time range:</b> ${first().time.toRelativeTime(requireApplicationContext())}<br/>   to ${last().time.toRelativeTime(requireApplicationContext())}
|
||||
<br/><b>total empty blocks:</b> ${emptyCount.withCommas()}
|
||||
<br/><b>total TXs:</b> ${txCount.withCommas()}
|
||||
<br/><b>total outputs:</b> ${outCount.withCommas()}
|
||||
<br/><b>total inputs:</b> ${inCount.withCommas()}
|
||||
<br/><b>avg TXs/block:</b> ${"%.1f".format(txCount / count.toDouble())}
|
||||
<br/><b>avg TXs (excluding empty blocks):</b> ${"%.1f".format(txCount.toDouble() / (count - emptyCount))}
|
||||
<br/><b>avg OUTs [per block / per TX]:</b> ${"%.1f / %.1f".format(outCount.toDouble() / (count - emptyCount), outCount.toDouble() / txCount)}
|
||||
<br/><b>avg INs [per block / per TX]:</b> ${"%.1f / %.1f".format(inCount.toDouble() / (count - emptyCount), inCount.toDouble() / txCount)}
|
||||
<br/><b>most shielded TXs:</b> ${if (maxTxs == null) "none" else "${maxTxs.vtxCount} in block ${maxTxs.height.withCommas()}"}
|
||||
<br/><b>most shielded INs:</b> ${if (maxInTx == null) "none" else "${maxInTx.spendsCount} in block ${maxIns?.height.withCommas()} at tx index ${maxInTx.index}"}
|
||||
<br/><b>most shielded OUTs:</b> ${if (maxOutTx == null) "none" else "${maxOutTx?.outputsCount} in block ${maxOuts?.height.withCommas()} at tx index ${maxOutTx?.index}"}
|
||||
""".trimIndent()
|
||||
} ?: "No blocks found in that range."
|
||||
)
|
||||
}
|
||||
|
||||
private fun onApply(_unused: View) {
|
||||
val start = binding.textStartHeight.text.toString().toInt()
|
||||
val end = binding.textEndHeight.text.toString().toInt()
|
||||
if (start <= end) {
|
||||
try {
|
||||
with(binding.buttonApply) {
|
||||
isEnabled = false
|
||||
setText(R.string.loading)
|
||||
binding.textInfo.setText(R.string.loading)
|
||||
post {
|
||||
setBlockRange(start..end)
|
||||
isEnabled = true
|
||||
setText(R.string.apply)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
setError(t.toString())
|
||||
}
|
||||
} else {
|
||||
setError("Invalid range")
|
||||
}
|
||||
mainActivity()?.hideKeyboard()
|
||||
}
|
||||
|
||||
private fun setError(message: String) {
|
||||
binding.textInfo.text = "Error: $message"
|
||||
}
|
||||
|
||||
//
|
||||
// Android Lifecycle overrides
|
||||
//
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.buttonApply.setOnClickListener(::onApply)
|
||||
}
|
||||
|
||||
//
|
||||
// Base Fragment overrides
|
||||
//
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetBlockRangeBinding =
|
||||
FragmentGetBlockRangeBinding.inflate(layoutInflater)
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
super.onActionButtonClicked()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.getlatestheight
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetLatestHeightBinding
|
||||
|
||||
/**
|
||||
* Retrieves the latest block height from the lightwalletd server. This is the simplest test for
|
||||
* connectivity with the server. Modify the `host` and the `port` inside of
|
||||
* `App.instance.defaultConfig` to check the SDK's ability to communicate with a given lightwalletd
|
||||
* instance.
|
||||
*/
|
||||
class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>() {
|
||||
|
||||
private fun displayLatestHeight() {
|
||||
// note: this is a blocking call, a real app wouldn't do this on the main thread
|
||||
// instead, a production app would leverage the synchronizer like in the other demos
|
||||
binding.textInfo.text = lightwalletService?.getLatestBlockHeight().toString()
|
||||
}
|
||||
|
||||
//
|
||||
// Android Lifecycle overrides
|
||||
//
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
displayLatestHeight()
|
||||
}
|
||||
|
||||
//
|
||||
// Base Fragment overrides
|
||||
//
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetLatestHeightBinding =
|
||||
FragmentGetLatestHeightBinding.inflate(layoutInflater)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.getprivatekey
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Displays the viewing key and spending key associated with the seed used during the demo. The
|
||||
* seedPhrase defaults to the value of`DemoConfig.seedWords` but can be set by the user on the
|
||||
* HomeFragment.
|
||||
*/
|
||||
class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
|
||||
|
||||
private lateinit var seedPhrase: String
|
||||
private lateinit var seed: ByteArray
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
* here for completeness so that each demo file can serve as a standalone example.
|
||||
*/
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
}
|
||||
|
||||
private fun displayKeys() {
|
||||
// derive the keys from the seed:
|
||||
// demonstrate deriving spending keys for five accounts but only take the first one
|
||||
lifecycleScope.launchWhenStarted {
|
||||
val spendingKey = DerivationTool.deriveSpendingKeys(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext()),
|
||||
5
|
||||
).first()
|
||||
|
||||
// derive the key that allows you to view but not spend transactions
|
||||
val viewingKey = DerivationTool.deriveViewingKey(
|
||||
spendingKey,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
)
|
||||
|
||||
// display the keys in the UI
|
||||
binding.textInfo.setText("Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey")
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Android Lifecycle overrides
|
||||
//
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
displayKeys()
|
||||
}
|
||||
|
||||
//
|
||||
// Base Fragment overrides
|
||||
//
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
lifecycleScope.launch {
|
||||
copyToClipboard(
|
||||
DerivationTool.deriveUnifiedViewingKeys(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
).first().extpub,
|
||||
"ViewingKey copied to clipboard!"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentGetPrivateKeyBinding =
|
||||
FragmentGetPrivateKeyBinding.inflate(layoutInflater)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.home
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentHomeBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* The landing page for the demo. Every time the app returns to this screen, it clears all demo
|
||||
* data just for sanity. The goal is for each demo to be self-contained so that the behavior is
|
||||
* repeatable and independent of pre-existing state.
|
||||
*/
|
||||
class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
|
||||
|
||||
private val homeViewModel: HomeViewModel by viewModels()
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater) =
|
||||
FragmentHomeBinding.inflate(layoutInflater)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.textSeedPhrase.setOnClickListener(::onEditSeedPhrase)
|
||||
binding.buttonPaste.setOnClickListener(::onPasteSeedPhrase)
|
||||
binding.buttonAccept.setOnClickListener(::onAcceptSeedPhrase)
|
||||
binding.buttonCancel.setOnClickListener(::onCancelSeedPhrase)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
mainActivity()?.setClipboardListener(::updatePasteButton)
|
||||
|
||||
lifecycleScope.launch {
|
||||
sharedViewModel.seedPhrase.collect {
|
||||
binding.textSeedPhrase.text = "Seed Phrase: ${it.toAbbreviatedPhrase()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
mainActivity()?.removeClipboardListener()
|
||||
}
|
||||
|
||||
private fun onEditSeedPhrase(unused: View) {
|
||||
setEditShown(true)
|
||||
binding.inputSeedPhrase.setText(sharedViewModel.seedPhrase.value)
|
||||
binding.textLayoutSeedPhrase.helperText = ""
|
||||
}
|
||||
|
||||
private fun onAcceptSeedPhrase(unused: View) {
|
||||
if (applySeedPhrase()) {
|
||||
setEditShown(false)
|
||||
binding.inputSeedPhrase.setText("")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCancelSeedPhrase(unused: View) {
|
||||
setEditShown(false)
|
||||
}
|
||||
|
||||
private fun onPasteSeedPhrase(unused: View) {
|
||||
mainActivity()?.getClipboardText().let { clipboardText ->
|
||||
binding.inputSeedPhrase.setText(clipboardText)
|
||||
applySeedPhrase()
|
||||
}
|
||||
}
|
||||
|
||||
private fun applySeedPhrase(): Boolean {
|
||||
val newPhrase = binding.inputSeedPhrase.text.toString()
|
||||
return if (!sharedViewModel.updateSeedPhrase(newPhrase)) {
|
||||
binding.textLayoutSeedPhrase.helperText = "Invalid seed phrase"
|
||||
binding.textLayoutSeedPhrase.setHelperTextColor(ColorStateList.valueOf(Color.RED))
|
||||
false
|
||||
} else {
|
||||
binding.textLayoutSeedPhrase.helperText = "valid seed phrase"
|
||||
binding.textLayoutSeedPhrase.setHelperTextColor(ColorStateList.valueOf(Color.GREEN))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEditShown(isShown: Boolean) {
|
||||
with(binding) {
|
||||
textSeedPhrase.visibility = if (isShown) View.GONE else View.VISIBLE
|
||||
textInstructions.visibility = if (isShown) View.GONE else View.VISIBLE
|
||||
groupEdit.visibility = if (isShown) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePasteButton(clipboardText: String? = mainActivity()?.getClipboardText()) {
|
||||
clipboardText.let {
|
||||
val isEditing = binding.groupEdit.visibility == View.VISIBLE
|
||||
if (isEditing && (it != null && it.split(' ').size > 2)) {
|
||||
binding.buttonPaste.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.buttonPaste.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toAbbreviatedPhrase(): String {
|
||||
this.trim().apply {
|
||||
val firstSpace = indexOf(' ')
|
||||
val lastSpace = lastIndexOf(' ')
|
||||
return if (firstSpace != -1 && lastSpace >= firstSpace) {
|
||||
"${take(firstSpace)}...${takeLast(length - 1 - lastSpace)}"
|
||||
} else this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class HomeViewModel : ViewModel()
|
||||
@@ -0,0 +1,154 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* List all transactions related to the given seed, since the given birthday. This begins by
|
||||
* downloading any missing blocks and then validating and scanning their contents. Once scan is
|
||||
* complete, the transactions are available in the database and can be accessed by any SQL tool.
|
||||
* By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the
|
||||
* database in a paged format that works natively with RecyclerViews.
|
||||
*/
|
||||
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
|
||||
private lateinit var initializer: Initializer
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var adapter: TransactionAdapter<ConfirmedTransaction>
|
||||
private lateinit var address: String
|
||||
private var status: Synchronizer.Status? = null
|
||||
private val isSynced get() = status == Synchronizer.Status.SYNCED
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
* here for completeness so that each demo file can serve as a standalone example.
|
||||
*/
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
var seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
initializer = runBlocking {
|
||||
Initializer.new(requireApplicationContext()) {
|
||||
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
|
||||
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
|
||||
}
|
||||
}
|
||||
address = runBlocking {
|
||||
DerivationTool.deriveShieldedAddress(
|
||||
seed,
|
||||
ZcashNetwork.fromResources(requireApplicationContext())
|
||||
)
|
||||
}
|
||||
synchronizer = Synchronizer.newBlocking(initializer)
|
||||
}
|
||||
|
||||
private fun initTransactionUI() {
|
||||
binding.recyclerTransactions.layoutManager =
|
||||
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||
adapter = TransactionAdapter()
|
||||
binding.recyclerTransactions.adapter = adapter
|
||||
}
|
||||
|
||||
private fun monitorChanges() {
|
||||
// the lifecycleScope is used to stop everything when the fragment dies
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
|
||||
}
|
||||
|
||||
//
|
||||
// Change listeners
|
||||
//
|
||||
|
||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
||||
if (info.isScanning) binding.textInfo.text = "Scanning blocks...${info.scanProgress}%"
|
||||
}
|
||||
|
||||
private fun onProgress(i: Int) {
|
||||
if (i < 100) binding.textInfo.text = "Downloading blocks...$i%"
|
||||
}
|
||||
|
||||
private fun onStatus(status: Synchronizer.Status) {
|
||||
this.status = status
|
||||
binding.textStatus.text = "Status: $status"
|
||||
if (isSynced) onSyncComplete()
|
||||
}
|
||||
|
||||
private fun onSyncComplete() {
|
||||
binding.textInfo.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
private fun onTransactionsUpdated(transactions: List<ConfirmedTransaction>) {
|
||||
twig("got a new paged list of transactions")
|
||||
adapter.submitList(transactions)
|
||||
|
||||
// show message when there are no transactions
|
||||
if (isSynced) {
|
||||
binding.textInfo.apply {
|
||||
if (transactions.isEmpty()) {
|
||||
visibility = View.VISIBLE
|
||||
text =
|
||||
"No transactions found. Try to either change the seed words " +
|
||||
"or send funds to this address (tap the FAB to copy it):\n\n $address"
|
||||
} else {
|
||||
visibility = View.INVISIBLE
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Android Lifecycle overrides
|
||||
//
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initTransactionUI()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// the lifecycleScope is used to dispose of the synchronizer when the fragment dies
|
||||
synchronizer.start(lifecycleScope)
|
||||
monitorChanges()
|
||||
}
|
||||
|
||||
//
|
||||
// Base Fragment overrides
|
||||
//
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
if (::address.isInitialized) copyToClipboard(address)
|
||||
}
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListTransactionsBinding =
|
||||
FragmentListTransactionsBinding.inflate(layoutInflater)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
|
||||
/**
|
||||
* Simple adapter implementation that knows how to bind a recyclerview to ClearedTransactions.
|
||||
*/
|
||||
class TransactionAdapter<T : ConfirmedTransaction> :
|
||||
ListAdapter<T, TransactionViewHolder<T>>(
|
||||
object : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: T,
|
||||
newItem: T
|
||||
) = oldItem.minedHeight == newItem.minedHeight
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: T,
|
||||
newItem: T
|
||||
) = oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
) = TransactionViewHolder<T>(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.item_transaction, parent, false)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: TransactionViewHolder<T>,
|
||||
position: Int
|
||||
) = holder.bindTo(getItem(position))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Simple view holder for displaying confirmed transactions in the recyclerview.
|
||||
*/
|
||||
class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
|
||||
private val infoText = itemView.findViewById<TextView>(R.id.text_transaction_info)
|
||||
private val timeText = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
|
||||
private val icon = itemView.findViewById<ImageView>(R.id.image_transaction_type)
|
||||
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
|
||||
|
||||
fun bindTo(transaction: T?) {
|
||||
val isInbound = transaction?.toAddress.isNullOrEmpty()
|
||||
amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
|
||||
timeText.text =
|
||||
if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending"
|
||||
else formatter.format(transaction.blockTimeInSeconds * 1000L)
|
||||
infoText.text = getMemoString(transaction)
|
||||
|
||||
icon.rotation = if (isInbound) 0f else 180f
|
||||
icon.rotation = if (isInbound) 0f else 180f
|
||||
icon.setColorFilter(ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound))
|
||||
}
|
||||
|
||||
private fun getMemoString(transaction: T?): String {
|
||||
return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.listutxos
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.DemoConstants
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListUtxosBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* ===============================================================================================
|
||||
* NOTE: this is still a WIP because t-addrs are not officially supported by the SDK yet
|
||||
* ===============================================================================================
|
||||
*
|
||||
*
|
||||
* List all transactions related to the given seed, since the given birthday. This begins by
|
||||
* downloading any missing blocks and then validating and scanning their contents. Once scan is
|
||||
* complete, the transactions are available in the database and can be accessed by any SQL tool.
|
||||
* By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the
|
||||
* database in a paged format that works natively with RecyclerViews.
|
||||
*/
|
||||
class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
||||
private lateinit var seed: ByteArray
|
||||
private lateinit var initializer: Initializer
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
private lateinit var adapter: UtxoAdapter<ConfirmedTransaction>
|
||||
private val address: String = "t1RwbKka1CnktvAJ1cSqdn7c6PXWG4tZqgd"
|
||||
private var status: Synchronizer.Status? = null
|
||||
|
||||
private val isSynced get() = status == Synchronizer.Status.SYNCED
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentListUtxosBinding =
|
||||
FragmentListUtxosBinding.inflate(layoutInflater)
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
* here for completeness so that each demo file can serve as a standalone example.
|
||||
*/
|
||||
private fun setup() {
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
|
||||
initializer = runBlocking {
|
||||
Initializer.new(requireApplicationContext()) {
|
||||
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
|
||||
it.alias = "Demo_Utxos"
|
||||
}
|
||||
}
|
||||
synchronizer = runBlocking { Synchronizer.new(initializer) }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setup()
|
||||
}
|
||||
|
||||
fun initUi() {
|
||||
binding.inputAddress.setText(address)
|
||||
binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString())
|
||||
binding.inputRangeEnd.setText(DemoConstants.utxoEndHeight.toString())
|
||||
|
||||
binding.buttonLoad.setOnClickListener {
|
||||
mainActivity()?.hideKeyboard()
|
||||
downloadTransactions()
|
||||
}
|
||||
|
||||
initTransactionUi()
|
||||
}
|
||||
|
||||
fun downloadTransactions() {
|
||||
binding.textStatus.text = "loading..."
|
||||
binding.textStatus.post {
|
||||
binding.textStatus.requestFocus()
|
||||
val addressToUse = binding.inputAddress.text.toString()
|
||||
val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight
|
||||
val endToUse = binding.inputRangeEnd.text.toString().toIntOrNull() ?: DemoConstants.utxoEndHeight
|
||||
var allStart = now
|
||||
twig("loading transactions in range $startToUse..$endToUse")
|
||||
val txids = lightwalletService?.getTAddressTransactions(addressToUse, startToUse..endToUse)
|
||||
var delta = now - allStart
|
||||
updateStatus("found ${txids?.size} transactions in ${delta}ms.", false)
|
||||
|
||||
txids?.map {
|
||||
it.data.apply {
|
||||
try {
|
||||
runBlocking { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) }
|
||||
} catch (t: Throwable) {
|
||||
twig("failed to decrypt and store transaction due to: $t")
|
||||
}
|
||||
}
|
||||
}?.let { txData ->
|
||||
// Disabled during migration to newer SDK version; this appears to have been
|
||||
// leveraging non-public APIs in the SDK so perhaps should be removed
|
||||
// val parseStart = now
|
||||
// val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build()
|
||||
// val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList)
|
||||
// delta = now - parseStart
|
||||
// updateStatus("parsed txs in ${delta}ms.")
|
||||
}
|
||||
(synchronizer as SdkSynchronizer).refreshTransactions()
|
||||
// val finalCount = (synchronizer as SdkSynchronizer).getTransactionCount()
|
||||
// "found ${finalCount - initialCount} shielded outputs.
|
||||
delta = now - allStart
|
||||
updateStatus("Total time ${delta}ms.")
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
finalCount = (synchronizer as SdkSynchronizer).getTransactionCount()
|
||||
withContext(Dispatchers.Main) {
|
||||
delay(100)
|
||||
updateStatus("Also found ${finalCount - initialCount} shielded txs")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val now get() = System.currentTimeMillis()
|
||||
|
||||
private fun updateStatus(message: String, append: Boolean = true) {
|
||||
if (append) {
|
||||
binding.textStatus.text = "${binding.textStatus.text} $message"
|
||||
} else {
|
||||
binding.textStatus.text = message
|
||||
}
|
||||
twig(message)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initUi()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resetInBackground()
|
||||
val seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
|
||||
binding.inputAddress.setText(DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext())))
|
||||
}
|
||||
}
|
||||
|
||||
var initialCount: Int = 0
|
||||
var finalCount: Int = 0
|
||||
fun resetInBackground() {
|
||||
try {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
synchronizer.prepare()
|
||||
initialCount = (synchronizer as SdkSynchronizer).getTransactionCount()
|
||||
}
|
||||
}
|
||||
synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
|
||||
// synchronizer.receivedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
|
||||
} catch (t: Throwable) {
|
||||
twig("failed to start the synchronizer!!! due to : $t")
|
||||
}
|
||||
}
|
||||
|
||||
fun onResetComplete() {
|
||||
initTransactionUi()
|
||||
startSynchronizer()
|
||||
monitorStatus()
|
||||
}
|
||||
|
||||
fun onClear() {
|
||||
synchronizer.stop()
|
||||
}
|
||||
|
||||
private fun initTransactionUi() {
|
||||
binding.recyclerTransactions.layoutManager =
|
||||
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||
adapter = UtxoAdapter()
|
||||
binding.recyclerTransactions.adapter = adapter
|
||||
// lifecycleScope.launch {
|
||||
// // address = synchronizer.getAddress()
|
||||
// synchronizer.receivedTransactions.onEach {
|
||||
// onTransactionsUpdated(it)
|
||||
// }.launchIn(this)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun startSynchronizer() {
|
||||
lifecycleScope.apply {
|
||||
synchronizer.start(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun monitorStatus() {
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
}
|
||||
|
||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
||||
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
|
||||
}
|
||||
|
||||
private fun onProgress(i: Int) {
|
||||
if (i < 100) binding.textStatus.text = "Downloading blocks...$i%"
|
||||
}
|
||||
|
||||
private fun onStatus(status: Synchronizer.Status) {
|
||||
this.status = status
|
||||
binding.textStatus.text = "Status: $status"
|
||||
if (isSynced) onSyncComplete()
|
||||
}
|
||||
|
||||
private fun onSyncComplete() {
|
||||
binding.textStatus.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
private fun onTransactionsUpdated(transactions: List<ConfirmedTransaction>) {
|
||||
twig("got a new paged list of transactions of size ${transactions.size}")
|
||||
adapter.submitList(transactions)
|
||||
}
|
||||
|
||||
override fun onActionButtonClicked() {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
twig("current count: ${(synchronizer as SdkSynchronizer).getTransactionCount()}")
|
||||
twig("refreshing transactions")
|
||||
(synchronizer as SdkSynchronizer).refreshTransactions()
|
||||
twig("current count: ${(synchronizer as SdkSynchronizer).getTransactionCount()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.listutxos
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
|
||||
/**
|
||||
* Simple adapter implementation that knows how to bind a recyclerview to ClearedTransactions.
|
||||
*/
|
||||
class UtxoAdapter<T : ConfirmedTransaction> :
|
||||
ListAdapter<T, UtxoViewHolder<T>>(
|
||||
object : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: T,
|
||||
newItem: T
|
||||
) = oldItem.minedHeight == newItem.minedHeight
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: T,
|
||||
newItem: T
|
||||
) = oldItem == newItem
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
) = UtxoViewHolder<T>(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.item_transaction, parent, false)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: UtxoViewHolder<T>,
|
||||
position: Int
|
||||
) = holder.bindTo(getItem(position))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.listutxos
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.valueInZatoshi
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Simple view holder for displaying confirmed transactions in the recyclerview.
|
||||
*/
|
||||
class UtxoViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
|
||||
private val infoText = itemView.findViewById<TextView>(R.id.text_transaction_info)
|
||||
private val timeText = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
|
||||
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
|
||||
|
||||
fun bindTo(transaction: T?) {
|
||||
amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
|
||||
timeText.text =
|
||||
if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending"
|
||||
else formatter.format(transaction.blockTimeInSeconds * 1000L)
|
||||
infoText.text = getMemoString(transaction)
|
||||
}
|
||||
|
||||
private fun getMemoString(transaction: T?): String {
|
||||
return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.demos.send
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import cash.z.ecc.android.sdk.Initializer
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
|
||||
import cash.z.ecc.android.sdk.db.entity.isCreated
|
||||
import cash.z.ecc.android.sdk.db.entity.isCreating
|
||||
import cash.z.ecc.android.sdk.db.entity.isFailedEncoding
|
||||
import cash.z.ecc.android.sdk.db.entity.isFailedSubmit
|
||||
import cash.z.ecc.android.sdk.db.entity.isMined
|
||||
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||
import cash.z.ecc.android.sdk.demoapp.DemoConstants
|
||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding
|
||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
||||
import cash.z.ecc.android.sdk.ext.toZecString
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.internal.twig
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.android.sdk.type.WalletBalance
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Demonstrates sending funds to an address. This is the most complex example that puts all of the
|
||||
* pieces of the SDK together, including monitoring transactions for completion. It begins by
|
||||
* downloading, validating and scanning any missing blocks. Once that is complete, the wallet is
|
||||
* in a SYNCED state and available to send funds. Calling `sendToAddress` produces a flow of
|
||||
* PendingTransaction objects which represent the active state of the transaction that was sent.
|
||||
* Any time the state of that transaction changes, a new instance will be emitted.
|
||||
*/
|
||||
class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
||||
private lateinit var synchronizer: Synchronizer
|
||||
|
||||
private lateinit var amountInput: TextView
|
||||
private lateinit var addressInput: TextView
|
||||
|
||||
// in a normal app, this would be stored securely with the trusted execution environment (TEE)
|
||||
// but since this is a demo, we'll derive it on the fly
|
||||
private lateinit var spendingKey: String
|
||||
|
||||
/**
|
||||
* Initialize the required values that would normally live outside the demo but are repeated
|
||||
* here for completeness so that each demo file can serve as a standalone example.
|
||||
*/
|
||||
private fun setup() {
|
||||
// defaults to the value of `DemoConfig.seedWords` but can also be set by the user
|
||||
var seedPhrase = sharedViewModel.seedPhrase.value
|
||||
|
||||
// Use a BIP-39 library to convert a seed phrase into a byte array. Most wallets already
|
||||
// have the seed stored
|
||||
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
|
||||
|
||||
runBlocking {
|
||||
Initializer.new(requireApplicationContext()) {
|
||||
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) }
|
||||
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext()))
|
||||
}
|
||||
}.let { initializer ->
|
||||
synchronizer = Synchronizer.newBlocking(initializer)
|
||||
}
|
||||
spendingKey = runBlocking { DerivationTool.deriveSpendingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }
|
||||
}
|
||||
|
||||
//
|
||||
// Observable properties (done without livedata or flows for simplicity)
|
||||
//
|
||||
|
||||
private var balance: WalletBalance? = null
|
||||
set(value) {
|
||||
field = value
|
||||
onUpdateSendButton()
|
||||
}
|
||||
private var isSending = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) Twig.sprout("Sending") else Twig.clip("Sending")
|
||||
onUpdateSendButton()
|
||||
}
|
||||
private var isSyncing = true
|
||||
set(value) {
|
||||
field = value
|
||||
onUpdateSendButton()
|
||||
}
|
||||
|
||||
//
|
||||
// Private functions
|
||||
//
|
||||
|
||||
private fun initSendUi() {
|
||||
amountInput = binding.inputAmount.apply {
|
||||
setText(DemoConstants.sendAmount.toZecString())
|
||||
}
|
||||
addressInput = binding.inputAddress.apply {
|
||||
setText(DemoConstants.toAddress)
|
||||
}
|
||||
binding.buttonSend.setOnClickListener(::onSend)
|
||||
}
|
||||
|
||||
private fun monitorChanges() {
|
||||
synchronizer.status.collectWith(lifecycleScope, ::onStatus)
|
||||
synchronizer.progress.collectWith(lifecycleScope, ::onProgress)
|
||||
synchronizer.processorInfo.collectWith(lifecycleScope, ::onProcessorInfoUpdated)
|
||||
synchronizer.saplingBalances.collectWith(lifecycleScope, ::onBalance)
|
||||
}
|
||||
|
||||
//
|
||||
// Change listeners
|
||||
//
|
||||
|
||||
private fun onStatus(status: Synchronizer.Status) {
|
||||
binding.textStatus.text = "Status: $status"
|
||||
isSyncing = status != Synchronizer.Status.SYNCED
|
||||
if (status == Synchronizer.Status.SCANNING) {
|
||||
binding.textBalance.text = "Calculating balance..."
|
||||
} else {
|
||||
if (!isSyncing) onBalance(balance)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProgress(i: Int) {
|
||||
if (i < 100) {
|
||||
binding.textStatus.text = "Downloading blocks...$i%"
|
||||
binding.textBalance.visibility = View.INVISIBLE
|
||||
} else {
|
||||
binding.textBalance.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
||||
if (info.isScanning) binding.textStatus.text = "Scanning blocks...${info.scanProgress}%"
|
||||
}
|
||||
|
||||
private fun onBalance(balance: WalletBalance?) {
|
||||
this.balance = balance
|
||||
if (!isSyncing) {
|
||||
binding.textBalance.text = """
|
||||
Available balance: ${balance?.available.convertZatoshiToZecString(12)}
|
||||
Total balance: ${balance?.total.convertZatoshiToZecString(12)}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSend(unused: View) {
|
||||
isSending = true
|
||||
val amount = amountInput.text.toString().toDouble().convertZecToZatoshi()
|
||||
val toAddress = addressInput.text.toString().trim()
|
||||
synchronizer.sendToAddress(
|
||||
spendingKey,
|
||||
amount,
|
||||
toAddress,
|
||||
"Funds from Demo App"
|
||||
).collectWith(lifecycleScope, ::onPendingTxUpdated)
|
||||
mainActivity()?.hideKeyboard()
|
||||
}
|
||||
|
||||
private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) {
|
||||
val id = pendingTransaction?.id ?: -1
|
||||
val message = when {
|
||||
pendingTransaction == null -> "Transaction not found"
|
||||
pendingTransaction.isMined() -> "Transaction Mined (id: $id)!\n\nSEND COMPLETE".also { isSending = false }
|
||||
pendingTransaction.isSubmitSuccess() -> "Successfully submitted transaction!\nAwaiting confirmation..."
|
||||
pendingTransaction.isFailedEncoding() -> "ERROR: failed to encode transaction! (id: $id)".also { isSending = false }
|
||||
pendingTransaction.isFailedSubmit() -> "ERROR: failed to submit transaction! (id: $id)".also { isSending = false }
|
||||
pendingTransaction.isCreated() -> "Transaction creation complete! (id: $id)"
|
||||
pendingTransaction.isCreating() -> "Creating transaction!".also { onResetInfo() }
|
||||
else -> "Transaction updated!".also { twig("Unhandled TX state: $pendingTransaction") }
|
||||
}
|
||||
twig("Pending TX Updated: $message")
|
||||
binding.textInfo.apply {
|
||||
text = "$text\n$message"
|
||||
}
|
||||
}
|
||||
|
||||
private fun onUpdateSendButton() {
|
||||
with(binding.buttonSend) {
|
||||
when {
|
||||
isSending -> {
|
||||
text = "➡ sending"
|
||||
isEnabled = false
|
||||
}
|
||||
isSyncing -> {
|
||||
text = "⌛ syncing"
|
||||
isEnabled = false
|
||||
}
|
||||
(balance?.available?.value ?: 0) <= 0 -> isEnabled = false
|
||||
else -> {
|
||||
text = "send"
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onResetInfo() {
|
||||
binding.textInfo.text = "Active Transaction:"
|
||||
}
|
||||
|
||||
//
|
||||
// Android Lifecycle overrides
|
||||
//
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
setup()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initSendUi()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// the lifecycleScope is used to dispose of the synchronizer when the fragment dies
|
||||
synchronizer.start(lifecycleScope)
|
||||
monitorChanges()
|
||||
}
|
||||
|
||||
//
|
||||
// BaseDemoFragment overrides
|
||||
//
|
||||
|
||||
override fun inflateBinding(layoutInflater: LayoutInflater): FragmentSendBinding =
|
||||
FragmentSendBinding.inflate(layoutInflater)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@file:Suppress("ktlint:filename")
|
||||
|
||||
package cash.z.ecc.android.sdk.demoapp.ext
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
/**
|
||||
* A safer alternative to [Fragment.requireContext], as it avoids leaking Fragment or Activity context
|
||||
* when Application context is often sufficient.
|
||||
*/
|
||||
fun Fragment.requireApplicationContext(): Context = requireContext().applicationContext
|
||||
@@ -0,0 +1,40 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.util
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateUtils
|
||||
import androidx.fragment.app.Fragment
|
||||
import cash.z.ecc.android.sdk.demoapp.MainActivity
|
||||
import cash.z.wallet.sdk.rpc.CompactFormats
|
||||
|
||||
/**
|
||||
* Lazy extensions to make demo life easier.
|
||||
*/
|
||||
|
||||
fun Fragment.mainActivity() = context as? MainActivity
|
||||
|
||||
/**
|
||||
* Add locale-specific commas to a number, if it exists.
|
||||
*/
|
||||
fun Number?.withCommas() = this?.let { "%,d".format(it) } ?: "Unknown"
|
||||
|
||||
/**
|
||||
* Convert date time in seconds to relative time like (4 days ago).
|
||||
*/
|
||||
fun Int?.toRelativeTime(context: Context) =
|
||||
this?.let { timeInSeconds ->
|
||||
DateUtils.getRelativeDateTimeString(
|
||||
context,
|
||||
timeInSeconds * 1000L,
|
||||
DateUtils.SECOND_IN_MILLIS,
|
||||
DateUtils.WEEK_IN_MILLIS,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_YEAR or DateUtils.FORMAT_ABBREV_MONTH
|
||||
).toString()
|
||||
} ?: "Unknown"
|
||||
|
||||
fun List<CompactFormats.CompactTx>?.toHtml() =
|
||||
this.takeUnless { it.isNullOrEmpty() }?.let { txs ->
|
||||
buildString {
|
||||
append("<br/><b>transactions (shielded INs / OUTs):</b>")
|
||||
txs.forEach { append("<br/><b> tx${it.index}:</b> ${it.spendsCount} / ${it.outputsCount}") }
|
||||
}
|
||||
} ?: ""
|
||||
@@ -0,0 +1,13 @@
|
||||
@file:Suppress("ktlint:filename")
|
||||
|
||||
package cash.z.ecc.android.sdk.demoapp.util
|
||||
|
||||
import android.content.Context
|
||||
import cash.z.ecc.android.sdk.demoapp.R
|
||||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
|
||||
fun ZcashNetwork.Companion.fromResources(context: Context) = ZcashNetwork.valueOf(
|
||||
context.getString(
|
||||
R.string.network_name
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.util
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@Deprecated(
|
||||
message = "Do not use this! It is insecure and only intended for demo purposes to " +
|
||||
"show how to bridge to an existing key storage mechanism. Instead, use the Android " +
|
||||
"Keystore system or a 3rd party library that leverages it."
|
||||
)
|
||||
class SampleStorage(context: Context) {
|
||||
|
||||
private val prefs =
|
||||
context.applicationContext.getSharedPreferences("ExtremelyInsecureStorage", Context.MODE_PRIVATE)
|
||||
|
||||
fun saveSensitiveString(key: String, value: String) {
|
||||
prefs.edit().putString(key, value).apply()
|
||||
}
|
||||
|
||||
fun loadSensitiveString(key: String): String? = prefs.getString(key, null)
|
||||
|
||||
fun saveSensitiveBytes(key: String, value: ByteArray) {
|
||||
saveSensitiveString(key, value.toString(Charsets.ISO_8859_1))
|
||||
}
|
||||
|
||||
fun loadSensitiveBytes(key: String): ByteArray? =
|
||||
prefs.getString(key, null)?.toByteArray(Charsets.ISO_8859_1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple demonstration of how to take existing code that securely stores data and bridge it into
|
||||
* the SDK. This class delegates to the storage object. For demo purposes, we're using an insecure
|
||||
* SampleStorage implementation but this can easily be swapped for a truly secure storage solution.
|
||||
*/
|
||||
class SampleStorageBridge(context: Context) {
|
||||
private val delegate = SampleStorage(context.applicationContext)
|
||||
|
||||
/**
|
||||
* Just a sugar method to help with being explicit in sample code. We want to show developers
|
||||
* our intention that they write simple bridges to secure storage components.
|
||||
*/
|
||||
fun securelyStoreSeed(seed: ByteArray): SampleStorageBridge {
|
||||
delegate.saveSensitiveBytes(KEY_SEED, seed)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Just a sugar method to help with being explicit in sample code. We want to show developers
|
||||
* our intention that they write simple bridges to secure storage components.
|
||||
*/
|
||||
fun securelyStorePrivateKey(key: String): SampleStorageBridge {
|
||||
delegate.saveSensitiveString(KEY_PK, key)
|
||||
return this
|
||||
}
|
||||
|
||||
val seed: ByteArray get() = delegate.loadSensitiveBytes(KEY_SEED)!!
|
||||
val key get() = delegate.loadSensitiveString(KEY_PK)!!
|
||||
|
||||
companion object {
|
||||
private const val KEY_SEED = "cash.z.ecc.android.sdk.demoapp.SEED"
|
||||
private const val KEY_PK = "cash.z.ecc.android.sdk.demoapp.PK"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cash.z.ecc.android.sdk.demoapp.util
|
||||
|
||||
import cash.z.android.plugin.MnemonicPlugin
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
|
||||
import cash.z.ecc.android.bip39.Mnemonics.WordCount
|
||||
import cash.z.ecc.android.bip39.toEntropy
|
||||
import cash.z.ecc.android.bip39.toSeed
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* A sample implementation of a plugin for handling Mnemonic phrases. Any library can easily be
|
||||
* plugged into the SDK in this manner. In this case, we are wrapping a few example 3rd party
|
||||
* libraries with a thin layer that converts from their API to ours via the MnemonicPlugin
|
||||
* interface. We do not endorse these libraries, rather we just use them as an example of how to
|
||||
* take existing infrastructure and plug it into the SDK.
|
||||
*/
|
||||
class SimpleMnemonics : MnemonicPlugin {
|
||||
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
|
||||
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
|
||||
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
|
||||
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
|
||||
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
|
||||
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
|
||||
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
|
||||
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
android:width="108dp">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
10
demo-app/src/main/res/drawable/ic_baseline_check_24.xml
Normal file
10
demo-app/src/main/res/drawable/ic_baseline_check_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
||||
10
demo-app/src/main/res/drawable/ic_baseline_close_24.xml
Normal file
10
demo-app/src/main/res/drawable/ic_baseline_close_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
10
demo-app/src/main/res/drawable/ic_baseline_edit_24.xml
Normal file
10
demo-app/src/main/res/drawable/ic_baseline_edit_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,3L4.99,3c-1.11,0 -1.98,0.9 -1.98,2L3,19c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34 -3,-3L4.99,15L4.99,5L19,5v10zM16,10h-2L14,7h-4v3L8,10l4,4 4,-4z"/>
|
||||
</vector>
|
||||
5
demo-app/src/main/res/drawable/ic_floating_action.xml
Normal file
5
demo-app/src/main/res/drawable/ic_floating_action.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z"/>
|
||||
</vector>
|
||||
171
demo-app/src/main/res/drawable/ic_launcher_background.xml
Normal file
171
demo-app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108"
|
||||
android:width="108dp">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user