v1.9.0-beta01 changes

This commit includes HUSH specific changes starting at v.1.9.0-beta01 release here: https://github.com/zcash/zcash-android-wallet-sdk/releases/tag/v1.9.0-beta01
This commit is contained in:
fekt
2022-11-29 20:19:12 -05:00
parent 1b4660b53b
commit 2225f65ee7
115 changed files with 2335 additions and 1766 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

1
.gitignore vendored
View File

@@ -81,3 +81,4 @@ DecompileChecker.kt
backup-dbs/ backup-dbs/
*.db *.db
.DS_Store .DS_Store
sdk-lib/Cargo.lock

3
.idea/.gitignore generated vendored
View File

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

View File

@@ -3,7 +3,19 @@ Change Log
Upcoming Upcoming
------------------------------------ ------------------------------------
- Added `Zatoshi` typesafe object to represent amounts instead. - Split `ZcashNetwork` into `ZcashNetwork` and `LightWalletEndpoint` to decouple network and server configuration
Version 1.8.0-beta01
------------------------------------
- Added `BlockHeight` typesafe object to represent block heights
- Significantly reduced memory usage, fixing potential OutOfMemoryError during block download
- Kotlin 1.7.10
- Updated checkpoints
Version 1.7.0-beta01
------------------------------------
- Added `Zatoshi` typesafe object to represent amounts.
- Kotlin 1.7.0
Version 1.6.0-beta01 Version 1.6.0-beta01
------------------------------------ ------------------------------------

View File

@@ -1,17 +1,33 @@
Troubleshooting Migrations Troubleshooting Migrations
========== ==========
Upcoming Migration to Version 1.7 from 1.6 Upcoming
--------------------------------------
`ZcashNetwork` is no longer an enum. The prior enum values are now declared as object properties `ZcashNetwork.Mainnet` and `ZcashNetwork.Testnet`. For the most part, this change should have minimal impact. ZcashNetwork was also moved from the package `cash.z.ecc.android.sdk.type` to `cash.z.ecc.android.sdk.model`, which will require a change to your import statements. The server fields have been removed from `ZcashNetwork`, allowing server and network configuration to be done independently.
`LightWalletEndpoint` is a new object to represent server information. Default values can be obtained from `LightWalletEndpoint.defaultForNetwork(ZcashNetwork)`
`Synchronizer` no longer allows changing the endpoint after construction. Instead, construct a new `Synchronizer` with the desired endpoint.
Migration to Version 1.8 from 1.7
--------------------------------------
Various APIs used `Int` to represent network block heights. Those APIs now use a typesafe `BlockHeight` type. BlockHeight is constructed with a factory method `BlockHeight.new(ZcashNetwork, Long)` which uses the network to validate the height is above the network's sapling activation height.
`WalletBirthday` has been renamed to `Checkpoint` and removed from the public API. Where clients previously passed in a `WalletBirthday` object, now a `BlockHeight` can be passed in instead.
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`. 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. `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.
`WalletBalance` has been moved from the package `cash.z.ecc.android.sdk.type` to `cash.z.ecc.android.sdk.model`
`ZcashSdk.ZATOSHI_PER_ZEC` has been moved to `Zatoshi.ZATOSHI_PER_ZEC`. `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`. `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.* Migrating to Version 1.4.* from 1.3.*
-------------------------------------- --------------------------------------
The main entrypoint to the SDK has changed. The main entrypoint to the SDK has changed.

217
README.md
View File

@@ -16,211 +16,42 @@ This is a beta build and is currently under active development. Please be advise
--- ---
# Zcash Android SDK # Zcash Android SDK
This lightweight SDK connects Android to Zcash, allowing third-party Android apps to send and receive shielded transactions easily, securely and privately.
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. Different sections of this repository documentation are oriented to different roles, specifically Consumers (you want to use the SDK) and Maintainers (you want to modify the SDK).
## Contents Note: This SDK is designed to work with [lightwalletd](https://github.com/zcash-hackworks/lightwalletd). As either a consumer of the SDK or developer, you'll need a lightwalletd instance to connect to. These servers are maintained by the Zcash community.
- [Requirements](#requirements) Note: Because we have not deployed a non-beta release of the SDK yet, version numbers currently follow a variation of [semantic versioning](https://semver.org/). Generally a non-breaking change will increment the beta number while a breaking change will increment the minor number. 1.0.0-beta01 -> 1.0.0-beta02 is non-breaking, while 1.0.0-beta01 -> 1.1.0-beta01 is breaking. This is subject to change.
- [Structure](#structure)
- [Overview](#overview)
- [Components](#components)
- [Quickstart](#quickstart)
- [Examples](#examples)
- [Compiling Sources](#compiling-sources)
- [Versioning](#versioning)
- [Examples](#examples)
## Requirements # Zcash Networks
"mainnet" (main network) and "testnet" (test network) are terms used in the blockchain ecosystem to describe different blockchain networks. Mainnet is responsible for executing actual transactions within the network and storing them on the blockchain. In contrast, the testnet provides an alternative environment that mimics the mainnet's functionality to allow developers to build and test projects without needing to facilitate live transactions or the use of cryptocurrencies, for example.
This SDK is designed to work with [lightwalletd](https://github.com/zcash-hackworks/lightwalletd) The Zcash testnet is an alternative blockchain that attempts to mimic the mainnet (main Zcash network) for testing purposes. Testnet coins are distinct from actual ZEC and do not have value. Developers and users can experiment with the testnet without having to use valuable currency. The testnet is also used to test network upgrades and their activation before committing to the upgrade on the main Zcash network. For more information on how to add testnet funds visit [Testnet Guide](https://zcash.readthedocs.io/en/latest/rtd_pages/testnet_guide.html) or go right to the [Testnet Faucet](https://faucet.zecpages.com/).
## Structure This SDK supports both mainnet and testnet. Further details on switching networks are covered in the remaining documentation.
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: # Consumers
If you're a developer consuming this SDK in your own app, see [Consumers.md](docs/Consumers.md) for a discussion of setting up your app to consume the SDK and leverage the public APIs.
![SDK Diagram](assets/sdk_diagram_final.png?raw=true "SDK Diagram")
Thankfully, the only thing an app developer has to be concerned with is the following:
![SDK Diagram Developer Perspective](assets/sdk_dev_pov_final.png?raw=true "SDK Diagram Dev PoV")
[Back to contents](#contents)
## Overview
At a high level, this SDK simply helps native Android codebases connect to Zcash's Rust crypto libraries without needing to know Rust or be a Cryptographer. Think of it as welding. The SDK takes separate things and tightly bonds them together such that each can remain as idiomatic as possible. Its goal is to make it easy for an app to incorporate shielded transactions while remaining a good citizen on mobile devices.
Given all the moving parts, making things easy requires coordination. The [Synchronizer](docs/-synchronizer/README.md) provides that layer of abstraction so that the primary steps to make use of this SDK are simply:
1. Start the [Synchronizer](docs/-synchronizer/README.md)
2. Subscribe to wallet data
The [Synchronizer](docs/-synchronizer/README.md) takes care of
- Connecting to the light wallet server
- Downloading the latest compact blocks in a privacy-sensitive way
- Scanning and trial decrypting those blocks for shielded transactions related to the wallet
- Processing those related transactions into useful data for the UI
- Sending payments to a full node through [lightwalletd](https://github.com/zcash/lightwalletd)
- Monitoring sent payments for status updates
To accomplish this, these responsibilities of the SDK are divided into separate components. Each component is coordinated by the [Synchronizer](docs/-synchronizer/README.md), which is the thread that ties it all together.
#### Components
| Component | Summary |
| :----------------------------- | :---------------------------------------------------------------------------------------- |
| **LightWalletService** | Service used for requesting compact blocks |
| **CompactBlockStore** | Stores compact blocks that have been downloaded from the `LightWalletService` |
| **CompactBlockProcessor** | Validates and scans the compact blocks in the `CompactBlockStore` for transaction details |
| **OutboundTransactionManager** | Creates, Submits and manages transactions for spending funds |
| **Initializer** | Responsible for all setup that must happen before synchronization can begin. Loads the rust library and helps initialize databases. |
| **DerivationTool**, **BirthdayTool** | Utilities for deriving keys, addresses and loading wallet checkpoints, called "birthdays." |
| **RustBackend** | Wraps and simplifies the rust library and exposes its functionality to the Kotlin SDK |
[Back to contents](#contents)
## Quickstart
Add flavors for testnet v mainnet. Since `productFlavors` cannot start with the word 'test' we recommend:
build.gradle:
```groovy
flavorDimensions 'network'
productFlavors {
// would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test"
zcashtestnet {
dimension 'network'
matchingFallbacks = ['zcashtestnet', 'debug']
}
zcashmainnet {
dimension 'network'
matchingFallbacks = ['zcashmainnet', 'release']
}
}
```
build.gradle.kts
```kotlin
flavorDimensions.add("network")
productFlavors {
// would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test"
create("zcashtestnet") {
dimension = "network"
matchingFallbacks.addAll(listOf("zcashtestnet", "debug"))
}
create("zcashmainnet") {
dimension = "network"
matchingFallbacks.addAll(listOf("zcashmainnet", "release"))
}
}
```
Add the SDK dependency:
```kotlin
implementation("cash.z.ecc.android:zcash-android-sdk:1.4.0-beta01")
```
Start the [Synchronizer](docs/-synchronizer/README.md)
```kotlin
synchronizer.start(this)
```
Get the wallet's address
```kotlin
synchronizer.getAddress()
// or alternatively
DerivationTool.deriveShieldedAddress(viewingKey)
```
Send funds to another address
```kotlin
synchronizer.sendToAddress(spendingKey, zatoshi, address, memo)
```
[Back to contents](#contents)
## Examples
Full working examples can be found in the [demo app](demo-app), covering all major functionality of the SDK. Each demo strives to be self-contained so that a developer can understand everything required for it to work. Testnet builds of the demo app will soon be available to [download as github releases](https://github.com/zcash/zcash-android-wallet-sdk/releases).
### Demos
Menu Item|Related Code|Description
:-----|:-----|:-----
Get Private Key|[GetPrivateKeyFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt)|Given a seed, display its viewing key and spending key
Get Address|[GetAddressFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt)|Given a seed, display its z-addr
Get Balance|[GetBalanceFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt)|Display the balance
Get Latest Height|[GetLatestHeightFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt)|Given a lightwalletd server, retrieve the latest block height
Get Block|[GetBlockFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt)|Given a lightwalletd server, retrieve a compact block
Get Block Range|[GetBlockRangeFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt)|Given a lightwalletd server, retrieve a range of compact blocks
List Transactions|[ListTransactionsFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt)|Given a seed, list all related shielded transactions
Send|[SendFragment.kt](demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt)|Send and monitor a transaction, the most complex demo
[Back to contents](#contents)
## Compiling Sources
:warning: Compilation is not required unless you plan to submit a patch or fork the code. Instead, it is recommended to simply add the SDK dependencies via Gradle.
In the event that you *do* want to compile the SDK from sources, please see [Setup.md](docs/Setup.md).
[Back to contents](#contents)
## Versioning
This project follows [semantic versioning](https://semver.org/) with pre-release versions. An example of a valid version number is `1.0.4-alpha11` denoting the `11th` iteration of the `alpha` pre-release of version `1.0.4`. Stable releases, such as `1.0.4` will not contain any pre-release identifiers. Pre-releases include the following, in order of stability: `alpha`, `beta`, `rc`. Version codes offer a numeric representation of the build name that always increases. The first six significant digits represent the major, minor and patch number (two digits each) and the last 3 significant digits represent the pre-release identifier. The first digit of the identifier signals the build type. Lastly, each new build has a higher version code than all previous builds. The following table breaks this down:
#### Build Types
| Type | Purpose | Stability | Audience | Identifier | Example Version |
| :---- | :--------- | :---------- | :-------- | :------- | :--- |
| **alpha** | **Sandbox.** For developers to verify behavior and try features. Things seen here might never go to production. Most bugs here can be ignored.| Unstable: Expect bugs | Internal developers | 0XX | 1.2.3-alpha04 (10203004) |
| **beta** | **Hand-off.** For developers to present finished features. Bugs found here should be reported and immediately addressed, if they relate to recent changes. | Unstable: Report bugs | Internal stakeholders | 2XX | 1.2.3-beta04 (10203204) |
| **release candidate** | **Hardening.** Final testing for an app release that we believe is ready to go live. The focus here is regression testing to ensure that new changes have not introduced instability in areas that were previously working. | Stable: Hunt for bugs | External testers | 4XX | 1.2.3-rc04 (10203404) |
| **production** | **Delivery.** Deliver new features to end-users. Any bugs found here need to be prioritized. Some will require immediate attention but most can be worked into a future release. | Stable: Prioritize bugs | Public | 8XX | 1.2.3 (10203800) |
[Back to contents](#contents)
## Examples
A primitive example to exercise the SDK exists in this repo, under [Demo App](demo-app). 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). There are also more comprehensive sample walletes:
* [ECC Sample Wallet](https://github.com/zcash/zcash-android-wallet) — A basic sample application.
* [Secant Sample Wallet](https://github.com/zcash/secant-android-wallet) — A more modern codebase written in Compose. This repository is a work-in-progress and is not fully functional yet as of August 2022, although it will be our primary sample application in the future.
[Back to contents](#contents) # Maintainers and Contributors
If you're building the SDK from source or modifying the SDK:
* [Setup.md](docs/Setup.md) to configure building from source
* [Architecture.md](docs/Architecture.md) to understand the high level architecture of the code
* [CI.md](docs/CI.md) to understand the Continuous Integration build scripts
* [PUBLISHING.md](docs/PUBLISHING.md) to understand our deployment process
## Checkpoints Note that we aim for the main branch of this repository to be stable and releasable. We continuously deploy snapshot builds after a merge to the main branch, then manually deploy release builds. Our continuous deployment of snapshots implies two things:
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. * A pull request containing API changes should also bump the version
* Each pull request should be stable and ready to be consumed, to the best of your knowledge. Gating unstable functionality behind a flag is perfectly acceptable
To update the checkpoints, see [Checkmate](https://github.com/zcash-hackworks/checkmate). ## Known Issues
1. Intel-based machines may have trouble building in Android Studio. The workaround is to add the following line to `~/.gradle/gradle.properties`: `ZCASH_IS_DEPENDENCY_LOCKING_ENABLED=false`
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. 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. 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.

View File

@@ -6,12 +6,34 @@ plugins {
buildscript { buildscript {
dependencyLocking { dependencyLocking {
lockAllConfigurations() // This property is treated specially, as it is not defined by default in the root gradle.properties
// and declaring it in the root gradle.properties is ignored by included builds. This only picks up
// a value declared as a system property, a command line argument, or a an environment variable.
val isDependencyLockingEnabled = if (project.hasProperty("ZCASH_IS_DEPENDENCY_LOCKING_ENABLED")) {
project.property("ZCASH_IS_DEPENDENCY_LOCKING_ENABLED").toString().toBoolean()
} else {
true
}
if (isDependencyLockingEnabled) {
lockAllConfigurations()
}
} }
} }
dependencyLocking { dependencyLocking {
lockAllConfigurations() // This property is treated specially, as it is not defined by default in the root gradle.properties
// and declaring it in the root gradle.properties is ignored by included builds. This only picks up
// a value declared as a system property, a command line argument, or a an environment variable.
val isDependencyLockingEnabled = if (project.hasProperty("ZCASH_IS_DEPENDENCY_LOCKING_ENABLED")) {
project.property("ZCASH_IS_DEPENDENCY_LOCKING_ENABLED").toString().toBoolean()
} else {
true
}
if (isDependencyLockingEnabled) {
lockAllConfigurations()
}
} }
// Per conversation in the KotlinLang Slack, Gradle uses Java 8 compatibility internally // Per conversation in the KotlinLang Slack, Gradle uses Java 8 compatibility internally

View File

@@ -3,43 +3,43 @@
# This file is expected to be part of source control. # This file is expected to be part of source control.
com.github.gundy:semver4j:0.16.4=classpath com.github.gundy:semver4j:0.16.4=classpath
com.google.code.findbugs:jsr305:3.0.2=classpath com.google.code.findbugs:jsr305:3.0.2=classpath
com.google.code.gson:gson:2.8.6=classpath com.google.code.gson:gson:2.8.9=classpath
com.google.errorprone:error_prone_annotations:2.3.4=classpath com.google.errorprone:error_prone_annotations:2.3.4=classpath
com.google.guava:failureaccess:1.0.1=classpath com.google.guava:failureaccess:1.0.1=classpath
com.google.guava:guava:29.0-jre=classpath com.google.guava:guava:29.0-jre=classpath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=classpath
com.google.j2objc:j2objc-annotations:1.3=classpath com.google.j2objc:j2objc-annotations:1.3=classpath
de.undercouch:gradle-download-task:4.1.1=classpath de.undercouch:gradle-download-task:4.1.1=classpath
net.java.dev.jna:jna:5.6.0=classpath
org.checkerframework:checker-qual:2.11.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.kotlin-dsl:org.gradle.kotlin.kotlin-dsl.gradle.plugin:2.3.3=classpath
org.gradle.kotlin:gradle-kotlin-dsl-plugins:2.1.7=classpath org.gradle.kotlin:gradle-kotlin-dsl-plugins:2.3.3=classpath
org.jetbrains.intellij.deps:trove4j:1.0.20181211=classpath org.jetbrains.intellij.deps:trove4j:1.0.20200330=classpath
org.jetbrains.kotlin:kotlin-android-extensions:1.5.31=classpath org.jetbrains.kotlin:kotlin-android-extensions:1.6.21=classpath
org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.5.31=classpath org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.6.21=classpath
org.jetbrains.kotlin:kotlin-build-common:1.5.31=classpath org.jetbrains.kotlin:kotlin-build-common:1.6.21=classpath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.31=classpath org.jetbrains.kotlin:kotlin-compiler-embeddable:1.6.21=classpath
org.jetbrains.kotlin:kotlin-compiler-runner:1.5.31=classpath org.jetbrains.kotlin:kotlin-compiler-runner:1.6.21=classpath
org.jetbrains.kotlin:kotlin-daemon-client:1.5.31=classpath org.jetbrains.kotlin:kotlin-daemon-client:1.6.21=classpath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.31=classpath org.jetbrains.kotlin:kotlin-daemon-embeddable:1.6.21=classpath
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.31=classpath org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.6.21=classpath
org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.5.31=classpath org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.6.21=classpath
org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31=classpath org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21=classpath
org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.5.31=classpath org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.6.21=classpath
org.jetbrains.kotlin:kotlin-native-utils:1.5.31=classpath org.jetbrains.kotlin:kotlin-native-utils:1.6.21=classpath
org.jetbrains.kotlin:kotlin-project-model:1.5.31=classpath org.jetbrains.kotlin:kotlin-project-model:1.6.21=classpath
org.jetbrains.kotlin:kotlin-sam-with-receiver:1.5.31=classpath org.jetbrains.kotlin:kotlin-sam-with-receiver:1.6.21=classpath
org.jetbrains.kotlin:kotlin-scripting-common:1.5.31=classpath org.jetbrains.kotlin:kotlin-scripting-common:1.6.21=classpath
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.5.31=classpath org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.6.21=classpath
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.5.31=classpath org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.6.21=classpath
org.jetbrains.kotlin:kotlin-scripting-jvm:1.5.31=classpath org.jetbrains.kotlin:kotlin-scripting-jvm:1.6.21=classpath
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=classpath org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=classpath org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21=classpath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=classpath org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21=classpath
org.jetbrains.kotlin:kotlin-stdlib:1.5.31=classpath org.jetbrains.kotlin:kotlin-stdlib:1.6.21=classpath
org.jetbrains.kotlin:kotlin-tooling-metadata:1.5.31=classpath org.jetbrains.kotlin:kotlin-tooling-metadata:1.6.21=classpath
org.jetbrains.kotlin:kotlin-util-io:1.5.31=classpath org.jetbrains.kotlin:kotlin-util-io:1.6.21=classpath
org.jetbrains.kotlin:kotlin-util-klib:1.5.31=classpath org.jetbrains.kotlin:kotlin-util-klib:1.6.21=classpath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=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 org.jetbrains:annotations:13.0=classpath
empty= empty=

View File

@@ -1,45 +1,45 @@
# This is a Gradle generated file for dependency locking. # This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised. # Manual edits can break the build and are not advised.
# This file is expected to be part of source control. # This file is expected to be part of source control.
androidx.databinding:databinding-common:7.2.1=runtimeClasspath androidx.databinding:databinding-common:7.2.2=runtimeClasspath
androidx.databinding:databinding-compiler-common:7.2.1=runtimeClasspath androidx.databinding:databinding-compiler-common:7.2.2=runtimeClasspath
com.android.databinding:baseLibrary:7.2.1=runtimeClasspath com.android.databinding:baseLibrary:7.2.2=runtimeClasspath
com.android.tools.analytics-library:crash:30.2.1=runtimeClasspath com.android.tools.analytics-library:crash:30.2.2=runtimeClasspath
com.android.tools.analytics-library:protos:30.2.1=runtimeClasspath com.android.tools.analytics-library:protos:30.2.2=runtimeClasspath
com.android.tools.analytics-library:shared:30.2.1=runtimeClasspath com.android.tools.analytics-library:shared:30.2.2=runtimeClasspath
com.android.tools.analytics-library:tracker:30.2.1=runtimeClasspath com.android.tools.analytics-library:tracker:30.2.2=runtimeClasspath
com.android.tools.build.jetifier:jetifier-core:1.0.0-beta09=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.jetifier:jetifier-processor:1.0.0-beta09=runtimeClasspath
com.android.tools.build:aapt2-proto:7.2.1-7984345=runtimeClasspath com.android.tools.build:aapt2-proto:7.2.2-7984345=runtimeClasspath
com.android.tools.build:aaptcompiler:7.2.1=runtimeClasspath com.android.tools.build:aaptcompiler:7.2.2=runtimeClasspath
com.android.tools.build:apksig:7.2.1=compileClasspath,runtimeClasspath com.android.tools.build:apksig:7.2.2=compileClasspath,runtimeClasspath
com.android.tools.build:apkzlib:7.2.1=compileClasspath,runtimeClasspath com.android.tools.build:apkzlib:7.2.2=compileClasspath,runtimeClasspath
com.android.tools.build:builder-model:7.2.1=compileClasspath,runtimeClasspath com.android.tools.build:builder-model:7.2.2=compileClasspath,runtimeClasspath
com.android.tools.build:builder-test-api:7.2.1=runtimeClasspath com.android.tools.build:builder-test-api:7.2.2=runtimeClasspath
com.android.tools.build:builder:7.2.1=compileClasspath,runtimeClasspath com.android.tools.build:builder:7.2.2=compileClasspath,runtimeClasspath
com.android.tools.build:bundletool:1.8.2=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-api:7.2.2=compileClasspath,runtimeClasspath
com.android.tools.build:gradle:7.2.1=compileClasspath,runtimeClasspath com.android.tools.build:gradle:7.2.2=compileClasspath,runtimeClasspath
com.android.tools.build:manifest-merger:30.2.1=compileClasspath,runtimeClasspath com.android.tools.build:manifest-merger:30.2.2=compileClasspath,runtimeClasspath
com.android.tools.build:transform-api:2.0.0-deprecated-use-gradle-api=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.ddms:ddmlib:30.2.2=runtimeClasspath
com.android.tools.layoutlib:layoutlib-api:30.2.1=runtimeClasspath com.android.tools.layoutlib:layoutlib-api:30.2.2=runtimeClasspath
com.android.tools.lint:lint-model:30.2.1=runtimeClasspath com.android.tools.lint:lint-model:30.2.2=runtimeClasspath
com.android.tools.lint:lint-typedef-remover:30.2.1=runtimeClasspath com.android.tools.lint:lint-typedef-remover:30.2.2=runtimeClasspath
com.android.tools.utp:android-device-provider-ddmlib-proto:30.2.1=runtimeClasspath com.android.tools.utp:android-device-provider-ddmlib-proto:30.2.2=runtimeClasspath
com.android.tools.utp:android-device-provider-gradle-proto:30.2.1=runtimeClasspath com.android.tools.utp:android-device-provider-gradle-proto:30.2.2=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-additional-test-output-proto:30.2.2=runtimeClasspath
com.android.tools.utp:android-test-plugin-host-coverage-proto:30.2.1=runtimeClasspath com.android.tools.utp:android-test-plugin-host-coverage-proto:30.2.2=runtimeClasspath
com.android.tools.utp:android-test-plugin-host-retention-proto:30.2.1=runtimeClasspath com.android.tools.utp:android-test-plugin-host-retention-proto:30.2.2=runtimeClasspath
com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.2.1=runtimeClasspath com.android.tools.utp:android-test-plugin-result-listener-gradle-proto:30.2.2=runtimeClasspath
com.android.tools:annotations:30.2.1=runtimeClasspath com.android.tools:annotations:30.2.2=runtimeClasspath
com.android.tools:common:30.2.1=runtimeClasspath com.android.tools:common:30.2.2=runtimeClasspath
com.android.tools:dvlib:30.2.1=runtimeClasspath com.android.tools:dvlib:30.2.2=runtimeClasspath
com.android.tools:repository:30.2.1=runtimeClasspath com.android.tools:repository:30.2.2=runtimeClasspath
com.android.tools:sdk-common:30.2.1=runtimeClasspath com.android.tools:sdk-common:30.2.2=runtimeClasspath
com.android.tools:sdklib:30.2.1=runtimeClasspath com.android.tools:sdklib:30.2.2=runtimeClasspath
com.android:signflinger:7.2.1=runtimeClasspath com.android:signflinger:7.2.2=runtimeClasspath
com.android:zipflinger:7.2.1=compileClasspath,runtimeClasspath com.android:zipflinger:7.2.2=compileClasspath,runtimeClasspath
com.fasterxml.jackson.core:jackson-annotations:2.11.1=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-core:2.11.1=runtimeClasspath
com.fasterxml.jackson.core:jackson-databind:2.11.1=runtimeClasspath com.fasterxml.jackson.core:jackson-databind:2.11.1=runtimeClasspath
@@ -101,7 +101,7 @@ jakarta.activation:jakarta.activation-api:1.2.1=runtimeClasspath
jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=runtimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=runtimeClasspath
javax.inject:javax.inject:1=runtimeClasspath javax.inject:javax.inject:1=runtimeClasspath
net.java.dev.jna:jna-platform:5.6.0=runtimeClasspath net.java.dev.jna:jna-platform:5.6.0=runtimeClasspath
net.java.dev.jna:jna:5.6.0=runtimeClasspath net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,runtimeClasspath
net.sf.jopt-simple:jopt-simple:4.9=runtimeClasspath net.sf.jopt-simple:jopt-simple:4.9=runtimeClasspath
net.sf.kxml:kxml2:2.3.0=runtimeClasspath net.sf.kxml:kxml2:2.3.0=runtimeClasspath
org.apache.commons:commons-compress:1.20=runtimeClasspath org.apache.commons:commons-compress:1.20=runtimeClasspath
@@ -118,51 +118,54 @@ org.glassfish.jaxb:jaxb-runtime:2.3.2=runtimeClasspath
org.glassfish.jaxb:txw2:2.3.2=runtimeClasspath org.glassfish.jaxb:txw2:2.3.2=runtimeClasspath
org.jdom:jdom2:2.0.6=runtimeClasspath org.jdom:jdom2:2.0.6=runtimeClasspath
org.jetbrains.dokka:dokka-core:1.4.32=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=kotlinCompilerClasspath,runtimeClasspath
org.jetbrains.intellij.deps:trove4j:1.0.20200330=runtimeClasspath org.jetbrains.kotlin:kotlin-android-extensions:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-android-extensions:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-build-common:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-build-common:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-compiler-embeddable:1.6.21=kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.5.31=kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-compiler-runner:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-daemon-client:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-daemon-client:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-daemon-embeddable:1.6.21=kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.5.31=kotlinCompilerClasspath org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.7.10=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.6.21=compileClasspath,runtimeClasspath org.jetbrains.kotlin:kotlin-gradle-plugin-idea:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.6.21=compileClasspath,runtimeClasspath org.jetbrains.kotlin:kotlin-gradle-plugin-model:1.7.10=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21=compileClasspath,runtimeClasspath org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-klib-commonizer-api:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-native-utils:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-native-utils:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-native-utils:1.6.21=compileClasspath,runtimeClasspath org.jetbrains.kotlin:kotlin-native-utils:1.7.10=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-project-model:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-project-model:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-project-model:1.6.21=compileClasspath,runtimeClasspath org.jetbrains.kotlin:kotlin-project-model:1.7.10=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-reflect:1.5.31=compileClasspath,kotlinCompilerClasspath,runtimeClasspath org.jetbrains.kotlin:kotlin-reflect:1.5.31=runtimeClasspath
org.jetbrains.kotlin:kotlin-sam-with-receiver:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-reflect:1.6.21=compileClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:1.5.31=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-sam-with-receiver:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-common:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-script-runtime:1.6.21=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-common:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-scripting-common:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-scripting-jvm:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-scripting-jvm:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-scripting-jvm:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,runtimeClasspath org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.0=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-stdlib-common:1.5.31=runtimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=compileClasspath,runtimeClasspath org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.0=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31=runtimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=compileClasspath,runtimeClasspath org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.21=compileClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.5.31=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,runtimeClasspath org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.31=runtimeClasspath
org.jetbrains.kotlin:kotlin-tooling-metadata:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21=compileClasspath
org.jetbrains.kotlin:kotlin-util-io:1.5.31=kotlinCompilerPluginClasspathMain org.jetbrains.kotlin:kotlin-stdlib:1.5.31=runtimeClasspath
org.jetbrains.kotlin:kotlin-util-io:1.6.21=compileClasspath,runtimeClasspath org.jetbrains.kotlin:kotlin-stdlib:1.6.21=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-util-klib:1.6.21=runtimeClasspath org.jetbrains.kotlin:kotlin-tooling-core:1.7.10=compileClasspath,runtimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=kotlinCompilerPluginClasspathMain,runtimeClasspath org.jetbrains.kotlin:kotlin-tooling-metadata:1.7.10=runtimeClasspath
org.jetbrains.kotlin:kotlin-util-io:1.6.21=kotlinCompilerPluginClasspathMain
org.jetbrains.kotlin:kotlin-util-io:1.7.10=compileClasspath,runtimeClasspath
org.jetbrains.kotlin:kotlin-util-klib:1.7.10=runtimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=runtimeClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1=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:annotations:13.0=compileClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,runtimeClasspath
org.jetbrains:markdown-jvm:0.2.1=runtimeClasspath org.jetbrains:markdown-jvm:0.2.1=runtimeClasspath
org.jetbrains:markdown:0.2.1=runtimeClasspath org.jetbrains:markdown:0.2.1=runtimeClasspath

View File

@@ -1,6 +1,17 @@
//dependencyLocking { //dependencyLocking {
// lockAllConfigurations() // This property is treated specially, as it is not defined by default in the root gradle.properties
// and declaring it in the root gradle.properties is ignored by included builds. This only picks up
// a value declared as a system property, a command line argument, or a an environment variable.
// val isDependencyLockingEnabled = if (project.hasProperty("ZCASH_IS_DEPENDENCY_LOCKING_ENABLED")) {
// project.property("ZCASH_IS_DEPENDENCY_LOCKING_ENABLED").toString().toBoolean()
// } else {
// true
// }
//
// if (isDependencyLockingEnabled) {
// lockAllConfigurations()
// }
//} //}
tasks { tasks {

View File

@@ -4,6 +4,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator 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.ScopedTest
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import org.junit.BeforeClass import org.junit.BeforeClass
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -18,12 +20,12 @@ class InboundTxTests : ScopedTest() {
@Test @Test
fun testTargetBlock_scanned() { fun testTargetBlock_scanned() {
validator.validateMinHeightScanned(targetTxBlock - 1) validator.validateMinHeightScanned(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))
} }
@Test @Test
fun testLatestHeight() { fun testLatestHeight() {
validator.validateLatestHeight(targetTxBlock - 1) validator.validateLatestHeight(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))
} }
@Test @Test
@@ -40,7 +42,7 @@ class InboundTxTests : ScopedTest() {
validator.validateTxCount(2) validator.validateTxCount(2)
} }
private fun addTransactions(targetHeight: Int, vararg txs: String) { private fun addTransactions(targetHeight: BlockHeight, vararg txs: String) {
val overwriteBlockCount = 5 val overwriteBlockCount = 5
chainMaker chainMaker
// .stageEmptyBlocks(targetHeight, overwriteBlockCount) // .stageEmptyBlocks(targetHeight, overwriteBlockCount)
@@ -78,8 +80,8 @@ class InboundTxTests : ScopedTest() {
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt" "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/71935e29127a7de0b96081f4c8a42a9c11584d83adedfaab414362a6f3d965cf.txt"
) )
private const val firstBlock = 663150 private val firstBlock = BlockHeight.new(ZcashNetwork.Mainnet, 663150L)
private const val targetTxBlock = 663188 private val targetTxBlock = BlockHeight.new(ZcashNetwork.Mainnet, 663188L)
private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a" private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"
private val sithLord = DarksideTestCoordinator() private val sithLord = DarksideTestCoordinator()
private val validator = sithLord.validator private val validator = sithLord.validator
@@ -93,7 +95,7 @@ class InboundTxTests : ScopedTest() {
chainMaker chainMaker
.resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = targetTxBlock) .resetBlocks(blocksUrl, startHeight = firstBlock, tipHeight = targetTxBlock)
.stageEmptyBlocks(firstBlock + 1, 100) .stageEmptyBlocks(firstBlock + 1, 100)
.applyTipHeight(targetTxBlock - 1) .applyTipHeight(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))
sithLord.synchronizer.start(classScope) sithLord.synchronizer.start(classScope)
sithLord.await() sithLord.await()

View File

@@ -3,6 +3,8 @@ package cash.z.ecc.android.sdk.darkside.reorgs
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator 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.ScopedTest
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import org.junit.Before import org.junit.Before
import org.junit.BeforeClass import org.junit.BeforeClass
import org.junit.Test import org.junit.Test
@@ -11,8 +13,8 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ReorgSetupTest : ScopedTest() { class ReorgSetupTest : ScopedTest() {
private val birthdayHeight = 663150 private val birthdayHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663150)
private val targetHeight = 663250 private val targetHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663250)
@Before @Before
fun setup() { fun setup() {

View File

@@ -4,6 +4,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator 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.ScopedTest
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.BeforeClass import org.junit.BeforeClass
@@ -13,7 +15,10 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ReorgSmallTest : ScopedTest() { class ReorgSmallTest : ScopedTest() {
private val targetHeight = 663250 private val targetHeight = BlockHeight.new(
ZcashNetwork.Mainnet,
663250
)
private val hashBeforeReorg = "09ec0d5de30d290bc5a2318fbf6a2427a81c7db4790ce0e341a96aeac77108b9" private val hashBeforeReorg = "09ec0d5de30d290bc5a2318fbf6a2427a81c7db4790ce0e341a96aeac77108b9"
private val hashAfterReorg = "tbd" private val hashAfterReorg = "tbd"

View File

@@ -1,10 +1,12 @@
package cash.z.ecc.android.sdk.darkside.test package cash.z.ecc.android.sdk.darkside.test
import android.content.Context 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.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Darkside
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.wallet.sdk.rpc.Darkside import cash.z.wallet.sdk.rpc.Darkside
import cash.z.wallet.sdk.rpc.Darkside.DarksideTransactionsURL import cash.z.wallet.sdk.rpc.Darkside.DarksideTransactionsURL
import cash.z.wallet.sdk.rpc.DarksideStreamerGrpc import cash.z.wallet.sdk.rpc.DarksideStreamerGrpc
@@ -22,17 +24,11 @@ class DarksideApi(
constructor( constructor(
appContext: Context, appContext: Context,
host: String, lightWalletEndpoint: LightWalletEndpoint
port: Int = ZcashNetwork.Mainnet.defaultPort,
usePlainText: Boolean = appContext.resources.getBoolean(
R.bool.lightwalletd_allow_very_insecure_connections
)
) : this( ) : this(
LightWalletGrpcService.createDefaultChannel( LightWalletGrpcService.createDefaultChannel(
appContext, appContext,
host, lightWalletEndpoint
port,
usePlainText
) )
) )
@@ -41,7 +37,7 @@ class DarksideApi(
// //
fun reset( fun reset(
saplingActivationHeight: Int = 419200, saplingActivationHeight: BlockHeight = ZcashNetwork.Mainnet.saplingActivationHeight,
branchId: String = "e9ff75a6", // Canopy, branchId: String = "e9ff75a6", // Canopy,
chainName: String = "darkside${ZcashNetwork.Mainnet.networkName}" chainName: String = "darkside${ZcashNetwork.Mainnet.networkName}"
) = apply { ) = apply {
@@ -49,7 +45,7 @@ class DarksideApi(
Darkside.DarksideMetaState.newBuilder() Darkside.DarksideMetaState.newBuilder()
.setBranchID(branchId) .setBranchID(branchId)
.setChainName(chainName) .setChainName(chainName)
.setSaplingActivation(saplingActivationHeight) .setSaplingActivation(saplingActivationHeight.value.toInt())
.build().let { request -> .build().let { request ->
createStub().reset(request) createStub().reset(request)
} }
@@ -60,21 +56,21 @@ class DarksideApi(
createStub().stageBlocks(url.toUrl()) createStub().stageBlocks(url.toUrl())
} }
fun stageTransactions(url: String, targetHeight: Int) = apply { fun stageTransactions(url: String, targetHeight: BlockHeight) = apply {
twig("staging transaction at height=$targetHeight from url=$url") twig("staging transaction at height=$targetHeight from url=$url")
createStub().stageTransactions( createStub().stageTransactions(
DarksideTransactionsURL.newBuilder().setHeight(targetHeight).setUrl(url).build() DarksideTransactionsURL.newBuilder().setHeight(targetHeight.value).setUrl(url).build()
) )
} }
fun stageEmptyBlocks(startHeight: Int, count: Int = 10, nonce: Int = Random.nextInt()) = apply { fun stageEmptyBlocks(startHeight: BlockHeight, count: Int = 10, nonce: Int = Random.nextInt()) = apply {
twig("staging $count empty blocks starting at $startHeight with nonce $nonce") twig("staging $count empty blocks starting at $startHeight with nonce $nonce")
createStub().stageBlocksCreate( createStub().stageBlocksCreate(
Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight).setCount(count).setNonce(nonce).build() Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight.value).setCount(count).setNonce(nonce).build()
) )
} }
fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: Int) { fun stageTransactions(txs: Iterator<Service.RawTransaction>?, tipHeight: BlockHeight) {
if (txs == null) { if (txs == null) {
twig("no transactions to stage") twig("no transactions to stage")
return return
@@ -84,7 +80,7 @@ class DarksideApi(
createStreamingStub().stageTransactionsStream(response).apply { createStreamingStub().stageTransactionsStream(response).apply {
txs.forEach { txs.forEach {
twig("stageTransactions: onNext calling!!!") 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) onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.value).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: onNext called")
} }
twig("stageTransactions: onCompleted calling!!!") twig("stageTransactions: onCompleted calling!!!")
@@ -94,7 +90,7 @@ class DarksideApi(
response.await() response.await()
} }
fun applyBlocks(tipHeight: Int) { fun applyBlocks(tipHeight: BlockHeight) {
twig("applying blocks up to tipHeight=$tipHeight") twig("applying blocks up to tipHeight=$tipHeight")
createStub().applyStaged(tipHeight.toHeight()) createStub().applyStaged(tipHeight.toHeight())
} }
@@ -146,7 +142,7 @@ class DarksideApi(
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS) .withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
private fun String.toUrl() = Darkside.DarksideBlocksURL.newBuilder().setUrl(this).build() private fun String.toUrl() = Darkside.DarksideBlocksURL.newBuilder().setUrl(this).build()
private fun Int.toHeight() = Darkside.DarksideHeight.newBuilder().setHeight(this).build() private fun BlockHeight.toHeight() = Darkside.DarksideHeight.newBuilder().setHeight(this.value).build()
class EmptyResponse : StreamObserver<Service.Empty> { class EmptyResponse : StreamObserver<Service.Empty> {
var completed = false var completed = false

View File

@@ -4,7 +4,10 @@ import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.SdkSynchronizer import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Darkside
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import io.grpc.StatusRuntimeException import io.grpc.StatusRuntimeException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
@@ -14,19 +17,19 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
class DarksideTestCoordinator(val wallet: TestWallet) { class DarksideTestCoordinator(val wallet: TestWallet) {
constructor( constructor(
alias: String = "DarksideTestCoordinator", alias: String = "DarksideTestCoordinator",
seedPhrase: String = DEFAULT_SEED_PHRASE, seedPhrase: String = DEFAULT_SEED_PHRASE,
startHeight: Int = DEFAULT_START_HEIGHT, startHeight: BlockHeight = DEFAULT_START_HEIGHT,
host: String = COMPUTER_LOCALHOST,
network: ZcashNetwork = ZcashNetwork.Mainnet, network: ZcashNetwork = ZcashNetwork.Mainnet,
port: Int = network.defaultPort endpoint: LightWalletEndpoint = LightWalletEndpoint.Darkside
) : this(TestWallet(seedPhrase, alias, network, host, startHeight = startHeight, port = port)) ) : this(TestWallet(seedPhrase, alias, network, endpoint, startHeight = startHeight))
private val targetHeight = 663250 private val targetHeight = BlockHeight.new(wallet.network, 663250)
private val context = InstrumentationRegistry.getInstrumentation().context private val context = InstrumentationRegistry.getInstrumentation().context
// dependencies: private // dependencies: private
@@ -91,20 +94,20 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
* Waits for, at most, the given amount of time for the synchronizer to download and scan blocks * Waits for, at most, the given amount of time for the synchronizer to download and scan blocks
* and reach a 'SYNCED' status. * and reach a 'SYNCED' status.
*/ */
fun await(timeout: Long = 60_000L, targetHeight: Int = -1) = runBlocking { fun await(timeout: Long = 60_000L, targetHeight: BlockHeight? = null) = runBlocking {
ScopedTest.timeoutWith(this, timeout) { ScopedTest.timeoutWith(this, timeout) {
twig("*** Waiting up to ${timeout / 1_000}s for sync ***") twig("*** Waiting up to ${timeout / 1_000}s for sync ***")
synchronizer.status.onEach { synchronizer.status.onEach {
twig("got processor status $it") twig("got processor status $it")
if (it == Synchronizer.Status.DISCONNECTED) { if (it == Synchronizer.Status.DISCONNECTED) {
twig("waiting a bit before giving up on connection...") twig("waiting a bit before giving up on connection...")
} else if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) { } else if (targetHeight != null && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("awaiting new blocks from server...") twig("awaiting new blocks from server...")
} }
}.map { }.map {
// whenever we're waiting for a target height, for simplicity, if we're sleeping, // 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 // and in between polls, then consider it that we're not synced
if (targetHeight != -1 && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) { if (targetHeight != null && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
twig("switching status to DOWNLOADING because we're still waiting for height $targetHeight") twig("switching status to DOWNLOADING because we're still waiting for height $targetHeight")
Synchronizer.Status.DOWNLOADING Synchronizer.Status.DOWNLOADING
} else { } else {
@@ -140,14 +143,14 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
inner class DarksideTestValidator { inner class DarksideTestValidator {
fun validateHasBlock(height: Int) { fun validateHasBlock(height: BlockHeight) {
runBlocking { runBlocking {
assertTrue((synchronizer as SdkSynchronizer).findBlockHashAsHex(height) != null) assertTrue((synchronizer as SdkSynchronizer).findBlockHashAsHex(height) != null)
assertTrue((synchronizer as SdkSynchronizer).findBlockHash(height)?.size ?: 0 > 0) assertTrue((synchronizer as SdkSynchronizer).findBlockHash(height)?.size ?: 0 > 0)
} }
} }
fun validateLatestHeight(height: Int) = runBlocking<Unit> { fun validateLatestHeight(height: BlockHeight) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first() val info = synchronizer.processorInfo.first()
val networkBlockHeight = info.networkBlockHeight val networkBlockHeight = info.networkBlockHeight
assertTrue( assertTrue(
@@ -157,41 +160,44 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
) )
} }
fun validateMinHeightDownloaded(minHeight: Int) = runBlocking<Unit> { fun validateMinHeightDownloaded(minHeight: BlockHeight) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first() val info = synchronizer.processorInfo.first()
val lastDownloadedHeight = info.lastDownloadedHeight val lastDownloadedHeight = info.lastDownloadedHeight
assertNotNull(lastDownloadedHeight)
assertTrue( assertTrue(
"Expected to have at least downloaded $minHeight but the last downloaded block was" + "Expected to have at least downloaded $minHeight but the last downloaded block was" +
" $lastDownloadedHeight! Full details: $info", " $lastDownloadedHeight! Full details: $info",
lastDownloadedHeight >= minHeight lastDownloadedHeight!! >= minHeight
) )
} }
fun validateMinHeightScanned(minHeight: Int) = runBlocking<Unit> { fun validateMinHeightScanned(minHeight: BlockHeight) = runBlocking<Unit> {
val info = synchronizer.processorInfo.first() val info = synchronizer.processorInfo.first()
val lastScannedHeight = info.lastScannedHeight val lastScannedHeight = info.lastScannedHeight
assertNotNull(lastScannedHeight)
assertTrue( assertTrue(
"Expected to have at least scanned $minHeight but the last scanned block was" + "Expected to have at least scanned $minHeight but the last scanned block was" +
" $lastScannedHeight! Full details: $info", " $lastScannedHeight! Full details: $info",
lastScannedHeight >= minHeight lastScannedHeight!! >= minHeight
) )
} }
fun validateMaxHeightScanned(maxHeight: Int) = runBlocking<Unit> { fun validateMaxHeightScanned(maxHeight: BlockHeight) = runBlocking<Unit> {
val lastDownloadedHeight = synchronizer.processorInfo.first().lastScannedHeight val lastDownloadedHeight = synchronizer.processorInfo.first().lastScannedHeight
assertNotNull(lastDownloadedHeight)
assertTrue( assertTrue(
"Did not expect to be synced beyond $maxHeight but we are synced to" + "Did not expect to be synced beyond $maxHeight but we are synced to" +
" $lastDownloadedHeight", " $lastDownloadedHeight",
lastDownloadedHeight <= maxHeight lastDownloadedHeight!! <= maxHeight
) )
} }
fun validateBlockHash(height: Int, expectedHash: String) { fun validateBlockHash(height: BlockHeight, expectedHash: String) {
val hash = runBlocking { (synchronizer as SdkSynchronizer).findBlockHashAsHex(height) } val hash = runBlocking { (synchronizer as SdkSynchronizer).findBlockHashAsHex(height) }
assertEquals(expectedHash, hash) assertEquals(expectedHash, hash)
} }
fun onReorg(callback: (errorHeight: Int, rewindHeight: Int) -> Unit) { fun onReorg(callback: (errorHeight: BlockHeight, rewindHeight: BlockHeight) -> Unit) {
synchronizer.onChainErrorHandler = callback synchronizer.onChainErrorHandler = callback
} }
@@ -225,7 +231,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
// //
inner class DarksideChainMaker { inner class DarksideChainMaker {
var lastTipHeight = -1 var lastTipHeight: BlockHeight? = null
/** /**
* Resets the darksidelightwalletd server, stages the blocks represented by the given URL, then * Resets the darksidelightwalletd server, stages the blocks represented by the given URL, then
@@ -233,8 +239,8 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
*/ */
fun resetBlocks( fun resetBlocks(
blocksUrl: String, blocksUrl: String,
startHeight: Int = DEFAULT_START_HEIGHT, startHeight: BlockHeight = DEFAULT_START_HEIGHT,
tipHeight: Int = startHeight + 100 tipHeight: BlockHeight = startHeight + 100
): DarksideChainMaker = apply { ): DarksideChainMaker = apply {
darkside darkside
.reset(startHeight) .reset(startHeight)
@@ -242,23 +248,23 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
applyTipHeight(tipHeight) applyTipHeight(tipHeight)
} }
fun stageTransaction(url: String, targetHeight: Int): DarksideChainMaker = apply { fun stageTransaction(url: String, targetHeight: BlockHeight): DarksideChainMaker = apply {
darkside.stageTransactions(url, targetHeight) darkside.stageTransactions(url, targetHeight)
} }
fun stageTransactions(targetHeight: Int, vararg urls: String): DarksideChainMaker = apply { fun stageTransactions(targetHeight: BlockHeight, vararg urls: String): DarksideChainMaker = apply {
urls.forEach { urls.forEach {
darkside.stageTransactions(it, targetHeight) darkside.stageTransactions(it, targetHeight)
} }
} }
fun stageEmptyBlocks(startHeight: Int, count: Int = 10): DarksideChainMaker = apply { fun stageEmptyBlocks(startHeight: BlockHeight, count: Int = 10): DarksideChainMaker = apply {
darkside.stageEmptyBlocks(startHeight, count) darkside.stageEmptyBlocks(startHeight, count)
} }
fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight + 1, 1) fun stageEmptyBlock() = stageEmptyBlocks(lastTipHeight!! + 1, 1)
fun applyTipHeight(tipHeight: Int): DarksideChainMaker = apply { fun applyTipHeight(tipHeight: BlockHeight): DarksideChainMaker = apply {
twig("applying tip height of $tipHeight") twig("applying tip height of $tipHeight")
darkside.applyBlocks(tipHeight) darkside.applyBlocks(tipHeight)
lastTipHeight = tipHeight lastTipHeight = tipHeight
@@ -277,14 +283,14 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
} }
fun advanceBy(numEmptyBlocks: Int) { fun advanceBy(numEmptyBlocks: Int) {
val nextBlock = lastTipHeight + 1 val nextBlock = lastTipHeight!! + 1
twig("adding $numEmptyBlocks empty blocks to the chain starting at $nextBlock") twig("adding $numEmptyBlocks empty blocks to the chain starting at $nextBlock")
darkside.stageEmptyBlocks(nextBlock, numEmptyBlocks) darkside.stageEmptyBlocks(nextBlock, numEmptyBlocks)
applyTipHeight(nextBlock + numEmptyBlocks) applyTipHeight(nextBlock + numEmptyBlocks)
} }
fun applyPendingTransactions(targetHeight: Int = lastTipHeight + 1) { fun applyPendingTransactions(targetHeight: BlockHeight = lastTipHeight!! + 1) {
stageEmptyBlocks(lastTipHeight + 1, targetHeight - lastTipHeight) stageEmptyBlocks(lastTipHeight!! + 1, (targetHeight.value - lastTipHeight!!.value).toInt())
darkside.stageTransactions(darkside.getSentTransactions()?.iterator(), targetHeight) darkside.stageTransactions(darkside.getSentTransactions()?.iterator(), targetHeight)
applyTipHeight(targetHeight) applyTipHeight(targetHeight)
} }
@@ -304,7 +310,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt" "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-small-reorg.txt"
private const val largeReorg = private const val largeReorg =
"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt" "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/after-large-reorg.txt"
private const val DEFAULT_START_HEIGHT = 663150 private val DEFAULT_START_HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 663150)
private const val DEFAULT_SEED_PHRASE = 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" "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"
} }

View File

@@ -10,10 +10,13 @@ import cash.z.ecc.android.sdk.db.entity.isPending
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Darkside
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool 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.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -35,9 +38,8 @@ class TestWallet(
val seedPhrase: String, val seedPhrase: String,
val alias: String = "TestWallet", val alias: String = "TestWallet",
val network: ZcashNetwork = ZcashNetwork.Testnet, val network: ZcashNetwork = ZcashNetwork.Testnet,
val host: String = network.defaultHost, val endpoint: LightWalletEndpoint = LightWalletEndpoint.Darkside,
startHeight: Int? = null, startHeight: BlockHeight? = null
val port: Int = network.defaultPort
) { ) {
constructor( constructor(
backup: Backups, backup: Backups,
@@ -65,7 +67,7 @@ class TestWallet(
runBlocking { DerivationTool.deriveTransparentSecretKey(seed, network = network) } runBlocking { DerivationTool.deriveTransparentSecretKey(seed, network = network) }
val initializer = runBlocking { val initializer = runBlocking {
Initializer.new(context) { config -> Initializer.new(context) { config ->
runBlocking { config.importWallet(seed, startHeight, network, host, alias = alias) } runBlocking { config.importWallet(seed, startHeight, network, endpoint, alias = alias) }
} }
} }
val synchronizer: SdkSynchronizer = runBlocking { Synchronizer.new(initializer) } as SdkSynchronizer val synchronizer: SdkSynchronizer = runBlocking { Synchronizer.new(initializer) } as SdkSynchronizer
@@ -78,14 +80,11 @@ class TestWallet(
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) } runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
val birthdayHeight get() = synchronizer.latestBirthdayHeight val birthdayHeight get() = synchronizer.latestBirthdayHeight
val networkName get() = synchronizer.network.networkName val networkName get() = synchronizer.network.networkName
val connectionInfo get() = service.connectionInfo.toString()
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun transparentBalance(): WalletBalance { suspend fun transparentBalance(): WalletBalance {
synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight) synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight)
return synchronizer.getTransparentBalance(transparentAddress) return synchronizer.getTransparentBalance(transparentAddress)
} }
*/
suspend fun sync(timeout: Long = -1): TestWallet { suspend fun sync(timeout: Long = -1): TestWallet {
val killSwitch = walletScope.launch { val killSwitch = walletScope.launch {
@@ -111,7 +110,7 @@ class TestWallet(
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet { suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
Twig.sprout("$alias sending") Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex) synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() } .takeWhile { it.isPending(null) }
.collect { .collect {
twig("Updated transaction: $it") twig("Updated transaction: $it")
} }
@@ -119,15 +118,14 @@ class TestWallet(
return this return this
} }
suspend fun rewindToHeight(height: Int): TestWallet { suspend fun rewindToHeight(height: BlockHeight): TestWallet {
synchronizer.rewindToNearestHeight(height, false) synchronizer.rewindToNearestHeight(height, false)
return this return this
} }
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun shieldFunds(): TestWallet { suspend fun shieldFunds(): TestWallet {
twig("checking $transparentAddress for transactions!") twig("checking $transparentAddress for transactions!")
synchronizer.refreshUtxos(transparentAddress, 935000).let { count -> synchronizer.refreshUtxos(transparentAddress, BlockHeight.new(ZcashNetwork.Mainnet, 935000)).let { count ->
twig("FOUND $count new UTXOs") twig("FOUND $count new UTXOs")
} }
@@ -144,7 +142,6 @@ class TestWallet(
return this return this
} }
*/
suspend fun join(timeout: Long? = null): TestWallet { suspend fun join(timeout: Long? = null): TestWallet {
// block until stopped // block until stopped
@@ -167,13 +164,48 @@ class TestWallet(
} }
} }
enum class Backups(val seedPhrase: String, val testnetBirthday: Int, val mainnetBirthday: Int) { enum class Backups(val seedPhrase: String, val testnetBirthday: BlockHeight, val mainnetBirthday: BlockHeight) {
// TODO: get the proper birthday values for these wallets // 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), DEFAULT(
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), "column rhythm acoustic gym cost fit keen maze fence seed mail medal shrimp tell relief clip cannon foster soldier shallow refuse lunar parrot banana",
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), BlockHeight.new(
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), ZcashNetwork.Testnet,
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), 1_355_928
),
BlockHeight.new(ZcashNetwork.Mainnet, 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",
BlockHeight.new(
ZcashNetwork.Testnet,
1_330_190
),
BlockHeight.new(ZcashNetwork.Mainnet, 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",
BlockHeight.new(
ZcashNetwork.Testnet,
1_000_000
),
BlockHeight.new(ZcashNetwork.Mainnet, 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",
BlockHeight.new(
ZcashNetwork.Testnet,
1_330_190
),
BlockHeight.new(ZcashNetwork.Mainnet, 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",
BlockHeight.new(
ZcashNetwork.Testnet,
1_330_190
),
BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)
),
; ;
} }
} }

View File

@@ -10,8 +10,11 @@ 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 cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.Mainnet
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@@ -86,17 +89,20 @@ class SampleCodeTest {
// /////////////////////////////////////////////////// // ///////////////////////////////////////////////////
// Query latest block height // Query latest block height
@Test fun getLatestBlockHeightTest() { @Test fun getLatestBlockHeightTest() {
val lightwalletService = LightWalletGrpcService(context, lightwalletdHost) val lightwalletService = LightWalletGrpcService.new(context, lightwalletdHost)
log("Latest Block: ${lightwalletService.getLatestBlockHeight()}") log("Latest Block: ${lightwalletService.getLatestBlockHeight()}")
} }
// /////////////////////////////////////////////////// // ///////////////////////////////////////////////////
// Download compact block range // Download compact block range
@Test fun getBlockRange() { @Test fun getBlockRange() {
val blockRange = 500_000..500_009 val blockRange = BlockHeight.new(ZcashNetwork.Mainnet, 500_000)..BlockHeight.new(
val lightwalletService = LightWalletGrpcService(context, lightwalletdHost) ZcashNetwork.Mainnet,
500_009
)
val lightwalletService = LightWalletGrpcService.new(context, lightwalletdHost)
val blocks = lightwalletService.getBlockRange(blockRange) val blocks = lightwalletService.getBlockRange(blockRange)
assertEquals(blockRange.count(), blocks.size) assertEquals(blockRange.endInclusive.value - blockRange.start.value, blocks.count())
blocks.forEachIndexed { i, block -> blocks.forEachIndexed { i, block ->
log("Block #$i: height:${block.height} hash:${block.hash.toByteArray().toHex()}") log("Block #$i: height:${block.height} hash:${block.hash.toByteArray().toHex()}")
@@ -140,7 +146,7 @@ class SampleCodeTest {
val transactionFlow = synchronizer.sendToAddress(spendingKey, amount, address, memo) val transactionFlow = synchronizer.sendToAddress(spendingKey, amount, address, memo)
transactionFlow.collect { transactionFlow.collect {
log("pending transaction updated $it") log("pending transaction updated $it")
assertTrue("Failed to send funds. See log for details.", !it?.isFailure()) assertTrue("Failed to send funds. See log for details.", !it.isFailure())
} }
} }
@@ -150,7 +156,7 @@ class SampleCodeTest {
companion object { companion object {
private val seed = "Insert seed for testing".toByteArray() private val seed = "Insert seed for testing".toByteArray()
private val lightwalletdHost: String = ZcashNetwork.Mainnet.defaultHost private val lightwalletdHost = LightWalletEndpoint.Mainnet
private val context = InstrumentationRegistry.getInstrumentation().targetContext private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val synchronizer: Synchronizer = run { private val synchronizer: Synchronizer = run {

View File

@@ -20,7 +20,9 @@ import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.sdk.demoapp.util.fromResources 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.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
@@ -108,7 +110,11 @@ class MainActivity :
if (lightwalletService != null) { if (lightwalletService != null) {
lightwalletService?.shutdown() lightwalletService?.shutdown()
} }
lightwalletService = LightWalletGrpcService(applicationContext, ZcashNetwork.fromResources(applicationContext)) val network = ZcashNetwork.fromResources(applicationContext)
lightwalletService = LightWalletGrpcService.new(
applicationContext,
LightWalletEndpoint.defaultForNetwork(network)
)
} }
private fun onFabClicked(view: View) { private fun onFabClicked(view: View) {

View File

@@ -9,9 +9,9 @@ import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetAddressBinding 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.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

View File

@@ -14,9 +14,11 @@ 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.fromResources
import cash.z.ecc.android.sdk.ext.collectWith import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool 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.flow.filterNotNull
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -50,8 +52,12 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
// using the ViewingKey to initialize // using the ViewingKey to initialize
runBlocking { runBlocking {
Initializer.new(requireApplicationContext(), null) { Initializer.new(requireApplicationContext(), null) {
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext())) val network = ZcashNetwork.fromResources(requireApplicationContext())
it.importWallet(viewingKey, network = ZcashNetwork.fromResources(requireApplicationContext())) it.newWallet(
viewingKey,
network = network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network)
)
} }
}.let { initializer -> }.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer) synchronizer = Synchronizer.newBlocking(initializer)
@@ -81,7 +87,7 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
private fun onStatus(status: Synchronizer.Status) { private fun onStatus(status: Synchronizer.Status) {
binding.textStatus.text = "Status: $status" binding.textStatus.text = "Status: $status"
val balance = synchronizer.saplingBalances.value val balance: WalletBalance? = synchronizer.saplingBalances.value
if (null == balance) { if (null == balance) {
binding.textBalance.text = "Calculating balance..." binding.textBalance.text = "Calculating balance..."
} else { } else {

View File

@@ -7,11 +7,15 @@ import android.view.View
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockBinding 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.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.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.demoapp.util.toHtml 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.toRelativeTime
import cash.z.ecc.android.sdk.demoapp.util.withCommas import cash.z.ecc.android.sdk.demoapp.util.withCommas
import cash.z.ecc.android.sdk.ext.toHex import cash.z.ecc.android.sdk.ext.toHex
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import kotlin.math.min
/** /**
* Retrieves a compact block from the lightwalletd service and displays basic information about it. * Retrieves a compact block from the lightwalletd service and displays basic information about it.
@@ -20,7 +24,7 @@ import cash.z.ecc.android.sdk.ext.toHex
*/ */
class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() { class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
private fun setBlockHeight(blockHeight: Int) { private fun setBlockHeight(blockHeight: BlockHeight) {
val blocks = val blocks =
lightwalletService?.getBlockRange(blockHeight..blockHeight) lightwalletService?.getBlockRange(blockHeight..blockHeight)
val block = blocks?.firstOrNull() val block = blocks?.firstOrNull()
@@ -38,8 +42,11 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
} }
private fun onApply(_unused: View? = null) { private fun onApply(_unused: View? = null) {
val network = ZcashNetwork.fromResources(requireApplicationContext())
val newHeight = min(binding.textBlockHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)
try { try {
setBlockHeight(binding.textBlockHeight.text.toString().toInt()) setBlockHeight(BlockHeight.new(network, newHeight))
} catch (t: Throwable) { } catch (t: Throwable) {
toast("Error: $t") toast("Error: $t")
} }

View File

@@ -8,9 +8,13 @@ import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockRangeBinding 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.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.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime
import cash.z.ecc.android.sdk.demoapp.util.withCommas import cash.z.ecc.android.sdk.demoapp.util.withCommas
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import kotlin.math.max
/** /**
* Retrieves a range of compact block from the lightwalletd service and displays basic information * Retrieves a range of compact block from the lightwalletd service and displays basic information
@@ -20,16 +24,17 @@ import cash.z.ecc.android.sdk.demoapp.util.withCommas
*/ */
class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() { class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
private fun setBlockRange(blockRange: IntRange) { private fun setBlockRange(blockRange: ClosedRange<BlockHeight>) {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val blocks = val blocks =
lightwalletService?.getBlockRange(blockRange) lightwalletService?.getBlockRange(blockRange)
val fetchDelta = System.currentTimeMillis() - start val fetchDelta = System.currentTimeMillis() - start
// Note: This is a demo so we won't worry about iterating efficiently over these blocks // Note: This is a demo so we won't worry about iterating efficiently over these blocks
// Note: Converting the blocks sequence to a list can consume a lot of memory and may
// cause OOM.
binding.textInfo.text = Html.fromHtml( binding.textInfo.text = Html.fromHtml(
blocks?.run { blocks?.toList()?.run {
val count = size val count = size
val emptyCount = count { it.vtxCount == 0 } val emptyCount = count { it.vtxCount == 0 }
val maxTxs = maxByOrNull { it.vtxCount } val maxTxs = maxByOrNull { it.vtxCount }
@@ -41,9 +46,9 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
block.vtxList.maxOfOrNull { it.outputsCount } ?: -1 block.vtxList.maxOfOrNull { it.outputsCount } ?: -1
} }
val maxOutTx = maxOuts?.vtxList?.maxByOrNull { it.outputsCount } val maxOutTx = maxOuts?.vtxList?.maxByOrNull { it.outputsCount }
val txCount = sumBy { it.vtxCount } val txCount = sumOf { it.vtxCount }
val outCount = sumBy { block -> block.vtxList.sumBy { it.outputsCount } } val outCount = sumOf { block -> block.vtxList.sumOf { it.outputsCount } }
val inCount = sumBy { block -> block.vtxList.sumBy { it.spendsCount } } val inCount = sumOf { block -> block.vtxList.sumOf { it.spendsCount } }
val processTime = System.currentTimeMillis() - start - fetchDelta val processTime = System.currentTimeMillis() - start - fetchDelta
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
@@ -69,8 +74,9 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
} }
private fun onApply(_unused: View) { private fun onApply(_unused: View) {
val start = binding.textStartHeight.text.toString().toInt() val network = ZcashNetwork.fromResources(requireApplicationContext())
val end = binding.textEndHeight.text.toString().toInt() val start = max(binding.textStartHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)
val end = max(binding.textEndHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)
if (start <= end) { if (start <= end) {
try { try {
with(binding.buttonApply) { with(binding.buttonApply) {
@@ -78,7 +84,7 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
setText(R.string.loading) setText(R.string.loading)
binding.textInfo.setText(R.string.loading) binding.textInfo.setText(R.string.loading)
post { post {
setBlockRange(start..end) setBlockRange(BlockHeight.new(network, start)..BlockHeight.new(network, end))
isEnabled = true isEnabled = true
setText(R.string.apply) setText(R.string.apply)
} }

View File

@@ -9,8 +9,8 @@ import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding 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.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** /**

View File

@@ -17,8 +17,10 @@ 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.fromResources
import cash.z.ecc.android.sdk.ext.collectWith import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
/** /**
@@ -48,12 +50,20 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
// have the seed stored // have the seed stored
val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed() val seed = Mnemonics.MnemonicCode(seedPhrase).toSeed()
initializer = runBlocking { initializer = Initializer.newBlocking(
Initializer.new(requireApplicationContext()) { requireApplicationContext(),
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) } Initializer.Config {
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext())) val network = ZcashNetwork.fromResources(requireApplicationContext())
runBlocking {
it.importWallet(
seed,
birthday = null,
network = network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network)
)
}
} }
} )
address = runBlocking { address = runBlocking {
DerivationTool.deriveShieldedAddress( DerivationTool.deriveShieldedAddress(
seed, seed,

View File

@@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.demoapp.demos.listutxos package cash.z.ecc.android.sdk.demoapp.demos.listutxos
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -13,20 +14,23 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment 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.databinding.FragmentListUtxosBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext 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.fromResources
import cash.z.ecc.android.sdk.demoapp.util.mainActivity import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.ext.collectWith import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.max
/** /**
* =============================================================================================== * ===============================================================================================
@@ -62,8 +66,15 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
// have the seed stored // have the seed stored
seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed() seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
initializer = runBlocking { initializer = runBlocking {
val network = ZcashNetwork.fromResources(requireApplicationContext())
Initializer.new(requireApplicationContext()) { Initializer.new(requireApplicationContext()) {
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) } runBlocking {
it.newWallet(
seed,
network = network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network)
)
}
it.alias = "Demo_Utxos" it.alias = "Demo_Utxos"
} }
} }
@@ -78,38 +89,48 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
fun initUi() { fun initUi() {
binding.inputAddress.setText(address) binding.inputAddress.setText(address)
binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString()) binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString())
binding.inputRangeEnd.setText(DemoConstants.utxoEndHeight.toString()) binding.inputRangeEnd.setText(getUxtoEndHeight(requireApplicationContext()).value.toString())
binding.buttonLoad.setOnClickListener { binding.buttonLoad.setOnClickListener {
mainActivity()?.hideKeyboard() mainActivity()?.hideKeyboard()
//downloadTransactions() downloadTransactions()
} }
initTransactionUi() initTransactionUi()
} }
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
fun downloadTransactions() { fun downloadTransactions() {
binding.textStatus.text = "loading..." binding.textStatus.text = "loading..."
binding.textStatus.post { binding.textStatus.post {
val network = ZcashNetwork.fromResources(requireApplicationContext())
binding.textStatus.requestFocus() binding.textStatus.requestFocus()
val addressToUse = binding.inputAddress.text.toString() val addressToUse = binding.inputAddress.text.toString()
val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight val startToUse = max(
val endToUse = binding.inputRangeEnd.text.toString().toIntOrNull() ?: DemoConstants.utxoEndHeight binding.inputRangeStart.text.toString().toLongOrNull()
?: network.saplingActivationHeight.value,
network.saplingActivationHeight.value
)
val endToUse = binding.inputRangeEnd.text.toString().toLongOrNull()
?: getUxtoEndHeight(requireApplicationContext()).value
var allStart = now var allStart = now
twig("loading transactions in range $startToUse..$endToUse") twig("loading transactions in range $startToUse..$endToUse")
val txids = lightwalletService?.getTAddressTransactions(addressToUse, startToUse..endToUse) val txids = lightwalletService?.getTAddressTransactions(
addressToUse,
BlockHeight.new(network, startToUse)..BlockHeight.new(network, endToUse)
)
var delta = now - allStart var delta = now - allStart
updateStatus("found ${txids?.size} transactions in ${delta}ms.", false) updateStatus("found ${txids?.size} transactions in ${delta}ms.", false)
txids?.map { txids?.map {
it.data.apply { // Disabled during migration to newer SDK version; this appears to have been
try { // leveraging non-public APIs in the SDK so perhaps should be removed
runBlocking { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) } // it.data.apply {
} catch (t: Throwable) { // try {
twig("failed to decrypt and store transaction due to: $t") // runBlocking { initializer.rustBackend.decryptAndStoreTransaction(toByteArray()) }
} // } catch (t: Throwable) {
} // twig("failed to decrypt and store transaction due to: $t")
// }
// }
}?.let { txData -> }?.let { txData ->
// Disabled during migration to newer SDK version; this appears to have been // Disabled during migration to newer SDK version; this appears to have been
// leveraging non-public APIs in the SDK so perhaps should be removed // leveraging non-public APIs in the SDK so perhaps should be removed
@@ -136,7 +157,6 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
} }
} }
} }
*/
private val now get() = System.currentTimeMillis() private val now get() = System.currentTimeMillis()
@@ -159,7 +179,12 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
resetInBackground() resetInBackground()
val seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed() val seed = Mnemonics.MnemonicCode(sharedViewModel.seedPhrase.value).toSeed()
viewLifecycleOwner.lifecycleScope.launchWhenStarted { viewLifecycleOwner.lifecycleScope.launchWhenStarted {
binding.inputAddress.setText(DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))) binding.inputAddress.setText(
DerivationTool.deriveTransparentAddress(
seed,
ZcashNetwork.fromResources(requireApplicationContext())
)
)
} }
} }
@@ -248,4 +273,9 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
} }
} }
} }
@Suppress("MagicNumber")
private fun getUxtoEndHeight(context: Context): BlockHeight {
return BlockHeight.new(ZcashNetwork.fromResources(context), 968085L)
}
} }

View File

@@ -29,9 +29,11 @@ import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
import cash.z.ecc.android.sdk.ext.toZecString 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.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool 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 import kotlinx.coroutines.runBlocking
/** /**
@@ -66,8 +68,14 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
runBlocking { runBlocking {
Initializer.new(requireApplicationContext()) { Initializer.new(requireApplicationContext()) {
runBlocking { it.importWallet(seed, network = ZcashNetwork.fromResources(requireApplicationContext())) } val network = ZcashNetwork.fromResources(requireApplicationContext())
it.setNetwork(ZcashNetwork.fromResources(requireApplicationContext())) runBlocking {
it.newWallet(
seed,
network = network,
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network)
)
}
} }
}.let { initializer -> }.let { initializer ->
synchronizer = Synchronizer.newBlocking(initializer) synchronizer = Synchronizer.newBlocking(initializer)

View File

@@ -4,10 +4,16 @@ package cash.z.ecc.android.sdk.demoapp.util
import android.content.Context import android.content.Context
import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.demoapp.R
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import java.util.*
fun ZcashNetwork.Companion.fromResources(context: Context) = ZcashNetwork.valueOf( fun ZcashNetwork.Companion.fromResources(context: Context): ZcashNetwork {
context.getString( val networkNameFromResources = context.getString(R.string.network_name).lowercase(Locale.ROOT)
R.string.network_name return if (networkNameFromResources == Testnet.networkName) {
) ZcashNetwork.Testnet
) } else if (networkNameFromResources.lowercase(Locale.ROOT) == Mainnet.networkName) {
ZcashNetwork.Mainnet
} else {
throw IllegalArgumentException("Unknown network name: $networkNameFromResources")
}
}

View File

@@ -1,8 +1,8 @@
package cash.z.ecc.android.sdk.demoapp package cash.z.ecc.android.sdk.demoapp
object DemoConstants { object DemoConstants {
val utxoEndHeight: Int = 1150000
val sendAmount: Double = 0.00000000 val sendAmount: Double = 0.000018
// corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx // corresponds to address: zs15tzaulx5weua5c7l47l4pku2pw9fzwvvnsp4y80jdpul0y3nwn5zp7tmkcclqaca3mdjqjkl7hx
val initialSeedWords: String = val initialSeedWords: String =

View File

@@ -1,7 +1,7 @@
package cash.z.ecc.android.sdk.demoapp package cash.z.ecc.android.sdk.demoapp
object DemoConstants { object DemoConstants {
val utxoEndHeight: Int = 1075590 val utxoEndHeight: Int = 1166699
val sendAmount: Double = 0.00017 val sendAmount: Double = 0.00017
// corresponds to address: ztestsapling1zhqvuq8zdwa8nsnde7074kcfsat0w25n08jzuvz5skzcs6h9raxu898l48xwr8fmkny3zqqrgd9 // corresponds to address: ztestsapling1zhqvuq8zdwa8nsnde7074kcfsat0w25n08jzuvz5skzcs6h9raxu898l48xwr8fmkny3zqqrgd9

View File

@@ -1 +1,35 @@
TODO # Overview
From an app developer's perspective, this SDK will encapsulate the most complex aspects of using Zcash, freeing the developer to focus on UI and UX, rather than scanning blockchains and building commitment trees! Internally, the SDK is structured as follows:
![SDK Diagram](assets/sdk_diagram_final.png?raw=true "SDK Diagram")
Thankfully, the only thing an app developer has to be concerned with is the following:
![SDK Diagram Developer Perspective](assets/sdk_dev_pov_final.png?raw=true "SDK Diagram Dev PoV")
# 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 |
# 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 [sdk-lib's 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

110
docs/Consumers.md Normal file
View File

@@ -0,0 +1,110 @@
# Configuring your build
Add flavors for testnet and 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"))
}
}
```
Resources
/src/main/res/values/bools.xml
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="zcash_is_testnet">false</bool>
</resources>
```
/src/zcashtestnet/res/values/bools.xml
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="zcash_is_testnet">true</bool>
</resources>
```
ZcashNetworkExt.kt
```
/**
* @return Zcash network determined from resources.
*/
fun ZcashNetwork.Companion.fromResources(context: Context) =
if (context.resources.getBoolean(R.bool.zcash_is_testnet)) {
ZcashNetwork.Testnet
} else {
ZcashNetwork.Mainnet
}
```
Add the SDK dependency:
```kotlin
implementation("cash.z.ecc.android:zcash-android-sdk:$LATEST_VERSION")
```
# Using the SDK
Start the [Synchronizer](-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)
```
The [Synchronizer](-synchronizer/README.md) is the primary entrypoint for the SDK.
1. Start the [Synchronizer](-synchronizer/README.md)
2. Subscribe to wallet data
The [Synchronizer](-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

View File

@@ -68,6 +68,7 @@ Start by making sure the command line with Gradle works first, because **all the
1. Note: When first opening the project, Android Studio will warn that Gradle checksums are not fully supported. Choose the "Use checksum" option. This is a security feature that we have explicitly enabled. 1. Note: When first opening the project, Android Studio will warn that Gradle checksums are not fully supported. Choose the "Use checksum" option. This is a security feature that we have explicitly enabled.
1. Shortly after opening the project, Android Studio may prompt about updating the Android Gradle Plugin. DO NOT DO THIS. If you do so, the build will fail because the project also has dependency locking enabled as a security feature. To learn more, see [Build integrity.md](Build%20Integrity.md) 1. Shortly after opening the project, Android Studio may prompt about updating the Android Gradle Plugin. DO NOT DO THIS. If you do so, the build will fail because the project also has dependency locking enabled as a security feature. To learn more, see [Build integrity.md](Build%20Integrity.md)
1. Android Studio may prompt about updating the Kotlin plugin. Do this. Our application often uses a newer version of Kotlin than is bundled with Android Studio. 1. Android Studio may prompt about updating the Kotlin plugin. Do this. Our application often uses a newer version of Kotlin than is bundled with Android Studio.
1. Note that some versions of Android Studio on Intel machines have trouble with dependency locks. If you experience this issue, the workaround is to add the following line to `~/.gradle/gradle.properties` `ZCASH_IS_DEPENDENCY_LOCKING_ENABLED=false`
1. After Android Studio finishes syncing with Gradle, look for the green "play" run button in the toolbar. To the left of it, choose the "demo-app" run configuration under the dropdown menu. Then hit the run button. 1. After Android Studio finishes syncing with Gradle, look for the green "play" run button in the toolbar. To the left of it, choose the "demo-app" run configuration under the dropdown menu. Then hit the run button.
1. Note: The SDK supports both testnet and mainnet. The decision to switch between them is made at the application level. To switch between build variants, look for "Build Variants" which is usually a small button in the left gutter of the Android Studio window. 1. Note: The SDK supports both testnet and mainnet. The decision to switch between them is made at the application level. To switch between build variants, look for "Build Variants" which is usually a small button in the left gutter of the Android Studio window.
@@ -87,7 +88,8 @@ Start by making sure the command line with Gradle works first, because **all the
## Gradle Tasks ## Gradle Tasks
A variety of Gradle tasks are set up within the project, and these tasks are also accessible in Android Studio as run configurations. A variety of Gradle tasks are set up within the project, and these tasks are also accessible in Android Studio as run configurations.
* `assemble` - Compiles the SDK and demo application but does not deploy it * `assemble` - Compiles the SDK and demo application but does not deploy it
* `sdk-lib:connectedAndroidTest` - Runs the tests against the SDK * `sdk-lib:test` - Runs unit tests in the SDK that don't require Android. This is generally a small number of tests against plain Kotlin code without Android dependencies.
* `sdk-lib:connectedAndroidTest` - Runs the tests against the SDK that require integration with Android.
* `darkside-test-lib:connectedAndroidTest` - Runs the tests against the SDK which require a localhost lightwalletd server running in darkside mode * `darkside-test-lib:connectedAndroidTest` - Runs the tests against the SDK which require a localhost lightwalletd server running in darkside mode
* `assembleAndroidTest` - Compiles the application and tests, but does not deploy the application or run the tests. The Android Studio run configuration actually runs all of these tasks because the debug APKs are necessary to run the tests: `assembleDebug assembleZcashmainnetDebug assembleZcashtestnetDebug assembleAndroidTest` * `assembleAndroidTest` - Compiles the application and tests, but does not deploy the application or run the tests. The Android Studio run configuration actually runs all of these tasks because the debug APKs are necessary to run the tests: `assembleDebug assembleZcashmainnetDebug assembleZcashtestnetDebug assembleAndroidTest`
* `detektAll` - Performs static analysis with Detekt * `detektAll` - Performs static analysis with Detekt
@@ -127,4 +129,4 @@ For Continuous Integration, see [CI.md](CI.md). The rest of this section is reg
1. If you are an Electric Coin Co team member: We are still setting up a process for this, because emulator.wtf does not yet support individual API tokens 1. If you are an Electric Coin Co team member: We are still setting up a process for this, because emulator.wtf does not yet support individual API tokens
1. If you are an open source contributor: Visit http://emulator.wtf and request an API key 1. If you are an open source contributor: Visit http://emulator.wtf and request an API key
1. Set the emulator.wtf API key as a global Gradle property `ZCASH_EMULATOR_WTF_API_KEY` under `~/.gradle/gradle.properties` 1. Set the emulator.wtf API key as a global Gradle property `ZCASH_EMULATOR_WTF_API_KEY` under `~/.gradle/gradle.properties`
1. Run the Gradle task `./gradlew testDebugWithEmulatorWtf :app:testZcashmainnetDebugWithEmulatorWtf` (emulator.wtf tasks do build the app, so you don't need to build them beforehand) 1. Run the Gradle task `./gradlew testDebugWithEmulatorWtf` (emulator.wtf tasks do build the tests and test APKs, so you don't need to build them beforehand. This is a different behavior compared to Firebase Test Lab)

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

View File

Before

Width:  |  Height:  |  Size: 445 KiB

After

Width:  |  Height:  |  Size: 445 KiB

View File

@@ -1,10 +1,9 @@
# Speed up builds. Keep these flags here for quick debugging of issues. # Speed up builds. Keep these flags here for quick debugging of issues.
# https://github.com/gradle/gradle/issues/13382
org.gradle.vfs.watch=false
org.gradle.configureondemand=false
org.gradle.caching=true org.gradle.caching=true
org.gradle.parallel=true org.gradle.configureondemand=false
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
org.gradle.parallel=true
org.gradle.vfs.watch=true
android.useAndroidX=true android.useAndroidX=true
android.builder.sdkDownload=true android.builder.sdkDownload=true
@@ -22,7 +21,7 @@ RELEASE_SIGNING_ENABLED=false
# Required by the maven publishing plugin # Required by the maven publishing plugin
SONATYPE_HOST=DEFAULT SONATYPE_HOST=DEFAULT
LIBRARY_VERSION=1.7.0-beta01 LIBRARY_VERSION=1.9.0-beta01
# Kotlin compiler warnings can be considered errors, failing the build. # Kotlin compiler warnings can be considered errors, failing the build.
# Currently set to false, because this project has a lot of warnings to fix first. # Currently set to false, because this project has a lot of warnings to fix first.
@@ -64,17 +63,17 @@ ANDROID_COMPILE_SDK_VERSION=33
# When changing this, be sure to update .github/actions/setup/action.yml # When changing this, be sure to update .github/actions/setup/action.yml
ANDROID_NDK_VERSION=22.1.7171670 ANDROID_NDK_VERSION=22.1.7171670
ANDROID_GRADLE_PLUGIN_VERSION=7.2.1 ANDROID_GRADLE_PLUGIN_VERSION=7.2.2
DETEKT_VERSION=1.20.0 DETEKT_VERSION=1.21.0
DOKKA_VERSION=1.6.21 DOKKA_VERSION=1.7.10
EMULATOR_WTF_GRADLE_PLUGIN_VERSION=0.0.10 EMULATOR_WTF_GRADLE_PLUGIN_VERSION=0.0.10
FLANK_VERSION=22.03.0 FLANK_VERSION=22.03.0
FULLADLE_VERSION=0.17.4 FULLADLE_VERSION=0.17.4
GRADLE_VERSIONS_PLUGIN_VERSION=0.42.0 GRADLE_VERSIONS_PLUGIN_VERSION=0.42.0
KTLINT_VERSION=0.46.1 KTLINT_VERSION=0.46.1
KSP_VERSION=1.6.21-1.0.6 KSP_VERSION=1.7.10-1.0.6
MAVEN_PUBLISH_GRADLE_PLUGIN=0.20.0 MAVEN_PUBLISH_GRADLE_PLUGIN=0.20.0
PROTOBUF_GRADLE_PLUGIN_VERSION=0.8.18 PROTOBUF_GRADLE_PLUGIN_VERSION=0.8.19
RUST_GRADLE_PLUGIN_VERSION=0.9.3 RUST_GRADLE_PLUGIN_VERSION=0.9.3
ANDROIDX_ANNOTATION_VERSION=1.3.0 ANDROIDX_ANNOTATION_VERSION=1.3.0
@@ -86,27 +85,27 @@ ANDROIDX_LIFECYCLE_VERSION=2.4.1
ANDROIDX_MULTIDEX_VERSION=2.0.1 ANDROIDX_MULTIDEX_VERSION=2.0.1
ANDROIDX_NAVIGATION_VERSION=2.4.2 ANDROIDX_NAVIGATION_VERSION=2.4.2
ANDROIDX_PAGING_VERSION=2.1.2 ANDROIDX_PAGING_VERSION=2.1.2
ANDROIDX_ROOM_VERSION=2.4.2 ANDROIDX_ROOM_VERSION=2.4.3
ANDROIDX_TEST_JUNIT_VERSION=1.1.3 ANDROIDX_TEST_JUNIT_VERSION=1.1.3
ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.1.0-alpha1 ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.1.0-alpha1
ANDROIDX_TEST_VERSION=1.4.0 ANDROIDX_TEST_VERSION=1.4.0
ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1 ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1
BIP39_VERSION=1.0.2 BIP39_VERSION=1.0.4
COROUTINES_OKHTTP=1.0 COROUTINES_OKHTTP=1.0
GOOGLE_MATERIAL_VERSION=1.6.1 GOOGLE_MATERIAL_VERSION=1.6.1
GRPC_VERSION=1.47.0 GRPC_VERSION=1.48.1
GSON_VERSION=2.9.0 GSON_VERSION=2.9.0
GUAVA_VERSION=31.1-android GUAVA_VERSION=31.1-android
JACOCO_VERSION=0.8.8 JACOCO_VERSION=0.8.8
JAVAX_ANNOTATION_VERSION=1.3.2 JAVAX_ANNOTATION_VERSION=1.3.2
JUNIT_VERSION=5.8.2 JUNIT_VERSION=5.9.0
KOTLINX_COROUTINES_VERSION=1.6.2 KOTLINX_COROUTINES_VERSION=1.6.4
KOTLIN_VERSION=1.6.21 KOTLIN_VERSION=1.7.10
MOCKITO_KOTLIN_VERSION=2.2.0 MOCKITO_KOTLIN_VERSION=2.2.0
MOCKITO_VERSION=4.6.1 MOCKITO_VERSION=4.6.1
OKHTTP_VERSION=4.10.0 OKHTTP_VERSION=4.10.0
OKIO_VERSION=3.1.0 OKIO_VERSION=3.2.0
PROTOC_VERSION=3.21.1 PROTOC_VERSION=3.21.4
ZCASH_WALLET_PLUGINS_VERSION=1.0.1 ZCASH_WALLET_PLUGINS_VERSION=1.0.1
# This shouldn't be changed, as Android doesn't support targets beyond Java 8 # This shouldn't be changed, as Android doesn't support targets beyond Java 8

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionSha256Sum=29e49b10984e585d8118b7d0bc452f944e386458df27371b49b4ac1dec4b7fda distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

6
gradlew vendored
View File

@@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \ org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args. # Use "xargs" to parse quoted args.
# #
# With -n1 it outputs one arg per line, with the quotes and backslashes removed. # With -n1 it outputs one arg per line, with the quotes and backslashes removed.

14
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

@@ -29,11 +29,11 @@ secp256k1 = "0.19"
# consensus branch ID and activation heights, and v5 transaction parsing, backported. # consensus branch ID and activation heights, and v5 transaction parsing, backported.
# https://github.com/zcash/librustzcash/pull/555 # https://github.com/zcash/librustzcash/pull/555
# https://github.com/zcash/librustzcash/pull/558 # https://github.com/zcash/librustzcash/pull/558
[patch.crates-io] #[patch.crates-io]
zcash_client_backend = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' } #zcash_client_backend = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' }
zcash_client_sqlite = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' } #zcash_client_sqlite = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' }
zcash_primitives = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' } #zcash_primitives = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' }
zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' } #zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b8acf336bdc20ba49083c66f114ecf6877' }
## Uncomment this to test librustzcash changes locally ## Uncomment this to test librustzcash changes locally
#[patch.crates-io] #[patch.crates-io]
@@ -43,11 +43,11 @@ zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='0a1ed9b
#zcash_proofs = { path = '../../clones/librustzcash/zcash_proofs' } #zcash_proofs = { path = '../../clones/librustzcash/zcash_proofs' }
## Uncomment this to test someone else's librustzcash changes in a branch ## Uncomment this to test someone else's librustzcash changes in a branch
#[patch.crates-io] [patch.crates-io]
#zcash_client_backend = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" } zcash_client_backend = { git = "https://git.hush.is/fekt/librustzcash", branch = "main" }
#zcash_client_sqlite = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" } zcash_client_sqlite = { git = "https://git.hush.is/fekt/librustzcash", branch = "main" }
#zcash_primitives = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" } zcash_primitives = { git = "https://git.hush.is/fekt/librustzcash", branch = "main" }
#zcash_proofs = { git = "https://github.com/zcash/librustzcash", branch = "branch-name" } zcash_proofs = { git = "https://git.hush.is/fekt/librustzcash", branch = "main" }
[features] [features]
mainnet = ["zcash_client_sqlite/mainnet"] mainnet = ["zcash_client_sqlite/mainnet"]

View File

@@ -3,8 +3,8 @@ package cash.z.ecc.android.sdk
import android.content.Context import android.content.Context
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.tool.CheckpointTool
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.json.JSONObject import org.json.JSONObject
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@@ -57,7 +57,7 @@ class AssetTest {
private fun assertFileContents(network: ZcashNetwork, files: Array<String>?) { private fun assertFileContents(network: ZcashNetwork, files: Array<String>?) {
files?.map { filename -> files?.map { filename ->
val filePath = "${WalletBirthdayTool.birthdayDirectory(network)}/$filename" val filePath = "${CheckpointTool.checkpointDirectory(network)}/$filename"
ApplicationProvider.getApplicationContext<Context>().assets.open(filePath) ApplicationProvider.getApplicationContext<Context>().assets.open(filePath)
.use { inputSteam -> .use { inputSteam ->
inputSteam.bufferedReader().use { bufferedReader -> inputSteam.bufferedReader().use { bufferedReader ->
@@ -77,12 +77,13 @@ class AssetTest {
val expectedNetworkName = when (network) { val expectedNetworkName = when (network) {
ZcashNetwork.Mainnet -> "main" ZcashNetwork.Mainnet -> "main"
ZcashNetwork.Testnet -> "test" ZcashNetwork.Testnet -> "test"
else -> IllegalArgumentException("Unsupported network $network")
} }
assertEquals("File: ${it.filename}", expectedNetworkName, jsonObject.getString("network")) assertEquals("File: ${it.filename}", expectedNetworkName, jsonObject.getString("network"))
assertEquals( assertEquals(
"File: ${it.filename}", "File: ${it.filename}",
WalletBirthdayTool.birthdayHeight(it.filename), CheckpointTool.checkpointHeightFromFilename(network, it.filename),
jsonObject.getInt("height") jsonObject.getInt("height")
) )
@@ -94,9 +95,9 @@ class AssetTest {
companion object { companion object {
fun listAssets(network: ZcashNetwork) = runBlocking { fun listAssets(network: ZcashNetwork) = runBlocking {
WalletBirthdayTool.listBirthdayDirectoryContents( CheckpointTool.listCheckpointDirectoryContents(
ApplicationProvider.getApplicationContext<Context>(), ApplicationProvider.getApplicationContext<Context>(),
WalletBirthdayTool.birthdayDirectory(network) CheckpointTool.checkpointDirectory(network)
) )
} }
} }

View File

@@ -1,7 +1,7 @@
package cash.z.ecc.android.sdk.ext package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.Initializer import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.util.SimpleMnemonics import cash.z.ecc.android.sdk.util.SimpleMnemonics
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -14,14 +14,14 @@ fun Initializer.Config.seedPhrase(seedPhrase: String, network: ZcashNetwork) {
} }
object BlockExplorer { object BlockExplorer {
suspend fun fetchLatestHeight(): Int { suspend fun fetchLatestHeight(): Long {
val client = OkHttpClient() val client = OkHttpClient()
val request = Request.Builder() val request = Request.Builder()
.url("https://api.blockchair.com/zcash/blocks?limit=1") .url("https://api.blockchair.com/zcash/blocks?limit=1")
.build() .build()
val result = client.newCall(request).await() val result = client.newCall(request).await()
val body = result.body?.string() val body = result.body?.string()
return JSONObject(body).getJSONArray("data").getJSONObject(0).getInt("id") return JSONObject(body).getJSONArray("data").getJSONObject(0).getLong("id")
} }
} }

View File

@@ -3,11 +3,11 @@ package cash.z.ecc.android.sdk.integration
import cash.z.ecc.android.sdk.annotation.MaintainedTest import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.ext.BlockExplorer import cash.z.ecc.android.sdk.ext.BlockExplorer
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -30,13 +30,6 @@ class SanityTest(
val networkName = wallet.networkName val networkName = wallet.networkName
val name = "$networkName wallet" val name = "$networkName wallet"
@Test
fun testNotPlaintext() {
val message =
"is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false"
assertFalse("$name $message", wallet.service.connectionInfo.usePlaintext)
}
@Test @Test
fun testFilePaths() { fun testFilePaths() {
assertEquals( assertEquals(
@@ -61,7 +54,7 @@ class SanityTest(
assertEquals( assertEquals(
"$name has invalid birthday height", "$name has invalid birthday height",
birthday, birthday,
wallet.initializer.birthday.height wallet.initializer.checkpoint.height
) )
} }
@@ -79,25 +72,15 @@ class SanityTest(
) )
} }
@Test
fun testServerConnection() {
assertEquals(
"$name has an invalid server connection",
"$networkName.lite.hushpool.is:9067?usePlaintext=true",
wallet.connectionInfo
)
}
@Test @Test
fun testLatestHeight() = runBlocking { fun testLatestHeight() = runBlocking {
if (wallet.networkName == "mainnet") { if (wallet.networkName == "mainnet") {
val expectedHeight = BlockExplorer.fetchLatestHeight() val expectedHeight = BlockExplorer.fetchLatestHeight()
// fetch height directly because the synchronizer hasn't started, yet // fetch height directly because the synchronizer hasn't started, yet
val downloaderHeight = wallet.service.getLatestBlockHeight() val downloaderHeight = wallet.service.getLatestBlockHeight()
val info = wallet.connectionInfo
assertTrue( assertTrue(
"$info\n ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight", "${wallet.endpoint} ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight",
expectedHeight - 10 < downloaderHeight expectedHeight - 10 < downloaderHeight.value
) )
} }
} }
@@ -105,9 +88,9 @@ class SanityTest(
@Test @Test
fun testSingleBlockDownload() = runBlocking { fun testSingleBlockDownload() = runBlocking {
// fetch block directly because the synchronizer hasn't started, yet // fetch block directly because the synchronizer hasn't started, yet
val height = 1_000_000 val height = BlockHeight.new(wallet.network, 1_000_000)
val block = wallet.service.getBlockRange(height..height)[0] val block = wallet.service.getBlockRange(height..height).first()
assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height.toInt() == height) assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height == height.value)
} }
companion object { companion object {

View File

@@ -4,7 +4,6 @@ import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.annotation.MaintainedTest import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.util.TestWallet import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert import org.junit.Assert
@@ -19,16 +18,6 @@ import org.junit.Test
@MediumTest @MediumTest
class SmokeTest { class SmokeTest {
@Test
fun testNotPlaintext() {
val service =
wallet.synchronizer.processor.downloader.lightWalletService as LightWalletGrpcService
Assert.assertFalse(
"Wallet is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false",
service.connectionInfo.usePlaintext
)
}
@Test @Test
fun testFilePaths() { fun testFilePaths() {
Assert.assertEquals("Invalid DataDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Data.db", wallet.initializer.rustBackend.pathDataDb) Assert.assertEquals("Invalid DataDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Data.db", wallet.initializer.rustBackend.pathDataDb)
@@ -38,7 +27,7 @@ class SmokeTest {
@Test @Test
fun testBirthday() { fun testBirthday() {
Assert.assertEquals("Invalid birthday height", 1_320_000, wallet.initializer.birthday.height) Assert.assertEquals("Invalid birthday height", 1_320_000, wallet.initializer.checkpoint.height)
} }
@Test @Test

View File

@@ -1,4 +1,4 @@
package cash.z.wallet.sdk.integration package cash.z.ecc.android.sdk.integration
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
@@ -12,11 +12,13 @@ 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 cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.test.ScopedTest import cash.z.ecc.android.sdk.test.ScopedTest
import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
@@ -38,9 +40,9 @@ class TestnetIntegrationTest : ScopedTest() {
@Test @Test
@Ignore("This test is broken") @Ignore("This test is broken")
fun testLatestBlockTest() { fun testLatestBlockTest() {
val service = LightWalletGrpcService( val service = LightWalletGrpcService.new(
context, context,
host lightWalletEndpoint
) )
val height = service.getLatestBlockHeight() val height = service.getLatestBlockHeight()
assertTrue(height > saplingActivation) assertTrue(height > saplingActivation)
@@ -49,7 +51,7 @@ class TestnetIntegrationTest : ScopedTest() {
@Test @Test
fun testLoadBirthday() { fun testLoadBirthday() {
val (height, hash, time, tree) = runBlocking { val (height, hash, time, tree) = runBlocking {
WalletBirthdayTool.loadNearest( CheckpointTool.loadNearest(
context, context,
synchronizer.network, synchronizer.network,
saplingActivation + 1 saplingActivation + 1
@@ -117,8 +119,8 @@ class TestnetIntegrationTest : ScopedTest() {
companion object { companion object {
init { Twig.plant(TroubleshootingTwig()) } init { Twig.plant(TroubleshootingTwig()) }
const val host = "lightwalletd.testnet.z.cash" val lightWalletEndpoint = LightWalletEndpoint("lightwalletd.testnet.z.cash", 9087, true)
private const val birthdayHeight = 963150 private const val birthdayHeight = 963150L
private const val targetHeight = 663250 private const val targetHeight = 663250
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 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"
val seed = "cash.z.ecc.android.sdk.integration.IntegrationTest.seed.value.64bytes".toByteArray() val seed = "cash.z.ecc.android.sdk.integration.IntegrationTest.seed.value.64bytes".toByteArray()
@@ -128,8 +130,8 @@ class TestnetIntegrationTest : ScopedTest() {
private val context = InstrumentationRegistry.getInstrumentation().context private val context = InstrumentationRegistry.getInstrumentation().context
private val initializer = runBlocking { private val initializer = runBlocking {
Initializer.new(context) { config -> Initializer.new(context) { config ->
config.setNetwork(ZcashNetwork.Testnet, host) config.setNetwork(ZcashNetwork.Testnet, lightWalletEndpoint)
runBlocking { config.importWallet(seed, birthdayHeight, ZcashNetwork.Testnet) } runBlocking { config.importWallet(seed, BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight), ZcashNetwork.Testnet, lightWalletEndpoint) }
} }
} }
private lateinit var synchronizer: Synchronizer private lateinit var synchronizer: Synchronizer

View File

@@ -11,8 +11,12 @@ import cash.z.ecc.android.sdk.internal.block.CompactBlockStore
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.Mainnet
import cash.z.ecc.android.sdk.model.Testnet
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.test.ScopedTest import cash.z.ecc.android.sdk.test.ScopedTest
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -34,13 +38,15 @@ import org.mockito.Spy
class ChangeServiceTest : ScopedTest() { class ChangeServiceTest : ScopedTest() {
val network = ZcashNetwork.Mainnet val network = ZcashNetwork.Mainnet
val lightWalletEndpoint = LightWalletEndpoint.Mainnet
private val eccEndpoint = LightWalletEndpoint("lightwalletd.electriccoin.co", 9087, true)
@Mock @Mock
lateinit var mockBlockStore: CompactBlockStore lateinit var mockBlockStore: CompactBlockStore
var mockCloseable: AutoCloseable? = null var mockCloseable: AutoCloseable? = null
@Spy @Spy
val service = LightWalletGrpcService(context, network) val service = LightWalletGrpcService.new(context, lightWalletEndpoint)
lateinit var downloader: CompactBlockDownloader lateinit var downloader: CompactBlockDownloader
lateinit var otherService: LightWalletService lateinit var otherService: LightWalletService
@@ -49,7 +55,7 @@ class ChangeServiceTest : ScopedTest() {
fun setup() { fun setup() {
initMocks() initMocks()
downloader = CompactBlockDownloader(service, mockBlockStore) downloader = CompactBlockDownloader(service, mockBlockStore)
otherService = LightWalletGrpcService(context, "lightwalletd.electriccoin.co") otherService = LightWalletGrpcService.new(context, eccEndpoint)
} }
@After @After
@@ -70,7 +76,7 @@ class ChangeServiceTest : ScopedTest() {
@Test @Test
fun testCleanSwitch() = runBlocking { fun testCleanSwitch() = runBlocking {
downloader.changeService(otherService) downloader.changeService(otherService)
val result = downloader.downloadBlockRange(900_000..901_000) val result = downloader.downloadBlockRange(BlockHeight.new(network, 900_000)..BlockHeight.new(network, 901_000))
assertEquals(1_001, result) assertEquals(1_001, result)
} }
@@ -81,7 +87,7 @@ class ChangeServiceTest : ScopedTest() {
@Test @Test
@Ignore("This test is broken") @Ignore("This test is broken")
fun testSwitchWhileActive() = runBlocking { fun testSwitchWhileActive() = runBlocking {
val start = 900_000 val start = BlockHeight.new(ZcashNetwork.Mainnet, 900_000)
val count = 5 val count = 5
val differentiators = mutableListOf<String>() val differentiators = mutableListOf<String>()
var initialValue = downloader.getServerInfo().buildUser var initialValue = downloader.getServerInfo().buildUser
@@ -105,7 +111,7 @@ class ChangeServiceTest : ScopedTest() {
@Test @Test
fun testSwitchToInvalidServer() = runBlocking { fun testSwitchToInvalidServer() = runBlocking {
var caughtException: Throwable? = null var caughtException: Throwable? = null
downloader.changeService(LightWalletGrpcService(context, "invalid.lightwalletd")) { downloader.changeService(LightWalletGrpcService.new(context, LightWalletEndpoint("invalid.lightwalletd", 9087, true))) {
caughtException = it caughtException = it
} }
assertNotNull("Using an invalid host should generate an exception.", caughtException) assertNotNull("Using an invalid host should generate an exception.", caughtException)
@@ -118,7 +124,7 @@ class ChangeServiceTest : ScopedTest() {
@Test @Test
fun testSwitchToTestnetFails() = runBlocking { fun testSwitchToTestnetFails() = runBlocking {
var caughtException: Throwable? = null var caughtException: Throwable? = null
downloader.changeService(LightWalletGrpcService(context, ZcashNetwork.Testnet)) { downloader.changeService(LightWalletGrpcService.new(context, LightWalletEndpoint.Testnet)) {
caughtException = it caughtException = it
} }
assertNotNull("Using an invalid host should generate an exception.", caughtException) assertNotNull("Using an invalid host should generate an exception.", caughtException)

View File

@@ -0,0 +1,47 @@
package cash.z.ecc.android.sdk.internal
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.fixture.CheckpointFixture
import cash.z.ecc.fixture.toJson
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Test
class CheckpointTest {
@Test
@SmallTest
fun deserialize() {
val fixtureCheckpoint = CheckpointFixture.new()
val deserialized = Checkpoint.from(CheckpointFixture.NETWORK, fixtureCheckpoint.toJson())
assertEquals(fixtureCheckpoint, deserialized)
}
@Test
@SmallTest
fun epoch_seconds_as_long_that_would_overflow_int() {
val jsonString = CheckpointFixture.new(time = Long.MAX_VALUE).toJson()
Checkpoint.from(CheckpointFixture.NETWORK, jsonString).also {
assertEquals(Long.MAX_VALUE, it.epochSeconds)
}
}
@Test
@SmallTest
fun parse_height_as_long_that_would_overflow_int() {
val jsonString = JSONObject().apply {
put(Checkpoint.KEY_VERSION, Checkpoint.VERSION_1)
put(Checkpoint.KEY_HEIGHT, UInt.MAX_VALUE.toLong())
put(Checkpoint.KEY_HASH, CheckpointFixture.HASH)
put(Checkpoint.KEY_EPOCH_SECONDS, CheckpointFixture.EPOCH_SECONDS)
put(Checkpoint.KEY_TREE, CheckpointFixture.TREE)
}.toString()
Checkpoint.from(CheckpointFixture.NETWORK, jsonString).also {
assertEquals(UInt.MAX_VALUE.toLong(), it.height.value)
}
}
}

View File

@@ -1,30 +0,0 @@
package cash.z.ecc.android.sdk.internal
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.fixture.WalletBirthdayFixture
import cash.z.ecc.fixture.toJson
import org.junit.Assert.assertEquals
import org.junit.Test
class WalletBirthdayTest {
@Test
@SmallTest
fun deserialize() {
val fixtureBirthday = WalletBirthdayFixture.new()
val deserialized = WalletBirthday.from(fixtureBirthday.toJson())
assertEquals(fixtureBirthday, deserialized)
}
@Test
@SmallTest
fun epoch_seconds_as_long_that_would_overflow_int() {
val jsonString = WalletBirthdayFixture.new(time = Long.MAX_VALUE).toJson()
WalletBirthday.from(jsonString).also {
assertEquals(Long.MAX_VALUE, it.time)
}
}
}

View File

@@ -2,7 +2,8 @@ package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.annotation.MaintainedTest import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@@ -14,9 +15,9 @@ import org.junit.runners.Parameterized
*/ */
@MaintainedTest(TestPurpose.REGRESSION) @MaintainedTest(TestPurpose.REGRESSION)
@RunWith(Parameterized::class) @RunWith(Parameterized::class)
class BranchIdTest( class BranchIdTest internal constructor(
private val networkName: String, private val networkName: String,
private val height: Int, private val height: BlockHeight,
private val branchId: Long, private val branchId: Long,
private val branchHex: String, private val branchHex: String,
private val rustBackend: RustBackendWelding private val rustBackend: RustBackendWelding
@@ -44,14 +45,14 @@ class BranchIdTest(
// is an abnormal use of the SDK because this really should run at the rust level // is an abnormal use of the SDK because this really should run at the rust level
// However, due to quirks on certain devices, we created this test at the Android level, // However, due to quirks on certain devices, we created this test at the Android level,
// as a sanity check // as a sanity check
val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet) } val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet, ZcashNetwork.Testnet.saplingActivationHeight) }
val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet) } val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight) }
return listOf( return listOf(
// Mainnet Cases // Mainnet Cases
arrayOf("Sapling", 419_200, 1991772603L, "76b809bb", mainnetBackend), arrayOf("Sapling", 1, 1991772603L, "76b809bb", mainnetBackend),
// Testnet Cases // Testnet Cases
arrayOf("Sapling", 280_000, 1991772603L, "76b809bb", testnetBackend) arrayOf("Sapling", 1, 1991772603L, "76b809bb", testnetBackend),
) )
} }
} }

View File

@@ -7,8 +7,8 @@ import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig 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 cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.BeforeClass import org.junit.BeforeClass

View File

@@ -2,7 +2,7 @@ package cash.z.ecc.android.sdk.sample
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert import org.junit.Assert

View File

@@ -3,8 +3,9 @@ package cash.z.ecc.android.sdk.sample
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.type.ZcashNetwork.Testnet import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -73,7 +74,7 @@ class TransparentRestoreSample {
// wallet.rewindToHeight(1343500).join(45_000) // wallet.rewindToHeight(1343500).join(45_000)
val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET, alias = "WalletC") val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET, alias = "WalletC")
// wallet.sync().rewindToHeight(1339178).join(10000) // wallet.sync().rewindToHeight(1339178).join(10000)
wallet.sync().rewindToHeight(1339178).send( wallet.sync().rewindToHeight(BlockHeight.new(ZcashNetwork.Testnet, 1339178)).send(
"ztestsapling17zazsl8rryl8kjaqxnr2r29rw9d9a2mud37ugapm0s8gmyv0ue43h9lqwmhdsp3nu9dazeqfs6l", "ztestsapling17zazsl8rryl8kjaqxnr2r29rw9d9a2mud37ugapm0s8gmyv0ue43h9lqwmhdsp3nu9dazeqfs6l",
"is send broken?" "is send broken?"
).join(5) ).join(5)
@@ -85,7 +86,15 @@ class TransparentRestoreSample {
@LargeTest @LargeTest
@Ignore("This test is extremely slow") @Ignore("This test is extremely slow")
fun kris() = runBlocking<Unit> { fun kris() = runBlocking<Unit> {
val wallet0 = TestWallet(TestWallet.Backups.SAMPLE_WALLET.seedPhrase, "tmpabc", Testnet, startHeight = 1330190) val wallet0 = TestWallet(
TestWallet.Backups.SAMPLE_WALLET.seedPhrase,
"tmpabc",
ZcashNetwork.Testnet,
startHeight = BlockHeight.new(
ZcashNetwork.Testnet,
1330190
)
)
// val wallet1 = SimpleWallet(WALLET0_PHRASE, "Wallet1") // val wallet1 = SimpleWallet(WALLET0_PHRASE, "Wallet1")
wallet0.sync() // .shieldFunds() wallet0.sync() // .shieldFunds()
@@ -107,7 +116,15 @@ class TransparentRestoreSample {
*/ */
// @Test // @Test
fun hasFunds() = runBlocking<Unit> { fun hasFunds() = runBlocking<Unit> {
val walletSandbox = TestWallet(TestWallet.Backups.SAMPLE_WALLET.seedPhrase, "WalletC", Testnet, startHeight = 1330190) val walletSandbox = TestWallet(
TestWallet.Backups.SAMPLE_WALLET.seedPhrase,
"WalletC",
ZcashNetwork.Testnet,
startHeight = BlockHeight.new(
ZcashNetwork.Testnet,
1330190
)
)
// val job = walletA.walletScope.launch { // val job = walletA.walletScope.launch {
// twig("Syncing WalletA") // twig("Syncing WalletA")
// walletA.sync() // walletA.sync()
@@ -125,7 +142,7 @@ class TransparentRestoreSample {
// send z->t // send z->t
// walletA.send(TX_VALUE, walletA.transparentAddress, "${TransparentRestoreSample::class.java.simpleName} z->t") // walletA.send(TX_VALUE, walletA.transparentAddress, "${TransparentRestoreSample::class.java.simpleName} z->t")
walletSandbox.rewindToHeight(1339178) walletSandbox.rewindToHeight(BlockHeight.new(ZcashNetwork.Testnet, 1339178))
twig("Done REWINDING!") twig("Done REWINDING!")
twig("T-ADDR (for the win!): ${walletSandbox.transparentAddress}") twig("T-ADDR (for the win!): ${walletSandbox.transparentAddress}")
delay(500) delay(500)

View File

@@ -4,18 +4,19 @@ import android.content.Context
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool.IS_FALLBACK_ON_FAILURE import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.CheckpointTool.IS_FALLBACK_ON_FAILURE
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class WalletBirthdayToolTest { class CheckpointToolTest {
@Test @Test
@SmallTest @SmallTest
fun birthday_height_from_filename() { fun birthday_height_from_filename() {
assertEquals(123, WalletBirthdayTool.birthdayHeight("123.json")) assertEquals(123, CheckpointTool.checkpointHeightFromFilename(ZcashNetwork.Mainnet, "123.json"))
} }
@Test @Test
@@ -27,13 +28,14 @@ class WalletBirthdayToolTest {
val context = ApplicationProvider.getApplicationContext<Context>() val context = ApplicationProvider.getApplicationContext<Context>()
val birthday = runBlocking { val birthday = runBlocking {
WalletBirthdayTool.getFirstValidWalletBirthday( CheckpointTool.getFirstValidWalletBirthday(
context, context,
ZcashNetwork.Mainnet,
directory, directory,
listOf("1300000.json", "1290000.json") listOf("1300000.json", "1290000.json")
) )
} }
assertEquals(1300000, birthday.height) assertEquals(1300000, birthday.height.value)
} }
@Test @Test
@@ -46,12 +48,13 @@ class WalletBirthdayToolTest {
val directory = "co.electriccoin.zcash/checkpoint/badnet" val directory = "co.electriccoin.zcash/checkpoint/badnet"
val context = ApplicationProvider.getApplicationContext<Context>() val context = ApplicationProvider.getApplicationContext<Context>()
val birthday = runBlocking { val birthday = runBlocking {
WalletBirthdayTool.getFirstValidWalletBirthday( CheckpointTool.getFirstValidWalletBirthday(
context, context,
ZcashNetwork.Mainnet,
directory, directory,
listOf("1300000.json", "1290000.json") listOf("1300000.json", "1290000.json")
) )
} }
assertEquals(1290000, birthday.height) assertEquals(1290000, birthday.height.value)
} }
} }

View File

@@ -1,10 +1,9 @@
package cash.z.ecc.android.sdk.util package cash.z.ecc.android.sdk.util
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

View File

@@ -6,10 +6,13 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig 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 cash.z.ecc.android.sdk.internal.ext.deleteSuspend import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.WalletBirthday import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.defaultForNetwork
import cash.z.ecc.android.sdk.tool.CheckpointTool
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -30,7 +33,7 @@ class BalancePrinterUtil {
private val network = ZcashNetwork.Mainnet private val network = ZcashNetwork.Mainnet
private val downloadBatchSize = 9_000 private val downloadBatchSize = 9_000
private val birthdayHeight = 523240 private val birthdayHeight = BlockHeight.new(network, 523240)
private val mnemonics = SimpleMnemonics() private val mnemonics = SimpleMnemonics()
private val context = InstrumentationRegistry.getInstrumentation().context private val context = InstrumentationRegistry.getInstrumentation().context
@@ -46,14 +49,14 @@ class BalancePrinterUtil {
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName) // private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
private lateinit var birthday: WalletBirthday private lateinit var birthday: Checkpoint
private var synchronizer: Synchronizer? = null private var synchronizer: Synchronizer? = null
@Before @Before
fun setup() { fun setup() {
Twig.plant(TroubleshootingTwig()) Twig.plant(TroubleshootingTwig())
cacheBlocks() cacheBlocks()
birthday = runBlocking { WalletBirthdayTool.loadNearest(context, network, birthdayHeight) } birthday = runBlocking { CheckpointTool.loadNearest(context, network, birthdayHeight) }
} }
private fun cacheBlocks() = runBlocking { private fun cacheBlocks() = runBlocking {
@@ -81,8 +84,8 @@ class BalancePrinterUtil {
}.collect { seed -> }.collect { seed ->
// TODO: clear the dataDb but leave the cacheDb // TODO: clear the dataDb but leave the cacheDb
val initializer = Initializer.new(context) { config -> val initializer = Initializer.new(context) { config ->
runBlocking { config.importWallet(seed, birthdayHeight, network) } val endpoint = LightWalletEndpoint.defaultForNetwork(network)
config.setNetwork(network) runBlocking { config.importWallet(seed, birthdayHeight, network, endpoint) }
config.alias = alias config.alias = alias
} }
/* /*

View File

@@ -6,6 +6,8 @@ import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig 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 cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@@ -36,7 +38,7 @@ class DataDbScannerUtil {
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName) // private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
private val birthdayHeight = 600_000 private val birthdayHeight = 600_000L
private lateinit var synchronizer: Synchronizer private lateinit var synchronizer: Synchronizer
@Before @Before
@@ -67,7 +69,11 @@ class DataDbScannerUtil {
val initializer = runBlocking { val initializer = runBlocking {
Initializer.new(context) { Initializer.new(context) {
it.setBirthdayHeight( it.setBirthdayHeight(
birthdayHeight BlockHeight.new(
ZcashNetwork.Mainnet,
birthdayHeight
),
false
) )
} }
} }

View File

@@ -10,10 +10,13 @@ import cash.z.ecc.android.sdk.db.entity.isPending
import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.Testnet
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool 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.CoroutineScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -35,9 +38,8 @@ class TestWallet(
val seedPhrase: String, val seedPhrase: String,
val alias: String = "TestWallet", val alias: String = "TestWallet",
val network: ZcashNetwork = ZcashNetwork.Testnet, val network: ZcashNetwork = ZcashNetwork.Testnet,
val host: String = network.defaultHost, val endpoint: LightWalletEndpoint = LightWalletEndpoint.Testnet,
startHeight: Int? = null, startHeight: BlockHeight? = null
val port: Int = network.defaultPort
) { ) {
constructor( constructor(
backup: Backups, backup: Backups,
@@ -65,7 +67,7 @@ class TestWallet(
runBlocking { DerivationTool.deriveTransparentSecretKey(seed, network = network) } runBlocking { DerivationTool.deriveTransparentSecretKey(seed, network = network) }
val initializer = runBlocking { val initializer = runBlocking {
Initializer.new(context) { config -> Initializer.new(context) { config ->
runBlocking { config.importWallet(seed, startHeight, network, host, alias = alias) } runBlocking { config.importWallet(seed, startHeight, network, endpoint, alias = alias) }
} }
} }
val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(initializer) as SdkSynchronizer val synchronizer: SdkSynchronizer = Synchronizer.newBlocking(initializer) as SdkSynchronizer
@@ -78,14 +80,11 @@ class TestWallet(
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) } runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
val birthdayHeight get() = synchronizer.latestBirthdayHeight val birthdayHeight get() = synchronizer.latestBirthdayHeight
val networkName get() = synchronizer.network.networkName val networkName get() = synchronizer.network.networkName
val connectionInfo get() = service.connectionInfo.toString()
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun transparentBalance(): WalletBalance { suspend fun transparentBalance(): WalletBalance {
synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight) synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight)
return synchronizer.getTransparentBalance(transparentAddress) return synchronizer.getTransparentBalance(transparentAddress)
} }
*/
suspend fun sync(timeout: Long = -1): TestWallet { suspend fun sync(timeout: Long = -1): TestWallet {
val killSwitch = walletScope.launch { val killSwitch = walletScope.launch {
@@ -111,7 +110,7 @@ class TestWallet(
suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet { suspend fun send(address: String = transparentAddress, memo: String = "", amount: Zatoshi = Zatoshi(500L), fromAccountIndex: Int = 0): TestWallet {
Twig.sprout("$alias sending") Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex) synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() } .takeWhile { it.isPending(null) }
.collect { .collect {
twig("Updated transaction: $it") twig("Updated transaction: $it")
} }
@@ -119,15 +118,14 @@ class TestWallet(
return this return this
} }
suspend fun rewindToHeight(height: Int): TestWallet { suspend fun rewindToHeight(height: BlockHeight): TestWallet {
synchronizer.rewindToNearestHeight(height, false) synchronizer.rewindToNearestHeight(height, false)
return this return this
} }
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun shieldFunds(): TestWallet { suspend fun shieldFunds(): TestWallet {
twig("checking $transparentAddress for transactions!") twig("checking $transparentAddress for transactions!")
synchronizer.refreshUtxos(transparentAddress, 935000).let { count -> synchronizer.refreshUtxos(transparentAddress, BlockHeight.new(ZcashNetwork.Mainnet, 935000)).let { count ->
twig("FOUND $count new UTXOs") twig("FOUND $count new UTXOs")
} }
@@ -144,7 +142,6 @@ class TestWallet(
return this return this
} }
*/
suspend fun join(timeout: Long? = null): TestWallet { suspend fun join(timeout: Long? = null): TestWallet {
// block until stopped // block until stopped
@@ -167,13 +164,48 @@ class TestWallet(
} }
} }
enum class Backups(val seedPhrase: String, val testnetBirthday: Int, val mainnetBirthday: Int) { enum class Backups(val seedPhrase: String, val testnetBirthday: BlockHeight, val mainnetBirthday: BlockHeight) {
// TODO: get the proper birthday values for these wallets // 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), DEFAULT(
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), "column rhythm acoustic gym cost fit keen maze fence seed mail medal shrimp tell relief clip cannon foster soldier shallow refuse lunar parrot banana",
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), BlockHeight.new(
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), ZcashNetwork.Testnet,
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), 1_355_928
),
BlockHeight.new(ZcashNetwork.Mainnet, 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",
BlockHeight.new(
ZcashNetwork.Testnet,
1_330_190
),
BlockHeight.new(ZcashNetwork.Mainnet, 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",
BlockHeight.new(
ZcashNetwork.Testnet,
1_000_000
),
BlockHeight.new(ZcashNetwork.Mainnet, 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",
BlockHeight.new(
ZcashNetwork.Testnet,
1_330_190
),
BlockHeight.new(ZcashNetwork.Mainnet, 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",
BlockHeight.new(
ZcashNetwork.Testnet,
1_330_190
),
BlockHeight.new(ZcashNetwork.Mainnet, 1_000_000)
),
; ;
} }
} }

View File

@@ -5,7 +5,10 @@ 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 cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.Mainnet
import cash.z.ecc.android.sdk.model.ZcashNetwork
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
@@ -13,7 +16,7 @@ class TransactionCounterUtil {
private val network = ZcashNetwork.Mainnet private val network = ZcashNetwork.Mainnet
private val context = InstrumentationRegistry.getInstrumentation().context private val context = InstrumentationRegistry.getInstrumentation().context
private val service = LightWalletGrpcService(context, network) private val service = LightWalletGrpcService.new(context, LightWalletEndpoint.Mainnet)
init { init {
Twig.plant(TroubleshootingTwig()) Twig.plant(TroubleshootingTwig())
@@ -23,7 +26,12 @@ class TransactionCounterUtil {
@Ignore("This test is broken") @Ignore("This test is broken")
fun testBlockSize() { fun testBlockSize() {
val sizes = mutableMapOf<Int, Int>() val sizes = mutableMapOf<Int, Int>()
service.getBlockRange(900_000..910_000).forEach { b -> service.getBlockRange(
BlockHeight.new(ZcashNetwork.Mainnet, 900_000)..BlockHeight.new(
ZcashNetwork.Mainnet,
910_000
)
).forEach { b ->
twig("h: ${b.header.size()}") twig("h: ${b.header.size()}")
val s = b.serializedSize val s = b.serializedSize
sizes[s] = (sizes[s] ?: 0) + 1 sizes[s] = (sizes[s] ?: 0) + 1
@@ -38,7 +46,12 @@ class TransactionCounterUtil {
val outputCounts = mutableMapOf<Int, Int>() val outputCounts = mutableMapOf<Int, Int>()
var totalOutputs = 0 var totalOutputs = 0
var totalTxs = 0 var totalTxs = 0
service.getBlockRange(900_000..950_000).forEach { b -> service.getBlockRange(
BlockHeight.new(ZcashNetwork.Mainnet, 900_000)..BlockHeight.new(
ZcashNetwork.Mainnet,
950_000
)
).forEach { b ->
b.header.size() b.header.size()
b.vtxList.map { it.outputsCount }.forEach { oCount -> b.vtxList.map { it.outputsCount }.forEach { oCount ->
outputCounts[oCount] = (outputCounts[oCount] ?: 0) + oCount.coerceAtLeast(1) outputCounts[oCount] = (outputCounts[oCount] ?: 0) + oCount.coerceAtLeast(1)

View File

@@ -6,31 +6,34 @@ import cash.z.ecc.android.sdk.internal.KEY_HEIGHT
import cash.z.ecc.android.sdk.internal.KEY_TREE import cash.z.ecc.android.sdk.internal.KEY_TREE
import cash.z.ecc.android.sdk.internal.KEY_VERSION import cash.z.ecc.android.sdk.internal.KEY_VERSION
import cash.z.ecc.android.sdk.internal.VERSION_1 import cash.z.ecc.android.sdk.internal.VERSION_1
import cash.z.ecc.android.sdk.type.WalletBirthday import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import org.json.JSONObject import org.json.JSONObject
object WalletBirthdayFixture { object CheckpointFixture {
val NETWORK = ZcashNetwork.Mainnet
// These came from the mainnet 1500000.json file // These came from the mainnet 1500000.json file
const val HEIGHT = 1500000 val HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 1500000L)
const val HASH = "00000000019e5b25a95c7607e7789eb326fddd69736970ebbe1c7d00247ef902" const val HASH = "00000000019e5b25a95c7607e7789eb326fddd69736970ebbe1c7d00247ef902"
const val EPOCH_SECONDS = 1639913234L const val EPOCH_SECONDS = 1639913234L
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
const val TREE = "01ce183032b16ed87fcc5052a42d908376526126346567773f55bc58a63e4480160013000001bae5112769a07772345dd402039f2949c457478fe9327363ff631ea9d78fb80d0177c0b6c21aa9664dc255336ed450914088108c38a9171c85875b4e53d31b3e140171add6f9129e124651ca894aa842a3c71b1738f3ee2b7ba829106524ef51e62101f9cebe2141ee9d0a3f3a3e28bce07fa6b6e1c7b42c01cc4fe611269e9d52da540001d0adff06de48569129bd2a211e3253716362da97270d3504d9c1b694689ebe3c0122aaaea90a7fa2773b8166937310f79a4278b25d759128adf3138d052da3725b0137fb2cbc176075a45db2a3c32d3f78e669ff2258fd974e99ec9fb314d7fd90180165aaee3332ea432d13a9398c4863b38b8a7a491877a5c46b0802dcd88f7e324301a9a262f8b92efc2e0e3e4bd1207486a79d62e87b4ab9cc41814d62a23c4e28040001e3c4ee998682df5c5e230d6968e947f83d0c03682f0cfc85f1e6ec8e8552c95a000155989fed7a8cc7a0d479498d6881ca3bafbe05c7095110f85c64442d6a06c25c0185cd8c141e620eda0ca0516f42240aedfabdf9189c8c6ac834b7bdebc171331d01ecceb776c043662617d62646ee60985521b61c0b860f3a9731e66ef74ed8fb320118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644" const val TREE = "01ce183032b16ed87fcc5052a42d908376526126346567773f55bc58a63e4480160013000001bae5112769a07772345dd402039f2949c457478fe9327363ff631ea9d78fb80d0177c0b6c21aa9664dc255336ed450914088108c38a9171c85875b4e53d31b3e140171add6f9129e124651ca894aa842a3c71b1738f3ee2b7ba829106524ef51e62101f9cebe2141ee9d0a3f3a3e28bce07fa6b6e1c7b42c01cc4fe611269e9d52da540001d0adff06de48569129bd2a211e3253716362da97270d3504d9c1b694689ebe3c0122aaaea90a7fa2773b8166937310f79a4278b25d759128adf3138d052da3725b0137fb2cbc176075a45db2a3c32d3f78e669ff2258fd974e99ec9fb314d7fd90180165aaee3332ea432d13a9398c4863b38b8a7a491877a5c46b0802dcd88f7e324301a9a262f8b92efc2e0e3e4bd1207486a79d62e87b4ab9cc41814d62a23c4e28040001e3c4ee998682df5c5e230d6968e947f83d0c03682f0cfc85f1e6ec8e8552c95a000155989fed7a8cc7a0d479498d6881ca3bafbe05c7095110f85c64442d6a06c25c0185cd8c141e620eda0ca0516f42240aedfabdf9189c8c6ac834b7bdebc171331d01ecceb776c043662617d62646ee60985521b61c0b860f3a9731e66ef74ed8fb320118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
fun new( internal fun new(
height: Int = HEIGHT, height: BlockHeight = HEIGHT,
hash: String = HASH, hash: String = HASH,
time: Long = EPOCH_SECONDS, time: Long = EPOCH_SECONDS,
tree: String = TREE tree: String = TREE
) = WalletBirthday(height = height, hash = hash, time = time, tree = tree) ) = Checkpoint(height = height, hash = hash, epochSeconds = time, tree = tree)
} }
fun WalletBirthday.toJson() = JSONObject().apply { internal fun Checkpoint.toJson() = JSONObject().apply {
put(WalletBirthday.KEY_VERSION, WalletBirthday.VERSION_1) put(Checkpoint.KEY_VERSION, Checkpoint.VERSION_1)
put(WalletBirthday.KEY_HEIGHT, height) put(Checkpoint.KEY_HEIGHT, height.value)
put(WalletBirthday.KEY_HASH, hash) put(Checkpoint.KEY_HASH, hash)
put(WalletBirthday.KEY_EPOCH_SECONDS, time) put(Checkpoint.KEY_EPOCH_SECONDS, epochSeconds)
put(WalletBirthday.KEY_TREE, tree) put(Checkpoint.KEY_TREE, tree)
}.toString() }.toString()

View File

@@ -3,5 +3,5 @@
"height": "1150000", "height": "1150000",
"hash": "0000000650e627bd7da6868f14070aff8fdbd31ef7125fe77851976ed3adfc54", "hash": "0000000650e627bd7da6868f14070aff8fdbd31ef7125fe77851976ed3adfc54",
"time": 1668316308, "time": 1668316308,
"saplingtree": "012e058162e4e6bc9553c413134b66e5e89cd63c330fc557060878c623fdedc63d01533dbef6ad6b226c7138bef1e1961ca170510a91fdb1ff5972e1cd79c347401c150168979af907639a39b427d83d4602fd22867faffb0942cbb11955dac680aab85901d8d396d94feb0cc78cbec422c6fd09834c4031a1ffdcbef04178827add5193160102e9b3fecaf39d40e5e63ef3253d1ac5aee4141bdf26763de74033d6016f5955019f59fdd4a570ad22980428caea7b5fa61f55b52e2e6fbb29600eee53d31ed35f017c5ad297c1e83430ce9d3768fd38e74bd06757b67e2917b34d0f3f763803f32600000000015c1016fb7c68d85099ce8423d6446c2ea3d77a63b5ab6044f6eb0c024ddb0d5a01627b5eae7588998ef2645fe8be1ec3227d560828956b7df00632b26784c4f80a0000012d8bdb15bce00ab0c8bd332355a100d9db356ac05fb97412b479214dcefea331000001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13" "saplingTree": "012e058162e4e6bc9553c413134b66e5e89cd63c330fc557060878c623fdedc63d01533dbef6ad6b226c7138bef1e1961ca170510a91fdb1ff5972e1cd79c347401c150168979af907639a39b427d83d4602fd22867faffb0942cbb11955dac680aab85901d8d396d94feb0cc78cbec422c6fd09834c4031a1ffdcbef04178827add5193160102e9b3fecaf39d40e5e63ef3253d1ac5aee4141bdf26763de74033d6016f5955019f59fdd4a570ad22980428caea7b5fa61f55b52e2e6fbb29600eee53d31ed35f017c5ad297c1e83430ce9d3768fd38e74bd06757b67e2917b34d0f3f763803f32600000000015c1016fb7c68d85099ce8423d6446c2ea3d77a63b5ab6044f6eb0c024ddb0d5a01627b5eae7588998ef2645fe8be1ec3227d560828956b7df00632b26784c4f80a0000012d8bdb15bce00ab0c8bd332355a100d9db356ac05fb97412b479214dcefea331000001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13"
} }

View File

@@ -0,0 +1,7 @@
{
"network": "main",
"height": "1160000",
"hash": "000000006904fea1620eb53ad7f912197881e3d47a8a6683d943efc0ff43c94e",
"time": 1669068992,
"saplingTree": "01569b6b693b7fc56715b01e81a3d07dcfd723f54dc8f31cdb465509f39304755800150001dcf14961a27da1444097a9618a6a3d4a6a198b4afc8c9c2960e30da42c5790210001f95f7e52338af7b69ae511d4ae5c1fbaa394380e92fbb51cddfdc4b8b0cfef0601cd00c0bca2308a408b94327da9505d55a241aa05ae730884ee38c8d553d218290000000001d637d869f0247b4dc198156dee890fa12f44ad83c1ecd9f73bb89ab578ff31260001cebd13b9f31ab7c442c5a89562817d5b590b51ac75d735041d2389ceabd3cf7101e3a962887edb0c646e6390f87eb64ab4b12753338b8b72f3afcbca0b499b1f3a01447d8106fe66cea724f27e0f0310821d7e5c536b02f4540ce14f6359ec30650a014431c3e5e81d9dec6f8c51f83ad971de208c6a7d990f727f2b203af910f760410001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13"
}

View File

@@ -1,7 +0,0 @@
{
"network": "test",
"height": "1000000",
"hash": "00000002e920ff85f57a164e0637d12147e95428b4da8ca2f0dde4192f5df829",
"time": 1657012340,
"saplingTree": "014e4c1dba2b623864148a9dc4828fc67ee7f231907425be3695949749092e845b01ebe665e0198710c608da05a8a7964700642cbfd9b7bcac005ed967df32357e6a140001688ee9449b2d45afe7fb411f19c58f56269a43967fc1d1fdb9fd28edc2344016018746e37c9fd6c64662e26f2e146427a69ed3800280a9124c4d8c19f25a2aa155011b980b27687b29f85d315c87db08ea5559cc812159a838240b45bd88fd545059000171bc61ad6eacc00f6252a2898470e6a6391311db651b443e74f1f506e35ae61b011ca124f40831dd203b8d4eb8386f58b593d503168df44aa2eed6f4e3fdacd51000000001216607336029bd6f322d3decb8dd7ae1f8df1760b240030f1879b7aa3b4c646e0001f529692b1ee5845a6e0681b2efcc66342586397c79b09cdefa957aa1b0e614310156e2babf0dca08c8b1991c00a5d74d740e7d0c4b95099065016719e93455833a018d57c3859e298989e2eae8e1d8f9135944eef930e3ad20330e0de0541aacc94801f2cec17739de7e1476938f895b1a6381b36ec44ccdbbac2eeb60be43be6f815e01a8d81d60d7de99c1e45988bc29029102ab653c13b490ee0133dc739bf63a971601437aa93f8bebde50ea70fd8c7b60fe826aa6892fd6a9c6d72a60a7f8d12bea58000108a67f0c370f350b9179a081f2fc8d33b62e01e729419860b5ae143cbbcd2769"
}

View File

@@ -0,0 +1,7 @@
{
"network": "main",
"height": "1150000",
"hash": "0000000650e627bd7da6868f14070aff8fdbd31ef7125fe77851976ed3adfc54",
"time": 1668316308,
"saplingTree": "012e058162e4e6bc9553c413134b66e5e89cd63c330fc557060878c623fdedc63d01533dbef6ad6b226c7138bef1e1961ca170510a91fdb1ff5972e1cd79c347401c150168979af907639a39b427d83d4602fd22867faffb0942cbb11955dac680aab85901d8d396d94feb0cc78cbec422c6fd09834c4031a1ffdcbef04178827add5193160102e9b3fecaf39d40e5e63ef3253d1ac5aee4141bdf26763de74033d6016f5955019f59fdd4a570ad22980428caea7b5fa61f55b52e2e6fbb29600eee53d31ed35f017c5ad297c1e83430ce9d3768fd38e74bd06757b67e2917b34d0f3f763803f32600000000015c1016fb7c68d85099ce8423d6446c2ea3d77a63b5ab6044f6eb0c024ddb0d5a01627b5eae7588998ef2645fe8be1ec3227d560828956b7df00632b26784c4f80a0000012d8bdb15bce00ab0c8bd332355a100d9db356ac05fb97412b479214dcefea331000001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13"
}

View File

@@ -6,13 +6,15 @@ import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.SdkDispatchers import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend
import cash.z.ecc.android.sdk.internal.ext.getDatabasePathSuspend import cash.z.ecc.android.sdk.internal.ext.getDatabasePathSuspend
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@@ -20,16 +22,16 @@ import java.io.File
/** /**
* Simplified Initializer focused on starting from a ViewingKey. * Simplified Initializer focused on starting from a ViewingKey.
*/ */
@Suppress("LongParameterList")
class Initializer private constructor( class Initializer private constructor(
val context: Context, val context: Context,
val rustBackend: RustBackend, internal val rustBackend: RustBackend,
val network: ZcashNetwork, val network: ZcashNetwork,
val alias: String, val alias: String,
val host: String, val lightWalletEndpoint: LightWalletEndpoint,
val port: Int,
val viewingKeys: List<UnifiedViewingKey>, val viewingKeys: List<UnifiedViewingKey>,
val overwriteVks: Boolean, val overwriteVks: Boolean,
val birthday: WalletBirthday internal val checkpoint: Checkpoint
) { ) {
suspend fun erase() = erase(context, network, alias) suspend fun erase() = erase(context, network, alias)
@@ -38,16 +40,13 @@ class Initializer private constructor(
val viewingKeys: MutableList<UnifiedViewingKey> = mutableListOf(), val viewingKeys: MutableList<UnifiedViewingKey> = mutableListOf(),
var alias: String = ZcashSdk.DEFAULT_ALIAS var alias: String = ZcashSdk.DEFAULT_ALIAS
) { ) {
var birthdayHeight: Int? = null var birthdayHeight: BlockHeight? = null
private set private set
lateinit var network: ZcashNetwork lateinit var network: ZcashNetwork
private set private set
lateinit var host: String lateinit var lightWalletEndpoint: LightWalletEndpoint
private set
var port: Int = ZcashNetwork.Mainnet.defaultPort
private set private set
/** /**
@@ -86,7 +85,7 @@ class Initializer private constructor(
* transactions. Again, this value is only considered when [height] is null. * transactions. Again, this value is only considered when [height] is null.
* *
*/ */
fun setBirthdayHeight(height: Int?, defaultToOldestHeight: Boolean = false): Config = fun setBirthdayHeight(height: BlockHeight?, defaultToOldestHeight: Boolean): Config =
apply { apply {
this.birthdayHeight = height this.birthdayHeight = height
this.defaultToOldestHeight = defaultToOldestHeight this.defaultToOldestHeight = defaultToOldestHeight
@@ -105,7 +104,7 @@ class Initializer private constructor(
* importing a pre-existing wallet. It is the same as calling * importing a pre-existing wallet. It is the same as calling
* `birthdayHeight = importedHeight`. * `birthdayHeight = importedHeight`.
*/ */
fun importedWalletBirthday(importedHeight: Int?): Config = apply { fun importedWalletBirthday(importedHeight: BlockHeight?): Config = apply {
birthdayHeight = importedHeight birthdayHeight = importedHeight
defaultToOldestHeight = true defaultToOldestHeight = true
} }
@@ -159,12 +158,10 @@ class Initializer private constructor(
*/ */
fun setNetwork( fun setNetwork(
network: ZcashNetwork, network: ZcashNetwork,
host: String = network.defaultHost, lightWalletEndpoint: LightWalletEndpoint
port: Int = network.defaultPort
): Config = apply { ): Config = apply {
this.network = network this.network = network
this.host = host this.lightWalletEndpoint = lightWalletEndpoint
this.port = port
} }
/** /**
@@ -172,18 +169,16 @@ class Initializer private constructor(
*/ */
suspend fun importWallet( suspend fun importWallet(
seed: ByteArray, seed: ByteArray,
birthdayHeight: Int? = null, birthday: BlockHeight?,
network: ZcashNetwork, network: ZcashNetwork,
host: String = network.defaultHost, lightWalletEndpoint: LightWalletEndpoint,
port: Int = network.defaultPort,
alias: String = ZcashSdk.DEFAULT_ALIAS alias: String = ZcashSdk.DEFAULT_ALIAS
): Config = ): Config =
importWallet( importWallet(
DerivationTool.deriveUnifiedViewingKeys(seed, network = network)[0], DerivationTool.deriveUnifiedViewingKeys(seed, network = network)[0],
birthdayHeight, birthday,
network, network,
host, lightWalletEndpoint,
port,
alias alias
) )
@@ -192,15 +187,14 @@ class Initializer private constructor(
*/ */
fun importWallet( fun importWallet(
viewingKey: UnifiedViewingKey, viewingKey: UnifiedViewingKey,
birthdayHeight: Int? = null, birthday: BlockHeight?,
network: ZcashNetwork, network: ZcashNetwork,
host: String = network.defaultHost, lightWalletEndpoint: LightWalletEndpoint,
port: Int = network.defaultPort,
alias: String = ZcashSdk.DEFAULT_ALIAS alias: String = ZcashSdk.DEFAULT_ALIAS
): Config = apply { ): Config = apply {
setViewingKeys(viewingKey) setViewingKeys(viewingKey)
setNetwork(network, host, port) setNetwork(network, lightWalletEndpoint)
importedWalletBirthday(birthdayHeight) importedWalletBirthday(birthday)
this.alias = alias this.alias = alias
} }
@@ -210,14 +204,12 @@ class Initializer private constructor(
suspend fun newWallet( suspend fun newWallet(
seed: ByteArray, seed: ByteArray,
network: ZcashNetwork, network: ZcashNetwork,
host: String = network.defaultHost, lightWalletEndpoint: LightWalletEndpoint,
port: Int = network.defaultPort,
alias: String = ZcashSdk.DEFAULT_ALIAS alias: String = ZcashSdk.DEFAULT_ALIAS
): Config = newWallet( ): Config = newWallet(
DerivationTool.deriveUnifiedViewingKeys(seed, network)[0], DerivationTool.deriveUnifiedViewingKeys(seed, network)[0],
network, network,
host, lightWalletEndpoint,
port,
alias alias
) )
@@ -227,12 +219,11 @@ class Initializer private constructor(
fun newWallet( fun newWallet(
viewingKey: UnifiedViewingKey, viewingKey: UnifiedViewingKey,
network: ZcashNetwork, network: ZcashNetwork,
host: String = network.defaultHost, lightWalletEndpoint: LightWalletEndpoint,
port: Int = network.defaultPort,
alias: String = ZcashSdk.DEFAULT_ALIAS alias: String = ZcashSdk.DEFAULT_ALIAS
): Config = apply { ): Config = apply {
setViewingKeys(viewingKey) setViewingKeys(viewingKey)
setNetwork(network, host, port) setNetwork(network, lightWalletEndpoint)
newWalletBirthday() newWalletBirthday()
this.alias = alias this.alias = alias
} }
@@ -284,8 +275,8 @@ class Initializer private constructor(
} }
// allow either null or a value greater than the activation height // allow either null or a value greater than the activation height
if ( if (
(birthdayHeight ?: network.saplingActivationHeight) (birthdayHeight?.value ?: network.saplingActivationHeight.value)
< network.saplingActivationHeight < network.saplingActivationHeight.value
) { ) {
throw InitializerException.InvalidBirthdayHeightException(birthdayHeight, network) throw InitializerException.InvalidBirthdayHeightException(birthdayHeight, network)
} }
@@ -328,25 +319,33 @@ class Initializer private constructor(
config: Config config: Config
): Initializer { ): Initializer {
config.validate() config.validate()
// heightToUse hardcoded for now, otherwise detects older JSON checkpoint files.
val heightToUse = 1150000
//config.birthdayHeight
//?: (if (config.defaultToOldestHeight == true) config.network.saplingActivationHeight else null)
val loadedBirthday =
WalletBirthdayTool.loadNearest(context, config.network, heightToUse)
val rustBackend = initRustBackend(context, config.network, config.alias, loadedBirthday) val loadedCheckpoint = run {
val height = config.birthdayHeight
?: if (config.defaultToOldestHeight == true) {
config.network.saplingActivationHeight
} else {
null
}
CheckpointTool.loadNearest(
context,
config.network,
height
)
}
val rustBackend = initRustBackend(context, config.network, config.alias, loadedCheckpoint.height)
return Initializer( return Initializer(
context.applicationContext, context.applicationContext,
rustBackend, rustBackend,
config.network, config.network,
config.alias, config.alias,
config.host, config.lightWalletEndpoint,
config.port,
config.viewingKeys, config.viewingKeys,
config.overwriteVks, config.overwriteVks,
loadedBirthday loadedCheckpoint
) )
} }
@@ -377,14 +376,14 @@ class Initializer private constructor(
context: Context, context: Context,
network: ZcashNetwork, network: ZcashNetwork,
alias: String, alias: String,
birthday: WalletBirthday blockHeight: BlockHeight
): RustBackend { ): RustBackend {
return RustBackend.init( return RustBackend.init(
cacheDbPath(context, network, alias), cacheDbPath(context, network, alias),
dataDbPath(context, network, alias), dataDbPath(context, network, alias),
File(context.getCacheDirSuspend(), "params").absolutePath, File(context.getCacheDirSuspend(), "params").absolutePath,
network, network,
birthday.height blockHeight
) )
} }

View File

@@ -36,6 +36,7 @@ import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.internal.block.CompactBlockStore import cash.z.ecc.android.sdk.internal.block.CompactBlockStore
import cash.z.ecc.android.sdk.internal.ext.toHexReversed import cash.z.ecc.android.sdk.internal.ext.toHexReversed
import cash.z.ecc.android.sdk.internal.ext.tryNull import cash.z.ecc.android.sdk.internal.ext.tryNull
import cash.z.ecc.android.sdk.internal.isEmpty
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManager import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManager
@@ -46,14 +47,15 @@ import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository
import cash.z.ecc.android.sdk.internal.transaction.WalletTransactionEncoder import cash.z.ecc.android.sdk.internal.transaction.WalletTransactionEncoder
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.sdk.type.AddressType.Shielded import cash.z.ecc.android.sdk.type.AddressType.Shielded
import cash.z.ecc.android.sdk.type.AddressType.Transparent import cash.z.ecc.android.sdk.type.AddressType.Transparent
import cash.z.ecc.android.sdk.type.ConsensusMatchType import cash.z.ecc.android.sdk.type.ConsensusMatchType
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.wallet.sdk.rpc.Service import cash.z.wallet.sdk.rpc.Service
import io.grpc.ManagedChannel import io.grpc.ManagedChannel
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
@@ -95,7 +97,6 @@ import kotlin.coroutines.EmptyCoroutineContext
*/ */
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@FlowPreview @FlowPreview
class SdkSynchronizer internal constructor( class SdkSynchronizer internal constructor(
private val storage: TransactionRepository, private val storage: TransactionRepository,
private val txManager: OutboundTransactionManager, private val txManager: OutboundTransactionManager,
@@ -103,9 +104,9 @@ class SdkSynchronizer internal constructor(
) : Synchronizer { ) : Synchronizer {
// pools // pools
//private val _orchardBalances = MutableStateFlow<WalletBalance?>(null) private val _orchardBalances = MutableStateFlow<WalletBalance?>(null)
private val _saplingBalances = MutableStateFlow<WalletBalance?>(null) private val _saplingBalances = MutableStateFlow<WalletBalance?>(null)
//private val _transparentBalances = MutableStateFlow<WalletBalance?>(null) private val _transparentBalances = MutableStateFlow<WalletBalance?>(null)
private val _status = ConflatedBroadcastChannel<Synchronizer.Status>(DISCONNECTED) private val _status = ConflatedBroadcastChannel<Synchronizer.Status>(DISCONNECTED)
@@ -144,9 +145,9 @@ class SdkSynchronizer internal constructor(
// Balances // Balances
// //
//override val orchardBalances = _orchardBalances.asStateFlow() override val orchardBalances = _orchardBalances.asStateFlow()
override val saplingBalances = _saplingBalances.asStateFlow() override val saplingBalances = _saplingBalances.asStateFlow()
//override val transparentBalances = _transparentBalances.asStateFlow() override val transparentBalances = _transparentBalances.asStateFlow()
// //
// Transactions // Transactions
@@ -189,7 +190,7 @@ class SdkSynchronizer internal constructor(
* The latest height seen on the network while processing blocks. This may differ from the * The latest height seen on the network while processing blocks. This may differ from the
* latest height scanned and is useful for determining block confirmations and expiration. * latest height scanned and is useful for determining block confirmations and expiration.
*/ */
override val networkHeight: StateFlow<Int> = processor.networkHeight override val networkHeight: StateFlow<BlockHeight?> = processor.networkHeight
// //
// Error Handling // Error Handling
@@ -231,7 +232,7 @@ class SdkSynchronizer internal constructor(
* A callback to invoke whenever a chain error is encountered. These occur whenever the * A callback to invoke whenever a chain error is encountered. These occur whenever the
* processor detects a missing or non-chain-sequential block (i.e. a reorg). * processor detects a missing or non-chain-sequential block (i.e. a reorg).
*/ */
override var onChainErrorHandler: ((errorHeight: Int, rewindHeight: Int) -> Any)? = null override var onChainErrorHandler: ((errorHeight: BlockHeight, rewindHeight: BlockHeight) -> Any)? = null
// //
// Public API // Public API
@@ -243,9 +244,11 @@ class SdkSynchronizer internal constructor(
* this, a wallet will more likely want to consume the flow of processor info using * this, a wallet will more likely want to consume the flow of processor info using
* [processorInfo]. * [processorInfo].
*/ */
override val latestHeight: Int get() = processor.currentInfo.networkBlockHeight override val latestHeight
get() = processor.currentInfo.networkBlockHeight
override val latestBirthdayHeight: Int get() = processor.birthdayHeight override val latestBirthdayHeight
get() = processor.birthdayHeight
override suspend fun prepare(): Synchronizer = apply { override suspend fun prepare(): Synchronizer = apply {
// Do nothing; this could likely be removed // Do nothing; this could likely be removed
@@ -305,24 +308,10 @@ class SdkSynchronizer internal constructor(
*/ */
override suspend fun getServerInfo(): Service.LightdInfo = processor.downloader.getServerInfo() override suspend fun getServerInfo(): Service.LightdInfo = processor.downloader.getServerInfo()
/** override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
* Changes the server that is being used to download compact blocks. This will throw an
* exception if it detects that the server change is invalid e.g. switching to testnet from
* mainnet.
*/
override suspend fun changeServer(host: String, port: Int, errorHandler: (Throwable) -> Unit) {
val info =
(processor.downloader.lightWalletService as LightWalletGrpcService).connectionInfo
processor.downloader.changeService(
LightWalletGrpcService(info.appContext, host, port),
errorHandler
)
}
override suspend fun getNearestRewindHeight(height: Int): Int =
processor.getNearestRewindHeight(height) processor.getNearestRewindHeight(height)
override suspend fun rewindToNearestHeight(height: Int, alsoClearBlockCache: Boolean) { override suspend fun rewindToNearestHeight(height: BlockHeight, alsoClearBlockCache: Boolean) {
processor.rewindToNearestHeight(height, alsoClearBlockCache) processor.rewindToNearestHeight(height, alsoClearBlockCache)
} }
@@ -336,11 +325,11 @@ class SdkSynchronizer internal constructor(
// TODO: turn this section into the data access API. For now, just aggregate all the things that we want to do with the underlying data // TODO: turn this section into the data access API. For now, just aggregate all the things that we want to do with the underlying data
suspend fun findBlockHash(height: Int): ByteArray? { suspend fun findBlockHash(height: BlockHeight): ByteArray? {
return (storage as? PagedTransactionRepository)?.findBlockHash(height) return (storage as? PagedTransactionRepository)?.findBlockHash(height)
} }
suspend fun findBlockHashAsHex(height: Int): String? { suspend fun findBlockHashAsHex(height: BlockHeight): String? {
return findBlockHash(height)?.toHexReversed() return findBlockHash(height)?.toHexReversed()
} }
@@ -356,12 +345,10 @@ class SdkSynchronizer internal constructor(
// Private API // Private API
// //
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun refreshUtxos() { suspend fun refreshUtxos() {
twig("refreshing utxos", -1) twig("refreshing utxos", -1)
refreshUtxos(getTransparentAddress()) refreshUtxos(getTransparentAddress())
} }
*/
/** /**
* Calculate the latest balance, based on the blocks that have been scanned and transmit this * Calculate the latest balance, based on the blocks that have been scanned and transmit this
@@ -369,7 +356,7 @@ class SdkSynchronizer internal constructor(
*/ */
suspend fun refreshAllBalances() { suspend fun refreshAllBalances() {
refreshSaplingBalance() refreshSaplingBalance()
// refreshTransparentBalance() refreshTransparentBalance()
// TODO: refresh orchard balance // TODO: refresh orchard balance
twig("Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place.") twig("Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place.")
} }
@@ -379,14 +366,11 @@ class SdkSynchronizer internal constructor(
_saplingBalances.value = processor.getBalanceInfo() _saplingBalances.value = processor.getBalanceInfo()
} }
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun refreshTransparentBalance() { suspend fun refreshTransparentBalance() {
twig("refreshing transparent balance") twig("refreshing transparent balance")
_transparentBalances.value = processor.getUtxoCacheBalance(getTransparentAddress()) _transparentBalances.value = processor.getUtxoCacheBalance(getTransparentAddress())
} }
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun isValidAddress(address: String): Boolean { suspend fun isValidAddress(address: String): Boolean {
try { try {
return !validateAddress(address).isNotValid return !validateAddress(address).isNotValid
@@ -394,7 +378,6 @@ class SdkSynchronizer internal constructor(
} }
return false return false
} }
*/
private fun CoroutineScope.onReady() = launch(CoroutineExceptionHandler(::onCriticalError)) { private fun CoroutineScope.onReady() = launch(CoroutineExceptionHandler(::onCriticalError)) {
twig("Preparing to start...") twig("Preparing to start...")
@@ -486,7 +469,7 @@ class SdkSynchronizer internal constructor(
return onSetupErrorHandler?.invoke(error) == true return onSetupErrorHandler?.invoke(error) == true
} }
private fun onChainError(errorHeight: Int, rewindHeight: Int) { private fun onChainError(errorHeight: BlockHeight, rewindHeight: BlockHeight) {
twig("Chain error detected at height: $errorHeight. Rewinding to: $rewindHeight") twig("Chain error detected at height: $errorHeight. Rewinding to: $rewindHeight")
if (onChainErrorHandler == null) { if (onChainErrorHandler == null) {
twig( twig(
@@ -501,7 +484,7 @@ class SdkSynchronizer internal constructor(
/** /**
* @param elapsedMillis the amount of time that passed since the last scan * @param elapsedMillis the amount of time that passed since the last scan
*/ */
private suspend fun onScanComplete(scannedRange: IntRange, elapsedMillis: Long) { private suspend fun onScanComplete(scannedRange: ClosedRange<BlockHeight>?, elapsedMillis: Long) {
// We don't need to update anything if there have been no blocks // We don't need to update anything if there have been no blocks
// refresh anyway if: // refresh anyway if:
// - if it's the first time we finished scanning // - if it's the first time we finished scanning
@@ -523,7 +506,7 @@ class SdkSynchronizer internal constructor(
// balance refresh is complete. // balance refresh is complete.
if (shouldRefresh) { if (shouldRefresh) {
twigTask("Triggering utxo refresh since $reason!", -1) { twigTask("Triggering utxo refresh since $reason!", -1) {
//refreshUtxos() refreshUtxos()
} }
twigTask("Triggering balance refresh since $reason!", -1) { twigTask("Triggering balance refresh since $reason!", -1) {
refreshAllBalances() refreshAllBalances()
@@ -701,22 +684,17 @@ class SdkSynchronizer internal constructor(
txManager.monitorById(it.id) txManager.monitorById(it.id)
}.distinctUntilChanged() }.distinctUntilChanged()
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD override suspend fun refreshUtxos(address: String, startHeight: BlockHeight): Int? {
override suspend fun refreshUtxos(address: String, startHeight: Int): Int? {
return processor.refreshUtxos(address, startHeight) return processor.refreshUtxos(address, startHeight)
} }
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override suspend fun getTransparentBalance(tAddr: String): WalletBalance { override suspend fun getTransparentBalance(tAddr: String): WalletBalance {
return processor.getUtxoCacheBalance(tAddr) return processor.getUtxoCacheBalance(tAddr)
} }
*/
override suspend fun isValidShieldedAddr(address: String) = override suspend fun isValidShieldedAddr(address: String) =
txManager.isValidShieldedAddress(address) txManager.isValidShieldedAddress(address)
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override suspend fun isValidTransparentAddr(address: String) = override suspend fun isValidTransparentAddr(address: String) =
txManager.isValidTransparentAddress(address) txManager.isValidTransparentAddress(address)
@@ -737,7 +715,6 @@ class SdkSynchronizer internal constructor(
} }
} }
} }
*/
override suspend fun validateConsensusBranch(): ConsensusMatchType { override suspend fun validateConsensusBranch(): ConsensusMatchType {
val serverBranchId = tryNull { processor.downloader.getServerInfo().consensusBranchId } val serverBranchId = tryNull { processor.downloader.getServerInfo().consensusBranchId }
@@ -797,18 +774,19 @@ object DefaultSynchronizerFactory {
suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository = suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository =
PagedTransactionRepository.new( PagedTransactionRepository.new(
initializer.context, initializer.context,
initializer.network,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
initializer.rustBackend, initializer.rustBackend,
initializer.birthday, initializer.checkpoint,
initializer.viewingKeys, initializer.viewingKeys,
initializer.overwriteVks initializer.overwriteVks
) )
fun defaultBlockStore(initializer: Initializer): CompactBlockStore = fun defaultBlockStore(initializer: Initializer): CompactBlockStore =
CompactBlockDbStore.new(initializer.context, initializer.rustBackend.pathCacheDb) CompactBlockDbStore.new(initializer.context, initializer.network, initializer.rustBackend.pathCacheDb)
fun defaultService(initializer: Initializer): LightWalletService = fun defaultService(initializer: Initializer): LightWalletService =
LightWalletGrpcService(initializer.context, initializer.host, initializer.port) LightWalletGrpcService.new(initializer.context, initializer.lightWalletEndpoint)
fun defaultEncoder( fun defaultEncoder(
initializer: Initializer, initializer: Initializer,

View File

@@ -4,11 +4,12 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.sdk.type.ConsensusMatchType import cash.z.ecc.android.sdk.type.ConsensusMatchType
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.wallet.sdk.rpc.Service import cash.z.wallet.sdk.rpc.Service
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -98,12 +99,12 @@ interface Synchronizer {
* latest downloaded height or scanned height. Although this is present in [processorInfo], it * latest downloaded height or scanned height. Although this is present in [processorInfo], it
* is such a frequently used value that it is convenient to have the real-time value by itself. * is such a frequently used value that it is convenient to have the real-time value by itself.
*/ */
val networkHeight: StateFlow<Int> val networkHeight: StateFlow<BlockHeight?>
/** /**
* A stream of balance values for the orchard pool. Includes the available and total balance. * A stream of balance values for the orchard pool. Includes the available and total balance.
*/ */
//val orchardBalances: StateFlow<WalletBalance?> val orchardBalances: StateFlow<WalletBalance?>
/** /**
* A stream of balance values for the sapling pool. Includes the available and total balance. * A stream of balance values for the sapling pool. Includes the available and total balance.
@@ -113,7 +114,7 @@ interface Synchronizer {
/** /**
* A stream of balance values for the transparent pool. Includes the available and total balance. * A stream of balance values for the transparent pool. Includes the available and total balance.
*/ */
//val transparentBalances: StateFlow<WalletBalance?> val transparentBalances: StateFlow<WalletBalance?>
/* Transactions */ /* Transactions */
@@ -145,13 +146,13 @@ interface Synchronizer {
/** /**
* An in-memory reference to the latest height seen on the network. * An in-memory reference to the latest height seen on the network.
*/ */
val latestHeight: Int val latestHeight: BlockHeight?
/** /**
* An in-memory reference to the best known birthday height, which can change if the first * An in-memory reference to the best known birthday height, which can change if the first
* transaction has not yet occurred. * transaction has not yet occurred.
*/ */
val latestBirthdayHeight: Int val latestBirthdayHeight: BlockHeight?
// //
// Operations // Operations
@@ -238,9 +239,7 @@ interface Synchronizer {
* *
* @throws RuntimeException when the address is invalid. * @throws RuntimeException when the address is invalid.
*/ */
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun isValidTransparentAddr(address: String): Boolean suspend fun isValidTransparentAddr(address: String): Boolean
*/
/** /**
* Validate whether the server and this SDK share the same consensus branch. This is * Validate whether the server and this SDK share the same consensus branch. This is
@@ -266,9 +265,7 @@ interface Synchronizer {
* *
* @return an instance of [AddressType] providing validation info regarding the given address. * @return an instance of [AddressType] providing validation info regarding the given address.
*/ */
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun validateAddress(address: String): AddressType suspend fun validateAddress(address: String): AddressType
*/
/** /**
* Attempts to cancel a transaction that is about to be sent. Typically, cancellation is only * Attempts to cancel a transaction that is about to be sent. Typically, cancellation is only
@@ -287,38 +284,22 @@ interface Synchronizer {
*/ */
suspend fun getServerInfo(): Service.LightdInfo suspend fun getServerInfo(): Service.LightdInfo
/**
* Gracefully change the server that the Synchronizer is currently using. In some cases, this
* will require waiting until current network activity is complete. Ideally, this would protect
* against accidentally switching between testnet and mainnet, by comparing the service info of
* the existing server with that of the new one.
*/
suspend fun changeServer(
host: String,
port: Int = network.defaultPort,
errorHandler: (Throwable) -> Unit = { throw it }
)
/** /**
* Download all UTXOs for the given address and store any new ones in the database. * Download all UTXOs for the given address and store any new ones in the database.
* *
* @return the number of utxos that were downloaded and addded to the UTXO table. * @return the number of utxos that were downloaded and addded to the UTXO table.
*/ */
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun refreshUtxos( suspend fun refreshUtxos(
tAddr: String, tAddr: String,
sinceHeight: Int = network.saplingActivationHeight since: BlockHeight = network.saplingActivationHeight
): Int? ): Int?
*/
/** /**
* Returns the balance that the wallet knows about. This should be called after [refreshUtxos]. * Returns the balance that the wallet knows about. This should be called after [refreshUtxos].
*/ */
/* THIS IS NOT SUPPORT IN HUSH LIGHTWALLETD
suspend fun getTransparentBalance(tAddr: String): WalletBalance suspend fun getTransparentBalance(tAddr: String): WalletBalance
*/
suspend fun getNearestRewindHeight(height: Int): Int suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight
/** /**
* Returns the safest height to which we can rewind, given a desire to rewind to the height * Returns the safest height to which we can rewind, given a desire to rewind to the height
@@ -326,7 +307,7 @@ interface Synchronizer {
* arbitrary height. This handles all that complexity yet remains flexible in the future as * arbitrary height. This handles all that complexity yet remains flexible in the future as
* improvements are made. * improvements are made.
*/ */
suspend fun rewindToNearestHeight(height: Int, alsoClearBlockCache: Boolean = false) suspend fun rewindToNearestHeight(height: BlockHeight, alsoClearBlockCache: Boolean = false)
suspend fun quickRewind() suspend fun quickRewind()
@@ -380,7 +361,7 @@ interface Synchronizer {
* best to log these errors because they are the most common source of bugs and unexpected * best to log these errors because they are the most common source of bugs and unexpected
* behavior in wallets, due to the chain data mutating and wallets becoming out of sync. * behavior in wallets, due to the chain data mutating and wallets becoming out of sync.
*/ */
var onChainErrorHandler: ((Int, Int) -> Any)? var onChainErrorHandler: ((BlockHeight, BlockHeight) -> Any)?
/** /**
* Represents the status of this Synchronizer, which is useful for communicating to the user. * Represents the status of this Synchronizer, which is useful for communicating to the user.

View File

@@ -33,13 +33,15 @@ import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.internal.ext.retryUpTo import cash.z.ecc.android.sdk.internal.ext.retryUpTo
import cash.z.ecc.android.sdk.internal.ext.retryWithBackoff import cash.z.ecc.android.sdk.internal.ext.retryWithBackoff
import cash.z.ecc.android.sdk.internal.ext.toHexReversed import cash.z.ecc.android.sdk.internal.ext.toHexReversed
import cash.z.ecc.android.sdk.internal.isEmpty
import cash.z.ecc.android.sdk.internal.transaction.PagedTransactionRepository import cash.z.ecc.android.sdk.internal.transaction.PagedTransactionRepository
import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.type.WalletBalance import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.wallet.sdk.rpc.Service import cash.z.wallet.sdk.rpc.Service
import io.grpc.StatusRuntimeException import io.grpc.StatusRuntimeException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -75,11 +77,11 @@ import kotlin.math.roundToInt
* of the current wallet--the height before which we do not need to scan for transactions. * of the current wallet--the height before which we do not need to scan for transactions.
*/ */
@OpenForTesting @OpenForTesting
class CompactBlockProcessor( class CompactBlockProcessor internal constructor(
val downloader: CompactBlockDownloader, val downloader: CompactBlockDownloader,
private val repository: TransactionRepository, private val repository: TransactionRepository,
private val rustBackend: RustBackendWelding, private val rustBackend: RustBackendWelding,
minimumHeight: Int = rustBackend.network.saplingActivationHeight minimumHeight: BlockHeight = rustBackend.network.saplingActivationHeight
) { ) {
/** /**
* Callback for any non-trivial errors that occur while processing compact blocks. * Callback for any non-trivial errors that occur while processing compact blocks.
@@ -93,7 +95,7 @@ class CompactBlockProcessor(
* Callback for reorgs. This callback is invoked when validation fails with the height at which * Callback for reorgs. This callback is invoked when validation fails with the height at which
* an error was found and the lower bound to which the data will rewind, at most. * an error was found and the lower bound to which the data will rewind, at most.
*/ */
var onChainErrorListener: ((errorHeight: Int, rewindHeight: Int) -> Any)? = null var onChainErrorListener: ((errorHeight: BlockHeight, rewindHeight: BlockHeight) -> Any)? = null
/** /**
* Callback for setup errors that occur prior to processing compact blocks. Can be used to * Callback for setup errors that occur prior to processing compact blocks. Can be used to
@@ -117,12 +119,18 @@ class CompactBlockProcessor(
var onScanMetricCompleteListener: ((BatchMetrics, Boolean) -> Unit)? = null var onScanMetricCompleteListener: ((BatchMetrics, Boolean) -> Unit)? = null
private val consecutiveChainErrors = AtomicInteger(0) private val consecutiveChainErrors = AtomicInteger(0)
private val lowerBoundHeight: Int = max(rustBackend.network.saplingActivationHeight, minimumHeight - MAX_REORG_SIZE) private val lowerBoundHeight: BlockHeight = BlockHeight(
max(
rustBackend.network.saplingActivationHeight.value,
minimumHeight.value - MAX_REORG_SIZE
)
)
private val _state: ConflatedBroadcastChannel<State> = ConflatedBroadcastChannel(Initialized) private val _state: ConflatedBroadcastChannel<State> = ConflatedBroadcastChannel(Initialized)
private val _progress = ConflatedBroadcastChannel(0) private val _progress = ConflatedBroadcastChannel(0)
private val _processorInfo = ConflatedBroadcastChannel(ProcessorInfo()) private val _processorInfo =
private val _networkHeight = MutableStateFlow(-1) ConflatedBroadcastChannel(ProcessorInfo(null, null, null, null, null))
private val _networkHeight = MutableStateFlow<BlockHeight?>(null)
private val processingMutex = Mutex() private val processingMutex = Mutex()
/** /**
@@ -139,7 +147,10 @@ class CompactBlockProcessor(
* sequentially, due to the way sqlite works so it is okay for this not to be threadsafe or * sequentially, due to the way sqlite works so it is okay for this not to be threadsafe or
* coroutine safe because processing cannot be concurrent. * coroutine safe because processing cannot be concurrent.
*/ */
internal var currentInfo = ProcessorInfo() // This accessed by the Dispatchers.IO thread, which means multiple threads are reading/writing
// concurrently.
@Volatile
internal var currentInfo = ProcessorInfo(null, null, null, null, null)
/** /**
* The zcash network that is being processed. Either Testnet or Mainnet. * The zcash network that is being processed. Either Testnet or Mainnet.
@@ -193,25 +204,38 @@ class CompactBlockProcessor(
processNewBlocks() processNewBlocks()
} }
// immediately process again after failures in order to download new blocks right away // immediately process again after failures in order to download new blocks right away
if (result == ERROR_CODE_RECONNECT) { when (result) {
val napTime = calculatePollInterval(true) BlockProcessingResult.Reconnecting -> {
twig("Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime}ms") val napTime = calculatePollInterval(true)
delay(napTime) twig("Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime}ms")
} else if (result == ERROR_CODE_NONE || result == ERROR_CODE_FAILED_ENHANCE) { delay(napTime)
val noWorkDone = currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty() }
val summary = if (noWorkDone) "Nothing to process: no new blocks to download or scan" else "Done processing blocks" BlockProcessingResult.NoBlocksToProcess, BlockProcessingResult.FailedEnhance -> {
consecutiveChainErrors.set(0) val noWorkDone =
val napTime = calculatePollInterval() currentInfo.lastDownloadRange?.isEmpty() ?: true && currentInfo.lastScanRange?.isEmpty() ?: true
twig("$summary${if (result == ERROR_CODE_FAILED_ENHANCE) " (but there were enhancement errors! We ignore those, for now. Memos in this block range are probably missing! This will be improved in a future release.)" else ""}! Sleeping for ${napTime}ms (latest height: ${currentInfo.networkBlockHeight}).") val summary = if (noWorkDone) {
delay(napTime) "Nothing to process: no new blocks to download or scan"
} else { } else {
if (consecutiveChainErrors.get() >= RETRIES) { "Done processing blocks"
val errorMessage = "ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!" }
fail(CompactBlockProcessorException.FailedReorgRepair(errorMessage)) consecutiveChainErrors.set(0)
} else { val napTime = calculatePollInterval()
handleChainError(result) twig("$summary${if (result == BlockProcessingResult.FailedEnhance) " (but there were enhancement errors! We ignore those, for now. Memos in this block range are probably missing! This will be improved in a future release.)" else ""}! Sleeping for ${napTime}ms (latest height: ${currentInfo.networkBlockHeight}).")
delay(napTime)
}
is BlockProcessingResult.Error -> {
if (consecutiveChainErrors.get() >= RETRIES) {
val errorMessage =
"ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!"
fail(CompactBlockProcessorException.FailedReorgRepair(errorMessage))
} else {
handleChainError(result.failedAtHeight)
}
consecutiveChainErrors.getAndIncrement()
}
is BlockProcessingResult.Success -> {
// Do nothing. We are done.
} }
consecutiveChainErrors.getAndIncrement()
} }
} }
} while (isActive && !_state.isClosedForSend && _state.value !is Stopped) } while (isActive && !_state.isClosedForSend && _state.value !is Stopped)
@@ -238,32 +262,37 @@ class CompactBlockProcessor(
throw error throw error
} }
/** private suspend fun processNewBlocks(): BlockProcessingResult = withContext(IO) {
* Process new blocks returning false whenever an error was found.
*
* @return -1 when processing was successful and did not encounter errors during validation or scanning. Otherwise
* return the block height where an error was found.
*/
private suspend fun processNewBlocks(): Int = withContext(IO) {
twig("beginning to process new blocks (with lower bound: $lowerBoundHeight)...", -1) twig("beginning to process new blocks (with lower bound: $lowerBoundHeight)...", -1)
if (!updateRanges()) { if (!updateRanges()) {
twig("Disconnection detected! Attempting to reconnect!") twig("Disconnection detected! Attempting to reconnect!")
setState(Disconnected) setState(Disconnected)
downloader.lightWalletService.reconnect() downloader.lightWalletService.reconnect()
ERROR_CODE_RECONNECT BlockProcessingResult.Reconnecting
} else if (currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()) { } else if (currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()) {
setState(Scanned(currentInfo.lastScanRange)) setState(Scanned(currentInfo.lastScanRange))
ERROR_CODE_NONE BlockProcessingResult.NoBlocksToProcess
} else { } else {
downloadNewBlocks(currentInfo.lastDownloadRange) downloadNewBlocks(currentInfo.lastDownloadRange)
val error = validateAndScanNewBlocks(currentInfo.lastScanRange) val error = validateAndScanNewBlocks(currentInfo.lastScanRange)
if (error != ERROR_CODE_NONE) error else { if (error != BlockProcessingResult.Success) {
enhanceTransactionDetails(currentInfo.lastScanRange) error
} else {
currentInfo.lastScanRange?.let { enhanceTransactionDetails(it) }
?: BlockProcessingResult.NoBlocksToProcess
} }
} }
} }
sealed class BlockProcessingResult {
object NoBlocksToProcess : BlockProcessingResult()
object Success : BlockProcessingResult()
object Reconnecting : BlockProcessingResult()
object FailedEnhance : BlockProcessingResult()
data class Error(val failedAtHeight: BlockHeight) : BlockProcessingResult()
}
/** /**
* Gets the latest range info and then uses that initialInfo to update (and transmit) * Gets the latest range info and then uses that initialInfo to update (and transmit)
* the scan/download ranges that require processing. * the scan/download ranges that require processing.
@@ -278,19 +307,39 @@ class CompactBlockProcessor(
ProcessorInfo( ProcessorInfo(
networkBlockHeight = downloader.getLatestBlockHeight(), networkBlockHeight = downloader.getLatestBlockHeight(),
lastScannedHeight = getLastScannedHeight(), lastScannedHeight = getLastScannedHeight(),
lastDownloadedHeight = max(getLastDownloadedHeight(), lowerBoundHeight - 1) lastDownloadedHeight = getLastDownloadedHeight()?.let {
BlockHeight.new(
network,
max(
it.value,
lowerBoundHeight.value - 1
)
)
},
lastDownloadRange = null,
lastScanRange = null
).let { initialInfo -> ).let { initialInfo ->
updateProgress( updateProgress(
networkBlockHeight = initialInfo.networkBlockHeight, networkBlockHeight = initialInfo.networkBlockHeight,
lastScannedHeight = initialInfo.lastScannedHeight, lastScannedHeight = initialInfo.lastScannedHeight,
lastDownloadedHeight = initialInfo.lastDownloadedHeight, lastDownloadedHeight = initialInfo.lastDownloadedHeight,
lastScanRange = (initialInfo.lastScannedHeight + 1)..initialInfo.networkBlockHeight, lastScanRange = if (initialInfo.lastScannedHeight != null && initialInfo.networkBlockHeight != null) {
lastDownloadRange = ( initialInfo.lastScannedHeight + 1..initialInfo.networkBlockHeight
max( } else {
initialInfo.lastDownloadedHeight, null
initialInfo.lastScannedHeight },
) + 1 lastDownloadRange = if (initialInfo.networkBlockHeight != null) {
BlockHeight.new(
network,
buildList {
add(network.saplingActivationHeight.value)
initialInfo.lastDownloadedHeight?.let { add(it.value + 1) }
initialInfo.lastScannedHeight?.let { add(it.value + 1) }
}.max()
)..initialInfo.networkBlockHeight )..initialInfo.networkBlockHeight
} else {
null
}
) )
} }
true true
@@ -306,35 +355,34 @@ class CompactBlockProcessor(
* prevHash value matches the preceding block in the chain. * prevHash value matches the preceding block in the chain.
* *
* @param lastScanRange the range to be validated and scanned. * @param lastScanRange the range to be validated and scanned.
*
* @return error code or [ERROR_CODE_NONE] when there is no error.
*/ */
private suspend fun validateAndScanNewBlocks(lastScanRange: IntRange): Int = withContext(IO) { private suspend fun validateAndScanNewBlocks(lastScanRange: ClosedRange<BlockHeight>?): BlockProcessingResult =
setState(Validating) withContext(IO) {
var error = validateNewBlocks(lastScanRange) setState(Validating)
if (error == ERROR_CODE_NONE) { val result = validateNewBlocks(lastScanRange)
// in theory, a scan should not fail after validation succeeds but maybe consider if (result == BlockProcessingResult.Success) {
// changing the rust layer to return the failed block height whenever scan does fail // in theory, a scan should not fail after validation succeeds but maybe consider
// rather than a boolean // changing the rust layer to return the failed block height whenever scan does fail
setState(Scanning) // rather than a boolean
val success = scanNewBlocks(lastScanRange) setState(Scanning)
if (!success) throw CompactBlockProcessorException.FailedScan() val success = scanNewBlocks(lastScanRange)
else { if (!success) {
setState(Scanned(lastScanRange)) throw CompactBlockProcessorException.FailedScan()
} else {
setState(Scanned(lastScanRange))
}
} }
ERROR_CODE_NONE
} else {
error
}
}
private suspend fun enhanceTransactionDetails(lastScanRange: IntRange): Int { result
}
private suspend fun enhanceTransactionDetails(lastScanRange: ClosedRange<BlockHeight>): BlockProcessingResult {
Twig.sprout("enhancing") Twig.sprout("enhancing")
twig("Enhancing transaction details for blocks $lastScanRange") twig("Enhancing transaction details for blocks $lastScanRange")
setState(Enhancing) setState(Enhancing)
return try { return try {
val newTxs = repository.findNewTransactions(lastScanRange) val newTxs = repository.findNewTransactions(lastScanRange)
if (newTxs == null) { if (newTxs.isEmpty()) {
twig("no new transactions found in $lastScanRange") twig("no new transactions found in $lastScanRange")
} else { } else {
twig("enhancing ${newTxs.size} transaction(s)!") twig("enhancing ${newTxs.size} transaction(s)!")
@@ -346,15 +394,18 @@ class CompactBlockProcessor(
} }
newTxs?.onEach { newTransaction -> newTxs?.onEach { newTransaction ->
if (newTransaction == null) twig("somehow, new transaction was null!!!") if (newTransaction == null) {
else enhance(newTransaction) twig("somehow, new transaction was null!!!")
} else {
enhance(newTransaction)
}
} }
twig("Done enhancing transaction details") twig("Done enhancing transaction details")
ERROR_CODE_NONE BlockProcessingResult.Success
} catch (t: Throwable) { } catch (t: Throwable) {
twig("Failed to enhance due to $t") twig("Failed to enhance due to $t")
t.printStackTrace() t.printStackTrace()
ERROR_CODE_FAILED_ENHANCE BlockProcessingResult.FailedEnhance
} finally { } finally {
Twig.clip("enhancing") Twig.clip("enhancing")
} }
@@ -374,8 +425,11 @@ class CompactBlockProcessor(
} catch (t: Throwable) { } catch (t: Throwable) {
twig("Warning: failure on transaction: error: $t\ttransaction: $transaction") twig("Warning: failure on transaction: error: $t\ttransaction: $transaction")
onProcessorError( onProcessorError(
if (downloaded) EnhanceTxDecryptError(transaction.minedHeight, t) if (downloaded) {
else EnhanceTxDownloadError(transaction.minedHeight, t) EnhanceTxDecryptError(transaction.minedBlockHeight, t)
} else {
EnhanceTxDownloadError(transaction.minedBlockHeight, t)
}
) )
} }
} }
@@ -391,12 +445,18 @@ class CompactBlockProcessor(
else -> { else -> {
// verify that the server is correct // verify that the server is correct
downloader.getServerInfo().let { info -> downloader.getServerInfo().let { info ->
//val clientBranch = "%x".format(rustBackend.getBranchIdForHeight(info.blockHeight.toInt()))
val clientBranch = "76b809bb" val clientBranch = "76b809bb"
val network = rustBackend.network.networkName val network = rustBackend.network.networkName
when { when {
!info.matchingNetwork(network) -> MismatchedNetwork(clientNetwork = network, serverNetwork = info.chainName) !info.matchingNetwork(network) -> MismatchedNetwork(
!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(clientBranch = clientBranch, serverBranch = info.consensusBranchId, networkName = network) clientNetwork = network,
serverNetwork = info.chainName
)
!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(
clientBranch = clientBranch,
serverBranch = info.consensusBranchId,
networkName = network
)
else -> null else -> null
} }
} }
@@ -426,34 +486,36 @@ class CompactBlockProcessor(
} }
} }
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
var failedUtxoFetches = 0 var failedUtxoFetches = 0
internal suspend fun refreshUtxos(tAddress: String, startHeight: Int): Int? = withContext(IO) { internal suspend fun refreshUtxos(tAddress: String, startHeight: BlockHeight): Int? =
var count: Int? = null withContext(IO) {
// todo: cleanup the way that we prevent this from running excessively var count: Int? = null
// For now, try for about 3 blocks per app launch. If the service fails it is // todo: cleanup the way that we prevent this from running excessively
// probably disabled on ligthtwalletd, so then stop trying until the next app launch. // For now, try for about 3 blocks per app launch. If the service fails it is
if (failedUtxoFetches < 9) { // there are 3 attempts per block // probably disabled on ligthtwalletd, so then stop trying until the next app launch.
try { if (failedUtxoFetches < 9) { // there are 3 attempts per block
retryUpTo(3) { try {
val result = downloader.lightWalletService.fetchUtxos(tAddress, startHeight) retryUpTo(3) {
count = processUtxoResult(result, tAddress, startHeight) val result = downloader.lightWalletService.fetchUtxos(tAddress, startHeight)
count = processUtxoResult(result, tAddress, startHeight)
}
} catch (e: Throwable) {
failedUtxoFetches++
twig("Warning: Fetching UTXOs is repeatedly failing! We will only try about ${(9 - failedUtxoFetches + 2) / 3} more times then give up for this session.")
} }
} catch (e: Throwable) { } else {
failedUtxoFetches++ twig("Warning: gave up on fetching UTXOs for this session. It seems to unavailable on lightwalletd.")
twig("Warning: Fetching UTXOs is repeatedly failing! We will only try about ${(9 - failedUtxoFetches + 2) / 3} more times then give up for this session.")
} }
} else { count
twig("Warning: gave up on fetching UTXOs for this session. It seems to unavailable on lightwalletd.")
} }
count
}
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD internal suspend fun processUtxoResult(
internal suspend fun processUtxoResult(result: List<Service.GetAddressUtxosReply>, tAddress: String, startHeight: Int): Int = withContext(IO) { result: List<Service.GetAddressUtxosReply>,
tAddress: String,
startHeight: BlockHeight
): Int = withContext(IO) {
var skipped = 0 var skipped = 0
val aboveHeight = startHeight - 1 val aboveHeight = startHeight
twig("Clearing utxos above height $aboveHeight", -1) twig("Clearing utxos above height $aboveHeight", -1)
rustBackend.clearUtxos(tAddress, aboveHeight) rustBackend.clearUtxos(tAddress, aboveHeight)
twig("Checking for UTXOs above height $aboveHeight") twig("Checking for UTXOs above height $aboveHeight")
@@ -466,7 +528,7 @@ class CompactBlockProcessor(
utxo.index, utxo.index,
utxo.script.toByteArray(), utxo.script.toByteArray(),
utxo.valueZat, utxo.valueZat,
utxo.height.toInt() BlockHeight(utxo.height)
) )
} catch (t: Throwable) { } catch (t: Throwable) {
// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons) // TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons)
@@ -477,7 +539,6 @@ class CompactBlockProcessor(
// return the number of UTXOs that were downloaded // return the number of UTXOs that were downloaded
result.size - skipped result.size - skipped
} }
*/
/** /**
* Request all blocks in the given range and persist them locally for processing, later. * Request all blocks in the given range and persist them locally for processing, later.
@@ -485,75 +546,81 @@ class CompactBlockProcessor(
* @param range the range of blocks to download. * @param range the range of blocks to download.
*/ */
@VisibleForTesting // allow mocks to verify how this is called, rather than the downloader, which is more complex @VisibleForTesting // allow mocks to verify how this is called, rather than the downloader, which is more complex
internal suspend fun downloadNewBlocks(range: IntRange) = withContext<Unit>(IO) { internal suspend fun downloadNewBlocks(range: ClosedRange<BlockHeight>?) =
if (range.isEmpty()) { withContext<Unit>(IO) {
twig("no blocks to download") if (null == range || range.isEmpty()) {
} else { twig("no blocks to download")
_state.send(Downloading) } else {
Twig.sprout("downloading") _state.send(Downloading)
twig("downloading blocks in range $range", -1) Twig.sprout("downloading")
twig("downloading blocks in range $range", -1)
var downloadedBlockHeight = range.first var downloadedBlockHeight = range.start
val missingBlockCount = range.last - range.first + 1 val missingBlockCount = range.endInclusive.value - range.start.value + 1
val batches = ( val batches = (
missingBlockCount / DOWNLOAD_BATCH_SIZE + missingBlockCount / DOWNLOAD_BATCH_SIZE +
(if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0) 0 else 1) (if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0L) 0 else 1)
) )
var progress: Int var progress: Int
twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...") twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...")
for (i in 1..batches) { for (i in 1..batches) {
retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) { retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) {
val end = min((range.first + (i * DOWNLOAD_BATCH_SIZE)) - 1, range.last) // subtract 1 on the first value because the range is inclusive val end = BlockHeight.new(
var count = 0 network,
twig("downloaded $downloadedBlockHeight..$end (batch $i of $batches) [${downloadedBlockHeight..end}]") { min(
count = downloader.downloadBlockRange(downloadedBlockHeight..end) (range.start.value + (i * DOWNLOAD_BATCH_SIZE)) - 1,
range.endInclusive.value
)
) // subtract 1 on the first value because the range is inclusive
var count = 0
twig("downloaded $downloadedBlockHeight..$end (batch $i of $batches) [${downloadedBlockHeight..end}]") {
count = downloader.downloadBlockRange(downloadedBlockHeight..end)
}
twig("downloaded $count blocks!")
progress = (i / batches.toFloat() * 100).roundToInt()
_progress.send(progress)
val lastDownloadedHeight = downloader.getLastDownloadedHeight()
updateProgress(lastDownloadedHeight = lastDownloadedHeight)
downloadedBlockHeight = end + 1
} }
twig("downloaded $count blocks!")
progress = (i / batches.toFloat() * 100).roundToInt()
_progress.send(progress)
val lastDownloadedHeight = downloader.getLastDownloadedHeight().takeUnless { it < network.saplingActivationHeight } ?: -1
updateProgress(lastDownloadedHeight = lastDownloadedHeight)
downloadedBlockHeight = end
} }
Twig.clip("downloading")
} }
Twig.clip("downloading") _progress.send(100)
} }
_progress.send(100)
}
/** /**
* Validate all blocks in the given range, ensuring that the blocks are in ascending order, with * Validate all blocks in the given range, ensuring that the blocks are in ascending order, with
* no gaps and are also chain-sequential. This means every block's prevHash value matches the * no gaps and are also chain-sequential. This means every block's prevHash value matches the
* preceding block in the chain. * preceding block in the chain. Validation starts at the back of the chain and works toward the tip.
* *
* @param range the range of blocks to validate. * @param range the range of blocks to validate.
*
* @return [ERROR_CODE_NONE] when there is no problem. Otherwise, return the lowest height where an error was
* found. In other words, validation starts at the back of the chain and works toward the tip.
*/ */
private suspend fun validateNewBlocks(range: IntRange?): Int { private suspend fun validateNewBlocks(range: ClosedRange<BlockHeight>?): BlockProcessingResult {
if (range?.isEmpty() != false) { if (null == range || range.isEmpty()) {
twig("no blocks to validate: $range") twig("no blocks to validate: $range")
return ERROR_CODE_NONE return BlockProcessingResult.NoBlocksToProcess
} }
Twig.sprout("validating") Twig.sprout("validating")
twig("validating blocks in range $range in db: ${(rustBackend as RustBackend).pathCacheDb}") twig("validating blocks in range $range in db: ${(rustBackend as RustBackend).pathCacheDb}")
val result = rustBackend.validateCombinedChain() val result = rustBackend.validateCombinedChain()
Twig.clip("validating") Twig.clip("validating")
return result
return if (null == result) {
BlockProcessingResult.Success
} else {
BlockProcessingResult.Error(result)
}
} }
/** /**
* Scan all blocks in the given range, decrypting and persisting anything that matches our * Scan all blocks in the given range, decrypting and persisting anything that matches our
* wallet. * wallet. Scanning starts at the back of the chain and works toward the tip.
* *
* @param range the range of blocks to scan. * @param range the range of blocks to scan.
*
* @return [ERROR_CODE_NONE] when there is no problem. Otherwise, return the lowest height where an error was
* found. In other words, scanning starts at the back of the chain and works toward the tip.
*/ */
private suspend fun scanNewBlocks(range: IntRange?): Boolean = withContext(IO) { private suspend fun scanNewBlocks(range: ClosedRange<BlockHeight>?): Boolean = withContext(IO) {
if (range?.isEmpty() != false) { if (null == range || range.isEmpty()) {
twig("no blocks to scan for range $range") twig("no blocks to scan for range $range")
true true
} else { } else {
@@ -570,16 +637,18 @@ class CompactBlockProcessor(
metrics.beginBatch() metrics.beginBatch()
result = rustBackend.scanBlocks(SCAN_BATCH_SIZE) result = rustBackend.scanBlocks(SCAN_BATCH_SIZE)
metrics.endBatch() metrics.endBatch()
val lastScannedHeight = range.start + metrics.cumulativeItems - 1 val lastScannedHeight =
val percentValue = (lastScannedHeight - range.first) / (range.last - range.first + 1).toFloat() * 100.0f BlockHeight.new(network, range.start.value + metrics.cumulativeItems - 1)
val percentValue =
(lastScannedHeight.value - range.start.value) / (range.endInclusive.value - range.start.value + 1).toFloat() * 100.0f
val percent = "%.0f".format(percentValue.coerceAtMost(100f).coerceAtLeast(0f)) val percent = "%.0f".format(percentValue.coerceAtMost(100f).coerceAtLeast(0f))
twig("batch scanned ($percent%): $lastScannedHeight/${range.last} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps") twig("batch scanned ($percent%): $lastScannedHeight/${range.endInclusive} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps")
if (currentInfo.lastScannedHeight != lastScannedHeight) { if (currentInfo.lastScannedHeight != lastScannedHeight) {
scannedNewBlocks = true scannedNewBlocks = true
updateProgress(lastScannedHeight = lastScannedHeight) updateProgress(lastScannedHeight = lastScannedHeight)
} }
// if we made progress toward our scan, then keep trying // if we made progress toward our scan, then keep trying
} while (result && scannedNewBlocks && lastScannedHeight < range.last) } while (result && scannedNewBlocks && lastScannedHeight < range.endInclusive)
twig("batch scan complete! Total time: ${metrics.cumulativeTime} Total blocks measured: ${metrics.cumulativeItems} Cumulative bps: ${metrics.cumulativeIps.format()}") twig("batch scan complete! Total time: ${metrics.cumulativeTime} Total blocks measured: ${metrics.cumulativeItems} Cumulative bps: ${metrics.cumulativeIps.format()}")
} }
Twig.clip("scanning") Twig.clip("scanning")
@@ -605,12 +674,12 @@ class CompactBlockProcessor(
* blocks that we don't yet have. * blocks that we don't yet have.
*/ */
private suspend fun updateProgress( private suspend fun updateProgress(
networkBlockHeight: Int = currentInfo.networkBlockHeight, networkBlockHeight: BlockHeight? = currentInfo.networkBlockHeight,
lastScannedHeight: Int = currentInfo.lastScannedHeight, lastScannedHeight: BlockHeight? = currentInfo.lastScannedHeight,
lastDownloadedHeight: Int = currentInfo.lastDownloadedHeight, lastDownloadedHeight: BlockHeight? = currentInfo.lastDownloadedHeight,
lastScanRange: IntRange = currentInfo.lastScanRange, lastScanRange: ClosedRange<BlockHeight>? = currentInfo.lastScanRange,
lastDownloadRange: IntRange = currentInfo.lastDownloadRange lastDownloadRange: ClosedRange<BlockHeight>? = currentInfo.lastDownloadRange
): Unit = withContext(IO) { ) {
currentInfo = currentInfo.copy( currentInfo = currentInfo.copy(
networkBlockHeight = networkBlockHeight, networkBlockHeight = networkBlockHeight,
lastScannedHeight = lastScannedHeight, lastScannedHeight = lastScannedHeight,
@@ -618,11 +687,14 @@ class CompactBlockProcessor(
lastScanRange = lastScanRange, lastScanRange = lastScanRange,
lastDownloadRange = lastDownloadRange lastDownloadRange = lastDownloadRange
) )
_networkHeight.value = networkBlockHeight
_processorInfo.send(currentInfo) withContext(IO) {
_networkHeight.value = networkBlockHeight
_processorInfo.send(currentInfo)
}
} }
private suspend fun handleChainError(errorHeight: Int) { private suspend fun handleChainError(errorHeight: BlockHeight) {
// TODO consider an error object containing hash information // TODO consider an error object containing hash information
printValidationErrorInfo(errorHeight) printValidationErrorInfo(errorHeight)
determineLowerBound(errorHeight).let { lowerBound -> determineLowerBound(errorHeight).let { lowerBound ->
@@ -632,14 +704,17 @@ class CompactBlockProcessor(
} }
} }
suspend fun getNearestRewindHeight(height: Int): Int { suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight {
// TODO: add a concept of original checkpoint height to the processor. For now, derive it // TODO: add a concept of original checkpoint height to the processor. For now, derive it
val originalCheckpoint = lowerBoundHeight + MAX_REORG_SIZE + 2 // add one because we already have the checkpoint. Add one again because we delete ABOVE the block val originalCheckpoint =
lowerBoundHeight + MAX_REORG_SIZE + 2 // add one because we already have the checkpoint. Add one again because we delete ABOVE the block
return if (height < originalCheckpoint) { return if (height < originalCheckpoint) {
originalCheckpoint originalCheckpoint
} else { } else {
// tricky: subtract one because we delete ABOVE this block // tricky: subtract one because we delete ABOVE this block
rustBackend.getNearestRewindHeight(height) - 1 // This could create an invalid height if if height was saplingActivationHeight
val rewindHeight = BlockHeight(height.value - 1)
rustBackend.getNearestRewindHeight(rewindHeight)
} }
} }
@@ -649,7 +724,10 @@ class CompactBlockProcessor(
suspend fun quickRewind() { suspend fun quickRewind() {
val height = max(currentInfo.lastScannedHeight, repository.lastScannedHeight()) val height = max(currentInfo.lastScannedHeight, repository.lastScannedHeight())
val blocksPerDay = 60 * 60 * 24 * 1000 / ZcashSdk.BLOCK_INTERVAL_MILLIS.toInt() val blocksPerDay = 60 * 60 * 24 * 1000 / ZcashSdk.BLOCK_INTERVAL_MILLIS.toInt()
val twoWeeksBack = (height - blocksPerDay * 14).coerceAtLeast(lowerBoundHeight) val twoWeeksBack = BlockHeight.new(
network,
(height.value - blocksPerDay * 14).coerceAtLeast(lowerBoundHeight.value)
)
rewindToNearestHeight(twoWeeksBack, false) rewindToNearestHeight(twoWeeksBack, false)
} }
@@ -657,45 +735,73 @@ class CompactBlockProcessor(
* @param alsoClearBlockCache when true, also clear the block cache which forces a redownload of * @param alsoClearBlockCache when true, also clear the block cache which forces a redownload of
* blocks. Otherwise, the cached blocks will be used in the rescan, which in most cases, is fine. * blocks. Otherwise, the cached blocks will be used in the rescan, which in most cases, is fine.
*/ */
suspend fun rewindToNearestHeight(height: Int, alsoClearBlockCache: Boolean = false) = withContext(IO) { suspend fun rewindToNearestHeight(
processingMutex.withLockLogged("rewindToHeight") { height: BlockHeight,
val lastScannedHeight = currentInfo.lastScannedHeight alsoClearBlockCache: Boolean = false
val lastLocalBlock = repository.lastScannedHeight() ) =
val targetHeight = getNearestRewindHeight(height) withContext(IO) {
twig("Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock") processingMutex.withLockLogged("rewindToHeight") {
if (targetHeight < lastScannedHeight || (lastScannedHeight == -1 && (targetHeight < lastLocalBlock))) { val lastScannedHeight = currentInfo.lastScannedHeight
rustBackend.rewindToHeight(targetHeight) val lastLocalBlock = repository.lastScannedHeight()
} else { val targetHeight = getNearestRewindHeight(height)
twig("not rewinding dataDb because the last scanned height is $lastScannedHeight and the last local block is $lastLocalBlock both of which are less than the target height of $targetHeight") twig("Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock")
} if ((null == lastScannedHeight && targetHeight < lastLocalBlock) || (null != lastScannedHeight && targetHeight < lastScannedHeight)) {
rustBackend.rewindToHeight(targetHeight)
} else {
twig("not rewinding dataDb because the last scanned height is $lastScannedHeight and the last local block is $lastLocalBlock both of which are less than the target height of $targetHeight")
}
if (alsoClearBlockCache) { val currentNetworkBlockHeight = currentInfo.networkBlockHeight
twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan")
downloader.rewindToHeight(targetHeight) if (alsoClearBlockCache) {
// communicate that the wallet is no longer synced because it might remain this way for 20+ seconds because we only download on 20s time boundaries so we can't trigger any immediate action twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan")
setState(Downloading) downloader.rewindToHeight(targetHeight)
updateProgress( // communicate that the wallet is no longer synced because it might remain this way for 20+ seconds because we only download on 20s time boundaries so we can't trigger any immediate action
lastScannedHeight = targetHeight, setState(Downloading)
lastDownloadedHeight = targetHeight, if (null == currentNetworkBlockHeight) {
lastScanRange = (targetHeight + 1)..currentInfo.networkBlockHeight, updateProgress(
lastDownloadRange = (targetHeight + 1)..currentInfo.networkBlockHeight lastScannedHeight = targetHeight,
) lastDownloadedHeight = targetHeight,
_progress.send(0) lastScanRange = null,
} else { lastDownloadRange = null
updateProgress( )
lastScannedHeight = targetHeight, } else {
lastScanRange = (targetHeight + 1)..currentInfo.networkBlockHeight updateProgress(
) lastScannedHeight = targetHeight,
_progress.send(0) lastDownloadedHeight = targetHeight,
val range = (targetHeight + 1)..lastScannedHeight lastScanRange = (targetHeight + 1)..currentNetworkBlockHeight,
twig("We kept the cache blocks in place so we don't need to wait for the next scheduled download to rescan. Instead we will rescan and validate blocks ${range.first}..${range.last}") lastDownloadRange = (targetHeight + 1)..currentNetworkBlockHeight
if (validateAndScanNewBlocks(range) == ERROR_CODE_NONE) enhanceTransactionDetails(range) )
}
_progress.send(0)
} else {
if (null == currentNetworkBlockHeight) {
updateProgress(
lastScannedHeight = targetHeight,
lastScanRange = null
)
} else {
updateProgress(
lastScannedHeight = targetHeight,
lastScanRange = (targetHeight + 1)..currentNetworkBlockHeight
)
}
_progress.send(0)
if (null != lastScannedHeight) {
val range = (targetHeight + 1)..lastScannedHeight
twig("We kept the cache blocks in place so we don't need to wait for the next scheduled download to rescan. Instead we will rescan and validate blocks ${range.start}..${range.endInclusive}")
if (validateAndScanNewBlocks(range) == BlockProcessingResult.Success) {
enhanceTransactionDetails(range)
}
}
}
} }
} }
}
/** insightful function for debugging these critical errors */ /** insightful function for debugging these critical errors */
private suspend fun printValidationErrorInfo(errorHeight: Int, count: Int = 11) { private suspend fun printValidationErrorInfo(errorHeight: BlockHeight, count: Int = 11) {
// Note: blocks are public information so it's okay to print them but, still, let's not unless we're debugging something // Note: blocks are public information so it's okay to print them but, still, let's not unless we're debugging something
if (!BuildConfig.DEBUG) return if (!BuildConfig.DEBUG) return
@@ -704,19 +810,25 @@ class CompactBlockProcessor(
errorInfo = fetchValidationErrorInfo(errorHeight + 1) errorInfo = fetchValidationErrorInfo(errorHeight + 1)
twig("The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}") twig("The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}")
twig("=================== BLOCKS [$errorHeight..${errorHeight + count - 1}]: START ========") twig("=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: START ========")
repeat(count) { i -> repeat(count) { i ->
val height = errorHeight + i val height = errorHeight + i
val block = downloader.compactBlockStore.findCompactBlock(height) val block = downloader.compactBlockStore.findCompactBlock(height)
// sometimes the initial block was inserted via checkpoint and will not appear in the cache. We can get the hash another way but prevHash is correctly null. // sometimes the initial block was inserted via checkpoint and will not appear in the cache. We can get the hash another way but prevHash is correctly null.
val hash = block?.hash?.toByteArray() ?: (repository as PagedTransactionRepository).findBlockHash(height) val hash = block?.hash?.toByteArray()
twig("block: $height\thash=${hash?.toHexReversed()} \tprevHash=${block?.prevHash?.toByteArray()?.toHexReversed()}") ?: (repository as PagedTransactionRepository).findBlockHash(height)
twig(
"block: $height\thash=${hash?.toHexReversed()} \tprevHash=${
block?.prevHash?.toByteArray()?.toHexReversed()
}"
)
} }
twig("=================== BLOCKS [$errorHeight..${errorHeight + count - 1}]: END ========") twig("=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: END ========")
} }
private suspend fun fetchValidationErrorInfo(errorHeight: Int): ValidationErrorInfo { private suspend fun fetchValidationErrorInfo(errorHeight: BlockHeight): ValidationErrorInfo {
val hash = (repository as PagedTransactionRepository).findBlockHash(errorHeight + 1)?.toHexReversed() val hash = (repository as PagedTransactionRepository).findBlockHash(errorHeight + 1)
?.toHexReversed()
val prevHash = repository.findBlockHash(errorHeight)?.toHexReversed() val prevHash = repository.findBlockHash(errorHeight)?.toHexReversed()
val compactBlock = downloader.compactBlockStore.findCompactBlock(errorHeight + 1) val compactBlock = downloader.compactBlockStore.findCompactBlock(errorHeight + 1)
@@ -734,9 +846,9 @@ class CompactBlockProcessor(
return onProcessorErrorListener?.invoke(throwable) ?: true return onProcessorErrorListener?.invoke(throwable) ?: true
} }
private fun determineLowerBound(errorHeight: Int): Int { private fun determineLowerBound(errorHeight: BlockHeight): BlockHeight {
val offset = Math.min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1)) val offset = min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1))
return Math.max(errorHeight - offset, lowerBoundHeight).also { return BlockHeight(max(errorHeight.value - offset, lowerBoundHeight.value)).also {
twig("offset = min($MAX_REORG_SIZE, $REWIND_DISTANCE * (${consecutiveChainErrors.get() + 1})) = $offset") twig("offset = min($MAX_REORG_SIZE, $REWIND_DISTANCE * (${consecutiveChainErrors.get() + 1})) = $offset")
twig("lowerBound = max($errorHeight - $offset, $lowerBoundHeight) = $it") twig("lowerBound = max($errorHeight - $offset, $lowerBoundHeight) = $it")
} }
@@ -759,19 +871,28 @@ class CompactBlockProcessor(
return deltaToNextInteral return deltaToNextInteral
} }
suspend fun calculateBirthdayHeight(): Int { suspend fun calculateBirthdayHeight(): BlockHeight {
var oldestTransactionHeight = 0 var oldestTransactionHeight: BlockHeight? = null
try { try {
oldestTransactionHeight = repository.receivedTransactions.first().lastOrNull()?.minedHeight ?: lowerBoundHeight val tempOldestTransactionHeight = repository.receivedTransactions
.first()
.lastOrNull()
?.minedBlockHeight
?: lowerBoundHeight
// to be safe adjust for reorgs (and generally a little cushion is good for privacy) // to be safe adjust for reorgs (and generally a little cushion is good for privacy)
// so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least 100 blocks away // so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least 100 blocks away
oldestTransactionHeight = ZcashSdk.MAX_REORG_SIZE.let { boundary -> oldestTransactionHeight = BlockHeight.new(
oldestTransactionHeight.let { it - it.rem(boundary) - boundary } network,
} tempOldestTransactionHeight.value - tempOldestTransactionHeight.value.rem(ZcashSdk.MAX_REORG_SIZE) - ZcashSdk.MAX_REORG_SIZE.toLong()
)
} catch (t: Throwable) { } catch (t: Throwable) {
twig("failed to calculate birthday due to: $t") twig("failed to calculate birthday due to: $t")
} }
return maxOf(lowerBoundHeight, oldestTransactionHeight, rustBackend.network.saplingActivationHeight) return buildList<BlockHeight> {
add(lowerBoundHeight)
add(rustBackend.network.saplingActivationHeight)
oldestTransactionHeight?.let { add(it) }
}.maxOf { it }
} }
/** /**
@@ -824,7 +945,8 @@ class CompactBlockProcessor(
} }
} }
suspend fun getUtxoCacheBalance(address: String): WalletBalance = rustBackend.getDownloadedUtxoBalance(address) suspend fun getUtxoCacheBalance(address: String): WalletBalance =
rustBackend.getDownloadedUtxoBalance(address)
/** /**
* Transmits the given state for this processor. * Transmits the given state for this processor.
@@ -869,7 +991,7 @@ class CompactBlockProcessor(
/** /**
* [State] for when we are done decrypting blocks, for now. * [State] for when we are done decrypting blocks, for now.
*/ */
class Scanned(val scannedRange: IntRange) : Connected, Syncing, State() class Scanned(val scannedRange: ClosedRange<BlockHeight>?) : Connected, Syncing, State()
/** /**
* [State] for when transaction details are being retrieved. This typically means the wallet * [State] for when transaction details are being retrieved. This typically means the wallet
@@ -912,11 +1034,11 @@ class CompactBlockProcessor(
* @param lastScanRange inclusive range to scan. * @param lastScanRange inclusive range to scan.
*/ */
data class ProcessorInfo( data class ProcessorInfo(
val networkBlockHeight: Int = -1, val networkBlockHeight: BlockHeight?,
val lastScannedHeight: Int = -1, val lastScannedHeight: BlockHeight?,
val lastDownloadedHeight: Int = -1, val lastDownloadedHeight: BlockHeight?,
val lastDownloadRange: IntRange = 0..-1, // empty range val lastDownloadRange: ClosedRange<BlockHeight>?,
val lastScanRange: IntRange = 0..-1 // empty range val lastScanRange: ClosedRange<BlockHeight>?
) { ) {
/** /**
@@ -924,19 +1046,24 @@ class CompactBlockProcessor(
* *
* @return false when all values match their defaults. * @return false when all values match their defaults.
*/ */
val hasData get() = networkBlockHeight != -1 || val hasData
lastScannedHeight != -1 || get() = networkBlockHeight != null ||
lastDownloadedHeight != -1 || lastScannedHeight != null ||
lastDownloadRange != 0..-1 || lastDownloadedHeight != null ||
lastScanRange != 0..-1 lastDownloadRange != null ||
lastScanRange != null
/** /**
* Determines whether this instance is actively downloading compact blocks. * Determines whether this instance is actively downloading compact blocks.
* *
* @return true when there are more than zero blocks remaining to download. * @return true when there are more than zero blocks remaining to download.
*/ */
val isDownloading: Boolean get() = !lastDownloadRange.isEmpty() && val isDownloading: Boolean
lastDownloadedHeight < lastDownloadRange.last get() =
lastDownloadedHeight != null &&
lastDownloadRange != null &&
!lastDownloadRange.isEmpty() &&
lastDownloadedHeight < lastDownloadRange.endInclusive
/** /**
* Determines whether this instance is actively scanning or validating compact blocks. * Determines whether this instance is actively scanning or validating compact blocks.
@@ -944,32 +1071,39 @@ class CompactBlockProcessor(
* @return true when downloading has completed and there are more than zero blocks remaining * @return true when downloading has completed and there are more than zero blocks remaining
* to be scanned. * to be scanned.
*/ */
val isScanning: Boolean get() = !isDownloading && val isScanning: Boolean
!lastScanRange.isEmpty() && get() =
lastScannedHeight < lastScanRange.last !isDownloading &&
lastScannedHeight != null &&
lastScanRange != null &&
!lastScanRange.isEmpty() &&
lastScannedHeight < lastScanRange.endInclusive
/** /**
* The amount of scan progress from 0 to 100. * The amount of scan progress from 0 to 100.
*/ */
val scanProgress get() = when { val scanProgress
lastScannedHeight <= -1 -> 0 get() = when {
lastScanRange.isEmpty() -> 100 lastScannedHeight == null -> 0
lastScannedHeight >= lastScanRange.last -> 100 lastScanRange == null -> 100
else -> { lastScannedHeight >= lastScanRange.endInclusive -> 100
// when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets else -> {
val blocksScanned = (lastScannedHeight - lastScanRange.first + 1).coerceAtLeast(0) // when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets
// we scan the range inclusively so 100..100 is one block to scan, thus the offset val blocksScanned =
val numberOfBlocks = lastScanRange.last - lastScanRange.first + 1 (lastScannedHeight.value - lastScanRange.start.value + 1).coerceAtLeast(0)
// take the percentage then convert and round // we scan the range inclusively so 100..100 is one block to scan, thus the offset
((blocksScanned.toFloat() / numberOfBlocks) * 100.0f).let { percent -> val numberOfBlocks =
percent.coerceAtMost(100.0f).roundToInt() lastScanRange.endInclusive.value - lastScanRange.start.value + 1
// take the percentage then convert and round
((blocksScanned.toFloat() / numberOfBlocks) * 100.0f).let { percent ->
percent.coerceAtMost(100.0f).roundToInt()
}
} }
} }
}
} }
data class ValidationErrorInfo( data class ValidationErrorInfo(
val errorHeight: Int, val errorHeight: BlockHeight,
val hash: String?, val hash: String?,
val expectedPrevHash: String?, val expectedPrevHash: String?,
val actualPrevHash: String? val actualPrevHash: String?
@@ -1007,10 +1141,12 @@ class CompactBlockProcessor(
} }
twig("$name MUTEX: withLock complete", -1) twig("$name MUTEX: withLock complete", -1)
} }
}
companion object {
const val ERROR_CODE_NONE = -1 private fun max(a: BlockHeight?, b: BlockHeight) = if (null == a) {
const val ERROR_CODE_RECONNECT = 20 b
const val ERROR_CODE_FAILED_ENHANCE = 40 } else if (a.value > b.value) {
} a
} else {
b
} }

View File

@@ -5,7 +5,7 @@ import androidx.room.Entity
@Entity(primaryKeys = ["height"], tableName = "compactblocks") @Entity(primaryKeys = ["height"], tableName = "compactblocks")
data class CompactBlockEntity( data class CompactBlockEntity(
val height: Int, val height: Long,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val data: ByteArray val data: ByteArray
) { ) {
@@ -20,7 +20,7 @@ data class CompactBlockEntity(
} }
override fun hashCode(): Int { override fun hashCode(): Int {
var result = height var result = height.hashCode()
result = 31 * result + data.contentHashCode() result = 31 * result + data.contentHashCode()
return result return result
} }

View File

@@ -5,6 +5,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
// //
@@ -81,8 +82,8 @@ data class PendingTransactionEntity(
override val value: Long = -1, override val value: Long = -1,
override val memo: ByteArray? = byteArrayOf(), override val memo: ByteArray? = byteArrayOf(),
override val accountIndex: Int, override val accountIndex: Int,
override val minedHeight: Int = -1, override val minedHeight: Long = -1,
override val expiryHeight: Int = -1, override val expiryHeight: Long = -1,
override val cancelled: Int = 0, override val cancelled: Int = 0,
override val encodeAttempts: Int = -1, override val encodeAttempts: Int = -1,
@@ -96,6 +97,10 @@ data class PendingTransactionEntity(
@ColumnInfo(typeAffinity = ColumnInfo.BLOB) @ColumnInfo(typeAffinity = ColumnInfo.BLOB)
override val rawTransactionId: ByteArray? = byteArrayOf() override val rawTransactionId: ByteArray? = byteArrayOf()
) : PendingTransaction { ) : PendingTransaction {
val valueZatoshi: Zatoshi
get() = Zatoshi(value)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is PendingTransactionEntity) return false if (other !is PendingTransactionEntity) return false
@@ -131,8 +136,8 @@ data class PendingTransactionEntity(
result = 31 * result + value.hashCode() result = 31 * result + value.hashCode()
result = 31 * result + (memo?.contentHashCode() ?: 0) result = 31 * result + (memo?.contentHashCode() ?: 0)
result = 31 * result + accountIndex result = 31 * result + accountIndex
result = 31 * result + minedHeight result = 31 * result + minedHeight.hashCode()
result = 31 * result + expiryHeight result = 31 * result + expiryHeight.hashCode()
result = 31 * result + cancelled result = 31 * result + cancelled
result = 31 * result + encodeAttempts result = 31 * result + encodeAttempts
result = 31 * result + submitAttempts result = 31 * result + submitAttempts
@@ -159,7 +164,7 @@ data class ConfirmedTransaction(
override val memo: ByteArray? = ByteArray(0), override val memo: ByteArray? = ByteArray(0),
override val noteId: Long = 0L, override val noteId: Long = 0L,
override val blockTimeInSeconds: Long = 0L, override val blockTimeInSeconds: Long = 0L,
override val minedHeight: Int = -1, override val minedHeight: Long = -1,
override val transactionIndex: Int, override val transactionIndex: Int,
override val rawTransactionId: ByteArray = ByteArray(0), override val rawTransactionId: ByteArray = ByteArray(0),
@@ -169,6 +174,13 @@ data class ConfirmedTransaction(
override val raw: ByteArray? = byteArrayOf() override val raw: ByteArray? = byteArrayOf()
) : MinedTransaction, SignedTransaction { ) : MinedTransaction, SignedTransaction {
val minedBlockHeight
get() = if (minedHeight == -1L) {
null
} else {
BlockHeight(minedHeight)
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is ConfirmedTransaction) return false if (other !is ConfirmedTransaction) return false
@@ -200,7 +212,7 @@ data class ConfirmedTransaction(
result = 31 * result + (memo?.contentHashCode() ?: 0) result = 31 * result + (memo?.contentHashCode() ?: 0)
result = 31 * result + noteId.hashCode() result = 31 * result + noteId.hashCode()
result = 31 * result + blockTimeInSeconds.hashCode() result = 31 * result + blockTimeInSeconds.hashCode()
result = 31 * result + minedHeight result = 31 * result + minedHeight.hashCode()
result = 31 * result + transactionIndex result = 31 * result + transactionIndex
result = 31 * result + rawTransactionId.contentHashCode() result = 31 * result + rawTransactionId.contentHashCode()
result = 31 * result + (toAddress?.hashCode() ?: 0) result = 31 * result + (toAddress?.hashCode() ?: 0)
@@ -213,8 +225,12 @@ data class ConfirmedTransaction(
val ConfirmedTransaction.valueInZatoshi val ConfirmedTransaction.valueInZatoshi
get() = Zatoshi(value) get() = Zatoshi(value)
data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray, val expiryHeight: Int?) : data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray, val expiryHeight: Long?) :
SignedTransaction { SignedTransaction {
val expiryBlockHeight
get() = expiryHeight?.let { BlockHeight(it) }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is EncodedTransaction) return false if (other !is EncodedTransaction) return false
@@ -229,7 +245,7 @@ data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray,
override fun hashCode(): Int { override fun hashCode(): Int {
var result = txId.contentHashCode() var result = txId.contentHashCode()
result = 31 * result + raw.contentHashCode() result = 31 * result + raw.contentHashCode()
result = 31 * result + (expiryHeight ?: 0) result = 31 * result + (expiryHeight?.hashCode() ?: 0)
return result return result
} }
} }
@@ -260,7 +276,7 @@ interface SignedTransaction {
* one list for things like history. A mined tx should have all properties, except possibly a memo. * one list for things like history. A mined tx should have all properties, except possibly a memo.
*/ */
interface MinedTransaction : Transaction { interface MinedTransaction : Transaction {
val minedHeight: Int val minedHeight: Long
val noteId: Long val noteId: Long
val blockTimeInSeconds: Long val blockTimeInSeconds: Long
val transactionIndex: Int val transactionIndex: Int
@@ -273,8 +289,8 @@ interface PendingTransaction : SignedTransaction, Transaction {
override val memo: ByteArray? override val memo: ByteArray?
val toAddress: String val toAddress: String
val accountIndex: Int val accountIndex: Int
val minedHeight: Int val minedHeight: Long // apparently this can be -1 as an uninitialized value
val expiryHeight: Int val expiryHeight: Long // apparently this can be -1 as an uninitialized value
val cancelled: Int val cancelled: Int
val encodeAttempts: Int val encodeAttempts: Int
val submitAttempts: Int val submitAttempts: Int
@@ -333,16 +349,16 @@ fun PendingTransaction.isSubmitted(): Boolean {
return submitAttempts > 0 return submitAttempts > 0
} }
fun PendingTransaction.isExpired(latestHeight: Int?, saplingActivationHeight: Int): Boolean { fun PendingTransaction.isExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
// TODO: test for off-by-one error here. Should we use <= or < // TODO: test for off-by-one error here. Should we use <= or <
if (latestHeight == null || latestHeight < saplingActivationHeight || expiryHeight < saplingActivationHeight) return false if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false
return expiryHeight < latestHeight return expiryHeight < latestHeight.value
} }
// if we don't have info on a pendingtx after 100 blocks then it's probably safe to stop polling! // if we don't have info on a pendingtx after 100 blocks then it's probably safe to stop polling!
fun PendingTransaction.isLongExpired(latestHeight: Int?, saplingActivationHeight: Int): Boolean { fun PendingTransaction.isLongExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
if (latestHeight == null || latestHeight < saplingActivationHeight || expiryHeight < saplingActivationHeight) return false if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false
return (latestHeight - expiryHeight) > 100 return (latestHeight.value - expiryHeight) > 100
} }
fun PendingTransaction.isMarkedForDeletion(): Boolean { fun PendingTransaction.isMarkedForDeletion(): Boolean {
@@ -369,10 +385,10 @@ fun PendingTransaction.isSafeToDiscard(): Boolean {
} }
} }
fun PendingTransaction.isPending(currentHeight: Int = -1): Boolean { fun PendingTransaction.isPending(currentHeight: BlockHeight?): Boolean {
// not mined and not expired and successfully created // not mined and not expired and successfully created
return !isSubmitSuccess() && minedHeight == -1 && return !isSubmitSuccess() && minedHeight == -1L &&
(expiryHeight == -1 || expiryHeight > currentHeight) && raw != null (expiryHeight == -1L || expiryHeight > (currentHeight?.value ?: 0L)) && raw != null
} }
fun PendingTransaction.isSubmitSuccess(): Boolean { fun PendingTransaction.isSubmitSuccess(): Boolean {

View File

@@ -1,6 +1,8 @@
package cash.z.ecc.android.sdk.exception package cash.z.ecc.android.sdk.exception
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.wallet.sdk.rpc.Service import cash.z.wallet.sdk.rpc.Service
import io.grpc.Status import io.grpc.Status
import io.grpc.Status.Code.UNAVAILABLE import io.grpc.Status.Code.UNAVAILABLE
@@ -15,8 +17,7 @@ open class SdkException(message: String, cause: Throwable?) : RuntimeException(m
* Exceptions thrown in the Rust layer of the SDK. We may not always be able to surface details about this * Exceptions thrown in the Rust layer of the SDK. We may not always be able to surface details about this
* exception so it's important for the SDK to provide helpful messages whenever these errors are encountered. * exception so it's important for the SDK to provide helpful messages whenever these errors are encountered.
*/ */
sealed class RustLayerException(message: String, cause: Throwable? = null) : sealed class RustLayerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
SdkException(message, cause) {
class BalanceException(cause: Throwable) : RustLayerException( class BalanceException(cause: Throwable) : RustLayerException(
"Error while requesting the current balance over " + "Error while requesting the current balance over " +
"JNI. This might mean that the database has been corrupted and needs to be rebuilt. Verify that " + "JNI. This might mean that the database has been corrupted and needs to be rebuilt. Verify that " +
@@ -28,13 +29,11 @@ sealed class RustLayerException(message: String, cause: Throwable? = null) :
/** /**
* User-facing exceptions thrown by the transaction repository. * User-facing exceptions thrown by the transaction repository.
*/ */
sealed class RepositoryException(message: String, cause: Throwable? = null) : sealed class RepositoryException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
SdkException(message, cause) {
object FalseStart : RepositoryException( object FalseStart : RepositoryException(
"The channel is closed. Note that once a repository has stopped it " + "The channel is closed. Note that once a repository has stopped it " +
"cannot be restarted. Verify that the repository is not being restarted." "cannot be restarted. Verify that the repository is not being restarted."
) )
object Unprepared : RepositoryException( object Unprepared : RepositoryException(
"Unprepared repository: Data cannot be accessed before the repository is prepared." + "Unprepared repository: Data cannot be accessed before the repository is prepared." +
" Ensure that things have been properly initialized. If you see this error it most" + " Ensure that things have been properly initialized. If you see this error it most" +
@@ -49,13 +48,11 @@ sealed class RepositoryException(message: String, cause: Throwable? = null) :
* High-level exceptions thrown by the synchronizer, which do not fall within the umbrella of a * High-level exceptions thrown by the synchronizer, which do not fall within the umbrella of a
* child component. * child component.
*/ */
sealed class SynchronizerException(message: String, cause: Throwable? = null) : sealed class SynchronizerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
SdkException(message, cause) {
object FalseStart : SynchronizerException( object FalseStart : SynchronizerException(
"This synchronizer was already started. Multiple calls to start are not" + "This synchronizer was already started. Multiple calls to start are not" +
"allowed and once a synchronizer has stopped it cannot be restarted." "allowed and once a synchronizer has stopped it cannot be restarted."
) )
object NotYetStarted : SynchronizerException( object NotYetStarted : SynchronizerException(
"The synchronizer has not yet started. Verify that" + "The synchronizer has not yet started. Verify that" +
" start has been called prior to this operation and that the coroutineScope is not" + " start has been called prior to this operation and that the coroutineScope is not" +
@@ -66,16 +63,12 @@ sealed class SynchronizerException(message: String, cause: Throwable? = null) :
/** /**
* Potentially user-facing exceptions that occur while processing compact blocks. * Potentially user-facing exceptions that occur while processing compact blocks.
*/ */
sealed class CompactBlockProcessorException(message: String, cause: Throwable? = null) : sealed class CompactBlockProcessorException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
SdkException(message, cause) {
class DataDbMissing(path: String) : CompactBlockProcessorException( class DataDbMissing(path: String) : CompactBlockProcessorException(
"No data db file found at path $path. Verify " + "No data db file found at path $path. Verify " +
"that the data DB has been initialized via `rustBackend.initDataDb(path)`" "that the data DB has been initialized via `rustBackend.initDataDb(path)`"
) )
open class ConfigurationException(message: String, cause: Throwable?) : CompactBlockProcessorException(message, cause)
open class ConfigurationException(message: String, cause: Throwable?) :
CompactBlockProcessorException(message, cause)
class FileInsteadOfPath(fileName: String) : ConfigurationException( class FileInsteadOfPath(fileName: String) : ConfigurationException(
"Invalid Path: the given path appears to be a" + "Invalid Path: the given path appears to be a" +
" file name instead of a path: $fileName. The RustBackend expects the absolutePath to the database rather" + " file name instead of a path: $fileName. The RustBackend expects the absolutePath to the database rather" +
@@ -83,137 +76,100 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
" So pass in context.getDatabasePath(dbFileName).absolutePath instead of just dbFileName alone.", " So pass in context.getDatabasePath(dbFileName).absolutePath instead of just dbFileName alone.",
null null
) )
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message) class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
class FailedDownload(cause: Throwable? = null) : CompactBlockProcessorException( class FailedDownload(cause: Throwable? = null) : CompactBlockProcessorException(
"Error while downloading blocks. This most " + "Error while downloading blocks. This most " +
"likely means the server is down or slow to respond. See logs for details.", "likely means the server is down or slow to respond. See logs for details.",
cause cause
) )
class FailedScan(cause: Throwable? = null) : CompactBlockProcessorException( class FailedScan(cause: Throwable? = null) : CompactBlockProcessorException(
"Error while scanning blocks. This most " + "Error while scanning blocks. This most " +
"likely means a block was missed or a reorg was mishandled. See logs for details.", "likely means a block was missed or a reorg was mishandled. See logs for details.",
cause cause
) )
class Disconnected(cause: Throwable? = null) : CompactBlockProcessorException("Disconnected Error. Unable to download blocks due to ${cause?.message}", cause)
class Disconnected(cause: Throwable? = null) : CompactBlockProcessorException(
"Disconnected Error. Unable to download blocks due to ${cause?.message}",
cause
)
object Uninitialized : CompactBlockProcessorException( object Uninitialized : CompactBlockProcessorException(
"Cannot process blocks because the wallet has not been" + "Cannot process blocks because the wallet has not been" +
" initialized. Verify that the seed phrase was properly created or imported. If so, then this problem" + " initialized. Verify that the seed phrase was properly created or imported. If so, then this problem" +
" can be fixed by re-importing the wallet." " can be fixed by re-importing the wallet."
) )
object NoAccount : CompactBlockProcessorException( object NoAccount : CompactBlockProcessorException(
"Attempting to scan without an account. This is probably a setup error or a race condition." "Attempting to scan without an account. This is probably a setup error or a race condition."
) )
open class EnhanceTransactionError(message: String, val height: Int, cause: Throwable) : open class EnhanceTransactionError(message: String, val height: BlockHeight?, cause: Throwable) : CompactBlockProcessorException(message, cause) {
CompactBlockProcessorException(message, cause) { class EnhanceTxDownloadError(height: BlockHeight?, cause: Throwable) : EnhanceTransactionError("Error while attempting to download a transaction to enhance", height, cause)
class EnhanceTxDownloadError(height: Int, cause: Throwable) : EnhanceTransactionError( class EnhanceTxDecryptError(height: BlockHeight?, cause: Throwable) : EnhanceTransactionError("Error while attempting to decrypt and store a transaction to enhance", height, cause)
"Error while attempting to download a transaction to enhance",
height,
cause
)
class EnhanceTxDecryptError(height: Int, cause: Throwable) : EnhanceTransactionError(
"Error while attempting to decrypt and store a transaction to enhance",
height,
cause
)
} }
class MismatchedNetwork(clientNetwork: String?, serverNetwork: String?) : class MismatchedNetwork(clientNetwork: String?, serverNetwork: String?) : CompactBlockProcessorException(
CompactBlockProcessorException( "Incompatible server: this client expects a server using $clientNetwork but it was $serverNetwork! Try updating the client or switching servers."
"Incompatible server: this client expects a server using $clientNetwork but it was $serverNetwork! Try updating the client or switching servers." )
)
class MismatchedBranch(clientBranch: String?, serverBranch: String?, networkName: String?) : class MismatchedBranch(clientBranch: String?, serverBranch: String?, networkName: String?) : CompactBlockProcessorException(
CompactBlockProcessorException( "Incompatible server: this client expects a server following consensus branch $clientBranch on $networkName but it was $serverBranch! Try updating the client or switching servers."
"Incompatible server: this client expects a server following consensus branch $clientBranch on $networkName but it was $serverBranch! Try updating the client or switching servers." )
)
} }
/** /**
* Exceptions related to the wallet's birthday. * Exceptions related to the wallet's birthday.
*/ */
sealed class BirthdayException(message: String, cause: Throwable? = null) : sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
SdkException(message, cause) {
object UninitializedBirthdayException : BirthdayException( object UninitializedBirthdayException : BirthdayException(
"Error the birthday cannot be" + "Error the birthday cannot be" +
" accessed before it is initialized. Verify that the new, import or open functions" + " accessed before it is initialized. Verify that the new, import or open functions" +
" have been called on the initializer." " have been called on the initializer."
) )
class MissingBirthdayFilesException(directory: String) : BirthdayException( class MissingBirthdayFilesException(directory: String) : BirthdayException(
"Cannot initialize wallet because no birthday files were found in the $directory directory." "Cannot initialize wallet because no birthday files were found in the $directory directory."
) )
class ExactBirthdayNotFoundException internal constructor(birthday: BlockHeight, nearestMatch: Checkpoint? = null) : BirthdayException(
class ExactBirthdayNotFoundException(height: Int, nearestMatch: Int? = null) : "Unable to find birthday that exactly matches $birthday.${
BirthdayException( if (nearestMatch != null) {
"Unable to find birthday that exactly matches $height.${ " An exact match was request but the nearest match found was ${nearestMatch.height}."
if (nearestMatch != null) { } else ""
" An exact match was request but the nearest match found was $nearestMatch." }"
} else { )
"" class BirthdayFileNotFoundException(directory: String, height: BlockHeight?) : BirthdayException(
}
}"
)
class BirthdayFileNotFoundException(directory: String, height: Int?) : BirthdayException(
"Unable to find birthday file for $height verify that $directory/$height.json exists." "Unable to find birthday file for $height verify that $directory/$height.json exists."
) )
class MalformattedBirthdayFilesException(directory: String, file: String, cause: Throwable?) : BirthdayException(
class MalformattedBirthdayFilesException(directory: String, file: String, cause: Throwable?) : "Failed to parse file $directory/$file verify that it is formatted as #####.json, " +
BirthdayException( "where the first portion is an Int representing the height of the tree contained in the file",
"Failed to parse file $directory/$file verify that it is formatted as #####.json, " + cause
"where the first portion is an Int representing the height of the tree contained in the file", )
cause
)
} }
/** /**
* Exceptions thrown by the initializer. * Exceptions thrown by the initializer.
*/ */
sealed class InitializerException(message: String, cause: Throwable? = null) : sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
SdkException(message, cause) { class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause)
class FalseStart(cause: Throwable?) :
InitializerException("Failed to initialize accounts due to: $cause", cause)
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException( class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException(
"Failed to initialize the blocks table" + "Failed to initialize the blocks table" +
" because it already exists in $dbPath", " because it already exists in $dbPath",
cause cause
) )
object MissingBirthdayException : InitializerException( object MissingBirthdayException : InitializerException(
"Expected a birthday for this wallet but failed to find one. This usually means that " + "Expected a birthday for this wallet but failed to find one. This usually means that " +
"wallet setup did not happen correctly. A workaround might be to interpret the " + "wallet setup did not happen correctly. A workaround might be to interpret the " +
"birthday, based on the contents of the wallet data but it is probably better " + "birthday, based on the contents of the wallet data but it is probably better " +
"not to mask this error because the root issue should be addressed." "not to mask this error because the root issue should be addressed."
) )
object MissingViewingKeyException : InitializerException( object MissingViewingKeyException : InitializerException(
"Expected a unified viewingKey for this wallet but failed to find one. This usually means" + "Expected a unified viewingKey for this wallet but failed to find one. This usually means" +
" that wallet setup happened incorrectly. A workaround might be to derive the" + " that wallet setup happened incorrectly. A workaround might be to derive the" +
" unified viewingKey from the seed or seedPhrase, if they exist, but it is probably" + " unified viewingKey from the seed or seedPhrase, if they exist, but it is probably" +
" better not to mask this error because the root issue should be addressed." " better not to mask this error because the root issue should be addressed."
) )
class MissingAddressException(description: String, cause: Throwable? = null) : InitializerException(
class MissingAddressException(description: String, cause: Throwable? = null) : "Expected a $description address for this wallet but failed to find one. This usually" +
InitializerException( " means that wallet setup happened incorrectly. If this problem persists, a" +
"Expected a $description address for this wallet but failed to find one. This usually" + " workaround might be to go to settings and WIPE the wallet and rescan. Doing so" +
" means that wallet setup happened incorrectly. If this problem persists, a" + " will restore any missing address information. Meanwhile, please report that" +
" workaround might be to go to settings and WIPE the wallet and rescan. Doing so" + " this happened so that the root issue can be uncovered and corrected." +
" will restore any missing address information. Meanwhile, please report that" + if (cause != null) "\nCaused by: $cause" else ""
" this happened so that the root issue can be uncovered and corrected." + )
if (cause != null) "\nCaused by: $cause" else ""
)
object DatabasePathException : object DatabasePathException :
InitializerException( InitializerException(
"Critical failure to locate path for storing databases. Perhaps this device prevents" + "Critical failure to locate path for storing databases. Perhaps this device prevents" +
@@ -221,11 +177,10 @@ sealed class InitializerException(message: String, cause: Throwable? = null) :
" data." " data."
) )
class InvalidBirthdayHeightException(height: Int?, network: ZcashNetwork) : class InvalidBirthdayHeightException(birthday: BlockHeight?, network: ZcashNetwork) : InitializerException(
InitializerException( "Invalid birthday height of ${birthday?.value}. The birthday height must be at least the height of" +
"Invalid birthday height of $height. The birthday height must be at least the height of" + " Sapling activation on ${network.networkName} (${network.saplingActivationHeight})."
" Sapling activation on ${network.networkName} (${network.saplingActivationHeight})." )
)
object MissingDefaultBirthdayException : InitializerException( object MissingDefaultBirthdayException : InitializerException(
"The birthday height is missing and it is unclear which value to use as a default." "The birthday height is missing and it is unclear which value to use as a default."
@@ -235,15 +190,13 @@ sealed class InitializerException(message: String, cause: Throwable? = null) :
/** /**
* Exceptions thrown while interacting with lightwalletd. * Exceptions thrown while interacting with lightwalletd.
*/ */
sealed class LightWalletException(message: String, cause: Throwable? = null) : sealed class LightWalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
SdkException(message, cause) {
object InsecureConnection : LightWalletException( object InsecureConnection : LightWalletException(
"Error: attempted to connect to lightwalletd" + "Error: attempted to connect to lightwalletd" +
" with an insecure connection! Plaintext connections are only allowed when the" + " with an insecure connection! Plaintext connections are only allowed when the" +
" resource value for 'R.bool.lightwalletd_allow_very_insecure_connections' is true" + " resource value for 'R.bool.lightwalletd_allow_very_insecure_connections' is true" +
" because this choice should be explicit." " because this choice should be explicit."
) )
class ConsensusBranchException(sdkBranch: String, lwdBranch: String) : class ConsensusBranchException(sdkBranch: String, lwdBranch: String) :
LightWalletException( LightWalletException(
"Error: the lightwalletd server is using a consensus branch" + "Error: the lightwalletd server is using a consensus branch" +
@@ -253,18 +206,11 @@ sealed class LightWalletException(message: String, cause: Throwable? = null) :
" update the SDK to match lightwalletd or use a lightwalletd that matches the SDK." " update the SDK to match lightwalletd or use a lightwalletd that matches the SDK."
) )
open class ChangeServerException(message: String, cause: Throwable? = null) : open class ChangeServerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
SdkException(message, cause) { class ChainInfoNotMatching(val propertyNames: String, val expectedInfo: Service.LightdInfo, val actualInfo: Service.LightdInfo) : ChangeServerException(
class ChainInfoNotMatching(
val propertyNames: String,
val expectedInfo: Service.LightdInfo,
val actualInfo: Service.LightdInfo
) : ChangeServerException(
"Server change error: the $propertyNames values did not match." "Server change error: the $propertyNames values did not match."
) )
class StatusException(val status: Status, cause: Throwable? = null) : SdkException(status.toMessage(), cause) {
class StatusException(val status: Status, cause: Throwable? = null) :
SdkException(status.toMessage(), cause) {
companion object { companion object {
private fun Status.toMessage(): String { private fun Status.toMessage(): String {
return when (this.code) { return when (this.code) {
@@ -282,29 +228,23 @@ sealed class LightWalletException(message: String, cause: Throwable? = null) :
/** /**
* Potentially user-facing exceptions thrown while encoding transactions. * Potentially user-facing exceptions thrown while encoding transactions.
*/ */
sealed class TransactionEncoderException(message: String, cause: Throwable? = null) : sealed class TransactionEncoderException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
SdkException(message, cause) { class FetchParamsException(message: String) : TransactionEncoderException("Failed to fetch params due to: $message")
class FetchParamsException(message: String) :
TransactionEncoderException("Failed to fetch params due to: $message")
object MissingParamsException : TransactionEncoderException( object MissingParamsException : TransactionEncoderException(
"Cannot send funds due to missing spend or output params and attempting to download them failed." "Cannot send funds due to missing spend or output params and attempting to download them failed."
) )
class TransactionNotFoundException(transactionId: Long) : TransactionEncoderException( class TransactionNotFoundException(transactionId: Long) : TransactionEncoderException(
"Unable to find transactionId " + "Unable to find transactionId " +
"$transactionId in the repository. This means the wallet created a transaction and then returned a row ID " + "$transactionId in the repository. This means the wallet created a transaction and then returned a row ID " +
"that does not actually exist. This is a scenario where the wallet should have thrown an exception but failed " + "that does not actually exist. This is a scenario where the wallet should have thrown an exception but failed " +
"to do so." "to do so."
) )
class TransactionNotEncodedException(transactionId: Long) : TransactionEncoderException( class TransactionNotEncodedException(transactionId: Long) : TransactionEncoderException(
"The transaction returned by the wallet," + "The transaction returned by the wallet," +
" with id $transactionId, does not have any raw data. This is a scenario where the wallet should have thrown" + " with id $transactionId, does not have any raw data. This is a scenario where the wallet should have thrown" +
" an exception but failed to do so." " an exception but failed to do so."
) )
class IncompleteScanException(lastScannedHeight: BlockHeight) : TransactionEncoderException(
class IncompleteScanException(lastScannedHeight: Int) : TransactionEncoderException(
"Cannot" + "Cannot" +
" create spending transaction because scanning is incomplete. We must scan up to the" + " create spending transaction because scanning is incomplete. We must scan up to the" +
" latest height to know which consensus rules to apply. However, the last scanned" + " latest height to know which consensus rules to apply. However, the last scanned" +

View File

@@ -1,23 +1,24 @@
package cash.z.ecc.android.sdk.ext package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.model.BlockHeight
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class BatchMetrics(val range: IntRange, val batchSize: Int, private val onMetricComplete: ((BatchMetrics, Boolean) -> Unit)? = null) { class BatchMetrics(val range: ClosedRange<BlockHeight>, val batchSize: Int, private val onMetricComplete: ((BatchMetrics, Boolean) -> Unit)? = null) {
private var completedBatches = 0 private var completedBatches = 0
private var rangeStartTime = 0L private var rangeStartTime = 0L
private var batchStartTime = 0L private var batchStartTime = 0L
private var batchEndTime = 0L private var batchEndTime = 0L
private var rangeSize = range.last - range.first + 1 private var rangeSize = range.endInclusive.value - range.start.value + 1
private inline fun now() = System.currentTimeMillis() private inline fun now() = System.currentTimeMillis()
private inline fun ips(blocks: Int, time: Long) = 1000.0f * blocks / time private inline fun ips(blocks: Long, time: Long) = 1000.0f * blocks / time
val isComplete get() = completedBatches * batchSize >= rangeSize val isComplete get() = completedBatches * batchSize >= rangeSize
val isBatchComplete get() = batchEndTime > batchStartTime val isBatchComplete get() = batchEndTime > batchStartTime
val cumulativeItems get() = min(completedBatches * batchSize, rangeSize) val cumulativeItems get() = min(completedBatches * batchSize.toLong(), rangeSize)
val cumulativeTime get() = (if (isComplete) batchEndTime else now()) - rangeStartTime val cumulativeTime get() = (if (isComplete) batchEndTime else now()) - rangeStartTime
val batchTime get() = max(batchEndTime - batchStartTime, now() - batchStartTime) val batchTime get() = max(batchEndTime - batchStartTime, now() - batchStartTime)
val batchItems get() = min(batchSize, batchSize - (completedBatches * batchSize - rangeSize)) val batchItems get() = min(batchSize.toLong(), batchSize - (completedBatches * batchSize - rangeSize))
val batchIps get() = ips(batchItems, batchTime) val batchIps get() = ips(batchItems, batchTime)
val cumulativeIps get() = ips(cumulativeItems, cumulativeTime) val cumulativeIps get() = ips(cumulativeItems, cumulativeTime)

View File

@@ -9,7 +9,12 @@ import java.util.Locale
*/ */
enum class ConsensusBranchId(val displayName: String, val id: Long, val hexId: String) { enum class ConsensusBranchId(val displayName: String, val id: Long, val hexId: String) {
// TODO: see if we can find a way to not rely on this separate source of truth (either stop converting from hex to display name in the apps or use Rust to get this info) // TODO: see if we can find a way to not rely on this separate source of truth (either stop converting from hex to display name in the apps or use Rust to get this info)
SAPLING("Sapling", 0x76b8_09bb, "76b809bb"); SPROUT("Sprout", 0, "0"),
OVERWINTER("Overwinter", 0x5ba8_1b19, "5ba81b19"),
SAPLING("Sapling", 0x76b8_09bb, "76b809bb"),
BLOSSOM("Blossom", 0x2bb4_0e60, "2bb40e60"),
HEARTWOOD("Heartwood", 0xf5b9_230b, "f5b9230b"),
CANOPY("Canopy", 0xe9ff_75a6, "e9ff75a6");
override fun toString(): String = displayName override fun toString(): String = displayName

View File

@@ -34,7 +34,10 @@ object ZcashSdk {
/** /**
* Default size of batches of blocks to request from the compact block service. * Default size of batches of blocks to request from the compact block service.
*/ */
val DOWNLOAD_BATCH_SIZE = 100 // Because blocks are buffered in memory upon download and storage into SQLite, there is an upper bound
// above which OutOfMemoryError is thrown. Experimentally, this value is below 50 blocks.
// Back of the envelope calculation says the maximum block size is ~100kb.
const val DOWNLOAD_BATCH_SIZE = 10
/** /**
* Default size of batches of blocks to scan via librustzcash. The smaller this number the more granular information * Default size of batches of blocks to scan via librustzcash. The smaller this number the more granular information

View File

@@ -0,0 +1,46 @@
package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import org.json.JSONObject
// Version is not returned from the server, so version 1 is implied. A version is declared here
// to structure the parsing to be version-aware in the future.
internal val Checkpoint.Companion.VERSION_1
get() = 1
internal val Checkpoint.Companion.KEY_VERSION
get() = "version"
internal val Checkpoint.Companion.KEY_HEIGHT
get() = "height"
internal val Checkpoint.Companion.KEY_HASH
get() = "hash"
internal val Checkpoint.Companion.KEY_EPOCH_SECONDS
get() = "time"
internal val Checkpoint.Companion.KEY_TREE
get() = "saplingTree"
internal fun Checkpoint.Companion.from(zcashNetwork: ZcashNetwork, jsonString: String) =
from(zcashNetwork, JSONObject(jsonString))
private fun Checkpoint.Companion.from(
zcashNetwork: ZcashNetwork,
jsonObject: JSONObject
): Checkpoint {
when (val version = jsonObject.optInt(Checkpoint.KEY_VERSION, Checkpoint.VERSION_1)) {
Checkpoint.VERSION_1 -> {
val height = run {
val heightLong = jsonObject.getLong(Checkpoint.KEY_HEIGHT)
BlockHeight.new(zcashNetwork, heightLong)
}
val hash = jsonObject.getString(Checkpoint.KEY_HASH)
val epochSeconds = jsonObject.getLong(Checkpoint.KEY_EPOCH_SECONDS)
val tree = jsonObject.getString(Checkpoint.KEY_TREE)
return Checkpoint(height, hash, epochSeconds, tree)
}
else -> {
throw IllegalArgumentException("Unsupported version $version")
}
}
}

View File

@@ -0,0 +1,5 @@
package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.model.BlockHeight
internal fun ClosedRange<BlockHeight>?.isEmpty() = this?.isEmpty() ?: true

View File

@@ -75,6 +75,7 @@ interface Twig {
* @see [Twig.clip] * @see [Twig.clip]
*/ */
object Bush { object Bush {
@Volatile
var trunk: Twig = SilentTwig() var trunk: Twig = SilentTwig()
val leaves: MutableSet<Leaf> = CopyOnWriteArraySet<Leaf>() val leaves: MutableSet<Leaf> = CopyOnWriteArraySet<Leaf>()
} }

View File

@@ -1,37 +0,0 @@
package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.type.WalletBirthday
import org.json.JSONObject
// Version is not returned from the server, so version 1 is implied. A version is declared here
// to structure the parsing to be version-aware in the future.
internal val WalletBirthday.Companion.VERSION_1
get() = 1
internal val WalletBirthday.Companion.KEY_VERSION
get() = "version"
internal val WalletBirthday.Companion.KEY_HEIGHT
get() = "height"
internal val WalletBirthday.Companion.KEY_HASH
get() = "hash"
internal val WalletBirthday.Companion.KEY_EPOCH_SECONDS
get() = "time"
internal val WalletBirthday.Companion.KEY_TREE
get() = "saplingTree"
fun WalletBirthday.Companion.from(jsonString: String) = from(JSONObject(jsonString))
private fun WalletBirthday.Companion.from(jsonObject: JSONObject): WalletBirthday {
when (val version = jsonObject.optInt(WalletBirthday.KEY_VERSION, WalletBirthday.VERSION_1)) {
WalletBirthday.VERSION_1 -> {
val height = jsonObject.getInt(WalletBirthday.KEY_HEIGHT)
val hash = jsonObject.getString(WalletBirthday.KEY_HASH)
val epochSeconds = jsonObject.getLong(WalletBirthday.KEY_EPOCH_SECONDS)
val tree = jsonObject.getString(WalletBirthday.KEY_TREE)
return WalletBirthday(height, hash, epochSeconds, tree)
}
else -> {
throw IllegalArgumentException("Unsupported version $version")
}
}
}

View File

@@ -7,30 +7,34 @@ import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity
import cash.z.ecc.android.sdk.internal.SdkDispatchers import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.SdkExecutors import cash.z.ecc.android.sdk.internal.SdkExecutors
import cash.z.ecc.android.sdk.internal.db.CompactBlockDb import cash.z.ecc.android.sdk.internal.db.CompactBlockDb
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.wallet.sdk.rpc.CompactFormats import cash.z.wallet.sdk.rpc.CompactFormats
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.max
/** /**
* An implementation of CompactBlockStore that persists information to a database in the given * An implementation of CompactBlockStore that persists information to a database in the given
* path. This represents the "cache db" or local cache of compact blocks waiting to be scanned. * path. This represents the "cache db" or local cache of compact blocks waiting to be scanned.
*/ */
class CompactBlockDbStore private constructor( class CompactBlockDbStore private constructor(
private val network: ZcashNetwork,
private val cacheDb: CompactBlockDb private val cacheDb: CompactBlockDb
) : CompactBlockStore { ) : CompactBlockStore {
private val cacheDao = cacheDb.compactBlockDao() private val cacheDao = cacheDb.compactBlockDao()
override suspend fun getLatestHeight(): Int = max(0, cacheDao.latestBlockHeight()) override suspend fun getLatestHeight(): BlockHeight? = runCatching {
BlockHeight.new(network, cacheDao.latestBlockHeight())
}.getOrNull()
override suspend fun findCompactBlock(height: Int): CompactFormats.CompactBlock? = override suspend fun findCompactBlock(height: BlockHeight): CompactFormats.CompactBlock? =
cacheDao.findCompactBlock(height)?.let { CompactFormats.CompactBlock.parseFrom(it) } cacheDao.findCompactBlock(height.value)?.let { CompactFormats.CompactBlock.parseFrom(it) }
override suspend fun write(result: List<CompactFormats.CompactBlock>) = override suspend fun write(result: Sequence<CompactFormats.CompactBlock>) =
cacheDao.insert(result.map { CompactBlockEntity(it.height.toInt(), it.toByteArray()) }) cacheDao.insert(result.map { CompactBlockEntity(it.height, it.toByteArray()) })
override suspend fun rewindTo(height: Int) = override suspend fun rewindTo(height: BlockHeight) =
cacheDao.rewindTo(height) cacheDao.rewindTo(height.value)
override suspend fun close() { override suspend fun close() {
withContext(SdkDispatchers.DATABASE_IO) { withContext(SdkDispatchers.DATABASE_IO) {
@@ -43,10 +47,14 @@ class CompactBlockDbStore private constructor(
* @param appContext the application context. This is used for creating the database. * @param appContext the application context. This is used for creating the database.
* @property dbPath the absolute path to the database. * @property dbPath the absolute path to the database.
*/ */
fun new(appContext: Context, dbPath: String): CompactBlockDbStore { fun new(
appContext: Context,
zcashNetwork: ZcashNetwork,
dbPath: String
): CompactBlockDbStore {
val cacheDb = createCompactBlockCacheDb(appContext.applicationContext, dbPath) val cacheDb = createCompactBlockCacheDb(appContext.applicationContext, dbPath)
return CompactBlockDbStore(cacheDb) return CompactBlockDbStore(zcashNetwork, cacheDb)
} }
private fun createCompactBlockCacheDb( private fun createCompactBlockCacheDb(

View File

@@ -4,6 +4,7 @@ import cash.z.ecc.android.sdk.internal.ext.retryUpTo
import cash.z.ecc.android.sdk.internal.ext.tryWarn import cash.z.ecc.android.sdk.internal.ext.tryWarn
import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.wallet.sdk.rpc.Service import cash.z.wallet.sdk.rpc.Service
import io.grpc.StatusRuntimeException import io.grpc.StatusRuntimeException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -43,10 +44,9 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
* *
* @return the number of blocks that were returned in the results from the lightwalletService. * @return the number of blocks that were returned in the results from the lightwalletService.
*/ */
suspend fun downloadBlockRange(heightRange: IntRange): Int = withContext(IO) { suspend fun downloadBlockRange(heightRange: ClosedRange<BlockHeight>): Int = withContext(IO) {
val result = lightWalletService.getBlockRange(heightRange) val result = lightWalletService.getBlockRange(heightRange)
compactBlockStore.write(result) compactBlockStore.write(result)
result.size
} }
/** /**
@@ -55,7 +55,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com
* *
* @param height the height to which the data will rewind. * @param height the height to which the data will rewind.
*/ */
suspend fun rewindToHeight(height: Int) = suspend fun rewindToHeight(height: BlockHeight) =
// TODO: cancel anything in flight // TODO: cancel anything in flight
compactBlockStore.rewindTo(height) compactBlockStore.rewindTo(height)

View File

@@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.internal.block package cash.z.ecc.android.sdk.internal.block
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.wallet.sdk.rpc.CompactFormats import cash.z.wallet.sdk.rpc.CompactFormats
/** /**
@@ -11,28 +12,29 @@ interface CompactBlockStore {
* *
* @return the latest block height. * @return the latest block height.
*/ */
suspend fun getLatestHeight(): Int suspend fun getLatestHeight(): BlockHeight?
/** /**
* Fetch the compact block for the given height, if it exists. * Fetch the compact block for the given height, if it exists.
* *
* @return the compact block or null when it did not exist. * @return the compact block or null when it did not exist.
*/ */
suspend fun findCompactBlock(height: Int): CompactFormats.CompactBlock? suspend fun findCompactBlock(height: BlockHeight): CompactFormats.CompactBlock?
/** /**
* Write the given blocks to this store, which may be anything from an in-memory cache to a DB. * Write the given blocks to this store, which may be anything from an in-memory cache to a DB.
* *
* @param result the list of compact blocks to persist. * @param result the list of compact blocks to persist.
* @return Number of blocks that were written.
*/ */
suspend fun write(result: List<CompactFormats.CompactBlock>) suspend fun write(result: Sequence<CompactFormats.CompactBlock>): Int
/** /**
* Remove every block above the given height. * Remove every block above the given height.
* *
* @param height the target height to which to rewind. * @param height the target height to which to rewind.
*/ */
suspend fun rewindTo(height: Int) suspend fun rewindTo(height: BlockHeight)
/** /**
* Close any connections to the block store. * Close any connections to the block store.

View File

@@ -6,6 +6,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.Transaction
import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity
// //
@@ -42,12 +43,24 @@ interface CompactBlockDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(block: List<CompactBlockEntity>) suspend fun insert(block: List<CompactBlockEntity>)
@Transaction
suspend fun insert(blocks: Sequence<CompactBlockEntity>): Int {
var count = 0
blocks.forEach {
insert(it)
count++
}
return count
}
@Query("DELETE FROM compactblocks WHERE height > :height") @Query("DELETE FROM compactblocks WHERE height > :height")
suspend fun rewindTo(height: Int) suspend fun rewindTo(height: Long)
@Query("SELECT MAX(height) FROM compactblocks") @Query("SELECT MAX(height) FROM compactblocks")
suspend fun latestBlockHeight(): Int suspend fun latestBlockHeight(): Long
@Query("SELECT data FROM compactblocks WHERE height = :height") @Query("SELECT data FROM compactblocks WHERE height = :height")
suspend fun findCompactBlock(height: Int): ByteArray? suspend fun findCompactBlock(height: Long): ByteArray?
} }

View File

@@ -198,13 +198,13 @@ interface BlockDao {
suspend fun count(): Int suspend fun count(): Int
@Query("SELECT MAX(height) FROM blocks") @Query("SELECT MAX(height) FROM blocks")
suspend fun lastScannedHeight(): Int suspend fun lastScannedHeight(): Long
@Query("SELECT MIN(height) FROM blocks") @Query("SELECT MIN(height) FROM blocks")
suspend fun firstScannedHeight(): Int suspend fun firstScannedHeight(): Long
@Query("SELECT hash FROM BLOCKS WHERE height = :height") @Query("SELECT hash FROM BLOCKS WHERE height = :height")
suspend fun findHashByHeight(height: Int): ByteArray? suspend fun findHashByHeight(height: Long): ByteArray?
} }
/** /**
@@ -273,7 +273,7 @@ interface TransactionDao {
LIMIT 1 LIMIT 1
""" """
) )
suspend fun findMinedHeight(rawTransactionId: ByteArray): Int? suspend fun findMinedHeight(rawTransactionId: ByteArray): Long?
/** /**
* Query sent transactions that have been mined, sorted so the newest data is at the top. * Query sent transactions that have been mined, sorted so the newest data is at the top.
@@ -418,7 +418,7 @@ interface TransactionDao {
LIMIT :limit LIMIT :limit
""" """
) )
suspend fun findAllTransactionsByRange(blockRangeStart: Int, blockRangeEnd: Int = blockRangeStart, limit: Int = Int.MAX_VALUE): List<ConfirmedTransaction> suspend fun findAllTransactionsByRange(blockRangeStart: Long, blockRangeEnd: Long = blockRangeStart, limit: Int = Int.MAX_VALUE): List<ConfirmedTransaction>
// Experimental: cleanup cancelled transactions // Experimental: cleanup cancelled transactions
// This should probably be a rust call but there's not a lot of bandwidth for this // This should probably be a rust call but there's not a lot of bandwidth for this
@@ -474,7 +474,7 @@ interface TransactionDao {
} }
@Transaction @Transaction
suspend fun deleteExpired(lastHeight: Int): Int { suspend fun deleteExpired(lastHeight: Long): Int {
var count = 0 var count = 0
findExpiredTxs(lastHeight).forEach { transactionId -> findExpiredTxs(lastHeight).forEach { transactionId ->
if (removeInvalidOutboundTransaction(transactionId)) count++ if (removeInvalidOutboundTransaction(transactionId)) count++
@@ -537,5 +537,5 @@ interface TransactionDao {
AND expiry_height < :lastheight AND expiry_height < :lastheight
""" """
) )
suspend fun findExpiredTxs(lastheight: Int): List<Long> suspend fun findExpiredTxs(lastheight: Long): List<Long>
} }

View File

@@ -70,10 +70,10 @@ interface PendingTransactionDao {
suspend fun removeRawTransactionId(id: Long) suspend fun removeRawTransactionId(id: Long)
@Query("UPDATE pending_transactions SET minedHeight = :minedHeight WHERE id = :id") @Query("UPDATE pending_transactions SET minedHeight = :minedHeight WHERE id = :id")
suspend fun updateMinedHeight(id: Long, minedHeight: Int) suspend fun updateMinedHeight(id: Long, minedHeight: Long)
@Query("UPDATE pending_transactions SET raw = :raw, rawTransactionId = :rawTransactionId, expiryHeight = :expiryHeight WHERE id = :id") @Query("UPDATE pending_transactions SET raw = :raw, rawTransactionId = :rawTransactionId, expiryHeight = :expiryHeight WHERE id = :id")
suspend fun updateEncoding(id: Long, raw: ByteArray, rawTransactionId: ByteArray, expiryHeight: Int?) suspend fun updateEncoding(id: Long, raw: ByteArray, rawTransactionId: ByteArray, expiryHeight: Long?)
@Query("UPDATE pending_transactions SET errorMessage = :errorMessage, errorCode = :errorCode WHERE id = :id") @Query("UPDATE pending_transactions SET errorMessage = :errorMessage, errorCode = :errorCode WHERE id = :id")
suspend fun updateError(id: Long, errorMessage: String?, errorCode: Int?) suspend fun updateError(id: Long, errorMessage: String?, errorCode: Int?)

View File

@@ -0,0 +1,22 @@
package cash.z.ecc.android.sdk.internal.model
import cash.z.ecc.android.sdk.model.BlockHeight
/**
* Represents a checkpoint, which is used to speed sync times.
*
* @param height the height of the checkpoint.
* @param hash the hash of the block at [height].
* @param epochSeconds the time of the block at [height].
* @param tree the sapling tree corresponding to [height].
*/
internal data class Checkpoint(
val height: BlockHeight,
val hash: String,
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
val epochSeconds: Long,
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
val tree: String
) {
internal companion object
}

View File

@@ -1,11 +1,10 @@
package cash.z.ecc.android.sdk.internal.service package cash.z.ecc.android.sdk.internal.service
import android.content.Context import android.content.Context
import cash.z.ecc.android.sdk.R
import cash.z.ecc.android.sdk.annotation.OpenForTesting import cash.z.ecc.android.sdk.annotation.OpenForTesting
import cash.z.ecc.android.sdk.exception.LightWalletException
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.wallet.sdk.rpc.CompactFormats import cash.z.wallet.sdk.rpc.CompactFormats
import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc import cash.z.wallet.sdk.rpc.CompactTxStreamerGrpc
import cash.z.wallet.sdk.rpc.Service import cash.z.wallet.sdk.rpc.Service
@@ -15,69 +14,50 @@ import io.grpc.ConnectivityState
import io.grpc.ManagedChannel import io.grpc.ManagedChannel
import io.grpc.android.AndroidChannelBuilder import io.grpc.android.AndroidChannelBuilder
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/** /**
* Implementation of LightwalletService using gRPC for requests to lightwalletd. * Implementation of LightwalletService using gRPC for requests to lightwalletd.
* *
* @property channel the channel to use for communicating with the lightwalletd server. * @property channel the channel to use for communicating with the lightwalletd server.
* @property singleRequestTimeoutSec the timeout to use for non-streaming requests. When a new stub * @property singleRequestTimeout the timeout to use for non-streaming requests. When a new stub
* is created, it will use a deadline that is after the given duration from now. * is created, it will use a deadline that is after the given duration from now.
* @property streamingRequestTimeoutSec the timeout to use for streaming requests. When a new stub * @property streamingRequestTimeout the timeout to use for streaming requests. When a new stub
* is created for streaming requests, it will use a deadline that is after the given duration from * is created for streaming requests, it will use a deadline that is after the given duration from
* now. * now.
*/ */
@OpenForTesting @OpenForTesting
class LightWalletGrpcService private constructor( class LightWalletGrpcService private constructor(
context: Context,
private val lightWalletEndpoint: LightWalletEndpoint,
var channel: ManagedChannel, var channel: ManagedChannel,
private val singleRequestTimeoutSec: Long = 10L, private val singleRequestTimeout: Duration = 10.seconds,
private val streamingRequestTimeoutSec: Long = 90L private val streamingRequestTimeout: Duration = 90.seconds
) : LightWalletService { ) : LightWalletService {
lateinit var connectionInfo: ConnectionInfo private val applicationContext = context.applicationContext
constructor(
appContext: Context,
network: ZcashNetwork,
usePlaintext: Boolean =
appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)
) : this(appContext, network.defaultHost, network.defaultPort, true)
/**
* Construct an instance that corresponds to the given host and port.
*
* @param appContext the application context used to check whether TLS is required by this build
* flavor.
* @param host the host of the server to use.
* @param port the port of the server to use.
* @param usePlaintext whether to use TLS or plaintext for requests. Plaintext is dangerous so
* it requires jumping through a few more hoops.
*/
constructor(
appContext: Context,
host: String,
port: Int = ZcashNetwork.Mainnet.defaultPort,
usePlaintext: Boolean =
appContext.resources.getBoolean(R.bool.lightwalletd_allow_very_insecure_connections)
) : this(createDefaultChannel(appContext, host, port, true)) {
connectionInfo = ConnectionInfo(appContext.applicationContext, host, port, true)
}
/* LightWalletService implementation */ /* LightWalletService implementation */
override fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock> { override fun getBlockRange(heightRange: ClosedRange<BlockHeight>): Sequence<CompactFormats.CompactBlock> {
if (heightRange.isEmpty()) return listOf() if (heightRange.isEmpty()) {
return emptySequence()
}
return requireChannel().createStub(streamingRequestTimeoutSec) return requireChannel().createStub(streamingRequestTimeout)
.getBlockRange(heightRange.toBlockRange()).toList() .getBlockRange(heightRange.toBlockRange()).iterator().asSequence()
} }
override fun getLatestBlockHeight(): Int { override fun getLatestBlockHeight(): BlockHeight {
return requireChannel().createStub(singleRequestTimeoutSec) return BlockHeight(
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height.toInt() requireChannel().createStub(singleRequestTimeout)
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height
)
} }
override fun getServerInfo(): Service.LightdInfo { override fun getServerInfo(): Service.LightdInfo {
return requireChannel().createStub(singleRequestTimeoutSec) return requireChannel().createStub(singleRequestTimeout)
.getLightdInfo(Service.Empty.newBuilder().build()) .getLightdInfo(Service.Empty.newBuilder().build())
} }
@@ -109,23 +89,20 @@ class LightWalletGrpcService private constructor(
) )
} }
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override fun fetchUtxos( override fun fetchUtxos(
tAddress: String, tAddress: String,
startHeight: Int startHeight: BlockHeight
): List<Service.GetAddressUtxosReply> { ): List<Service.GetAddressUtxosReply> {
val result = requireChannel().createStub().getAddressUtxos( val result = requireChannel().createStub().getAddressUtxos(
Service.GetAddressUtxosArg.newBuilder().setAddress(tAddress) Service.GetAddressUtxosArg.newBuilder().setAddress(tAddress)
.setStartHeight(startHeight.toLong()).build() .setStartHeight(startHeight.value).build()
) )
return result.addressUtxosList return result.addressUtxosList
} }
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override fun getTAddressTransactions( override fun getTAddressTransactions(
tAddress: String, tAddress: String,
blockHeightRange: IntRange blockHeightRange: ClosedRange<BlockHeight>
): List<Service.RawTransaction> { ): List<Service.RawTransaction> {
if (blockHeightRange.isEmpty() || tAddress.isBlank()) return listOf() if (blockHeightRange.isEmpty() || tAddress.isBlank()) return listOf()
@@ -135,75 +112,38 @@ class LightWalletGrpcService private constructor(
) )
return result.toList() return result.toList()
} }
*/
override fun reconnect() { override fun reconnect() {
twig( twig("closing existing channel and then reconnecting")
"closing existing channel and then reconnecting to ${connectionInfo.host}:" +
"${connectionInfo.port}?usePlaintext=${connectionInfo.usePlaintext}"
)
channel.shutdown() channel.shutdown()
channel = createDefaultChannel( channel = createDefaultChannel(applicationContext, lightWalletEndpoint)
connectionInfo.appContext,
connectionInfo.host,
connectionInfo.port,
true
)
} }
// test code // test code
var stateCount = 0 internal var stateCount = 0
var state: ConnectivityState? = null internal var state: ConnectivityState? = null
private fun requireChannel(): ManagedChannel { private fun requireChannel(): ManagedChannel {
state = channel.getState(false).let { new -> state = channel.getState(false).let { new ->
if (state == new) stateCount++ else stateCount = 0 if (state == new) stateCount++ else stateCount = 0
new new
} }
channel.resetConnectBackoff() channel.resetConnectBackoff()
twig("getting channel isShutdown: ${channel.isShutdown} isTerminated: ${channel.isTerminated} getState: $state stateCount: $stateCount", -1) twig(
"getting channel isShutdown: ${channel.isShutdown} " +
"isTerminated: ${channel.isTerminated} " +
"getState: $state stateCount: $stateCount",
-1
)
return channel return channel
} }
//
// Utilities
//
private fun Channel.createStub(timeoutSec: Long = 60L) = CompactTxStreamerGrpc
.newBlockingStub(this)
.withDeadlineAfter(timeoutSec, TimeUnit.SECONDS)
private inline fun Int.toBlockHeight(): Service.BlockID =
Service.BlockID.newBuilder().setHeight(this.toLong()).build()
private inline fun IntRange.toBlockRange(): Service.BlockRange =
Service.BlockRange.newBuilder()
.setStart(first.toBlockHeight())
.setEnd(last.toBlockHeight())
.build()
/**
* This function effectively parses streaming responses. Each call to next(), on the iterators
* returned from grpc, triggers a network call.
*/
private fun <T> Iterator<T>.toList(): List<T> =
mutableListOf<T>().apply {
while (hasNext()) {
this@apply += next()
}
}
inner class ConnectionInfo(
val appContext: Context,
val host: String,
val port: Int,
val usePlaintext: Boolean
) {
override fun toString(): String {
return "$host:$port?usePlaintext=true"
}
}
companion object { companion object {
fun new(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletGrpcService {
val channel = createDefaultChannel(context, lightWalletEndpoint)
return LightWalletGrpcService(context, lightWalletEndpoint, channel)
}
/** /**
* Convenience function for creating the default channel to be used for all connections. It * Convenience function for creating the default channel to be used for all connections. It
* is important that this channel can handle transitioning from WiFi to Cellular connections * is important that this channel can handle transitioning from WiFi to Cellular connections
@@ -211,27 +151,53 @@ class LightWalletGrpcService private constructor(
*/ */
fun createDefaultChannel( fun createDefaultChannel(
appContext: Context, appContext: Context,
host: String, lightWalletEndpoint: LightWalletEndpoint
port: Int,
usePlaintext: Boolean
): ManagedChannel { ): ManagedChannel {
twig("Creating channel that will connect to $host:$port?usePlaintext=$usePlaintext") twig(
"Creating channel that will connect to " +
"${lightWalletEndpoint.host}:${lightWalletEndpoint.port}" +
"/?usePlaintext=${!lightWalletEndpoint.isSecure}"
)
return AndroidChannelBuilder return AndroidChannelBuilder
.forAddress(host, port) .forAddress(lightWalletEndpoint.host, lightWalletEndpoint.port)
.context(appContext) .context(appContext)
.enableFullStreamDecompression() .enableFullStreamDecompression()
.apply { .apply {
if (usePlaintext) { usePlaintext()
if (!appContext.resources.getBoolean( /*
R.bool.lightwalletd_allow_very_insecure_connections if (lightWalletEndpoint.isSecure) {
)
) throw LightWalletException.InsecureConnection
usePlaintext()
} else {
useTransportSecurity() useTransportSecurity()
} else {
twig("WARNING: Using insecure channel")
usePlaintext()
} }
*/
} }
.build() .build()
} }
} }
} }
private fun Channel.createStub(timeoutSec: Duration = 60.seconds) = CompactTxStreamerGrpc
.newBlockingStub(this)
.withDeadlineAfter(timeoutSec.inWholeSeconds, TimeUnit.SECONDS)
private fun BlockHeight.toBlockHeight(): Service.BlockID =
Service.BlockID.newBuilder().setHeight(value).build()
private fun ClosedRange<BlockHeight>.toBlockRange(): Service.BlockRange =
Service.BlockRange.newBuilder()
.setStart(start.toBlockHeight())
.setEnd(endInclusive.toBlockHeight())
.build()
/**
* This function effectively parses streaming responses. Each call to next(), on the iterators
* returned from grpc, triggers a network call.
*/
private fun <T> Iterator<T>.toList(): List<T> =
mutableListOf<T>().apply {
while (hasNext()) {
this@apply += next()
}
}

View File

@@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.internal.service package cash.z.ecc.android.sdk.internal.service
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.wallet.sdk.rpc.CompactFormats import cash.z.wallet.sdk.rpc.CompactFormats
import cash.z.wallet.sdk.rpc.Service import cash.z.wallet.sdk.rpc.Service
@@ -24,9 +25,7 @@ interface LightWalletService {
* *
* @return the UTXOs for the given address from the startHeight. * @return the UTXOs for the given address from the startHeight.
*/ */
/* THIS IS NOT SUPPORT IN HUSH LIGHTWALLETD fun fetchUtxos(tAddress: String, startHeight: BlockHeight): List<Service.GetAddressUtxosReply>
fun fetchUtxos(tAddress: String, startHeight: Int): List<Service.GetAddressUtxosReply>
*/
/** /**
* Return the given range of blocks. * Return the given range of blocks.
@@ -37,14 +36,14 @@ interface LightWalletService {
* @return a list of compact blocks for the given range * @return a list of compact blocks for the given range
* *
*/ */
fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock> fun getBlockRange(heightRange: ClosedRange<BlockHeight>): Sequence<CompactFormats.CompactBlock>
/** /**
* Return the latest block height known to the service. * Return the latest block height known to the service.
* *
* @return the latest block height known to the service. * @return the latest block height known to the service.
*/ */
fun getLatestBlockHeight(): Int fun getLatestBlockHeight(): BlockHeight
/** /**
* Return basic information about the server such as: * Return basic information about the server such as:
@@ -72,9 +71,7 @@ interface LightWalletService {
* *
* @return a list of transactions that correspond to the given address for the given range. * @return a list of transactions that correspond to the given address for the given range.
*/ */
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD fun getTAddressTransactions(tAddress: String, blockHeightRange: ClosedRange<BlockHeight>): List<Service.RawTransaction>
fun getTAddressTransactions(tAddress: String, blockHeightRange: IntRange): List<Service.RawTransaction>
*/
/** /**
* Reconnect to the same or a different server. This is useful when the connection is * Reconnect to the same or a different server. This is useful when the connection is

View File

@@ -12,12 +12,13 @@ import cash.z.ecc.android.sdk.internal.db.DerivedDataDb
import cash.z.ecc.android.sdk.internal.ext.android.toFlowPagedList import cash.z.ecc.android.sdk.internal.ext.android.toFlowPagedList
import cash.z.ecc.android.sdk.internal.ext.android.toRefreshable import cash.z.ecc.android.sdk.internal.ext.android.toRefreshable
import cash.z.ecc.android.sdk.internal.ext.tryWarn import cash.z.ecc.android.sdk.internal.ext.tryWarn
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.UnifiedViewingKey import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.WalletBirthday
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -28,7 +29,8 @@ import kotlinx.coroutines.withContext
* *
* @param pageSize transactions per page. This influences pre-fetch and memory configuration. * @param pageSize transactions per page. This influences pre-fetch and memory configuration.
*/ */
class PagedTransactionRepository private constructor( internal class PagedTransactionRepository private constructor(
private val zcashNetwork: ZcashNetwork,
private val db: DerivedDataDb, private val db: DerivedDataDb,
private val pageSize: Int private val pageSize: Int
) : TransactionRepository { ) : TransactionRepository {
@@ -62,20 +64,20 @@ class PagedTransactionRepository private constructor(
override fun invalidate() = allTransactionsFactory.refresh() override fun invalidate() = allTransactionsFactory.refresh()
override suspend fun lastScannedHeight() = blocks.lastScannedHeight() override suspend fun lastScannedHeight() = BlockHeight.new(zcashNetwork, blocks.lastScannedHeight())
override suspend fun firstScannedHeight() = blocks.firstScannedHeight() override suspend fun firstScannedHeight() = BlockHeight.new(zcashNetwork, blocks.firstScannedHeight())
override suspend fun isInitialized() = blocks.count() > 0 override suspend fun isInitialized() = blocks.count() > 0
override suspend fun findEncodedTransactionById(txId: Long) = override suspend fun findEncodedTransactionById(txId: Long) =
transactions.findEncodedTransactionById(txId) transactions.findEncodedTransactionById(txId)
override suspend fun findNewTransactions(blockHeightRange: IntRange): List<ConfirmedTransaction> = override suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<ConfirmedTransaction> =
transactions.findAllTransactionsByRange(blockHeightRange.first, blockHeightRange.last) transactions.findAllTransactionsByRange(blockHeightRange.start.value, blockHeightRange.endInclusive.value)
override suspend fun findMinedHeight(rawTransactionId: ByteArray) = override suspend fun findMinedHeight(rawTransactionId: ByteArray) =
transactions.findMinedHeight(rawTransactionId) transactions.findMinedHeight(rawTransactionId)?.let { BlockHeight.new(zcashNetwork, it) }
override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long? = override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long? =
transactions.findMatchingTransactionId(rawTransactionId) transactions.findMatchingTransactionId(rawTransactionId)
@@ -84,8 +86,8 @@ class PagedTransactionRepository private constructor(
transactions.cleanupCancelledTx(rawTransactionId) transactions.cleanupCancelledTx(rawTransactionId)
// let expired transactions linger in the UI for a little while // let expired transactions linger in the UI for a little while
override suspend fun deleteExpired(lastScannedHeight: Int) = override suspend fun deleteExpired(lastScannedHeight: BlockHeight) =
transactions.deleteExpired(lastScannedHeight - (ZcashSdk.EXPIRY_OFFSET / 2)) transactions.deleteExpired(lastScannedHeight.value - (ZcashSdk.EXPIRY_OFFSET / 2))
override suspend fun count() = transactions.count() override suspend fun count() = transactions.count()
@@ -103,17 +105,18 @@ class PagedTransactionRepository private constructor(
} }
// TODO: begin converting these into Data Access API. For now, just collect the desired operations and iterate/refactor, later // TODO: begin converting these into Data Access API. For now, just collect the desired operations and iterate/refactor, later
suspend fun findBlockHash(height: Int): ByteArray? = blocks.findHashByHeight(height) suspend fun findBlockHash(height: BlockHeight): ByteArray? = blocks.findHashByHeight(height.value)
suspend fun getTransactionCount(): Int = transactions.count() suspend fun getTransactionCount(): Int = transactions.count()
// TODO: convert this into a wallet repository rather than "transaction repository" // TODO: convert this into a wallet repository rather than "transaction repository"
companion object { companion object {
suspend fun new( internal suspend fun new(
appContext: Context, appContext: Context,
zcashNetwork: ZcashNetwork,
pageSize: Int = 10, pageSize: Int = 10,
rustBackend: RustBackend, rustBackend: RustBackend,
birthday: WalletBirthday, birthday: Checkpoint,
viewingKeys: List<UnifiedViewingKey>, viewingKeys: List<UnifiedViewingKey>,
overwriteVks: Boolean = false overwriteVks: Boolean = false
): PagedTransactionRepository { ): PagedTransactionRepository {
@@ -122,7 +125,7 @@ class PagedTransactionRepository private constructor(
val db = buildDatabase(appContext.applicationContext, rustBackend.pathDataDb) val db = buildDatabase(appContext.applicationContext, rustBackend.pathDataDb)
applyKeyMigrations(rustBackend, overwriteVks, viewingKeys) applyKeyMigrations(rustBackend, overwriteVks, viewingKeys)
return PagedTransactionRepository(db, pageSize) return PagedTransactionRepository(zcashNetwork, db, pageSize)
} }
/** /**
@@ -155,7 +158,7 @@ class PagedTransactionRepository private constructor(
*/ */
private suspend fun initMissingDatabases( private suspend fun initMissingDatabases(
rustBackend: RustBackend, rustBackend: RustBackend,
birthday: WalletBirthday, birthday: Checkpoint,
viewingKeys: List<UnifiedViewingKey> viewingKeys: List<UnifiedViewingKey>
) { ) {
maybeCreateDataDb(rustBackend) maybeCreateDataDb(rustBackend)
@@ -178,20 +181,15 @@ class PagedTransactionRepository private constructor(
*/ */
private suspend fun maybeInitBlocksTable( private suspend fun maybeInitBlocksTable(
rustBackend: RustBackend, rustBackend: RustBackend,
birthday: WalletBirthday checkpoint: Checkpoint
) { ) {
// TODO: consider converting these to typed exceptions in the welding layer // TODO: consider converting these to typed exceptions in the welding layer
tryWarn( tryWarn(
"Warning: did not initialize the blocks table. It probably was already initialized.", "Warning: did not initialize the blocks table. It probably was already initialized.",
ifContains = "table is not empty" ifContains = "table is not empty"
) { ) {
rustBackend.initBlocksTable( rustBackend.initBlocksTable(checkpoint)
birthday.height, twig("seeded the database with sapling tree at height ${checkpoint.height}")
birthday.hash,
birthday.time,
birthday.tree
)
twig("seeded the database with sapling tree at height ${birthday.height}")
} }
twig("database file: ${rustBackend.pathDataDb}") twig("database file: ${rustBackend.pathDataDb}")
} }

View File

@@ -12,6 +12,7 @@ import cash.z.ecc.android.sdk.internal.db.PendingTransactionDao
import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb
import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@@ -98,10 +99,10 @@ class PersistentTransactionManager(
tx tx
} }
override suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: Int) { override suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: BlockHeight) {
twig("a pending transaction has been mined!") twig("a pending transaction has been mined!")
safeUpdate("updating mined height for pending tx id: ${pendingTx.id} to $minedHeight") { safeUpdate("updating mined height for pending tx id: ${pendingTx.id} to $minedHeight") {
updateMinedHeight(pendingTx.id, minedHeight) updateMinedHeight(pendingTx.id, minedHeight.value)
} }
} }
@@ -115,7 +116,7 @@ class PersistentTransactionManager(
twig("beginning to encode transaction with : $encoder") twig("beginning to encode transaction with : $encoder")
val encodedTx = encoder.createTransaction( val encodedTx = encoder.createTransaction(
spendingKey, spendingKey,
tx.value, tx.valueZatoshi,
tx.toAddress, tx.toAddress,
tx.memo, tx.memo,
tx.accountIndex tx.accountIndex
@@ -230,10 +231,8 @@ class PersistentTransactionManager(
override suspend fun isValidShieldedAddress(address: String) = override suspend fun isValidShieldedAddress(address: String) =
encoder.isValidShieldedAddress(address) encoder.isValidShieldedAddress(address)
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override suspend fun isValidTransparentAddress(address: String) = override suspend fun isValidTransparentAddress(address: String) =
encoder.isValidTransparentAddress(address) encoder.isValidTransparentAddress(address)
*/
override suspend fun cancel(pendingId: Long): Boolean { override suspend fun cancel(pendingId: Long): Boolean {
return pendingTransactionDao { return pendingTransactionDao {

View File

@@ -1,6 +1,7 @@
package cash.z.ecc.android.sdk.internal.transaction package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
import cash.z.ecc.android.sdk.model.Zatoshi
interface TransactionEncoder { interface TransactionEncoder {
/** /**
@@ -9,7 +10,7 @@ interface TransactionEncoder {
* exception ourselves (rather than using double-bangs for things). * exception ourselves (rather than using double-bangs for things).
* *
* @param spendingKey the key associated with the notes that will be spent. * @param spendingKey the key associated with the notes that will be spent.
* @param zatoshi the amount of zatoshi to send. * @param amount the amount of zatoshi to send.
* @param toAddress the recipient's address. * @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction. * @param memo the optional memo to include as part of the transaction.
* @param fromAccountIndex the optional account id to use. By default, the 1st account is used. * @param fromAccountIndex the optional account id to use. By default, the 1st account is used.
@@ -18,7 +19,7 @@ interface TransactionEncoder {
*/ */
suspend fun createTransaction( suspend fun createTransaction(
spendingKey: String, spendingKey: String,
zatoshi: Long, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: ByteArray? = byteArrayOf(), memo: ByteArray? = byteArrayOf(),
fromAccountIndex: Int = 0 fromAccountIndex: Int = 0
@@ -48,9 +49,7 @@ interface TransactionEncoder {
* *
* @return true when the given address is a valid t-addr * @return true when the given address is a valid t-addr
*/ */
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun isValidTransparentAddress(address: String): Boolean suspend fun isValidTransparentAddress(address: String): Boolean
*/
/** /**
* Return the consensus branch that the encoder is using when making transactions. * Return the consensus branch that the encoder is using when making transactions.

View File

@@ -1,6 +1,7 @@
package cash.z.ecc.android.sdk.internal.transaction package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -65,7 +66,7 @@ interface OutboundTransactionManager {
* @param minedHeight the height at which the given transaction was mined, according to the data * @param minedHeight the height at which the given transaction was mined, according to the data
* that has been processed from the blockchain. * that has been processed from the blockchain.
*/ */
suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: Int) suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: BlockHeight)
/** /**
* Generate a flow of information about the given id where a new pending transaction is emitted * Generate a flow of information about the given id where a new pending transaction is emitted
@@ -94,9 +95,7 @@ interface OutboundTransactionManager {
* *
* @return true when the given address is a valid z-addr. * @return true when the given address is a valid z-addr.
*/ */
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun isValidTransparentAddress(address: String): Boolean suspend fun isValidTransparentAddress(address: String): Boolean
*/
/** /**
* Attempt to cancel a transaction. * Attempt to cancel a transaction.

View File

@@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.UnifiedAddressAccount import cash.z.ecc.android.sdk.type.UnifiedAddressAccount
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -15,14 +16,14 @@ interface TransactionRepository {
* *
* @return the last height scanned by this repository. * @return the last height scanned by this repository.
*/ */
suspend fun lastScannedHeight(): Int suspend fun lastScannedHeight(): BlockHeight
/** /**
* The height of the first block in this repository. This is typically the checkpoint that was * The height of the first block in this repository. This is typically the checkpoint that was
* used to initialize this wallet. If we overwrite this block, it breaks our ability to spend * used to initialize this wallet. If we overwrite this block, it breaks our ability to spend
* funds. * funds.
*/ */
suspend fun firstScannedHeight(): Int suspend fun firstScannedHeight(): BlockHeight
/** /**
* Returns true when this repository has been initialized and seeded with the initial checkpoint. * Returns true when this repository has been initialized and seeded with the initial checkpoint.
@@ -51,7 +52,7 @@ interface TransactionRepository {
* *
* @return a list of transactions that were mined in the given range, inclusive. * @return a list of transactions that were mined in the given range, inclusive.
*/ */
suspend fun findNewTransactions(blockHeightRange: IntRange): List<ConfirmedTransaction> suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<ConfirmedTransaction>
/** /**
* Find the mined height that matches the given raw tx_id in bytes. This is useful for matching * Find the mined height that matches the given raw tx_id in bytes. This is useful for matching
@@ -61,7 +62,7 @@ interface TransactionRepository {
* *
* @return the mined height of the given transaction, if it is known to this wallet. * @return the mined height of the given transaction, if it is known to this wallet.
*/ */
suspend fun findMinedHeight(rawTransactionId: ByteArray): Int? suspend fun findMinedHeight(rawTransactionId: ByteArray): BlockHeight?
suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long? suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long?
@@ -79,7 +80,7 @@ interface TransactionRepository {
*/ */
suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean
suspend fun deleteExpired(lastScannedHeight: Int): Int suspend fun deleteExpired(lastScannedHeight: BlockHeight): Int
suspend fun count(): Int suspend fun count(): Int

View File

@@ -8,6 +8,7 @@ import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.model.Zatoshi
/** /**
* Class responsible for encoding a transaction in a consistent way. This bridges the gap by * Class responsible for encoding a transaction in a consistent way. This bridges the gap by
@@ -18,7 +19,7 @@ import cash.z.ecc.android.sdk.jni.RustBackendWelding
* @property repository the repository that stores information about the transactions being created * @property repository the repository that stores information about the transactions being created
* such as the raw bytes and raw txId. * such as the raw bytes and raw txId.
*/ */
class WalletTransactionEncoder( internal class WalletTransactionEncoder(
private val rustBackend: RustBackendWelding, private val rustBackend: RustBackendWelding,
private val repository: TransactionRepository private val repository: TransactionRepository
) : TransactionEncoder { ) : TransactionEncoder {
@@ -29,7 +30,7 @@ class WalletTransactionEncoder(
* exception ourselves (rather than using double-bangs for things). * exception ourselves (rather than using double-bangs for things).
* *
* @param spendingKey the key associated with the notes that will be spent. * @param spendingKey the key associated with the notes that will be spent.
* @param zatoshi the amount of zatoshi to send. * @param amount the amount of zatoshi to send.
* @param toAddress the recipient's address. * @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction. * @param memo the optional memo to include as part of the transaction.
* @param fromAccountIndex the optional account id to use. By default, the 1st account is used. * @param fromAccountIndex the optional account id to use. By default, the 1st account is used.
@@ -38,12 +39,12 @@ class WalletTransactionEncoder(
*/ */
override suspend fun createTransaction( override suspend fun createTransaction(
spendingKey: String, spendingKey: String,
zatoshi: Long, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: ByteArray?, memo: ByteArray?,
fromAccountIndex: Int fromAccountIndex: Int
): EncodedTransaction { ): EncodedTransaction {
val transactionId = createSpend(spendingKey, zatoshi, toAddress, memo) val transactionId = createSpend(spendingKey, amount, toAddress, memo)
return repository.findEncodedTransactionById(transactionId) return repository.findEncodedTransactionById(transactionId)
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId) ?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
} }
@@ -77,10 +78,8 @@ class WalletTransactionEncoder(
* *
* @return true when the given address is a valid t-addr * @return true when the given address is a valid t-addr
*/ */
/* THIS IS NOT SUPPORTED BY HUSH LIGHTWALLETD
override suspend fun isValidTransparentAddress(address: String): Boolean = override suspend fun isValidTransparentAddress(address: String): Boolean =
rustBackend.isValidTransparentAddr(address) rustBackend.isValidTransparentAddr(address)
*/
override suspend fun getConsensusBranchId(): Long { override suspend fun getConsensusBranchId(): Long {
val height = repository.lastScannedHeight() val height = repository.lastScannedHeight()
@@ -95,7 +94,7 @@ class WalletTransactionEncoder(
* the result in the database. On average, this call takes over 10 seconds. * the result in the database. On average, this call takes over 10 seconds.
* *
* @param spendingKey the key associated with the notes that will be spent. * @param spendingKey the key associated with the notes that will be spent.
* @param zatoshi the amount of zatoshi to send. * @param amount the amount of zatoshi to send.
* @param toAddress the recipient's address. * @param toAddress the recipient's address.
* @param memo the optional memo to include as part of the transaction. * @param memo the optional memo to include as part of the transaction.
* @param fromAccountIndex the optional account id to use. By default, the 1st account is used. * @param fromAccountIndex the optional account id to use. By default, the 1st account is used.
@@ -105,13 +104,13 @@ class WalletTransactionEncoder(
*/ */
private suspend fun createSpend( private suspend fun createSpend(
spendingKey: String, spendingKey: String,
zatoshi: Long, amount: Zatoshi,
toAddress: String, toAddress: String,
memo: ByteArray? = byteArrayOf(), memo: ByteArray? = byteArrayOf(),
fromAccountIndex: Int = 0 fromAccountIndex: Int = 0
): Long { ): Long {
return twigTask( return twigTask(
"creating transaction to spend $zatoshi zatoshi to" + "creating transaction to spend $amount zatoshi to" +
" ${toAddress.masked()} with memo $memo" " ${toAddress.masked()} with memo $memo"
) { ) {
try { try {
@@ -123,7 +122,7 @@ class WalletTransactionEncoder(
fromAccountIndex, fromAccountIndex,
spendingKey, spendingKey,
toAddress, toAddress,
zatoshi, amount.value,
memo memo
) )
} catch (t: Throwable) { } catch (t: Throwable) {

View File

@@ -1,16 +1,17 @@
package cash.z.ecc.android.sdk.jni package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.exception.BirthdayException
import cash.z.ecc.android.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME import cash.z.ecc.android.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME import cash.z.ecc.android.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.internal.SdkDispatchers import cash.z.ecc.android.sdk.internal.SdkDispatchers
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@@ -19,21 +20,13 @@ import java.io.File
* not be called directly by code outside of the SDK. Instead, one of the higher-level components * not be called directly by code outside of the SDK. Instead, one of the higher-level components
* should be used such as Wallet.kt or CompactBlockProcessor.kt. * should be used such as Wallet.kt or CompactBlockProcessor.kt.
*/ */
class RustBackend private constructor() : RustBackendWelding { internal class RustBackend private constructor(
override val network: ZcashNetwork,
// Paths val birthdayHeight: BlockHeight,
lateinit var pathDataDb: String val pathDataDb: String,
internal set val pathCacheDb: String,
lateinit var pathCacheDb: String val pathParamsDir: String
internal set ) : RustBackendWelding {
lateinit var pathParamsDir: String
internal set
override lateinit var network: ZcashNetwork
internal var birthdayHeight: Int = -1
get() = if (field != -1) field else throw BirthdayException.UninitializedBirthdayException
private set
suspend fun clear(clearCacheDb: Boolean = true, clearDataDb: Boolean = true) { suspend fun clear(clearCacheDb: Boolean = true, clearDataDb: Boolean = true) {
if (clearCacheDb) { if (clearCacheDb) {
@@ -84,18 +77,15 @@ class RustBackend private constructor() : RustBackendWelding {
} }
override suspend fun initBlocksTable( override suspend fun initBlocksTable(
height: Int, checkpoint: Checkpoint
hash: String,
time: Long,
saplingTree: String
): Boolean { ): Boolean {
return withContext(SdkDispatchers.DATABASE_IO) { return withContext(SdkDispatchers.DATABASE_IO) {
initBlocksTable( initBlocksTable(
pathDataDb, pathDataDb,
height, checkpoint.height.value,
hash, checkpoint.hash,
time, checkpoint.epochSeconds,
saplingTree, checkpoint.tree,
networkId = network.id networkId = network.id
) )
} }
@@ -110,11 +100,9 @@ class RustBackend private constructor() : RustBackendWelding {
) )
} }
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override suspend fun getTransparentAddress(account: Int, index: Int): String { override suspend fun getTransparentAddress(account: Int, index: Int): String {
throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds") throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds")
} }
*/
override suspend fun getBalance(account: Int): Zatoshi { override suspend fun getBalance(account: Int): Zatoshi {
val longValue = withContext(SdkDispatchers.DATABASE_IO) { val longValue = withContext(SdkDispatchers.DATABASE_IO) {
@@ -158,19 +146,28 @@ class RustBackend private constructor() : RustBackendWelding {
} }
override suspend fun validateCombinedChain() = withContext(SdkDispatchers.DATABASE_IO) { override suspend fun validateCombinedChain() = withContext(SdkDispatchers.DATABASE_IO) {
validateCombinedChain( val validationResult = validateCombinedChain(
pathCacheDb, pathCacheDb,
pathDataDb, pathDataDb,
networkId = network.id networkId = network.id
) )
if (-1L == validationResult) {
null
} else {
BlockHeight.new(network, validationResult)
}
} }
override suspend fun getNearestRewindHeight(height: Int): Int = override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
withContext(SdkDispatchers.DATABASE_IO) { withContext(SdkDispatchers.DATABASE_IO) {
getNearestRewindHeight( BlockHeight.new(
pathDataDb, network,
height, getNearestRewindHeight(
networkId = network.id pathDataDb,
height.value,
networkId = network.id
)
) )
} }
@@ -179,11 +176,11 @@ class RustBackend private constructor() : RustBackendWelding {
* *
* DELETE FROM blocks WHERE height > ? * DELETE FROM blocks WHERE height > ?
*/ */
override suspend fun rewindToHeight(height: Int) = override suspend fun rewindToHeight(height: BlockHeight) =
withContext(SdkDispatchers.DATABASE_IO) { withContext(SdkDispatchers.DATABASE_IO) {
rewindToHeight( rewindToHeight(
pathDataDb, pathDataDb,
height, height.value,
networkId = network.id networkId = network.id
) )
} }
@@ -266,7 +263,7 @@ class RustBackend private constructor() : RustBackendWelding {
index: Int, index: Int,
script: ByteArray, script: ByteArray,
value: Long, value: Long,
height: Int height: BlockHeight
): Boolean = withContext(SdkDispatchers.DATABASE_IO) { ): Boolean = withContext(SdkDispatchers.DATABASE_IO) {
putUtxo( putUtxo(
pathDataDb, pathDataDb,
@@ -275,19 +272,21 @@ class RustBackend private constructor() : RustBackendWelding {
index, index,
script, script,
value, value,
height, height.value,
networkId = network.id networkId = network.id
) )
} }
override suspend fun clearUtxos( override suspend fun clearUtxos(
tAddress: String, tAddress: String,
aboveHeight: Int aboveHeightInclusive: BlockHeight
): Boolean = withContext(SdkDispatchers.DATABASE_IO) { ): Boolean = withContext(SdkDispatchers.DATABASE_IO) {
clearUtxos( clearUtxos(
pathDataDb, pathDataDb,
tAddress, tAddress,
aboveHeight, // The Kotlin API is inclusive, but the Rust API is exclusive.
// This can create invalid BlockHeights if the height is saplingActivationHeight.
aboveHeightInclusive.value - 1,
networkId = network.id networkId = network.id
) )
} }
@@ -313,13 +312,11 @@ class RustBackend private constructor() : RustBackendWelding {
override fun isValidShieldedAddr(addr: String) = override fun isValidShieldedAddr(addr: String) =
isValidShieldedAddress(addr, networkId = network.id) isValidShieldedAddress(addr, networkId = network.id)
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override fun isValidTransparentAddr(addr: String) = override fun isValidTransparentAddr(addr: String) =
isValidTransparentAddress(addr, networkId = network.id) isValidTransparentAddress(addr, networkId = network.id)
*/
override fun getBranchIdForHeight(height: Int): Long = override fun getBranchIdForHeight(height: BlockHeight): Long =
branchIdForHeight(height, networkId = network.id) branchIdForHeight(height.value, networkId = network.id)
// /** // /**
// * This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI // * This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI
@@ -355,19 +352,17 @@ class RustBackend private constructor() : RustBackendWelding {
dataDbPath: String, dataDbPath: String,
paramsPath: String, paramsPath: String,
zcashNetwork: ZcashNetwork, zcashNetwork: ZcashNetwork,
birthdayHeight: Int? = null birthdayHeight: BlockHeight
): RustBackend { ): RustBackend {
rustLibraryLoader.load() rustLibraryLoader.load()
return RustBackend().apply { return RustBackend(
pathCacheDb = cacheDbPath zcashNetwork,
pathDataDb = dataDbPath birthdayHeight,
pathDataDb = dataDbPath,
pathCacheDb = cacheDbPath,
pathParamsDir = paramsPath pathParamsDir = paramsPath
network = zcashNetwork )
if (birthdayHeight != null) {
this.birthdayHeight = birthdayHeight
}
}
} }
/** /**
@@ -400,7 +395,7 @@ class RustBackend private constructor() : RustBackendWelding {
@JvmStatic @JvmStatic
private external fun initBlocksTable( private external fun initBlocksTable(
dbDataPath: String, dbDataPath: String,
height: Int, height: Long,
hash: String, hash: String,
time: Long, time: Long,
saplingTree: String, saplingTree: String,
@@ -417,10 +412,8 @@ class RustBackend private constructor() : RustBackendWelding {
@JvmStatic @JvmStatic
private external fun isValidShieldedAddress(addr: String, networkId: Int): Boolean private external fun isValidShieldedAddress(addr: String, networkId: Int): Boolean
/* THIS IS NOT SUPPORT IN HUSH LIGHTWALLETD
@JvmStatic @JvmStatic
private external fun isValidTransparentAddress(addr: String, networkId: Int): Boolean private external fun isValidTransparentAddress(addr: String, networkId: Int): Boolean
*/
@JvmStatic @JvmStatic
private external fun getBalance(dbDataPath: String, account: Int, networkId: Int): Long private external fun getBalance(dbDataPath: String, account: Int, networkId: Int): Long
@@ -451,19 +444,19 @@ class RustBackend private constructor() : RustBackendWelding {
dbCachePath: String, dbCachePath: String,
dbDataPath: String, dbDataPath: String,
networkId: Int networkId: Int
): Int ): Long
@JvmStatic @JvmStatic
private external fun getNearestRewindHeight( private external fun getNearestRewindHeight(
dbDataPath: String, dbDataPath: String,
height: Int, height: Long,
networkId: Int networkId: Int
): Int ): Long
@JvmStatic @JvmStatic
private external fun rewindToHeight( private external fun rewindToHeight(
dbDataPath: String, dbDataPath: String,
height: Int, height: Long,
networkId: Int networkId: Int
): Boolean ): Boolean
@@ -519,7 +512,7 @@ class RustBackend private constructor() : RustBackendWelding {
private external fun initLogs() private external fun initLogs()
@JvmStatic @JvmStatic
private external fun branchIdForHeight(height: Int, networkId: Int): Long private external fun branchIdForHeight(height: Long, networkId: Int): Long
@JvmStatic @JvmStatic
private external fun putUtxo( private external fun putUtxo(
@@ -529,7 +522,7 @@ class RustBackend private constructor() : RustBackendWelding {
index: Int, index: Int,
script: ByteArray, script: ByteArray,
value: Long, value: Long,
height: Int, height: Long,
networkId: Int networkId: Int
): Boolean ): Boolean
@@ -537,7 +530,7 @@ class RustBackend private constructor() : RustBackendWelding {
private external fun clearUtxos( private external fun clearUtxos(
dbDataPath: String, dbDataPath: String,
tAddress: String, tAddress: String,
aboveHeight: Int, aboveHeight: Long,
networkId: Int networkId: Int
): Boolean ): Boolean

View File

@@ -1,9 +1,11 @@
package cash.z.ecc.android.sdk.jni package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.UnifiedViewingKey import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
/** /**
* Contract defining the exposed capabilities of the Rust backend. * Contract defining the exposed capabilities of the Rust backend.
@@ -11,7 +13,7 @@ import cash.z.ecc.android.sdk.type.ZcashNetwork
* It is not documented because it is not intended to be used, directly. * It is not documented because it is not intended to be used, directly.
* Instead, use the synchronizer or one of its subcomponents. * Instead, use the synchronizer or one of its subcomponents.
*/ */
interface RustBackendWelding { internal interface RustBackendWelding {
val network: ZcashNetwork val network: ZcashNetwork
@@ -36,21 +38,21 @@ interface RustBackendWelding {
suspend fun initAccountsTable(vararg keys: UnifiedViewingKey): Boolean suspend fun initAccountsTable(vararg keys: UnifiedViewingKey): Boolean
suspend fun initBlocksTable(height: Int, hash: String, time: Long, saplingTree: String): Boolean suspend fun initBlocksTable(checkpoint: Checkpoint): Boolean
suspend fun initDataDb(): Boolean suspend fun initDataDb(): Boolean
fun isValidShieldedAddr(addr: String): Boolean fun isValidShieldedAddr(addr: String): Boolean
//fun isValidTransparentAddr(addr: String): Boolean fun isValidTransparentAddr(addr: String): Boolean
suspend fun getShieldedAddress(account: Int = 0): String suspend fun getShieldedAddress(account: Int = 0): String
//suspend fun getTransparentAddress(account: Int = 0, index: Int = 0): String suspend fun getTransparentAddress(account: Int = 0, index: Int = 0): String
suspend fun getBalance(account: Int = 0): Zatoshi suspend fun getBalance(account: Int = 0): Zatoshi
fun getBranchIdForHeight(height: Int): Long fun getBranchIdForHeight(height: BlockHeight): Long
suspend fun getReceivedMemoAsUtf8(idNote: Long): String suspend fun getReceivedMemoAsUtf8(idNote: Long): String
@@ -60,13 +62,16 @@ interface RustBackendWelding {
// fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList // fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList
suspend fun getNearestRewindHeight(height: Int): Int suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight
suspend fun rewindToHeight(height: Int): Boolean suspend fun rewindToHeight(height: BlockHeight): Boolean
suspend fun scanBlocks(limit: Int = -1): Boolean suspend fun scanBlocks(limit: Int = -1): Boolean
suspend fun validateCombinedChain(): Int /**
* @return Null if successful. If an error occurs, the height will be the height where the error was detected.
*/
suspend fun validateCombinedChain(): BlockHeight?
suspend fun putUtxo( suspend fun putUtxo(
tAddress: String, tAddress: String,
@@ -74,10 +79,10 @@ interface RustBackendWelding {
index: Int, index: Int,
script: ByteArray, script: ByteArray,
value: Long, value: Long,
height: Int height: BlockHeight
): Boolean ): Boolean
suspend fun clearUtxos(tAddress: String, aboveHeight: Int = network.saplingActivationHeight - 1): Boolean suspend fun clearUtxos(tAddress: String, aboveHeightInclusive: BlockHeight = BlockHeight(network.saplingActivationHeight.value)): Boolean
suspend fun getDownloadedUtxoBalance(address: String): WalletBalance suspend fun getDownloadedUtxoBalance(address: String): WalletBalance

View File

@@ -0,0 +1,67 @@
package cash.z.ecc.android.sdk.model
import android.content.Context
import cash.z.ecc.android.sdk.tool.CheckpointTool
/**
* Represents a block height, which is a UInt32. SDK clients use this class to represent the "birthday" of a wallet.
*
* New instances are constructed using the [new] factory method.
*
* @param value The block height. Must be in range of a UInt32.
*/
/*
* For easier compatibility with Java clients, this class represents the height value as a Long with
* assertions to ensure that it is a 32-bit unsigned integer.
*/
data class BlockHeight internal constructor(val value: Long) : Comparable<BlockHeight> {
init {
require(UINT_RANGE.contains(value)) { "Height $value is outside of allowed range $UINT_RANGE" }
}
override fun compareTo(other: BlockHeight): Int = value.compareTo(other.value)
operator fun plus(other: BlockHeight) = BlockHeight(value + other.value)
operator fun plus(other: Int): BlockHeight {
if (other < 0) {
throw IllegalArgumentException("Cannot add negative value $other to BlockHeight")
}
return BlockHeight(value + other.toLong())
}
operator fun plus(other: Long): BlockHeight {
if (other < 0) {
throw IllegalArgumentException("Cannot add negative value $other to BlockHeight")
}
return BlockHeight(value + other)
}
companion object {
private val UINT_RANGE = 0.toLong()..UInt.MAX_VALUE.toLong()
/**
* @param zcashNetwork Network to use for the block height.
* @param blockHeight The block height. Must be in range of a UInt32 AND must be greater than the network's sapling activation height.
*/
fun new(zcashNetwork: ZcashNetwork, blockHeight: Long): BlockHeight {
require(blockHeight >= zcashNetwork.saplingActivationHeight.value) {
"Height $blockHeight is below sapling activation height ${zcashNetwork.saplingActivationHeight}"
}
return BlockHeight(blockHeight)
}
/**
* Useful when creating a new wallet to reduce sync times.
*
* @param zcashNetwork Network to use for the block height.
* @return The block height of the newest checkpoint known by the SDK.
*/
suspend fun ofLatestCheckpoint(context: Context, zcashNetwork: ZcashNetwork): BlockHeight {
return CheckpointTool.loadNearest(context, zcashNetwork, null).height
}
}
}

View File

@@ -0,0 +1,5 @@
package cash.z.ecc.android.sdk.model
data class LightWalletEndpoint(val host: String, val port: Int, val isSecure: Boolean) {
companion object
}

View File

@@ -0,0 +1,44 @@
@file:Suppress("ktlint:filename")
package cash.z.ecc.android.sdk.model
/*
* This is a set of extension functions currently, because we expect them to change in the future.
*/
fun LightWalletEndpoint.Companion.defaultForNetwork(zcashNetwork: ZcashNetwork): LightWalletEndpoint {
return when (zcashNetwork.id) {
ZcashNetwork.Mainnet.id -> LightWalletEndpoint.Mainnet
ZcashNetwork.Testnet.id -> LightWalletEndpoint.Testnet
else -> error("Unknown network id: ${zcashNetwork.id}")
}
}
/**
* This is a special localhost value on the Android emulator, which allows it to contact
* the localhost of the computer running the emulator.
*/
private const val COMPUTER_LOCALHOST = "10.0.2.2"
private const val DEFAULT_PORT = 9067
val LightWalletEndpoint.Companion.Mainnet
get() = LightWalletEndpoint(
"lite2.hushpool.is",
DEFAULT_PORT,
isSecure = false
)
val LightWalletEndpoint.Companion.Testnet
get() = LightWalletEndpoint(
"lite2.hushpool.is",
DEFAULT_PORT,
isSecure = false
)
val LightWalletEndpoint.Companion.Darkside
get() = LightWalletEndpoint(
COMPUTER_LOCALHOST,
DEFAULT_PORT,
isSecure = false
)

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