Browse Source

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
pull/1/head
fekt 1 year ago
parent
commit
2225f65ee7
  1. 2
      .gitattributes
  2. 1
      .gitignore
  3. 3
      .idea/.gitignore
  4. 14
      CHANGELOG.md
  5. 20
      MIGRATIONS.md
  6. 217
      README.md
  7. 26
      build-conventions/build.gradle.kts
  8. 60
      build-conventions/buildscript-gradle.lockfile
  9. 163
      build-conventions/gradle.lockfile
  10. 13
      build-conventions/src/main/kotlin/zcash-sdk.dependency-conventions.gradle.kts
  11. 14
      darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/InboundTxTests.kt
  12. 6
      darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSetupTest.kt
  13. 7
      darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSmallTest.kt
  14. 36
      darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideApi.kt
  15. 68
      darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt
  16. 72
      darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt
  17. 20
      demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt
  18. 10
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt
  19. 2
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt
  20. 16
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt
  21. 11
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt
  22. 24
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt
  23. 2
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt
  24. 22
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt
  25. 66
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt
  26. 16
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt
  27. 18
      demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/NetworkExt.kt
  28. 4
      demo-app/src/zcashmainnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt
  29. 2
      demo-app/src/zcashtestnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt
  30. 36
      docs/Architecture.md
  31. 110
      docs/Consumers.md
  32. 6
      docs/Setup.md
  33. 0
      docs/assets/build-variants.png
  34. 0
      docs/assets/ndk-window.png
  35. 0
      docs/assets/sdk-manager-icon.png
  36. 0
      docs/assets/sdk_dev_pov_final.png
  37. 0
      docs/assets/sdk_diagram_final.png
  38. 35
      gradle.properties
  39. BIN
      gradle/wrapper/gradle-wrapper.jar
  40. 4
      gradle/wrapper/gradle-wrapper.properties
  41. 6
      gradlew
  42. 14
      gradlew.bat
  43. 20
      sdk-lib/Cargo.toml
  44. 13
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/AssetTest.kt
  45. 6
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/TestExtensions.kt
  46. 33
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt
  47. 13
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt
  48. 22
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt
  49. 20
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/service/ChangeServiceTest.kt
  50. 47
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/CheckpointTest.kt
  51. 30
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/WalletBirthdayTest.kt
  52. 15
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/BranchIdTest.kt
  53. 2
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/TransparentTest.kt
  54. 2
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt
  55. 27
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt
  56. 17
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/CheckpointToolTest.kt
  57. 3
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/AddressGeneratorUtil.kt
  58. 19
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt
  59. 10
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt
  60. 72
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt
  61. 21
      sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TransactionCounterUtil.kt
  62. 27
      sdk-lib/src/androidTest/java/cash/z/ecc/fixture/CheckpointFixture.kt
  63. 4
      sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1150000.json
  64. 7
      sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1160000.json
  65. 7
      sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1000000.json
  66. 7
      sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1150000.json
  67. 99
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt
  68. 76
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt
  69. 43
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt
  70. 628
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt
  71. 4
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/CompactBlockEntity.kt
  72. 56
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt
  73. 166
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt
  74. 11
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/BatchMetrics.kt
  75. 7
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ConsensusBranchId.kt
  76. 5
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdk.kt
  77. 46
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/CheckpointExt.kt
  78. 5
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/IsEmpty.kt
  79. 1
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Twig.kt
  80. 37
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/WalletBirthdayExt.kt
  81. 28
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDbStore.kt
  82. 6
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDownloader.kt
  83. 10
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockStore.kt
  84. 19
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CompactBlockDb.kt
  85. 14
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt
  86. 4
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/PendingTransactionDb.kt
  87. 22
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/Checkpoint.kt
  88. 190
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletGrpcService.kt
  89. 13
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletService.kt
  90. 44
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt
  91. 9
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt
  92. 7
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt
  93. 5
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt
  94. 11
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt
  95. 19
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt
  96. 119
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt
  97. 29
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt
  98. 67
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/BlockHeight.kt
  99. 5
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpoint.kt
  100. 44
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpointExt.kt
  101. 28
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletBalance.kt
  102. 4
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zatoshi.kt
  103. 40
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/ZcashNetwork.kt
  104. 72
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/CheckpointTool.kt
  105. 2
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt
  106. 62
      sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt
  107. 6
      sdk-lib/src/main/proto/darkside.proto
  108. 172
      sdk-lib/src/main/proto/service.proto
  109. 4
      sdk-lib/src/main/res/values/bools.xml
  110. 3
      sdk-lib/src/main/res/values/strings.xml
  111. 28
      sdk-lib/src/main/rust/lib.rs
  112. 9
      sdk-lib/src/test/java/cash/z/ecc/android/sdk/ext/ConversionsTest.kt
  113. 16
      sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/ZatoshiTest.kt
  114. 101
      tools/detekt-baseline.xml
  115. 111
      tools/detekt.yml

2
.gitattributes

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

1
.gitignore

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

3
.idea/.gitignore

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

14
CHANGELOG.md

@ -3,7 +3,19 @@ Change Log
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
------------------------------------

20
MIGRATIONS.md

@ -1,17 +1,33 @@
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`.
`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.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.

217
README.md

@ -16,211 +16,42 @@ This is a beta build and is currently under active development. Please be advise
---
# 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)
- [Structure](#structure)
- [Overview](#overview)
- [Components](#components)
- [Quickstart](#quickstart)
- [Examples](#examples)
- [Compiling Sources](#compiling-sources)
- [Versioning](#versioning)
- [Examples](#examples)
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.
## 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:
![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
# 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.
A primitive example to exercise the SDK exists in this repo, under [Demo App](demo-app).
There's also a more comprehensive [Sample Wallet](https://github.com/zcash/zcash-android-wallet).
[Back to contents](#contents)
## Checkpoints
To improve the speed of syncing with the Zcash network, the SDK contains a series of embedded checkpoints. These should be updated periodically, as new transactions are added to the network. Checkpoints are stored under the [assets](sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint) directory as JSON files. Checkpoints for both mainnet and testnet are bundled into the SDK.
To update the checkpoints, see [Checkmate](https://github.com/zcash-hackworks/checkmate).
We generally recommend adding new checkpoints every few weeks. By convention, checkpoints are added in block increments of 10,000 which provides a reasonable tradeoff in terms of number of checkpoints versus performance.
There are two special checkpoints, one for sapling activation and another for orchard activation. These are mentioned because they don't follow the "round 10,000" rule.
* Sapling activation
* Mainnet: 419200
* Testnet: 280000
* Orchard activation
* Mainnet: 1687104
* Testnet: 1842420
## Publishing
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.
Publishing instructions for maintainers of this repository can be found in [PUBLISHING.md](PUBLISHING.md)
# 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
[Back to contents](#contents)
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:
* 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
# Known Issues
## 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`
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.

26
build-conventions/build.gradle.kts

@ -6,12 +6,34 @@ plugins {
buildscript {
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 {
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

60
build-conventions/buildscript-gradle.lockfile

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

163
build-conventions/gradle.lockfile

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

13
build-conventions/src/main/kotlin/zcash-sdk.dependency-conventions.gradle.kts

@ -1,6 +1,17 @@
//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 {

14
darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/InboundTxTests.kt

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

6
darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSetupTest.kt

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

7
darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSmallTest.kt

@ -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.ScopedTest
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.Before
import org.junit.BeforeClass
@ -13,7 +15,10 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReorgSmallTest : ScopedTest() {
private val targetHeight = 663250
private val targetHeight = BlockHeight.new(
ZcashNetwork.Mainnet,
663250
)
private val hashBeforeReorg = "09ec0d5de30d290bc5a2318fbf6a2427a81c7db4790ce0e341a96aeac77108b9"
private val hashAfterReorg = "tbd"

36
darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideApi.kt

@ -1,10 +1,12 @@
package cash.z.ecc.android.sdk.darkside.test
import android.content.Context
import cash.z.ecc.android.sdk.R
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.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.DarksideTransactionsURL
import cash.z.wallet.sdk.rpc.DarksideStreamerGrpc
@ -22,17 +24,11 @@ class DarksideApi(
constructor(
appContext: Context,
host: String,
port: Int = ZcashNetwork.Mainnet.defaultPort,
usePlainText: Boolean = appContext.resources.getBoolean(
R.bool.lightwalletd_allow_very_insecure_connections
)
lightWalletEndpoint: LightWalletEndpoint
) : this(
LightWalletGrpcService.createDefaultChannel(
appContext,
host,
port,
usePlainText
lightWalletEndpoint
)
)
@ -41,7 +37,7 @@ class DarksideApi(
//
fun reset(
saplingActivationHeight: Int = 419200,
saplingActivationHeight: BlockHeight = ZcashNetwork.Mainnet.saplingActivationHeight,
branchId: String = "e9ff75a6", // Canopy,
chainName: String = "darkside${ZcashNetwork.Mainnet.networkName}"
) = apply {
@ -49,7 +45,7 @@ class DarksideApi(
Darkside.DarksideMetaState.newBuilder()
.setBranchID(branchId)
.setChainName(chainName)
.setSaplingActivation(saplingActivationHeight)
.setSaplingActivation(saplingActivationHeight.value.toInt())
.build().let { request ->
createStub().reset(request)
}
@ -60,21 +56,21 @@ class DarksideApi(
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")
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")
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) {
twig("no transactions to stage")
return
@ -84,7 +80,7 @@ class DarksideApi(
createStreamingStub().stageTransactionsStream(response).apply {
txs.forEach {
twig("stageTransactions: onNext calling!!!")
onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.toLong()).build()) // apply the tipHeight because the passed in txs might not know their destination height (if they were created via SendTransaction)
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: onCompleted calling!!!")
@ -94,7 +90,7 @@ class DarksideApi(
response.await()
}
fun applyBlocks(tipHeight: Int) {
fun applyBlocks(tipHeight: BlockHeight) {
twig("applying blocks up to tipHeight=$tipHeight")
createStub().applyStaged(tipHeight.toHeight())
}
@ -146,7 +142,7 @@ class DarksideApi(
.withDeadlineAfter(singleRequestTimeoutSec, TimeUnit.SECONDS)
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> {
var completed = false

68
darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt

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

72
darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt

@ -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.service.LightWalletGrpcService
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.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
@ -35,9 +38,8 @@ class TestWallet(
val seedPhrase: String,
val alias: String = "TestWallet",
val network: ZcashNetwork = ZcashNetwork.Testnet,
val host: String = network.defaultHost,
startHeight: Int? = null,
val port: Int = network.defaultPort
val endpoint: LightWalletEndpoint = LightWalletEndpoint.Darkside,
startHeight: BlockHeight? = null
) {
constructor(
backup: Backups,
@ -65,7 +67,7 @@ class TestWallet(
runBlocking { DerivationTool.deriveTransparentSecretKey(seed, network = network) }
val initializer = runBlocking {
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
@ -78,14 +80,11 @@ class TestWallet(
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
val birthdayHeight get() = synchronizer.latestBirthdayHeight
val networkName get() = synchronizer.network.networkName
val connectionInfo get() = service.connectionInfo.toString()
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun transparentBalance(): WalletBalance {
synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight)
return synchronizer.getTransparentBalance(transparentAddress)
}
*/
suspend fun sync(timeout: Long = -1): TestWallet {
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 {
Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() }
.takeWhile { it.isPending(null) }
.collect {
twig("Updated transaction: $it")
}
@ -119,15 +118,14 @@ class TestWallet(
return this
}
suspend fun rewindToHeight(height: Int): TestWallet {
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
synchronizer.rewindToNearestHeight(height, false)
return this
}
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun shieldFunds(): TestWallet {
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")
}
@ -144,7 +142,6 @@ class TestWallet(
return this
}
*/
suspend fun join(timeout: Long? = null): TestWallet {
// 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
DEFAULT("column rhythm acoustic gym cost fit keen maze fence seed mail medal shrimp tell relief clip cannon foster soldier shallow refuse lunar parrot banana", 1_355_928, 1_000_000),
SAMPLE_WALLET("input frown warm senior anxiety abuse yard prefer churn reject people glimpse govern glory crumble swallow verb laptop switch trophy inform friend permit purpose", 1_330_190, 1_000_000),
DEV_WALLET("still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread", 1_000_000, 991645),
ALICE("quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten", 1_330_190, 1_000_000),
BOB("canvas wine sugar acquire garment spy tongue odor hole cage year habit bullet make label human unit option top calm neutral try vocal arena", 1_330_190, 1_000_000),
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",
BlockHeight.new(
ZcashNetwork.Testnet,
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)
),
;
}
}

20
demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt

@ -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.service.LightWalletGrpcService
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.type.ZcashNetwork
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
@ -86,17 +89,20 @@ class SampleCodeTest {
// ///////////////////////////////////////////////////
// Query latest block height
@Test fun getLatestBlockHeightTest() {
val lightwalletService = LightWalletGrpcService(context, lightwalletdHost)
val lightwalletService = LightWalletGrpcService.new(context, lightwalletdHost)
log("Latest Block: ${lightwalletService.getLatestBlockHeight()}")
}
// ///////////////////////////////////////////////////
// Download compact block range
@Test fun getBlockRange() {
val blockRange = 500_000..500_009
val lightwalletService = LightWalletGrpcService(context, lightwalletdHost)
val blockRange = BlockHeight.new(ZcashNetwork.Mainnet, 500_000)..BlockHeight.new(
ZcashNetwork.Mainnet,
500_009
)
val lightwalletService = LightWalletGrpcService.new(context, lightwalletdHost)
val blocks = lightwalletService.getBlockRange(blockRange)
assertEquals(blockRange.count(), blocks.size)
assertEquals(blockRange.endInclusive.value - blockRange.start.value, blocks.count())
blocks.forEachIndexed { i, block ->
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)
transactionFlow.collect {
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 {
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 synchronizer: Synchronizer = run {

10
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt

@ -20,7 +20,9 @@ import androidx.viewbinding.ViewBinding
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.type.ZcashNetwork
import 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.navigation.NavigationView
@ -108,7 +110,11 @@ class MainActivity :
if (lightwalletService != null) {
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) {

2
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt

@ -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.ext.requireApplicationContext
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.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

16
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt

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

11
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt

@ -7,11 +7,15 @@ import android.view.View
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBlockBinding
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.demoapp.util.mainActivity
import cash.z.ecc.android.sdk.demoapp.util.toHtml
import cash.z.ecc.android.sdk.demoapp.util.toRelativeTime
import cash.z.ecc.android.sdk.demoapp.util.withCommas
import cash.z.ecc.android.sdk.ext.toHex
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.
@ -20,7 +24,7 @@ import cash.z.ecc.android.sdk.ext.toHex
*/
class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
private fun setBlockHeight(blockHeight: Int) {
private fun setBlockHeight(blockHeight: BlockHeight) {
val blocks =
lightwalletService?.getBlockRange(blockHeight..blockHeight)
val block = blocks?.firstOrNull()
@ -38,8 +42,11 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
}
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 {
setBlockHeight(binding.textBlockHeight.text.toString().toInt())
setBlockHeight(BlockHeight.new(network, newHeight))
} catch (t: Throwable) {
toast("Error: $t")
}

24
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt

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

2
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt

@ -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.ext.requireApplicationContext
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.type.ZcashNetwork
import kotlinx.coroutines.launch
/**

22
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt

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

66
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt

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

16
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt

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

18
demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/NetworkExt.kt

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

4
demo-app/src/zcashmainnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt

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

2
demo-app/src/zcashtestnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt

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

36
docs/Architecture.md

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

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

6
docs/Setup.md

@ -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. 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. 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. 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
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
* `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
* `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
@ -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 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. 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)

0
assets/build-variants.png → docs/assets/build-variants.png

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

0
assets/ndk-window.png → docs/assets/ndk-window.png

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

0
assets/sdk-manager-icon.png → docs/assets/sdk-manager-icon.png

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

0
assets/sdk_dev_pov_final.png → docs/assets/sdk_dev_pov_final.png

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

0
assets/sdk_diagram_final.png → docs/assets/sdk_diagram_final.png

Before

Width:  |  Height:  |  Size: 445 KiB

After

Width:  |  Height:  |  Size: 445 KiB

35
gradle.properties

@ -1,10 +1,9 @@
# 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.parallel=true
org.gradle.configureondemand=false
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
org.gradle.parallel=true
org.gradle.vfs.watch=true
android.useAndroidX=true
android.builder.sdkDownload=true
@ -22,7 +21,7 @@ RELEASE_SIGNING_ENABLED=false
# Required by the maven publishing plugin
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.
# 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
ANDROID_NDK_VERSION=22.1.7171670
ANDROID_GRADLE_PLUGIN_VERSION=7.2.1
DETEKT_VERSION=1.20.0
DOKKA_VERSION=1.6.21
ANDROID_GRADLE_PLUGIN_VERSION=7.2.2
DETEKT_VERSION=1.21.0
DOKKA_VERSION=1.7.10
EMULATOR_WTF_GRADLE_PLUGIN_VERSION=0.0.10
FLANK_VERSION=22.03.0
FULLADLE_VERSION=0.17.4
GRADLE_VERSIONS_PLUGIN_VERSION=0.42.0
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
PROTOBUF_GRADLE_PLUGIN_VERSION=0.8.18
PROTOBUF_GRADLE_PLUGIN_VERSION=0.8.19
RUST_GRADLE_PLUGIN_VERSION=0.9.3
ANDROIDX_ANNOTATION_VERSION=1.3.0
@ -86,27 +85,27 @@ ANDROIDX_LIFECYCLE_VERSION=2.4.1
ANDROIDX_MULTIDEX_VERSION=2.0.1
ANDROIDX_NAVIGATION_VERSION=2.4.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_ORCHESTRATOR_VERSION=1.1.0-alpha1
ANDROIDX_TEST_VERSION=1.4.0
ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1
BIP39_VERSION=1.0.2
BIP39_VERSION=1.0.4
COROUTINES_OKHTTP=1.0
GOOGLE_MATERIAL_VERSION=1.6.1
GRPC_VERSION=1.47.0
GRPC_VERSION=1.48.1
GSON_VERSION=2.9.0
GUAVA_VERSION=31.1-android
JACOCO_VERSION=0.8.8
JAVAX_ANNOTATION_VERSION=1.3.2
JUNIT_VERSION=5.8.2
KOTLINX_COROUTINES_VERSION=1.6.2
KOTLIN_VERSION=1.6.21
JUNIT_VERSION=5.9.0
KOTLINX_COROUTINES_VERSION=1.6.4
KOTLIN_VERSION=1.7.10
MOCKITO_KOTLIN_VERSION=2.2.0
MOCKITO_VERSION=4.6.1
OKHTTP_VERSION=4.10.0
OKIO_VERSION=3.1.0
PROTOC_VERSION=3.21.1
OKIO_VERSION=3.2.0
PROTOC_VERSION=3.21.4
ZCASH_WALLET_PLUGINS_VERSION=1.0.1
# This shouldn't be changed, as Android doesn't support targets beyond Java 8

BIN
gradle/wrapper/gradle-wrapper.jar

Binary file not shown.

4
gradle/wrapper/gradle-wrapper.properties

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

6
gradlew

@ -205,6 +205,12 @@ set -- \
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.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

14
gradlew.bat

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
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
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

20
sdk-lib/Cargo.toml

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

13
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/AssetTest.kt

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

6
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/TestExtensions.kt

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

33
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt

@ -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.TestPurpose
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 kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@ -30,13 +30,6 @@ class SanityTest(
val networkName = wallet.networkName
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
fun testFilePaths() {
assertEquals(
@ -61,7 +54,7 @@ class SanityTest(
assertEquals(
"$name has invalid birthday height",
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
fun testLatestHeight() = runBlocking {
if (wallet.networkName == "mainnet") {
val expectedHeight = BlockExplorer.fetchLatestHeight()
// fetch height directly because the synchronizer hasn't started, yet
val downloaderHeight = wallet.service.getLatestBlockHeight()
val info = wallet.connectionInfo
assertTrue(
"$info\n ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight",
expectedHeight - 10 < downloaderHeight
"${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.value
)
}
}
@ -105,9 +88,9 @@ class SanityTest(
@Test
fun testSingleBlockDownload() = runBlocking {
// fetch block directly because the synchronizer hasn't started, yet
val height = 1_000_000
val block = wallet.service.getBlockRange(height..height)[0]
assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height.toInt() == height)
val height = BlockHeight.new(wallet.network, 1_000_000)
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 == height.value)
}
companion object {

13
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt

@ -4,7 +4,6 @@ import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.annotation.MaintainedTest
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 kotlinx.coroutines.runBlocking
import org.junit.Assert
@ -19,16 +18,6 @@ import org.junit.Test
@MediumTest
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
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)
@ -38,7 +27,7 @@ class SmokeTest {
@Test
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

22
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt

@ -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.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.service.LightWalletGrpcService
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.ZcashNetwork
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.WalletBirthdayTool
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
@ -38,9 +40,9 @@ class TestnetIntegrationTest : ScopedTest() {
@Test
@Ignore("This test is broken")
fun testLatestBlockTest() {
val service = LightWalletGrpcService(
val service = LightWalletGrpcService.new(
context,
host
lightWalletEndpoint
)
val height = service.getLatestBlockHeight()
assertTrue(height > saplingActivation)
@ -49,7 +51,7 @@ class TestnetIntegrationTest : ScopedTest() {
@Test
fun testLoadBirthday() {
val (height, hash, time, tree) = runBlocking {
WalletBirthdayTool.loadNearest(
CheckpointTool.loadNearest(
context,
synchronizer.network,
saplingActivation + 1
@ -117,8 +119,8 @@ class TestnetIntegrationTest : ScopedTest() {
companion object {
init { Twig.plant(TroubleshootingTwig()) }
const val host = "lightwalletd.testnet.z.cash"
private const val birthdayHeight = 963150
val lightWalletEndpoint = LightWalletEndpoint("lightwalletd.testnet.z.cash", 9087, true)
private const val birthdayHeight = 963150L
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"
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 initializer = runBlocking {
Initializer.new(context) { config ->
config.setNetwork(ZcashNetwork.Testnet, host)
runBlocking { config.importWallet(seed, birthdayHeight, ZcashNetwork.Testnet) }
config.setNetwork(ZcashNetwork.Testnet, lightWalletEndpoint)
runBlocking { config.importWallet(seed, BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight), ZcashNetwork.Testnet, lightWalletEndpoint) }
}
}
private lateinit var synchronizer: Synchronizer

20
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/service/ChangeServiceTest.kt

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

47
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/CheckpointTest.kt

@ -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)
}
}
}

30
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/WalletBirthdayTest.kt

@ -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)
}
}
}

15
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/BranchIdTest.kt

@ -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.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 org.junit.Assert.assertEquals
import org.junit.Test
@ -14,9 +15,9 @@ import org.junit.runners.Parameterized
*/
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(Parameterized::class)
class BranchIdTest(
class BranchIdTest internal constructor(
private val networkName: String,
private val height: Int,
private val height: BlockHeight,
private val branchId: Long,
private val branchHex: String,
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
// However, due to quirks on certain devices, we created this test at the Android level,
// as a sanity check
val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet) }
val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet) }
val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet, ZcashNetwork.Testnet.saplingActivationHeight) }
val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight) }
return listOf(
// Mainnet Cases
arrayOf("Sapling", 419_200, 1991772603L, "76b809bb", mainnetBackend),
arrayOf("Sapling", 1, 1991772603L, "76b809bb", mainnetBackend),
// Testnet Cases
arrayOf("Sapling", 280_000, 1991772603L, "76b809bb", testnetBackend)
arrayOf("Sapling", 1, 1991772603L, "76b809bb", testnetBackend),
)
}
}

2
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/TransparentTest.kt

@ -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.internal.TroubleshootingTwig
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.type.ZcashNetwork
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.BeforeClass

2
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt

@ -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.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 kotlinx.coroutines.runBlocking
import org.junit.Assert

27
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt

@ -3,8 +3,9 @@ package cash.z.ecc.android.sdk.sample
import androidx.test.filters.LargeTest
import cash.z.ecc.android.sdk.ext.ZcashSdk
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.type.ZcashNetwork.Testnet
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
@ -73,7 +74,7 @@ class TransparentRestoreSample {
// wallet.rewindToHeight(1343500).join(45_000)
val wallet = TestWallet(TestWallet.Backups.SAMPLE_WALLET, alias = "WalletC")
// wallet.sync().rewindToHeight(1339178).join(10000)
wallet.sync().rewindToHeight(1339178).send(
wallet.sync().rewindToHeight(BlockHeight.new(ZcashNetwork.Testnet, 1339178)).send(
"ztestsapling17zazsl8rryl8kjaqxnr2r29rw9d9a2mud37ugapm0s8gmyv0ue43h9lqwmhdsp3nu9dazeqfs6l",
"is send broken?"
).join(5)
@ -85,7 +86,15 @@ class TransparentRestoreSample {
@LargeTest
@Ignore("This test is extremely slow")
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")
wallet0.sync() // .shieldFunds()
@ -107,7 +116,15 @@ class TransparentRestoreSample {
*/
// @Test
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 {
// twig("Syncing WalletA")
// walletA.sync()
@ -125,7 +142,7 @@ class TransparentRestoreSample {
// send 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("T-ADDR (for the win!): ${walletSandbox.transparentAddress}")
delay(500)

17
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/WalletBirthdayToolTest.kt → sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/CheckpointToolTest.kt

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

3
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/AddressGeneratorUtil.kt

@ -1,10 +1,9 @@
package cash.z.ecc.android.sdk.util
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.type.ZcashNetwork
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking

19
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt

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

10
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt

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

72
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt

@ -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.service.LightWalletGrpcService
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.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.android.sdk.type.ZcashNetwork
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
@ -35,9 +38,8 @@ class TestWallet(
val seedPhrase: String,
val alias: String = "TestWallet",
val network: ZcashNetwork = ZcashNetwork.Testnet,
val host: String = network.defaultHost,
startHeight: Int? = null,
val port: Int = network.defaultPort
val endpoint: LightWalletEndpoint = LightWalletEndpoint.Testnet,
startHeight: BlockHeight? = null
) {
constructor(
backup: Backups,
@ -65,7 +67,7 @@ class TestWallet(
runBlocking { DerivationTool.deriveTransparentSecretKey(seed, network = network) }
val initializer = runBlocking {
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
@ -78,14 +80,11 @@ class TestWallet(
runBlocking { DerivationTool.deriveTransparentAddress(seed, network = network) }
val birthdayHeight get() = synchronizer.latestBirthdayHeight
val networkName get() = synchronizer.network.networkName
val connectionInfo get() = service.connectionInfo.toString()
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun transparentBalance(): WalletBalance {
synchronizer.refreshUtxos(transparentAddress, synchronizer.latestBirthdayHeight)
return synchronizer.getTransparentBalance(transparentAddress)
}
*/
suspend fun sync(timeout: Long = -1): TestWallet {
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 {
Twig.sprout("$alias sending")
synchronizer.sendToAddress(shieldedSpendingKey, amount, address, memo, fromAccountIndex)
.takeWhile { it.isPending() }
.takeWhile { it.isPending(null) }
.collect {
twig("Updated transaction: $it")
}
@ -119,15 +118,14 @@ class TestWallet(
return this
}
suspend fun rewindToHeight(height: Int): TestWallet {
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
synchronizer.rewindToNearestHeight(height, false)
return this
}
/* NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun shieldFunds(): TestWallet {
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")
}
@ -144,7 +142,6 @@ class TestWallet(
return this
}
*/
suspend fun join(timeout: Long? = null): TestWallet {
// 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
DEFAULT("column rhythm acoustic gym cost fit keen maze fence seed mail medal shrimp tell relief clip cannon foster soldier shallow refuse lunar parrot banana", 1_355_928, 1_000_000),
SAMPLE_WALLET("input frown warm senior anxiety abuse yard prefer churn reject people glimpse govern glory crumble swallow verb laptop switch trophy inform friend permit purpose", 1_330_190, 1_000_000),
DEV_WALLET("still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread", 1_000_000, 991645),
ALICE("quantum whisper lion route fury lunar pelican image job client hundred sauce chimney barely life cliff spirit admit weekend message recipe trumpet impact kitten", 1_330_190, 1_000_000),
BOB("canvas wine sugar acquire garment spy tongue odor hole cage year habit bullet make label human unit option top calm neutral try vocal arena", 1_330_190, 1_000_000),
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",
BlockHeight.new(
ZcashNetwork.Testnet,
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)
),
;
}
}

21
sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TransactionCounterUtil.kt

@ -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.service.LightWalletGrpcService
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.Test
@ -13,7 +16,7 @@ class TransactionCounterUtil {
private val network = ZcashNetwork.Mainnet
private val context = InstrumentationRegistry.getInstrumentation().context
private val service = LightWalletGrpcService(context, network)
private val service = LightWalletGrpcService.new(context, LightWalletEndpoint.Mainnet)
init {
Twig.plant(TroubleshootingTwig())
@ -23,7 +26,12 @@ class TransactionCounterUtil {
@Ignore("This test is broken")
fun testBlockSize() {
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()}")
val s = b.serializedSize
sizes[s] = (sizes[s] ?: 0) + 1
@ -38,7 +46,12 @@ class TransactionCounterUtil {
val outputCounts = mutableMapOf<Int, Int>()
var totalOutputs = 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.vtxList.map { it.outputsCount }.forEach { oCount ->
outputCounts[oCount] = (outputCounts[oCount] ?: 0) + oCount.coerceAtLeast(1)

27
sdk-lib/src/androidTest/java/cash/z/ecc/fixture/WalletBirthdayFixture.kt → sdk-lib/src/androidTest/java/cash/z/ecc/fixture/CheckpointFixture.kt

@ -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_VERSION
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
object WalletBirthdayFixture {
object CheckpointFixture {
val NETWORK = ZcashNetwork.Mainnet
// 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 EPOCH_SECONDS = 1639913234L
@Suppress("MaxLineLength")
const val TREE = "01ce183032b16ed87fcc5052a42d908376526126346567773f55bc58a63e4480160013000001bae5112769a07772345dd402039f2949c457478fe9327363ff631ea9d78fb80d0177c0b6c21aa9664dc255336ed450914088108c38a9171c85875b4e53d31b3e140171add6f9129e124651ca894aa842a3c71b1738f3ee2b7ba829106524ef51e62101f9cebe2141ee9d0a3f3a3e28bce07fa6b6e1c7b42c01cc4fe611269e9d52da540001d0adff06de48569129bd2a211e3253716362da97270d3504d9c1b694689ebe3c0122aaaea90a7fa2773b8166937310f79a4278b25d759128adf3138d052da3725b0137fb2cbc176075a45db2a3c32d3f78e669ff2258fd974e99ec9fb314d7fd90180165aaee3332ea432d13a9398c4863b38b8a7a491877a5c46b0802dcd88f7e324301a9a262f8b92efc2e0e3e4bd1207486a79d62e87b4ab9cc41814d62a23c4e28040001e3c4ee998682df5c5e230d6968e947f83d0c03682f0cfc85f1e6ec8e8552c95a000155989fed7a8cc7a0d479498d6881ca3bafbe05c7095110f85c64442d6a06c25c0185cd8c141e620eda0ca0516f42240aedfabdf9189c8c6ac834b7bdebc171331d01ecceb776c043662617d62646ee60985521b61c0b860f3a9731e66ef74ed8fb320118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
fun new(
height: Int = HEIGHT,
internal fun new(
height: BlockHeight = HEIGHT,
hash: String = HASH,
time: Long = EPOCH_SECONDS,
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 {
put(WalletBirthday.KEY_VERSION, WalletBirthday.VERSION_1)
put(WalletBirthday.KEY_HEIGHT, height)
put(WalletBirthday.KEY_HASH, hash)
put(WalletBirthday.KEY_EPOCH_SECONDS, time)
put(WalletBirthday.KEY_TREE, tree)
internal fun Checkpoint.toJson() = JSONObject().apply {
put(Checkpoint.KEY_VERSION, Checkpoint.VERSION_1)
put(Checkpoint.KEY_HEIGHT, height.value)
put(Checkpoint.KEY_HASH, hash)
put(Checkpoint.KEY_EPOCH_SECONDS, epochSeconds)
put(Checkpoint.KEY_TREE, tree)
}.toString()

4
sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1150000.json

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

7
sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1160000.json

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

7
sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1000000.json

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

7
sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1150000.json

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

99
sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt

@ -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.ext.getCacheDirSuspend
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.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.WalletBirthdayTool
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.withContext
import java.io.File
@ -20,16 +22,16 @@ import java.io.File
/**
* Simplified Initializer focused on starting from a ViewingKey.
*/
@Suppress("LongParameterList")
class Initializer private constructor(
val context: Context,
val rustBackend: RustBackend,
internal val rustBackend: RustBackend,
val network: ZcashNetwork,
val alias: String,
val host: String,
val port: Int,
val lightWalletEndpoint: LightWalletEndpoint,
val viewingKeys: List<UnifiedViewingKey>,
val overwriteVks: Boolean,
val birthday: WalletBirthday
internal val checkpoint: Checkpoint
) {
suspend fun erase() = erase(context, network, alias)
@ -38,16 +40,13 @@ class Initializer private constructor(
val viewingKeys: MutableList<UnifiedViewingKey> = mutableListOf(),
var alias: String = ZcashSdk.DEFAULT_ALIAS
) {
var birthdayHeight: Int? = null
var birthdayHeight: BlockHeight? = null
private set
lateinit var network: ZcashNetwork
private set
lateinit var host: String
private set
var port: Int = ZcashNetwork.Mainnet.defaultPort
lateinit var lightWalletEndpoint: LightWalletEndpoint
private set
/**
@ -86,7 +85,7 @@ class Initializer private constructor(
* 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 {
this.birthdayHeight = height
this.defaultToOldestHeight = defaultToOldestHeight
@ -105,7 +104,7 @@ class Initializer private constructor(
* importing a pre-existing wallet. It is the same as calling
* `birthdayHeight = importedHeight`.
*/
fun importedWalletBirthday(importedHeight: Int?): Config = apply {
fun importedWalletBirthday(importedHeight: BlockHeight?): Config = apply {
birthdayHeight = importedHeight
defaultToOldestHeight = true
}
@ -159,12 +158,10 @@ class Initializer private constructor(
*/
fun setNetwork(
network: ZcashNetwork,
host: String = network.defaultHost,
port: Int = network.defaultPort
lightWalletEndpoint: LightWalletEndpoint
): Config = apply {
this.network = network
this.host = host
this.port = port
this.lightWalletEndpoint = lightWalletEndpoint
}
/**
@ -172,18 +169,16 @@ class Initializer private constructor(
*/
suspend fun importWallet(
seed: ByteArray,
birthdayHeight: Int? = null,
birthday: BlockHeight?,
network: ZcashNetwork,
host: String = network.defaultHost,
port: Int = network.defaultPort,
lightWalletEndpoint: LightWalletEndpoint,
alias: String = ZcashSdk.DEFAULT_ALIAS
): Config =
importWallet(
DerivationTool.deriveUnifiedViewingKeys(seed, network = network)[0],
birthdayHeight,
birthday,
network,
host,
port,
lightWalletEndpoint,
alias
)
@ -192,15 +187,14 @@ class Initializer private constructor(
*/
fun importWallet(
viewingKey: UnifiedViewingKey,
birthdayHeight: Int? = null,
birthday: BlockHeight?,
network: ZcashNetwork,
host: String = network.defaultHost,
port: Int = network.defaultPort,
lightWalletEndpoint: LightWalletEndpoint,
alias: String = ZcashSdk.DEFAULT_ALIAS
): Config = apply {
setViewingKeys(viewingKey)
setNetwork(network, host, port)
importedWalletBirthday(birthdayHeight)
setNetwork(network, lightWalletEndpoint)
importedWalletBirthday(birthday)
this.alias = alias
}
@ -210,14 +204,12 @@ class Initializer private constructor(
suspend fun newWallet(
seed: ByteArray,
network: ZcashNetwork,
host: String = network.defaultHost,
port: Int = network.defaultPort,
lightWalletEndpoint: LightWalletEndpoint,
alias: String = ZcashSdk.DEFAULT_ALIAS
): Config = newWallet(
DerivationTool.deriveUnifiedViewingKeys(seed, network)[0],
network,
host,
port,
lightWalletEndpoint,
alias
)
@ -227,12 +219,11 @@ class Initializer private constructor(
fun newWallet(
viewingKey: UnifiedViewingKey,
network: ZcashNetwork,
host: String = network.defaultHost,
port: Int = network.defaultPort,
lightWalletEndpoint: LightWalletEndpoint,
alias: String = ZcashSdk.DEFAULT_ALIAS
): Config = apply {
setViewingKeys(viewingKey)
setNetwork(network, host, port)
setNetwork(network, lightWalletEndpoint)
newWalletBirthday()
this.alias = alias
}
@ -284,8 +275,8 @@ class Initializer private constructor(
}
// allow either null or a value greater than the activation height
if (
(birthdayHeight ?: network.saplingActivationHeight)
< network.saplingActivationHeight
(birthdayHeight?.value ?: network.saplingActivationHeight.value)
< network.saplingActivationHeight.value
) {
throw InitializerException.InvalidBirthdayHeightException(birthdayHeight, network)
}
@ -328,25 +319,33 @@ class Initializer private constructor(
config: Config
): Initializer {
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(
context.applicationContext,
rustBackend,
config.network,
config.alias,
config.host,
config.port,
config.lightWalletEndpoint,
config.viewingKeys,
config.overwriteVks,
loadedBirthday
loadedCheckpoint
)
}
@ -377,14 +376,14 @@ class Initializer private constructor(
context: Context,
network: ZcashNetwork,
alias: String,
birthday: WalletBirthday
blockHeight: BlockHeight
): RustBackend {
return RustBackend.init(
cacheDbPath(context, network, alias),
dataDbPath(context, network, alias),
File(context.getCacheDirSuspend(), "params").absolutePath,
network,
birthday.height
blockHeight
)
}

76
sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt

@ -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.ext.toHexReversed
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.LightWalletService
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.twig
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.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
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.Transparent
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 io.grpc.ManagedChannel
import kotlinx.coroutines.CoroutineExceptionHandler
@ -95,7 +97,6 @@ import kotlin.coroutines.EmptyCoroutineContext
*/
@ExperimentalCoroutinesApi
@FlowPreview
class SdkSynchronizer internal constructor(
private val storage: TransactionRepository,
private val txManager: OutboundTransactionManager,
@ -103,9 +104,9 @@ class SdkSynchronizer internal constructor(
) : Synchronizer {
// pools
//private val _orchardBalances = MutableStateFlow<WalletBalance?>(null)
private val _orchardBalances = 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)
@ -144,9 +145,9 @@ class SdkSynchronizer internal constructor(
// Balances
//
//override val orchardBalances = _orchardBalances.asStateFlow()
override val orchardBalances = _orchardBalances.asStateFlow()
override val saplingBalances = _saplingBalances.asStateFlow()
//override val transparentBalances = _transparentBalances.asStateFlow()
override val transparentBalances = _transparentBalances.asStateFlow()
//
// Transactions
@ -189,7 +190,7 @@ class SdkSynchronizer internal constructor(
* 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.
*/
override val networkHeight: StateFlow<Int> = processor.networkHeight
override val networkHeight: StateFlow<BlockHeight?> = processor.networkHeight
//
// Error Handling
@ -231,7 +232,7 @@ class SdkSynchronizer internal constructor(
* 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).
*/
override var onChainErrorHandler: ((errorHeight: Int, rewindHeight: Int) -> Any)? = null
override var onChainErrorHandler: ((errorHeight: BlockHeight, rewindHeight: BlockHeight) -> Any)? = null
//
// 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
* [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 {
// Do nothing; this could likely be removed
@ -305,24 +308,10 @@ class SdkSynchronizer internal constructor(
*/
override suspend fun getServerInfo(): Service.LightdInfo = processor.downloader.getServerInfo()
/**
* 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 =
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
processor.getNearestRewindHeight(height)
override suspend fun rewindToNearestHeight(height: Int, alsoClearBlockCache: Boolean) {
override suspend fun rewindToNearestHeight(height: BlockHeight, alsoClearBlockCache: Boolean) {
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
suspend fun findBlockHash(height: Int): ByteArray? {
suspend fun findBlockHash(height: BlockHeight): ByteArray? {
return (storage as? PagedTransactionRepository)?.findBlockHash(height)
}
suspend fun findBlockHashAsHex(height: Int): String? {
suspend fun findBlockHashAsHex(height: BlockHeight): String? {
return findBlockHash(height)?.toHexReversed()
}
@ -356,12 +345,10 @@ class SdkSynchronizer internal constructor(
// Private API
//
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun refreshUtxos() {
twig("refreshing utxos", -1)
refreshUtxos(getTransparentAddress())
}
*/
/**
* 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() {
refreshSaplingBalance()
// refreshTransparentBalance()
refreshTransparentBalance()
// TODO: refresh orchard balance
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()
}
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun refreshTransparentBalance() {
twig("refreshing transparent balance")
_transparentBalances.value = processor.getUtxoCacheBalance(getTransparentAddress())
}
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun isValidAddress(address: String): Boolean {
try {
return !validateAddress(address).isNotValid
@ -394,7 +378,6 @@ class SdkSynchronizer internal constructor(
}
return false
}
*/
private fun CoroutineScope.onReady() = launch(CoroutineExceptionHandler(::onCriticalError)) {
twig("Preparing to start...")
@ -486,7 +469,7 @@ class SdkSynchronizer internal constructor(
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")
if (onChainErrorHandler == null) {
twig(
@ -501,7 +484,7 @@ class SdkSynchronizer internal constructor(
/**
* @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
// refresh anyway if:
// - if it's the first time we finished scanning
@ -523,7 +506,7 @@ class SdkSynchronizer internal constructor(
// balance refresh is complete.
if (shouldRefresh) {
twigTask("Triggering utxo refresh since $reason!", -1) {
//refreshUtxos()
refreshUtxos()
}
twigTask("Triggering balance refresh since $reason!", -1) {
refreshAllBalances()
@ -701,22 +684,17 @@ class SdkSynchronizer internal constructor(
txManager.monitorById(it.id)
}.distinctUntilChanged()
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override suspend fun refreshUtxos(address: String, startHeight: Int): Int? {
override suspend fun refreshUtxos(address: String, startHeight: BlockHeight): Int? {
return processor.refreshUtxos(address, startHeight)
}
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override suspend fun getTransparentBalance(tAddr: String): WalletBalance {
return processor.getUtxoCacheBalance(tAddr)
}
*/
override suspend fun isValidShieldedAddr(address: String) =
txManager.isValidShieldedAddress(address)
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override suspend fun isValidTransparentAddr(address: String) =
txManager.isValidTransparentAddress(address)
@ -737,7 +715,6 @@ class SdkSynchronizer internal constructor(
}
}
}
*/
override suspend fun validateConsensusBranch(): ConsensusMatchType {
val serverBranchId = tryNull { processor.downloader.getServerInfo().consensusBranchId }
@ -797,18 +774,19 @@ object DefaultSynchronizerFactory {
suspend fun defaultTransactionRepository(initializer: Initializer): TransactionRepository =
PagedTransactionRepository.new(
initializer.context,
initializer.network,
DEFAULT_PAGE_SIZE,
initializer.rustBackend,
initializer.birthday,
initializer.checkpoint,
initializer.viewingKeys,
initializer.overwriteVks
)
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 =
LightWalletGrpcService(initializer.context, initializer.host, initializer.port)
LightWalletGrpcService.new(initializer.context, initializer.lightWalletEndpoint)
fun defaultEncoder(
initializer: Initializer,

43
sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt

@ -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.PendingTransaction
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.ZcashNetwork
import cash.z.ecc.android.sdk.type.AddressType
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@ -98,12 +99,12 @@ interface Synchronizer {
* 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.
*/
val networkHeight: StateFlow<Int>
val networkHeight: StateFlow<BlockHeight?>
/**
* 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.
@ -113,7 +114,7 @@ interface Synchronizer {
/**
* A stream of balance values for the transparent pool. Includes the available and total balance.
*/
//val transparentBalances: StateFlow<WalletBalance?>
val transparentBalances: StateFlow<WalletBalance?>
/* Transactions */
@ -145,13 +146,13 @@ interface Synchronizer {
/**
* 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
* transaction has not yet occurred.
*/
val latestBirthdayHeight: Int
val latestBirthdayHeight: BlockHeight?
//
// Operations
@ -238,9 +239,7 @@ interface Synchronizer {
*
* @throws RuntimeException when the address is invalid.
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun isValidTransparentAddr(address: String): Boolean
*/
/**
* 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.
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun validateAddress(address: String): AddressType
*/
/**
* 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
/**
* 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.
*
* @return the number of utxos that were downloaded and addded to the UTXO table.
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun refreshUtxos(
tAddr: String,
sinceHeight: Int = network.saplingActivationHeight
since: BlockHeight = network.saplingActivationHeight
): Int?
*/
/**
* 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 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
@ -326,7 +307,7 @@ interface Synchronizer {
* arbitrary height. This handles all that complexity yet remains flexible in the future as
* improvements are made.
*/
suspend fun rewindToNearestHeight(height: Int, alsoClearBlockCache: Boolean = false)
suspend fun rewindToNearestHeight(height: BlockHeight, alsoClearBlockCache: Boolean = false)
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
* 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.

628
sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt

@ -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.retryWithBackoff
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.TransactionRepository
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask
import cash.z.ecc.android.sdk.jni.RustBackend
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 io.grpc.StatusRuntimeException
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.
*/
@OpenForTesting
class CompactBlockProcessor(
class CompactBlockProcessor internal constructor(
val downloader: CompactBlockDownloader,
private val repository: TransactionRepository,
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.
@ -93,7 +95,7 @@ class CompactBlockProcessor(
* 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.
*/
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
@ -117,12 +119,18 @@ class CompactBlockProcessor(
var onScanMetricCompleteListener: ((BatchMetrics, Boolean) -> Unit)? = null
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 _progress = ConflatedBroadcastChannel(0)
private val _processorInfo = ConflatedBroadcastChannel(ProcessorInfo())
private val _networkHeight = MutableStateFlow(-1)
private val _processorInfo =
ConflatedBroadcastChannel(ProcessorInfo(null, null, null, null, null))
private val _networkHeight = MutableStateFlow<BlockHeight?>(null)
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
* 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.
@ -193,25 +204,38 @@ class CompactBlockProcessor(
processNewBlocks()
}
// immediately process again after failures in order to download new blocks right away
if (result == ERROR_CODE_RECONNECT) {
val napTime = calculatePollInterval(true)
twig("Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime}ms")
delay(napTime)
} else if (result == ERROR_CODE_NONE || result == ERROR_CODE_FAILED_ENHANCE) {
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"
consecutiveChainErrors.set(0)
val napTime = calculatePollInterval()
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}).")
delay(napTime)
} else {
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)
when (result) {
BlockProcessingResult.Reconnecting -> {
val napTime = calculatePollInterval(true)
twig("Unable to process new blocks because we are disconnected! Attempting to reconnect in ${napTime}ms")
delay(napTime)
}
BlockProcessingResult.NoBlocksToProcess, BlockProcessingResult.FailedEnhance -> {
val noWorkDone =
currentInfo.lastDownloadRange?.isEmpty() ?: true && currentInfo.lastScanRange?.isEmpty() ?: true
val summary = if (noWorkDone) {
"Nothing to process: no new blocks to download or scan"
} else {
"Done processing blocks"
}
consecutiveChainErrors.set(0)
val napTime = calculatePollInterval()
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)
@ -238,32 +262,37 @@ class CompactBlockProcessor(
throw error
}
/**
* 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) {
private suspend fun processNewBlocks(): BlockProcessingResult = withContext(IO) {
twig("beginning to process new blocks (with lower bound: $lowerBoundHeight)...", -1)
if (!updateRanges()) {
twig("Disconnection detected! Attempting to reconnect!")
setState(Disconnected)
downloader.lightWalletService.reconnect()
ERROR_CODE_RECONNECT
BlockProcessingResult.Reconnecting
} else if (currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()) {
setState(Scanned(currentInfo.lastScanRange))
ERROR_CODE_NONE
BlockProcessingResult.NoBlocksToProcess
} else {
downloadNewBlocks(currentInfo.lastDownloadRange)
val error = validateAndScanNewBlocks(currentInfo.lastScanRange)
if (error != ERROR_CODE_NONE) error else {
enhanceTransactionDetails(currentInfo.lastScanRange)
if (error != BlockProcessingResult.Success) {
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)
* the scan/download ranges that require processing.
@ -278,19 +307,39 @@ class CompactBlockProcessor(
ProcessorInfo(
networkBlockHeight = downloader.getLatestBlockHeight(),
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 ->
updateProgress(
networkBlockHeight = initialInfo.networkBlockHeight,
lastScannedHeight = initialInfo.lastScannedHeight,
lastDownloadedHeight = initialInfo.lastDownloadedHeight,
lastScanRange = (initialInfo.lastScannedHeight + 1)..initialInfo.networkBlockHeight,
lastDownloadRange = (
max(
initialInfo.lastDownloadedHeight,
initialInfo.lastScannedHeight
) + 1
lastScanRange = if (initialInfo.lastScannedHeight != null && initialInfo.networkBlockHeight != null) {
initialInfo.lastScannedHeight + 1..initialInfo.networkBlockHeight
} else {
null
},
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
} else {
null
}
)
}
true
@ -306,35 +355,34 @@ class CompactBlockProcessor(
* prevHash value matches the preceding block in the chain.
*
* @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) {
setState(Validating)
var error = validateNewBlocks(lastScanRange)
if (error == ERROR_CODE_NONE) {
// in theory, a scan should not fail after validation succeeds but maybe consider
// changing the rust layer to return the failed block height whenever scan does fail
// rather than a boolean
setState(Scanning)
val success = scanNewBlocks(lastScanRange)
if (!success) throw CompactBlockProcessorException.FailedScan()
else {
setState(Scanned(lastScanRange))
*/
private suspend fun validateAndScanNewBlocks(lastScanRange: ClosedRange<BlockHeight>?): BlockProcessingResult =
withContext(IO) {
setState(Validating)
val result = validateNewBlocks(lastScanRange)
if (result == BlockProcessingResult.Success) {
// in theory, a scan should not fail after validation succeeds but maybe consider
// changing the rust layer to return the failed block height whenever scan does fail
// rather than a boolean
setState(Scanning)
val success = scanNewBlocks(lastScanRange)
if (!success) {
throw CompactBlockProcessorException.FailedScan()
} else {
setState(Scanned(lastScanRange))
}
}
ERROR_CODE_NONE
} else {
error
result
}
}
private suspend fun enhanceTransactionDetails(lastScanRange: IntRange): Int {
private suspend fun enhanceTransactionDetails(lastScanRange: ClosedRange<BlockHeight>): BlockProcessingResult {
Twig.sprout("enhancing")
twig("Enhancing transaction details for blocks $lastScanRange")
setState(Enhancing)
return try {
val newTxs = repository.findNewTransactions(lastScanRange)
if (newTxs == null) {
if (newTxs.isEmpty()) {
twig("no new transactions found in $lastScanRange")
} else {
twig("enhancing ${newTxs.size} transaction(s)!")
@ -346,15 +394,18 @@ class CompactBlockProcessor(
}
newTxs?.onEach { newTransaction ->
if (newTransaction == null) twig("somehow, new transaction was null!!!")
else enhance(newTransaction)
if (newTransaction == null) {
twig("somehow, new transaction was null!!!")
} else {
enhance(newTransaction)
}
}
twig("Done enhancing transaction details")
ERROR_CODE_NONE
BlockProcessingResult.Success
} catch (t: Throwable) {
twig("Failed to enhance due to $t")
t.printStackTrace()
ERROR_CODE_FAILED_ENHANCE
BlockProcessingResult.FailedEnhance
} finally {
Twig.clip("enhancing")
}
@ -374,8 +425,11 @@ class CompactBlockProcessor(
} catch (t: Throwable) {
twig("Warning: failure on transaction: error: $t\ttransaction: $transaction")
onProcessorError(
if (downloaded) EnhanceTxDecryptError(transaction.minedHeight, t)
else EnhanceTxDownloadError(transaction.minedHeight, t)
if (downloaded) {
EnhanceTxDecryptError(transaction.minedBlockHeight, t)
} else {
EnhanceTxDownloadError(transaction.minedBlockHeight, t)
}
)
}
}
@ -391,12 +445,18 @@ class CompactBlockProcessor(
else -> {
// verify that the server is correct
downloader.getServerInfo().let { info ->
//val clientBranch = "%x".format(rustBackend.getBranchIdForHeight(info.blockHeight.toInt()))
val clientBranch = "76b809bb"
val network = rustBackend.network.networkName
when {
!info.matchingNetwork(network) -> MismatchedNetwork(clientNetwork = network, serverNetwork = info.chainName)
!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(clientBranch = clientBranch, serverBranch = info.consensusBranchId, networkName = network)
!info.matchingNetwork(network) -> MismatchedNetwork(
clientNetwork = network,
serverNetwork = info.chainName
)
!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(
clientBranch = clientBranch,
serverBranch = info.consensusBranchId,
networkName = network
)
else -> null
}
}
@ -426,34 +486,36 @@ class CompactBlockProcessor(
}
}
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
var failedUtxoFetches = 0
internal suspend fun refreshUtxos(tAddress: String, startHeight: Int): Int? = withContext(IO) {
var count: Int? = null
// todo: cleanup the way that we prevent this from running excessively
// For now, try for about 3 blocks per app launch. If the service fails it is
// probably disabled on ligthtwalletd, so then stop trying until the next app launch.
if (failedUtxoFetches < 9) { // there are 3 attempts per block
try {
retryUpTo(3) {
val result = downloader.lightWalletService.fetchUtxos(tAddress, startHeight)
count = processUtxoResult(result, tAddress, startHeight)
internal suspend fun refreshUtxos(tAddress: String, startHeight: BlockHeight): Int? =
withContext(IO) {
var count: Int? = null
// todo: cleanup the way that we prevent this from running excessively
// For now, try for about 3 blocks per app launch. If the service fails it is
// probably disabled on ligthtwalletd, so then stop trying until the next app launch.
if (failedUtxoFetches < 9) { // there are 3 attempts per block
try {
retryUpTo(3) {
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) {
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.")
} else {
twig("Warning: gave up on fetching UTXOs for this session. It seems to unavailable on lightwalletd.")
}
} else {
twig("Warning: gave up on fetching UTXOs for this session. It seems to unavailable on lightwalletd.")
count
}
count
}
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
internal suspend fun processUtxoResult(result: List<Service.GetAddressUtxosReply>, tAddress: String, startHeight: Int): Int = withContext(IO) {
internal suspend fun processUtxoResult(
result: List<Service.GetAddressUtxosReply>,
tAddress: String,
startHeight: BlockHeight
): Int = withContext(IO) {
var skipped = 0
val aboveHeight = startHeight - 1
val aboveHeight = startHeight
twig("Clearing utxos above height $aboveHeight", -1)
rustBackend.clearUtxos(tAddress, aboveHeight)
twig("Checking for UTXOs above height $aboveHeight")
@ -466,7 +528,7 @@ class CompactBlockProcessor(
utxo.index,
utxo.script.toByteArray(),
utxo.valueZat,
utxo.height.toInt()
BlockHeight(utxo.height)
)
} catch (t: Throwable) {
// 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
result.size - skipped
}
*/
/**
* 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.
*/
@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) {
if (range.isEmpty()) {
twig("no blocks to download")
} else {
_state.send(Downloading)
Twig.sprout("downloading")
twig("downloading blocks in range $range", -1)
var downloadedBlockHeight = range.first
val missingBlockCount = range.last - range.first + 1
val batches = (
missingBlockCount / DOWNLOAD_BATCH_SIZE +
(if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0) 0 else 1)
)
var progress: Int
twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...")
for (i in 1..batches) {
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
var count = 0
twig("downloaded $downloadedBlockHeight..$end (batch $i of $batches) [${downloadedBlockHeight..end}]") {
count = downloader.downloadBlockRange(downloadedBlockHeight..end)
internal suspend fun downloadNewBlocks(range: ClosedRange<BlockHeight>?) =
withContext<Unit>(IO) {
if (null == range || range.isEmpty()) {
twig("no blocks to download")
} else {
_state.send(Downloading)
Twig.sprout("downloading")
twig("downloading blocks in range $range", -1)
var downloadedBlockHeight = range.start
val missingBlockCount = range.endInclusive.value - range.start.value + 1
val batches = (
missingBlockCount / DOWNLOAD_BATCH_SIZE +
(if (missingBlockCount.rem(DOWNLOAD_BATCH_SIZE) == 0L) 0 else 1)
)
var progress: Int
twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...")
for (i in 1..batches) {
retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) {
val end = BlockHeight.new(
network,
min(
(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
* 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.
*
* @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 {
if (range?.isEmpty() != false) {
private suspend fun validateNewBlocks(range: ClosedRange<BlockHeight>?): BlockProcessingResult {
if (null == range || range.isEmpty()) {
twig("no blocks to validate: $range")
return ERROR_CODE_NONE
return BlockProcessingResult.NoBlocksToProcess
}
Twig.sprout("validating")
twig("validating blocks in range $range in db: ${(rustBackend as RustBackend).pathCacheDb}")
val result = rustBackend.validateCombinedChain()
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
* wallet.
* wallet. Scanning starts at the back of the chain and works toward the tip.
*
* @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) {
if (range?.isEmpty() != false) {
private suspend fun scanNewBlocks(range: ClosedRange<BlockHeight>?): Boolean = withContext(IO) {
if (null == range || range.isEmpty()) {
twig("no blocks to scan for range $range")
true
} else {
@ -570,16 +637,18 @@ class CompactBlockProcessor(
metrics.beginBatch()
result = rustBackend.scanBlocks(SCAN_BATCH_SIZE)
metrics.endBatch()
val lastScannedHeight = range.start + metrics.cumulativeItems - 1
val percentValue = (lastScannedHeight - range.first) / (range.last - range.first + 1).toFloat() * 100.0f
val lastScannedHeight =
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))
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) {
scannedNewBlocks = true
updateProgress(lastScannedHeight = lastScannedHeight)
}
// 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.clip("scanning")
@ -605,12 +674,12 @@ class CompactBlockProcessor(
* blocks that we don't yet have.
*/
private suspend fun updateProgress(
networkBlockHeight: Int = currentInfo.networkBlockHeight,
lastScannedHeight: Int = currentInfo.lastScannedHeight,
lastDownloadedHeight: Int = currentInfo.lastDownloadedHeight,
lastScanRange: IntRange = currentInfo.lastScanRange,
lastDownloadRange: IntRange = currentInfo.lastDownloadRange
): Unit = withContext(IO) {
networkBlockHeight: BlockHeight? = currentInfo.networkBlockHeight,
lastScannedHeight: BlockHeight? = currentInfo.lastScannedHeight,
lastDownloadedHeight: BlockHeight? = currentInfo.lastDownloadedHeight,
lastScanRange: ClosedRange<BlockHeight>? = currentInfo.lastScanRange,
lastDownloadRange: ClosedRange<BlockHeight>? = currentInfo.lastDownloadRange
) {
currentInfo = currentInfo.copy(
networkBlockHeight = networkBlockHeight,
lastScannedHeight = lastScannedHeight,
@ -618,11 +687,14 @@ class CompactBlockProcessor(
lastScanRange = lastScanRange,
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
printValidationErrorInfo(errorHeight)
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
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) {
originalCheckpoint
} else {
// 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() {
val height = max(currentInfo.lastScannedHeight, repository.lastScannedHeight())
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)
}
@ -657,45 +735,73 @@ class CompactBlockProcessor(
* @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.
*/
suspend fun rewindToNearestHeight(height: Int, alsoClearBlockCache: Boolean = false) = withContext(IO) {
processingMutex.withLockLogged("rewindToHeight") {
val lastScannedHeight = currentInfo.lastScannedHeight
val lastLocalBlock = repository.lastScannedHeight()
val targetHeight = getNearestRewindHeight(height)
twig("Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock")
if (targetHeight < lastScannedHeight || (lastScannedHeight == -1 && (targetHeight < lastLocalBlock))) {
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")
}
suspend fun rewindToNearestHeight(
height: BlockHeight,
alsoClearBlockCache: Boolean = false
) =
withContext(IO) {
processingMutex.withLockLogged("rewindToHeight") {
val lastScannedHeight = currentInfo.lastScannedHeight
val lastLocalBlock = repository.lastScannedHeight()
val targetHeight = getNearestRewindHeight(height)
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) {
twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan")
downloader.rewindToHeight(targetHeight)
// 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
setState(Downloading)
updateProgress(
lastScannedHeight = targetHeight,
lastDownloadedHeight = targetHeight,
lastScanRange = (targetHeight + 1)..currentInfo.networkBlockHeight,
lastDownloadRange = (targetHeight + 1)..currentInfo.networkBlockHeight
)
_progress.send(0)
} else {
updateProgress(
lastScannedHeight = targetHeight,
lastScanRange = (targetHeight + 1)..currentInfo.networkBlockHeight
)
_progress.send(0)
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.first}..${range.last}")
if (validateAndScanNewBlocks(range) == ERROR_CODE_NONE) enhanceTransactionDetails(range)
val currentNetworkBlockHeight = currentInfo.networkBlockHeight
if (alsoClearBlockCache) {
twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan")
downloader.rewindToHeight(targetHeight)
// 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
setState(Downloading)
if (null == currentNetworkBlockHeight) {
updateProgress(
lastScannedHeight = targetHeight,
lastDownloadedHeight = targetHeight,
lastScanRange = null,
lastDownloadRange = null
)
} else {
updateProgress(
lastScannedHeight = targetHeight,
lastDownloadedHeight = targetHeight,
lastScanRange = (targetHeight + 1)..currentNetworkBlockHeight,
lastDownloadRange = (targetHeight + 1)..currentNetworkBlockHeight
)
}
_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 */
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
if (!BuildConfig.DEBUG) return
@ -704,19 +810,25 @@ class CompactBlockProcessor(
errorInfo = fetchValidationErrorInfo(errorHeight + 1)
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 ->
val height = errorHeight + i
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.
val hash = block?.hash?.toByteArray() ?: (repository as PagedTransactionRepository).findBlockHash(height)
twig("block: $height\thash=${hash?.toHexReversed()} \tprevHash=${block?.prevHash?.toByteArray()?.toHexReversed()}")
val hash = block?.hash?.toByteArray()
?: (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 {
val hash = (repository as PagedTransactionRepository).findBlockHash(errorHeight + 1)?.toHexReversed()
private suspend fun fetchValidationErrorInfo(errorHeight: BlockHeight): ValidationErrorInfo {
val hash = (repository as PagedTransactionRepository).findBlockHash(errorHeight + 1)
?.toHexReversed()
val prevHash = repository.findBlockHash(errorHeight)?.toHexReversed()
val compactBlock = downloader.compactBlockStore.findCompactBlock(errorHeight + 1)
@ -734,9 +846,9 @@ class CompactBlockProcessor(
return onProcessorErrorListener?.invoke(throwable) ?: true
}
private fun determineLowerBound(errorHeight: Int): Int {
val offset = Math.min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1))
return Math.max(errorHeight - offset, lowerBoundHeight).also {
private fun determineLowerBound(errorHeight: BlockHeight): BlockHeight {
val offset = min(MAX_REORG_SIZE, REWIND_DISTANCE * (consecutiveChainErrors.get() + 1))
return BlockHeight(max(errorHeight.value - offset, lowerBoundHeight.value)).also {
twig("offset = min($MAX_REORG_SIZE, $REWIND_DISTANCE * (${consecutiveChainErrors.get() + 1})) = $offset")
twig("lowerBound = max($errorHeight - $offset, $lowerBoundHeight) = $it")
}
@ -759,19 +871,28 @@ class CompactBlockProcessor(
return deltaToNextInteral
}
suspend fun calculateBirthdayHeight(): Int {
var oldestTransactionHeight = 0
suspend fun calculateBirthdayHeight(): BlockHeight {
var oldestTransactionHeight: BlockHeight? = null
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)
// 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.let { it - it.rem(boundary) - boundary }
}
oldestTransactionHeight = BlockHeight.new(
network,
tempOldestTransactionHeight.value - tempOldestTransactionHeight.value.rem(ZcashSdk.MAX_REORG_SIZE) - ZcashSdk.MAX_REORG_SIZE.toLong()
)
} catch (t: Throwable) {
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.
@ -869,7 +991,7 @@ class CompactBlockProcessor(
/**
* [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
@ -912,11 +1034,11 @@ class CompactBlockProcessor(
* @param lastScanRange inclusive range to scan.
*/
data class ProcessorInfo(
val networkBlockHeight: Int = -1,
val lastScannedHeight: Int = -1,
val lastDownloadedHeight: Int = -1,
val lastDownloadRange: IntRange = 0..-1, // empty range
val lastScanRange: IntRange = 0..-1 // empty range
val networkBlockHeight: BlockHeight?,
val lastScannedHeight: BlockHeight?,
val lastDownloadedHeight: BlockHeight?,
val lastDownloadRange: ClosedRange<BlockHeight>?,
val lastScanRange: ClosedRange<BlockHeight>?
) {
/**
@ -924,19 +1046,24 @@ class CompactBlockProcessor(
*
* @return false when all values match their defaults.
*/
val hasData get() = networkBlockHeight != -1 ||
lastScannedHeight != -1 ||
lastDownloadedHeight != -1 ||
lastDownloadRange != 0..-1 ||
lastScanRange != 0..-1
val hasData
get() = networkBlockHeight != null ||
lastScannedHeight != null ||
lastDownloadedHeight != null ||
lastDownloadRange != null ||
lastScanRange != null
/**
* Determines whether this instance is actively downloading compact blocks.
*
* @return true when there are more than zero blocks remaining to download.
*/
val isDownloading: Boolean get() = !lastDownloadRange.isEmpty() &&
lastDownloadedHeight < lastDownloadRange.last
val isDownloading: Boolean
get() =
lastDownloadedHeight != null &&
lastDownloadRange != null &&
!lastDownloadRange.isEmpty() &&
lastDownloadedHeight < lastDownloadRange.endInclusive
/**
* 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
* to be scanned.
*/
val isScanning: Boolean get() = !isDownloading &&
!lastScanRange.isEmpty() &&
lastScannedHeight < lastScanRange.last
val isScanning: Boolean
get() =
!isDownloading &&
lastScannedHeight != null &&
lastScanRange != null &&
!lastScanRange.isEmpty() &&
lastScannedHeight < lastScanRange.endInclusive
/**
* The amount of scan progress from 0 to 100.
*/
val scanProgress get() = when {
lastScannedHeight <= -1 -> 0
lastScanRange.isEmpty() -> 100
lastScannedHeight >= lastScanRange.last -> 100
else -> {
// when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets
val blocksScanned = (lastScannedHeight - lastScanRange.first + 1).coerceAtLeast(0)
// we scan the range inclusively so 100..100 is one block to scan, thus the offset
val numberOfBlocks = lastScanRange.last - lastScanRange.first + 1
// take the percentage then convert and round
((blocksScanned.toFloat() / numberOfBlocks) * 100.0f).let { percent ->
percent.coerceAtMost(100.0f).roundToInt()
val scanProgress
get() = when {
lastScannedHeight == null -> 0
lastScanRange == null -> 100
lastScannedHeight >= lastScanRange.endInclusive -> 100
else -> {
// when lastScannedHeight == lastScanRange.first, we have scanned one block, thus the offsets
val blocksScanned =
(lastScannedHeight.value - lastScanRange.start.value + 1).coerceAtLeast(0)
// we scan the range inclusively so 100..100 is one block to scan, thus the offset
val numberOfBlocks =
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(
val errorHeight: Int,
val errorHeight: BlockHeight,
val hash: String?,
val expectedPrevHash: String?,
val actualPrevHash: String?
@ -1007,10 +1141,12 @@ class CompactBlockProcessor(
}
twig("$name MUTEX: withLock complete", -1)
}
}
companion object {
const val ERROR_CODE_NONE = -1
const val ERROR_CODE_RECONNECT = 20
const val ERROR_CODE_FAILED_ENHANCE = 40
}
private fun max(a: BlockHeight?, b: BlockHeight) = if (null == a) {
b
} else if (a.value > b.value) {
a
} else {
b
}

4
sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/CompactBlockEntity.kt

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

56
sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt

@ -5,6 +5,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi
//
@ -81,8 +82,8 @@ data class PendingTransactionEntity(
override val value: Long = -1,
override val memo: ByteArray? = byteArrayOf(),
override val accountIndex: Int,
override val minedHeight: Int = -1,
override val expiryHeight: Int = -1,
override val minedHeight: Long = -1,
override val expiryHeight: Long = -1,
override val cancelled: Int = 0,
override val encodeAttempts: Int = -1,
@ -96,6 +97,10 @@ data class PendingTransactionEntity(
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
override val rawTransactionId: ByteArray? = byteArrayOf()
) : PendingTransaction {
val valueZatoshi: Zatoshi
get() = Zatoshi(value)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is PendingTransactionEntity) return false
@ -131,8 +136,8 @@ data class PendingTransactionEntity(
result = 31 * result + value.hashCode()
result = 31 * result + (memo?.contentHashCode() ?: 0)
result = 31 * result + accountIndex
result = 31 * result + minedHeight
result = 31 * result + expiryHeight
result = 31 * result + minedHeight.hashCode()
result = 31 * result + expiryHeight.hashCode()
result = 31 * result + cancelled
result = 31 * result + encodeAttempts
result = 31 * result + submitAttempts
@ -159,7 +164,7 @@ data class ConfirmedTransaction(
override val memo: ByteArray? = ByteArray(0),
override val noteId: Long = 0L,
override val blockTimeInSeconds: Long = 0L,
override val minedHeight: Int = -1,
override val minedHeight: Long = -1,
override val transactionIndex: Int,
override val rawTransactionId: ByteArray = ByteArray(0),
@ -169,6 +174,13 @@ data class ConfirmedTransaction(
override val raw: ByteArray? = byteArrayOf()
) : MinedTransaction, SignedTransaction {
val minedBlockHeight
get() = if (minedHeight == -1L) {
null
} else {
BlockHeight(minedHeight)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ConfirmedTransaction) return false
@ -200,7 +212,7 @@ data class ConfirmedTransaction(
result = 31 * result + (memo?.contentHashCode() ?: 0)
result = 31 * result + noteId.hashCode()
result = 31 * result + blockTimeInSeconds.hashCode()
result = 31 * result + minedHeight
result = 31 * result + minedHeight.hashCode()
result = 31 * result + transactionIndex
result = 31 * result + rawTransactionId.contentHashCode()
result = 31 * result + (toAddress?.hashCode() ?: 0)
@ -213,8 +225,12 @@ data class ConfirmedTransaction(
val ConfirmedTransaction.valueInZatoshi
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 {
val expiryBlockHeight
get() = expiryHeight?.let { BlockHeight(it) }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is EncodedTransaction) return false
@ -229,7 +245,7 @@ data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray,
override fun hashCode(): Int {
var result = txId.contentHashCode()
result = 31 * result + raw.contentHashCode()
result = 31 * result + (expiryHeight ?: 0)
result = 31 * result + (expiryHeight?.hashCode() ?: 0)
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.
*/
interface MinedTransaction : Transaction {
val minedHeight: Int
val minedHeight: Long
val noteId: Long
val blockTimeInSeconds: Long
val transactionIndex: Int
@ -273,8 +289,8 @@ interface PendingTransaction : SignedTransaction, Transaction {
override val memo: ByteArray?
val toAddress: String
val accountIndex: Int
val minedHeight: Int
val expiryHeight: Int
val minedHeight: Long // apparently this can be -1 as an uninitialized value
val expiryHeight: Long // apparently this can be -1 as an uninitialized value
val cancelled: Int
val encodeAttempts: Int
val submitAttempts: Int
@ -333,16 +349,16 @@ fun PendingTransaction.isSubmitted(): Boolean {
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 <
if (latestHeight == null || latestHeight < saplingActivationHeight || expiryHeight < saplingActivationHeight) return false
return expiryHeight < latestHeight
if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false
return expiryHeight < latestHeight.value
}
// 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 {
if (latestHeight == null || latestHeight < saplingActivationHeight || expiryHeight < saplingActivationHeight) return false
return (latestHeight - expiryHeight) > 100
fun PendingTransaction.isLongExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean {
if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false
return (latestHeight.value - expiryHeight) > 100
}
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
return !isSubmitSuccess() && minedHeight == -1 &&
(expiryHeight == -1 || expiryHeight > currentHeight) && raw != null
return !isSubmitSuccess() && minedHeight == -1L &&
(expiryHeight == -1L || expiryHeight > (currentHeight?.value ?: 0L)) && raw != null
}
fun PendingTransaction.isSubmitSuccess(): Boolean {

166
sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt

@ -1,6 +1,8 @@
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 io.grpc.Status
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
* 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) :
SdkException(message, cause) {
sealed class RustLayerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class BalanceException(cause: Throwable) : RustLayerException(
"Error while requesting the current balance over " +
"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.
*/
sealed class RepositoryException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class RepositoryException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object FalseStart : RepositoryException(
"The channel is closed. Note that once a repository has stopped it " +
"cannot be restarted. Verify that the repository is not being restarted."
)
object Unprepared : RepositoryException(
"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" +
@ -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
* child component.
*/
sealed class SynchronizerException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class SynchronizerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object FalseStart : SynchronizerException(
"This synchronizer was already started. Multiple calls to start are not" +
"allowed and once a synchronizer has stopped it cannot be restarted."
)
object NotYetStarted : SynchronizerException(
"The synchronizer has not yet started. Verify that" +
" 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.
*/
sealed class CompactBlockProcessorException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class CompactBlockProcessorException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class DataDbMissing(path: String) : CompactBlockProcessorException(
"No data db file found at path $path. Verify " +
"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(
"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" +
@ -83,137 +76,100 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
" So pass in context.getDatabasePath(dbFileName).absolutePath instead of just dbFileName alone.",
null
)
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
class FailedDownload(cause: Throwable? = null) : CompactBlockProcessorException(
"Error while downloading blocks. This most " +
"likely means the server is down or slow to respond. See logs for details.",
cause
)
class FailedScan(cause: Throwable? = null) : CompactBlockProcessorException(
"Error while scanning blocks. This most " +
"likely means a block was missed or a reorg was mishandled. See logs for details.",
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(
"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" +
" can be fixed by re-importing the wallet."
)
object NoAccount : CompactBlockProcessorException(
"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) :
CompactBlockProcessorException(message, cause) {
class EnhanceTxDownloadError(height: Int, cause: Throwable) : EnhanceTransactionError(
"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
)
open class EnhanceTransactionError(message: String, val height: BlockHeight?, cause: Throwable) : CompactBlockProcessorException(message, cause) {
class EnhanceTxDownloadError(height: BlockHeight?, cause: Throwable) : EnhanceTransactionError("Error while attempting to download a transaction to enhance", height, cause)
class EnhanceTxDecryptError(height: BlockHeight?, cause: Throwable) : EnhanceTransactionError("Error while attempting to decrypt and store a transaction to enhance", height, cause)
}
class MismatchedNetwork(clientNetwork: String?, serverNetwork: String?) :
CompactBlockProcessorException(
"Incompatible server: this client expects a server using $clientNetwork but it was $serverNetwork! Try updating the client or switching servers."
)
class MismatchedNetwork(clientNetwork: String?, serverNetwork: String?) : CompactBlockProcessorException(
"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?) :
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."
)
class MismatchedBranch(clientBranch: String?, serverBranch: String?, networkName: String?) : 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."
)
}
/**
* Exceptions related to the wallet's birthday.
*/
sealed class BirthdayException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object UninitializedBirthdayException : BirthdayException(
"Error the birthday cannot be" +
" accessed before it is initialized. Verify that the new, import or open functions" +
" have been called on the initializer."
)
class MissingBirthdayFilesException(directory: String) : BirthdayException(
"Cannot initialize wallet because no birthday files were found in the $directory directory."
)
class ExactBirthdayNotFoundException(height: Int, nearestMatch: Int? = null) :
BirthdayException(
"Unable to find birthday that exactly matches $height.${
if (nearestMatch != null) {
" An exact match was request but the nearest match found was $nearestMatch."
} else {
""
}
}"
)
class BirthdayFileNotFoundException(directory: String, height: Int?) : BirthdayException(
class ExactBirthdayNotFoundException internal constructor(birthday: BlockHeight, nearestMatch: Checkpoint? = null) : BirthdayException(
"Unable to find birthday that exactly matches $birthday.${
if (nearestMatch != null) {
" An exact match was request but the nearest match found was ${nearestMatch.height}."
} else ""
}"
)
class BirthdayFileNotFoundException(directory: String, height: BlockHeight?) : BirthdayException(
"Unable to find birthday file for $height verify that $directory/$height.json exists."
)
class MalformattedBirthdayFilesException(directory: String, file: String, cause: Throwable?) :
BirthdayException(
"Failed to parse file $directory/$file verify that it is formatted as #####.json, " +
"where the first portion is an Int representing the height of the tree contained in the file",
cause
)
class MalformattedBirthdayFilesException(directory: String, file: String, cause: Throwable?) : BirthdayException(
"Failed to parse file $directory/$file verify that it is formatted as #####.json, " +
"where the first portion is an Int representing the height of the tree contained in the file",
cause
)
}
/**
* Exceptions thrown by the initializer.
*/
sealed class InitializerException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
class FalseStart(cause: Throwable?) :
InitializerException("Failed to initialize accounts due to: $cause", cause)
sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause)
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException(
"Failed to initialize the blocks table" +
" because it already exists in $dbPath",
cause
)
object MissingBirthdayException : InitializerException(
"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 " +
"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."
)
object MissingViewingKeyException : InitializerException(
"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" +
" 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."
)
class MissingAddressException(description: String, cause: Throwable? = null) :
InitializerException(
"Expected a $description address for this wallet but failed to find one. This usually" +
" means that wallet setup happened incorrectly. If this problem persists, a" +
" workaround might be to go to settings and WIPE the wallet and rescan. Doing so" +
" will restore any missing address information. Meanwhile, please report that" +
" this happened so that the root issue can be uncovered and corrected." +
if (cause != null) "\nCaused by: $cause" else ""
)
class MissingAddressException(description: String, cause: Throwable? = null) : InitializerException(
"Expected a $description address for this wallet but failed to find one. This usually" +
" means that wallet setup happened incorrectly. If this problem persists, a" +
" workaround might be to go to settings and WIPE the wallet and rescan. Doing so" +
" will restore any missing address information. Meanwhile, please report that" +
" this happened so that the root issue can be uncovered and corrected." +
if (cause != null) "\nCaused by: $cause" else ""
)
object DatabasePathException :
InitializerException(
"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."
)
class InvalidBirthdayHeightException(height: Int?, network: ZcashNetwork) :
InitializerException(
"Invalid birthday height of $height. The birthday height must be at least the height of" +
" Sapling activation on ${network.networkName} (${network.saplingActivationHeight})."
)
class InvalidBirthdayHeightException(birthday: BlockHeight?, network: ZcashNetwork) : InitializerException(
"Invalid birthday height of ${birthday?.value}. The birthday height must be at least the height of" +
" Sapling activation on ${network.networkName} (${network.saplingActivationHeight})."
)
object MissingDefaultBirthdayException : InitializerException(
"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.
*/
sealed class LightWalletException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
sealed class LightWalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
object InsecureConnection : LightWalletException(
"Error: attempted to connect to lightwalletd" +
" with an insecure connection! Plaintext connections are only allowed when the" +
" resource value for 'R.bool.lightwalletd_allow_very_insecure_connections' is true" +
" because this choice should be explicit."
)
class ConsensusBranchException(sdkBranch: String, lwdBranch: String) :
LightWalletException(
"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."
)
open class ChangeServerException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
class ChainInfoNotMatching(
val propertyNames: String,
val expectedInfo: Service.LightdInfo,
val actualInfo: Service.LightdInfo
) : ChangeServerException(
open class ChangeServerException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class ChainInfoNotMatching(val propertyNames: String, val expectedInfo: Service.LightdInfo, val actualInfo: Service.LightdInfo) : ChangeServerException(
"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 {
private fun Status.toMessage(): String {
return when (this.code) {
@ -282,29 +228,23 @@ sealed class LightWalletException(message: String, cause: Throwable? = null) :
/**
* Potentially user-facing exceptions thrown while encoding transactions.
*/
sealed class TransactionEncoderException(message: String, cause: Throwable? = null) :
SdkException(message, cause) {
class FetchParamsException(message: String) :
TransactionEncoderException("Failed to fetch params due to: $message")
sealed class TransactionEncoderException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
class FetchParamsException(message: String) : TransactionEncoderException("Failed to fetch params due to: $message")
object MissingParamsException : TransactionEncoderException(
"Cannot send funds due to missing spend or output params and attempting to download them failed."
)
class TransactionNotFoundException(transactionId: Long) : TransactionEncoderException(
"Unable to find transactionId " +
"$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 " +
"to do so."
)
class TransactionNotEncodedException(transactionId: Long) : TransactionEncoderException(
"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" +
" an exception but failed to do so."
)
class IncompleteScanException(lastScannedHeight: Int) : TransactionEncoderException(
class IncompleteScanException(lastScannedHeight: BlockHeight) : TransactionEncoderException(
"Cannot" +
" 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" +

11
sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/BatchMetrics.kt

@ -1,23 +1,24 @@
package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.model.BlockHeight
import kotlin.math.max
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 rangeStartTime = 0L
private var batchStartTime = 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 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 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 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 cumulativeIps get() = ips(cumulativeItems, cumulativeTime)

7
sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ConsensusBranchId.kt

@ -9,7 +9,12 @@ import java.util.Locale
*/
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)
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

5
sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdk.kt

@ -34,7 +34,10 @@ object ZcashSdk {
/**
* 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

46
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/CheckpointExt.kt

@ -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")
}
}
}

5
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/IsEmpty.kt

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

1
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Twig.kt

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

37
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/WalletBirthdayExt.kt

@ -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")
}
}
}

28
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDbStore.kt

@ -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.SdkExecutors
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 kotlinx.coroutines.withContext
import kotlin.math.max
/**
* 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.
*/
class CompactBlockDbStore private constructor(
private val network: ZcashNetwork,
private val cacheDb: CompactBlockDb
) : CompactBlockStore {
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? =
cacheDao.findCompactBlock(height)?.let { CompactFormats.CompactBlock.parseFrom(it) }
override suspend fun findCompactBlock(height: BlockHeight): CompactFormats.CompactBlock? =
cacheDao.findCompactBlock(height.value)?.let { CompactFormats.CompactBlock.parseFrom(it) }
override suspend fun write(result: List<CompactFormats.CompactBlock>) =
cacheDao.insert(result.map { CompactBlockEntity(it.height.toInt(), it.toByteArray()) })
override suspend fun write(result: Sequence<CompactFormats.CompactBlock>) =
cacheDao.insert(result.map { CompactBlockEntity(it.height, it.toByteArray()) })
override suspend fun rewindTo(height: Int) =
cacheDao.rewindTo(height)
override suspend fun rewindTo(height: BlockHeight) =
cacheDao.rewindTo(height.value)
override suspend fun close() {
withContext(SdkDispatchers.DATABASE_IO) {
@ -43,10 +47,14 @@ class CompactBlockDbStore private constructor(
* @param appContext the application context. This is used for creating 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)
return CompactBlockDbStore(cacheDb)
return CompactBlockDbStore(zcashNetwork, cacheDb)
}
private fun createCompactBlockCacheDb(

6
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDownloader.kt

@ -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.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.wallet.sdk.rpc.Service
import io.grpc.StatusRuntimeException
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.
*/
suspend fun downloadBlockRange(heightRange: IntRange): Int = withContext(IO) {
suspend fun downloadBlockRange(heightRange: ClosedRange<BlockHeight>): Int = withContext(IO) {
val result = lightWalletService.getBlockRange(heightRange)
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.
*/
suspend fun rewindToHeight(height: Int) =
suspend fun rewindToHeight(height: BlockHeight) =
// TODO: cancel anything in flight
compactBlockStore.rewindTo(height)

10
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockStore.kt

@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.internal.block
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.wallet.sdk.rpc.CompactFormats
/**
@ -11,28 +12,29 @@ interface CompactBlockStore {
*
* @return the latest block height.
*/
suspend fun getLatestHeight(): Int
suspend fun getLatestHeight(): BlockHeight?
/**
* Fetch the compact block for the given height, if it exists.
*
* @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.
*
* @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.
*
* @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.

19
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CompactBlockDb.kt

@ -6,6 +6,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RoomDatabase
import androidx.room.Transaction
import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity
//
@ -42,12 +43,24 @@ interface CompactBlockDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
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")
suspend fun rewindTo(height: Int)
suspend fun rewindTo(height: Long)
@Query("SELECT MAX(height) FROM compactblocks")
suspend fun latestBlockHeight(): Int
suspend fun latestBlockHeight(): Long
@Query("SELECT data FROM compactblocks WHERE height = :height")
suspend fun findCompactBlock(height: Int): ByteArray?
suspend fun findCompactBlock(height: Long): ByteArray?
}

14
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt

@ -198,13 +198,13 @@ interface BlockDao {
suspend fun count(): Int
@Query("SELECT MAX(height) FROM blocks")
suspend fun lastScannedHeight(): Int
suspend fun lastScannedHeight(): Long
@Query("SELECT MIN(height) FROM blocks")
suspend fun firstScannedHeight(): Int
suspend fun firstScannedHeight(): Long
@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
"""
)
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.
@ -418,7 +418,7 @@ interface TransactionDao {
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
// This should probably be a rust call but there's not a lot of bandwidth for this
@ -474,7 +474,7 @@ interface TransactionDao {
}
@Transaction
suspend fun deleteExpired(lastHeight: Int): Int {
suspend fun deleteExpired(lastHeight: Long): Int {
var count = 0
findExpiredTxs(lastHeight).forEach { transactionId ->
if (removeInvalidOutboundTransaction(transactionId)) count++
@ -537,5 +537,5 @@ interface TransactionDao {
AND expiry_height < :lastheight
"""
)
suspend fun findExpiredTxs(lastheight: Int): List<Long>
suspend fun findExpiredTxs(lastheight: Long): List<Long>
}

4
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/PendingTransactionDb.kt

@ -70,10 +70,10 @@ interface PendingTransactionDao {
suspend fun removeRawTransactionId(id: Long)
@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")
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")
suspend fun updateError(id: Long, errorMessage: String?, errorCode: Int?)

22
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/Checkpoint.kt

@ -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
}

190
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletGrpcService.kt

@ -1,11 +1,10 @@
package cash.z.ecc.android.sdk.internal.service
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.exception.LightWalletException
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.CompactTxStreamerGrpc
import cash.z.wallet.sdk.rpc.Service
@ -15,69 +14,50 @@ import io.grpc.ConnectivityState
import io.grpc.ManagedChannel
import io.grpc.android.AndroidChannelBuilder
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/**
* Implementation of LightwalletService using gRPC for requests to lightwalletd.
*
* @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.
* @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
* now.
*/
@OpenForTesting
class LightWalletGrpcService private constructor(
context: Context,
private val lightWalletEndpoint: LightWalletEndpoint,
var channel: ManagedChannel,
private val singleRequestTimeoutSec: Long = 10L,
private val streamingRequestTimeoutSec: Long = 90L
private val singleRequestTimeout: Duration = 10.seconds,
private val streamingRequestTimeout: Duration = 90.seconds
) : LightWalletService {
lateinit var connectionInfo: ConnectionInfo
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)
}
private val applicationContext = context.applicationContext
/* LightWalletService implementation */
override fun getBlockRange(heightRange: IntRange): List<CompactFormats.CompactBlock> {
if (heightRange.isEmpty()) return listOf()
override fun getBlockRange(heightRange: ClosedRange<BlockHeight>): Sequence<CompactFormats.CompactBlock> {
if (heightRange.isEmpty()) {
return emptySequence()
}
return requireChannel().createStub(streamingRequestTimeoutSec)
.getBlockRange(heightRange.toBlockRange()).toList()
return requireChannel().createStub(streamingRequestTimeout)
.getBlockRange(heightRange.toBlockRange()).iterator().asSequence()
}
override fun getLatestBlockHeight(): Int {
return requireChannel().createStub(singleRequestTimeoutSec)
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height.toInt()
override fun getLatestBlockHeight(): BlockHeight {
return BlockHeight(
requireChannel().createStub(singleRequestTimeout)
.getLatestBlock(Service.ChainSpec.newBuilder().build()).height
)
}
override fun getServerInfo(): Service.LightdInfo {
return requireChannel().createStub(singleRequestTimeoutSec)
return requireChannel().createStub(singleRequestTimeout)
.getLightdInfo(Service.Empty.newBuilder().build())
}
@ -109,23 +89,20 @@ class LightWalletGrpcService private constructor(
)
}
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override fun fetchUtxos(
tAddress: String,
startHeight: Int
startHeight: BlockHeight
): List<Service.GetAddressUtxosReply> {
val result = requireChannel().createStub().getAddressUtxos(
Service.GetAddressUtxosArg.newBuilder().setAddress(tAddress)
.setStartHeight(startHeight.toLong()).build()
.setStartHeight(startHeight.value).build()
)
return result.addressUtxosList
}
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override fun getTAddressTransactions(
tAddress: String,
blockHeightRange: IntRange
blockHeightRange: ClosedRange<BlockHeight>
): List<Service.RawTransaction> {
if (blockHeightRange.isEmpty() || tAddress.isBlank()) return listOf()
@ -135,75 +112,38 @@ class LightWalletGrpcService private constructor(
)
return result.toList()
}
*/
override fun reconnect() {
twig(
"closing existing channel and then reconnecting to ${connectionInfo.host}:" +
"${connectionInfo.port}?usePlaintext=${connectionInfo.usePlaintext}"
)
twig("closing existing channel and then reconnecting")
channel.shutdown()
channel = createDefaultChannel(
connectionInfo.appContext,
connectionInfo.host,
connectionInfo.port,
true
)
channel = createDefaultChannel(applicationContext, lightWalletEndpoint)
}
// test code
var stateCount = 0
var state: ConnectivityState? = null
internal var stateCount = 0
internal var state: ConnectivityState? = null
private fun requireChannel(): ManagedChannel {
state = channel.getState(false).let { new ->
if (state == new) stateCount++ else stateCount = 0
new
}
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
}
//
// 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()
}
}
companion object {
fun new(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletGrpcService {
val channel = createDefaultChannel(context, lightWalletEndpoint)
inner class ConnectionInfo(
val appContext: Context,
val host: String,
val port: Int,
val usePlaintext: Boolean
) {
override fun toString(): String {
return "$host:$port?usePlaintext=true"
return LightWalletGrpcService(context, lightWalletEndpoint, channel)
}
}
companion object {
/**
* 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
@ -211,27 +151,53 @@ class LightWalletGrpcService private constructor(
*/
fun createDefaultChannel(
appContext: Context,
host: String,
port: Int,
usePlaintext: Boolean
lightWalletEndpoint: LightWalletEndpoint
): 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
.forAddress(host, port)
.forAddress(lightWalletEndpoint.host, lightWalletEndpoint.port)
.context(appContext)
.enableFullStreamDecompression()
.apply {
if (usePlaintext) {
if (!appContext.resources.getBoolean(
R.bool.lightwalletd_allow_very_insecure_connections
)
) throw LightWalletException.InsecureConnection
usePlaintext()
} else {
usePlaintext()
/*
if (lightWalletEndpoint.isSecure) {
useTransportSecurity()
} else {
twig("WARNING: Using insecure channel")
usePlaintext()
}
*/
}
.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()
}
}

13
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletService.kt

@ -1,5 +1,6 @@
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.Service
@ -24,9 +25,7 @@ interface LightWalletService {
*
* @return the UTXOs for the given address from the startHeight.
*/
/* THIS IS NOT SUPPORT IN HUSH LIGHTWALLETD
fun fetchUtxos(tAddress: String, startHeight: Int): List<Service.GetAddressUtxosReply>
*/
fun fetchUtxos(tAddress: String, startHeight: BlockHeight): List<Service.GetAddressUtxosReply>
/**
* Return the given range of blocks.
@ -37,14 +36,14 @@ interface LightWalletService {
* @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.
*/
fun getLatestBlockHeight(): Int
fun getLatestBlockHeight(): BlockHeight
/**
* 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.
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
fun getTAddressTransactions(tAddress: String, blockHeightRange: IntRange): List<Service.RawTransaction>
*/
fun getTAddressTransactions(tAddress: String, blockHeightRange: ClosedRange<BlockHeight>): List<Service.RawTransaction>
/**
* Reconnect to the same or a different server. This is useful when the connection is

44
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt

@ -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.toRefreshable
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.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.WalletBirthday
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
@ -28,7 +29,8 @@ import kotlinx.coroutines.withContext
*
* @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 pageSize: Int
) : TransactionRepository {
@ -62,20 +64,20 @@ class PagedTransactionRepository private constructor(
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 findEncodedTransactionById(txId: Long) =
transactions.findEncodedTransactionById(txId)
override suspend fun findNewTransactions(blockHeightRange: IntRange): List<ConfirmedTransaction> =
transactions.findAllTransactionsByRange(blockHeightRange.first, blockHeightRange.last)
override suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<ConfirmedTransaction> =
transactions.findAllTransactionsByRange(blockHeightRange.start.value, blockHeightRange.endInclusive.value)
override suspend fun findMinedHeight(rawTransactionId: ByteArray) =
transactions.findMinedHeight(rawTransactionId)
transactions.findMinedHeight(rawTransactionId)?.let { BlockHeight.new(zcashNetwork, it) }
override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long? =
transactions.findMatchingTransactionId(rawTransactionId)
@ -84,8 +86,8 @@ class PagedTransactionRepository private constructor(
transactions.cleanupCancelledTx(rawTransactionId)
// let expired transactions linger in the UI for a little while
override suspend fun deleteExpired(lastScannedHeight: Int) =
transactions.deleteExpired(lastScannedHeight - (ZcashSdk.EXPIRY_OFFSET / 2))
override suspend fun deleteExpired(lastScannedHeight: BlockHeight) =
transactions.deleteExpired(lastScannedHeight.value - (ZcashSdk.EXPIRY_OFFSET / 2))
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
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()
// TODO: convert this into a wallet repository rather than "transaction repository"
companion object {
suspend fun new(
internal suspend fun new(
appContext: Context,
zcashNetwork: ZcashNetwork,
pageSize: Int = 10,
rustBackend: RustBackend,
birthday: WalletBirthday,
birthday: Checkpoint,
viewingKeys: List<UnifiedViewingKey>,
overwriteVks: Boolean = false
): PagedTransactionRepository {
@ -122,7 +125,7 @@ class PagedTransactionRepository private constructor(
val db = buildDatabase(appContext.applicationContext, rustBackend.pathDataDb)
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(
rustBackend: RustBackend,
birthday: WalletBirthday,
birthday: Checkpoint,
viewingKeys: List<UnifiedViewingKey>
) {
maybeCreateDataDb(rustBackend)
@ -178,20 +181,15 @@ class PagedTransactionRepository private constructor(
*/
private suspend fun maybeInitBlocksTable(
rustBackend: RustBackend,
birthday: WalletBirthday
checkpoint: Checkpoint
) {
// TODO: consider converting these to typed exceptions in the welding layer
tryWarn(
"Warning: did not initialize the blocks table. It probably was already initialized.",
ifContains = "table is not empty"
) {
rustBackend.initBlocksTable(
birthday.height,
birthday.hash,
birthday.time,
birthday.tree
)
twig("seeded the database with sapling tree at height ${birthday.height}")
rustBackend.initBlocksTable(checkpoint)
twig("seeded the database with sapling tree at height ${checkpoint.height}")
}
twig("database file: ${rustBackend.pathDataDb}")
}

9
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt

@ -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.service.LightWalletService
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
@ -98,10 +99,10 @@ class PersistentTransactionManager(
tx
}
override suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: Int) {
override suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: BlockHeight) {
twig("a pending transaction has been mined!")
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")
val encodedTx = encoder.createTransaction(
spendingKey,
tx.value,
tx.valueZatoshi,
tx.toAddress,
tx.memo,
tx.accountIndex
@ -230,10 +231,8 @@ class PersistentTransactionManager(
override suspend fun isValidShieldedAddress(address: String) =
encoder.isValidShieldedAddress(address)
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override suspend fun isValidTransparentAddress(address: String) =
encoder.isValidTransparentAddress(address)
*/
override suspend fun cancel(pendingId: Long): Boolean {
return pendingTransactionDao {

7
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt

@ -1,6 +1,7 @@
package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.db.entity.EncodedTransaction
import cash.z.ecc.android.sdk.model.Zatoshi
interface TransactionEncoder {
/**
@ -9,7 +10,7 @@ interface TransactionEncoder {
* exception ourselves (rather than using double-bangs for things).
*
* @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 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.
@ -18,7 +19,7 @@ interface TransactionEncoder {
*/
suspend fun createTransaction(
spendingKey: String,
zatoshi: Long,
amount: Zatoshi,
toAddress: String,
memo: ByteArray? = byteArrayOf(),
fromAccountIndex: Int = 0
@ -48,9 +49,7 @@ interface TransactionEncoder {
*
* @return true when the given address is a valid t-addr
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun isValidTransparentAddress(address: String): Boolean
*/
/**
* Return the consensus branch that the encoder is using when making transactions.

5
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt

@ -1,6 +1,7 @@
package cash.z.ecc.android.sdk.internal.transaction
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 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
* 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
@ -94,9 +95,7 @@ interface OutboundTransactionManager {
*
* @return true when the given address is a valid z-addr.
*/
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
suspend fun isValidTransparentAddress(address: String): Boolean
*/
/**
* Attempt to cancel a transaction.

11
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt

@ -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.EncodedTransaction
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.type.UnifiedAddressAccount
import kotlinx.coroutines.flow.Flow
@ -15,14 +16,14 @@ interface TransactionRepository {
*
* @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
* used to initialize this wallet. If we overwrite this block, it breaks our ability to spend
* funds.
*/
suspend fun firstScannedHeight(): Int
suspend fun firstScannedHeight(): BlockHeight
/**
* 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.
*/
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
@ -61,7 +62,7 @@ interface TransactionRepository {
*
* @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?
@ -79,7 +80,7 @@ interface TransactionRepository {
*/
suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean
suspend fun deleteExpired(lastScannedHeight: Int): Int
suspend fun deleteExpired(lastScannedHeight: BlockHeight): Int
suspend fun count(): Int

19
sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt

@ -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.jni.RustBackend
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
@ -18,7 +19,7 @@ import cash.z.ecc.android.sdk.jni.RustBackendWelding
* @property repository the repository that stores information about the transactions being created
* such as the raw bytes and raw txId.
*/
class WalletTransactionEncoder(
internal class WalletTransactionEncoder(
private val rustBackend: RustBackendWelding,
private val repository: TransactionRepository
) : TransactionEncoder {
@ -29,7 +30,7 @@ class WalletTransactionEncoder(
* exception ourselves (rather than using double-bangs for things).
*
* @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 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.
@ -38,12 +39,12 @@ class WalletTransactionEncoder(
*/
override suspend fun createTransaction(
spendingKey: String,
zatoshi: Long,
amount: Zatoshi,
toAddress: String,
memo: ByteArray?,
fromAccountIndex: Int
): EncodedTransaction {
val transactionId = createSpend(spendingKey, zatoshi, toAddress, memo)
val transactionId = createSpend(spendingKey, amount, toAddress, memo)
return repository.findEncodedTransactionById(transactionId)
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
}
@ -77,10 +78,8 @@ class WalletTransactionEncoder(
*
* @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 =
rustBackend.isValidTransparentAddr(address)
*/
override suspend fun getConsensusBranchId(): Long {
val height = repository.lastScannedHeight()
@ -95,7 +94,7 @@ class WalletTransactionEncoder(
* 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 zatoshi the amount of zatoshi to send.
* @param amount the amount of zatoshi to send.
* @param toAddress the recipient's address.
* @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.
@ -105,13 +104,13 @@ class WalletTransactionEncoder(
*/
private suspend fun createSpend(
spendingKey: String,
zatoshi: Long,
amount: Zatoshi,
toAddress: String,
memo: ByteArray? = byteArrayOf(),
fromAccountIndex: Int = 0
): Long {
return twigTask(
"creating transaction to spend $zatoshi zatoshi to" +
"creating transaction to spend $amount zatoshi to" +
" ${toAddress.masked()} with memo $memo"
) {
try {
@ -123,7 +122,7 @@ class WalletTransactionEncoder(
fromAccountIndex,
spendingKey,
toAddress,
zatoshi,
amount.value,
memo
)
} catch (t: Throwable) {

119
sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt

@ -1,16 +1,17 @@
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.SPEND_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.internal.SdkDispatchers
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.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.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
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 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
* should be used such as Wallet.kt or CompactBlockProcessor.kt.
*/
class RustBackend private constructor() : RustBackendWelding {
// Paths
lateinit var pathDataDb: String
internal set
lateinit var pathCacheDb: String
internal set
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
internal class RustBackend private constructor(
override val network: ZcashNetwork,
val birthdayHeight: BlockHeight,
val pathDataDb: String,
val pathCacheDb: String,
val pathParamsDir: String
) : RustBackendWelding {
suspend fun clear(clearCacheDb: Boolean = true, clearDataDb: Boolean = true) {
if (clearCacheDb) {
@ -84,18 +77,15 @@ class RustBackend private constructor() : RustBackendWelding {
}
override suspend fun initBlocksTable(
height: Int,
hash: String,
time: Long,
saplingTree: String
checkpoint: Checkpoint
): Boolean {
return withContext(SdkDispatchers.DATABASE_IO) {
initBlocksTable(
pathDataDb,
height,
hash,
time,
saplingTree,
checkpoint.height.value,
checkpoint.hash,
checkpoint.epochSeconds,
checkpoint.tree,
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 {
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 {
val longValue = withContext(SdkDispatchers.DATABASE_IO) {
@ -158,19 +146,28 @@ class RustBackend private constructor() : RustBackendWelding {
}
override suspend fun validateCombinedChain() = withContext(SdkDispatchers.DATABASE_IO) {
validateCombinedChain(
val validationResult = validateCombinedChain(
pathCacheDb,
pathDataDb,
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) {
getNearestRewindHeight(
pathDataDb,
height,
networkId = network.id
BlockHeight.new(
network,
getNearestRewindHeight(
pathDataDb,
height.value,
networkId = network.id
)
)
}
@ -179,11 +176,11 @@ class RustBackend private constructor() : RustBackendWelding {
*
* DELETE FROM blocks WHERE height > ?
*/
override suspend fun rewindToHeight(height: Int) =
override suspend fun rewindToHeight(height: BlockHeight) =
withContext(SdkDispatchers.DATABASE_IO) {
rewindToHeight(
pathDataDb,
height,
height.value,
networkId = network.id
)
}
@ -266,7 +263,7 @@ class RustBackend private constructor() : RustBackendWelding {
index: Int,
script: ByteArray,
value: Long,
height: Int
height: BlockHeight
): Boolean = withContext(SdkDispatchers.DATABASE_IO) {
putUtxo(
pathDataDb,
@ -275,19 +272,21 @@ class RustBackend private constructor() : RustBackendWelding {
index,
script,
value,
height,
height.value,
networkId = network.id
)
}
override suspend fun clearUtxos(
tAddress: String,
aboveHeight: Int
aboveHeightInclusive: BlockHeight
): Boolean = withContext(SdkDispatchers.DATABASE_IO) {
clearUtxos(
pathDataDb,
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
)
}
@ -313,13 +312,11 @@ class RustBackend private constructor() : RustBackendWelding {
override fun isValidShieldedAddr(addr: String) =
isValidShieldedAddress(addr, networkId = network.id)
/* THIS IS NOT SUPPORTED IN HUSH LIGHTWALLETD
override fun isValidTransparentAddr(addr: String) =
isValidTransparentAddress(addr, networkId = network.id)
*/
override fun getBranchIdForHeight(height: Int): Long =
branchIdForHeight(height, networkId = network.id)
override fun getBranchIdForHeight(height: BlockHeight): Long =
branchIdForHeight(height.value, networkId = network.id)
// /**
// * 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,
paramsPath: String,
zcashNetwork: ZcashNetwork,
birthdayHeight: Int? = null
birthdayHeight: BlockHeight
): RustBackend {
rustLibraryLoader.load()
return RustBackend().apply {
pathCacheDb = cacheDbPath
pathDataDb = dataDbPath
return RustBackend(
zcashNetwork,
birthdayHeight,
pathDataDb = dataDbPath,
pathCacheDb = cacheDbPath,
pathParamsDir = paramsPath
network = zcashNetwork
if (birthdayHeight != null) {
this.birthdayHeight = birthdayHeight
}
}
)
}
/**
@ -400,7 +395,7 @@ class RustBackend private constructor() : RustBackendWelding {
@JvmStatic
private external fun initBlocksTable(
dbDataPath: String,
height: Int,
height: Long,
hash: String,
time: Long,
saplingTree: String,
@ -417,10 +412,8 @@ class RustBackend private constructor() : RustBackendWelding {
@JvmStatic
private external fun isValidShieldedAddress(addr: String, networkId: Int): Boolean
/* THIS IS NOT SUPPORT IN HUSH LIGHTWALLETD
@JvmStatic
private external fun isValidTransparentAddress(addr: String, networkId: Int): Boolean
*/
@JvmStatic
private external fun getBalance(dbDataPath: String, account: Int, networkId: Int): Long
@ -451,19 +444,19 @@ class RustBackend private constructor() : RustBackendWelding {
dbCachePath: String,
dbDataPath: String,
networkId: Int
): Int
): Long
@JvmStatic
private external fun getNearestRewindHeight(
dbDataPath: String,
height: Int,
height: Long,
networkId: Int
): Int
): Long
@JvmStatic
private external fun rewindToHeight(
dbDataPath: String,
height: Int,
height: Long,
networkId: Int
): Boolean
@ -519,7 +512,7 @@ class RustBackend private constructor() : RustBackendWelding {
private external fun initLogs()
@JvmStatic
private external fun branchIdForHeight(height: Int, networkId: Int): Long
private external fun branchIdForHeight(height: Long, networkId: Int): Long
@JvmStatic
private external fun putUtxo(
@ -529,7 +522,7 @@ class RustBackend private constructor() : RustBackendWelding {
index: Int,
script: ByteArray,
value: Long,
height: Int,
height: Long,
networkId: Int
): Boolean
@ -537,7 +530,7 @@ class RustBackend private constructor() : RustBackendWelding {
private external fun clearUtxos(
dbDataPath: String,
tAddress: String,
aboveHeight: Int,
aboveHeight: Long,
networkId: Int
): Boolean

29
sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt

@ -1,9 +1,11 @@
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.ZcashNetwork
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.
@ -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.
* Instead, use the synchronizer or one of its subcomponents.
*/
interface RustBackendWelding {
internal interface RustBackendWelding {
val network: ZcashNetwork
@ -36,21 +38,21 @@ interface RustBackendWelding {
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
fun isValidShieldedAddr(addr: String): Boolean
//fun isValidTransparentAddr(addr: String): Boolean
fun isValidTransparentAddr(addr: String): Boolean
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
fun getBranchIdForHeight(height: Int): Long
fun getBranchIdForHeight(height: BlockHeight): Long
suspend fun getReceivedMemoAsUtf8(idNote: Long): String
@ -60,13 +62,16 @@ interface RustBackendWelding {
// 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 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(
tAddress: String,
@ -74,10 +79,10 @@ interface RustBackendWelding {
index: Int,
script: ByteArray,
value: Long,
height: Int
height: BlockHeight
): 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

67
sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/BlockHeight.kt

@ -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
}
}
}

5
sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpoint.kt

@ -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
}

44
sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpointExt.kt

@ -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
)

28
sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletBalance.kt

@ -0,0 +1,28 @@
package cash.z.ecc.android.sdk.model
/**
* Data structure to hold the total and available balance of the wallet. This is what is
* received on the balance channel.
*
* @param total the total balance, ignoring funds that cannot be used.
* @param available the amount of funds that are available for use. Typical reasons that funds
* may be unavailable include fairly new transactions that do not have enough confirmations or
* notes that are tied up because we are awaiting change from a transaction. When a note has
* been spent, its change cannot be used until there are enough confirmations.
*/
data class WalletBalance(
val total: Zatoshi,
val available: Zatoshi
) {
init {
require(total.value >= available.value) { "Wallet total balance must be >= available balance" }
}
val pending = total - available
operator fun plus(other: WalletBalance): WalletBalance =
WalletBalance(
total + other.total,
available + other.available
)
}

4
sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zatoshi.kt

@ -7,7 +7,7 @@ package cash.z.ecc.android.sdk.model
* with ZEC, which is a decimal value represented only as a String. ZEC are not used internally,
* to avoid floating point imprecision.
*/
data class Zatoshi(val value: Long) {
data class Zatoshi(val value: Long) : Comparable<Zatoshi> {
init {
require(value >= MIN_INCLUSIVE) { "Zatoshi must be in the range [$MIN_INCLUSIVE, $MAX_INCLUSIVE]" }
require(value <= MAX_INCLUSIVE) { "Zatoshi must be in the range [$MIN_INCLUSIVE, $MAX_INCLUSIVE]" }
@ -16,6 +16,8 @@ data class Zatoshi(val value: Long) {
operator fun plus(other: Zatoshi) = Zatoshi(value + other.value)
operator fun minus(other: Zatoshi) = Zatoshi(value - other.value)
override fun compareTo(other: Zatoshi) = value.compareTo(other.value)
companion object {
/**
* The number of Zatoshi that equal 1 ZEC.

40
sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/ZcashNetwork.kt

@ -0,0 +1,40 @@
package cash.z.ecc.android.sdk.model
/**
* The Zcash network. Should be one of [ZcashNetwork.Testnet] or [ZcashNetwork.Mainnet].
*
* The constructor for the network is public to allow for certain test cases to use a custom "darkside" network.
*/
data class ZcashNetwork(
val id: Int,
val networkName: String,
val saplingActivationHeight: BlockHeight
) {
@Suppress("MagicNumber")
companion object {
const val ID_TESTNET = 0
const val ID_MAINNET = 1
// You may notice there are extra checkpoints bundled in the SDK that match the
// sapling/orchard activation heights.
val Testnet = ZcashNetwork(
ID_TESTNET,
"testnet",
saplingActivationHeight = BlockHeight(1),
)
val Mainnet = ZcashNetwork(
ID_MAINNET,
"mainnet",
saplingActivationHeight = BlockHeight(1),
)
fun from(id: Int) = when (id) {
0 -> Testnet
1 -> Mainnet
else -> throw IllegalArgumentException("Unknown network id: $id")
}
}
}

72
sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/WalletBirthdayTool.kt → sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/CheckpointTool.kt

@ -4,9 +4,10 @@ import android.content.Context
import androidx.annotation.VisibleForTesting
import cash.z.ecc.android.sdk.exception.BirthdayException
import cash.z.ecc.android.sdk.internal.from
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.type.WalletBirthday
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.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
@ -16,7 +17,7 @@ import java.util.*
/**
* Tool for loading checkpoints for the wallet, based on the height at which the wallet was born.
*/
object WalletBirthdayTool {
internal object CheckpointTool {
// Behavior change implemented as a fix for issue #270. Temporarily adding a boolean
// that allows the change to be rolled back quickly if needed, although long-term
@ -31,29 +32,29 @@ object WalletBirthdayTool {
suspend fun loadNearest(
context: Context,
network: ZcashNetwork,
birthdayHeight: Int? = null
): WalletBirthday {
birthdayHeight: BlockHeight?
): Checkpoint {
// TODO: potentially pull from shared preferences first
return loadBirthdayFromAssets(context, network, birthdayHeight)
return loadCheckpointFromAssets(context, network, birthdayHeight)
}
/**
* Useful for when an exact checkpoint is needed, like for SAPLING_ACTIVATION_HEIGHT. In
* most cases, loading the nearest checkpoint is preferred for privacy reasons.
*/
suspend fun loadExact(context: Context, network: ZcashNetwork, birthdayHeight: Int) =
loadNearest(context, network, birthdayHeight).also {
if (it.height != birthdayHeight) {
suspend fun loadExact(context: Context, network: ZcashNetwork, birthday: BlockHeight) =
loadNearest(context, network, birthday).also {
if (it.height != birthday) {
throw BirthdayException.ExactBirthdayNotFoundException(
birthdayHeight,
it.height
birthday,
it
)
}
}
// Converting this to suspending will then propagate
@Throws(IOException::class)
internal suspend fun listBirthdayDirectoryContents(context: Context, directory: String) =
internal suspend fun listCheckpointDirectoryContents(context: Context, directory: String) =
withContext(Dispatchers.IO) {
context.assets.list(directory)
}
@ -63,58 +64,64 @@ object WalletBirthdayTool {
* (i.e. sapling trees for a given height) can be found.
*/
@VisibleForTesting
internal fun birthdayDirectory(network: ZcashNetwork) =
"co.electriccoin.zcash/checkpoint/${(network.networkName as java.lang.String).toLowerCase(Locale.ROOT)}"
internal fun checkpointDirectory(network: ZcashNetwork) =
"co.electriccoin.zcash/checkpoint/${
(network.networkName as java.lang.String).toLowerCase(
Locale.ROOT
)
}"
internal fun birthdayHeight(fileName: String) = fileName.split('.').first().toInt()
internal fun checkpointHeightFromFilename(zcashNetwork: ZcashNetwork, fileName: String) =
BlockHeight.new(zcashNetwork, fileName.split('.').first().toLong())
private fun Array<String>.sortDescending() =
apply { sortByDescending { birthdayHeight(it) } }
private fun Array<String>.sortDescending(zcashNetwork: ZcashNetwork) =
apply { sortByDescending { checkpointHeightFromFilename(zcashNetwork, it).value } }
/**
* Load the given birthday file from the assets of the given context. When no height is
* specified, we default to the file with the greatest name.
*
* @param context the context from which to load assets.
* @param birthdayHeight the height file to look for among the file names.
* @param birthday the height file to look for among the file names.
*
* @return a WalletBirthday that reflects the contents of the file or an exception when
* parsing fails.
*/
private suspend fun loadBirthdayFromAssets(
private suspend fun loadCheckpointFromAssets(
context: Context,
network: ZcashNetwork,
birthdayHeight: Int? = null
): WalletBirthday {
twig("loading birthday from assets: $birthdayHeight")
val directory = birthdayDirectory(network)
val treeFiles = getFilteredFileNames(context, directory, birthdayHeight)
birthday: BlockHeight?
): Checkpoint {
twig("loading checkpoint from assets: $birthday")
val directory = checkpointDirectory(network)
val treeFiles = getFilteredFileNames(context, network, directory, birthday)
twig("found ${treeFiles.size} sapling tree checkpoints: $treeFiles")
return getFirstValidWalletBirthday(context, directory, treeFiles)
return getFirstValidWalletBirthday(context, network, directory, treeFiles)
}
private suspend fun getFilteredFileNames(
context: Context,
network: ZcashNetwork,
directory: String,
birthdayHeight: Int? = null
birthday: BlockHeight?
): List<String> {
val unfilteredTreeFiles = listBirthdayDirectoryContents(context, directory)
val unfilteredTreeFiles = listCheckpointDirectoryContents(context, directory)
if (unfilteredTreeFiles.isNullOrEmpty()) {
throw BirthdayException.MissingBirthdayFilesException(directory)
}
val filteredTreeFiles = unfilteredTreeFiles
.sortDescending()
.sortDescending(network)
.filter { filename ->
birthdayHeight?.let { birthdayHeight(filename) <= it } ?: true
birthday?.let { checkpointHeightFromFilename(network, filename) <= it } ?: true
}
if (filteredTreeFiles.isEmpty()) {
throw BirthdayException.BirthdayFileNotFoundException(
directory,
birthdayHeight
birthday
)
}
@ -127,9 +134,10 @@ object WalletBirthdayTool {
@VisibleForTesting
internal suspend fun getFirstValidWalletBirthday(
context: Context,
network: ZcashNetwork,
directory: String,
treeFiles: List<String>
): WalletBirthday {
): Checkpoint {
var lastException: Exception? = null
treeFiles.forEach { treefile ->
try {
@ -143,7 +151,7 @@ object WalletBirthdayTool {
}
}
return WalletBirthday.from(jsonString)
return Checkpoint.from(network, jsonString)
} catch (t: Throwable) {
val exception = BirthdayException.MalformattedBirthdayFilesException(
directory,

2
sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt

@ -2,8 +2,8 @@ package cash.z.ecc.android.sdk.tool
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.sdk.type.ZcashNetwork
class DerivationTool {

62
sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt

@ -1,52 +1,5 @@
package cash.z.ecc.android.sdk.type
import cash.z.ecc.android.sdk.model.Zatoshi
/**
* Data structure to hold the total and available balance of the wallet. This is what is
* received on the balance channel.
*
* @param total the total balance, ignoring funds that cannot be used.
* @param available the amount of funds that are available for use. Typical reasons that funds
* may be unavailable include fairly new transactions that do not have enough confirmations or
* notes that are tied up because we are awaiting change from a transaction. When a note has
* been spent, its change cannot be used until there are enough confirmations.
*/
data class WalletBalance(
val total: Zatoshi,
val available: Zatoshi
) {
init {
require(total.value >= available.value) { "Wallet total balance must be >= available balance" }
}
val pending = total - available
operator fun plus(other: WalletBalance): WalletBalance =
WalletBalance(
total + other.total,
available + other.available
)
}
/**
* Model object for holding a wallet birthday.
*
* @param height the height at the time the wallet was born.
* @param hash the hash of the block at the height.
* @param time the block time at the height. Represented as seconds since the Unix epoch.
* @param tree the sapling tree corresponding to the height.
*/
data class WalletBirthday(
val height: Int = -1,
val hash: String = "",
val time: Long = -1,
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
val tree: String = ""
) {
companion object
}
/**
* A grouping of keys that correspond to a single wallet account but do not have spend authority.
*
@ -70,18 +23,3 @@ interface UnifiedAddress {
val rawShieldedAddress: String
val rawTransparentAddress: String
}
enum class ZcashNetwork(
val id: Int,
val networkName: String,
val saplingActivationHeight: Int,
val defaultHost: String,
val defaultPort: Int
) {
Testnet(0, "testnet", 995_000, "lite.hushpool.is", 9067),
Mainnet(1, "mainnet", 995_000, "lite.hushpool.is", 9067);
companion object {
fun from(id: Int) = values().first { it.id == id }
}
}

6
sdk-lib/src/main/proto/darkside.proto

@ -29,16 +29,16 @@ message DarksideBlocksURL {
// of hex-encoded transactions, one per line, that are to be associated
// with the given height (fake-mined into the block at that height)
message DarksideTransactionsURL {
int32 height = 1;
int64 height = 1;
string url = 2;
}
message DarksideHeight {
int32 height = 1;
int64 height = 1;
}
message DarksideEmptyBlocks {
int32 height = 1;
int64 height = 1;
int32 nonce = 2;
int32 count = 3;
}

172
sdk-lib/src/main/proto/service.proto

@ -1,19 +1,22 @@
// Copyright (c) 2019-2020 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
syntax = "proto3";
package cash.z.wallet.sdk.rpc;
option go_package = "walletrpc";
option go_package = ".;walletrpc";
option swift_prefix = "";
import "compact_formats.proto";
// A BlockID message contains identifiers to select a block: a height or a
// hash. If the hash is present it takes precedence.
// hash. Specification by hash is not implemented, but may be in the future.
message BlockID {
uint64 height = 1;
bytes hash = 2;
uint64 height = 1;
bytes hash = 2;
}
// BlockRange technically allows ranging from hash to hash etc but this is not
// currently intended for support, though there is no reason you couldn't do
// it. Further permutations are left as an exercise.
// BlockRange specifies a series of blocks from start to end inclusive.
// Both BlockIDs must be heights; specification by hash is not yet supported.
message BlockRange {
BlockID start = 1;
BlockID end = 2;
@ -21,73 +24,154 @@ message BlockRange {
// A TxFilter contains the information needed to identify a particular
// transaction: either a block and an index, or a direct transaction hash.
// Currently, only specification by hash is supported.
message TxFilter {
BlockID block = 1;
uint64 index = 2;
bytes hash = 3;
BlockID block = 1; // block identifier, height or hash
uint64 index = 2; // index within the block
bytes hash = 3; // transaction ID (hash, txid)
}
// RawTransaction contains the complete transaction data. It also optionally includes
// the block height in which the transaction was included
// RawTransaction contains the complete transaction data. It also optionally includes
// the block height in which the transaction was included.
message RawTransaction {
bytes data = 1;
uint64 height = 2;
bytes data = 1; // exact data returned by Zcash 'getrawtransaction'
uint64 height = 2; // height that the transaction was mined (or -1)
}
// A SendResponse encodes an error code and a string. It is currently used
// only by SendTransaction(). If error code is zero, the operation was
// successful; if non-zero, it and the message specify the failure.
message SendResponse {
int32 errorCode = 1;
string errorMessage = 2;
}
// Empty placeholder. Someday we may want to specify e.g. a particular chain fork.
// Chainspec is a placeholder to allow specification of a particular chain fork.
message ChainSpec {}
// Empty is for gRPCs that take no arguments, currently only GetLightdInfo.
message Empty {}
// LightdInfo returns various information about this lightwalletd instance
// and the state of the blockchain.
message LightdInfo {
string version = 1;
string vendor = 2;
bool taddrSupport = 3;
string chainName = 4;
uint64 saplingActivationHeight = 5;
string consensusBranchId = 6; // This should really be u32 or []byte, but string for readability
uint64 blockHeight = 7;
uint64 difficulty = 8;
uint64 longestchain = 9;
uint64 notarized = 10;
}
message Coinsupply {
string result = 1;
string coin = 2;
uint64 height = 3;
uint64 supply = 4;
uint64 zfunds = 5;
uint64 total = 6;
}
message TransparentAddress {
string address = 1;
bool taddrSupport = 3; // true
string chainName = 4; // either "main" or "test"
uint64 saplingActivationHeight = 5; // depends on mainnet or testnet
string consensusBranchId = 6; // protocol identifier, see consensus/upgrades.cpp
uint64 blockHeight = 7; // latest block on the best chain
string gitCommit = 8;
string branch = 9;
string buildDate = 10;
string buildUser = 11;
uint64 estimatedHeight = 12; // less than tip height if zcashd is syncing
string zcashdBuild = 13; // example: "v4.1.1-877212414"
string zcashdSubversion = 14; // example: "/MagicBean:4.1.1/"
}
// TransparentAddressBlockFilter restricts the results to the given address
// or block range.
message TransparentAddressBlockFilter {
string address = 1; // t-address
BlockRange range = 2; // start, end heights
}
// Duration is currently used only for testing, so that the Ping rpc
// can simulate a delay, to create many simultaneous connections. Units
// are microseconds.
message Duration {
int64 intervalUs = 1;
}
// PingResponse is used to indicate concurrency, how many Ping rpcs
// are executing upon entry and upon exit (after the delay).
// This rpc is used for testing only.
message PingResponse {
int64 entry = 1;
int64 exit = 2;
}
message Address {
string address = 1;
}
message AddressList {
repeated string addresses = 1;
}
message Balance {
int64 valueZat = 1;
}
message Exclude {
repeated bytes txid = 1;
}
// The TreeState is derived from the Zcash z_gettreestate rpc.
message TreeState {
string network = 1; // "main" or "test"
uint64 height = 2;
string hash = 3; // block id
uint32 time = 4; // Unix epoch time when the block was mined
string tree = 5; // sapling commitment tree state
}
message GetAddressUtxosArg {
string address = 1;
BlockRange range = 2;
uint64 startHeight = 2;
uint32 maxEntries = 3; // zero means unlimited
}
message GetAddressUtxosReply {
bytes txid = 1;
int32 index = 2;
bytes script = 3;
int64 valueZat = 4;
uint64 height = 5;
}
message GetAddressUtxosReplyList {
repeated GetAddressUtxosReply addressUtxos = 1;
}
service CompactTxStreamer {
// Compact Blocks
// Return the height of the tip of the best chain
rpc GetLatestBlock(ChainSpec) returns (BlockID) {}
// Return the compact block corresponding to the given block identifier
rpc GetBlock(BlockID) returns (CompactBlock) {}
// Return a list of consecutive compact blocks
rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {}
// Transactions
// Return the requested full (not compact) transaction (as from zcashd)
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
// Submit the given transaction to the Zcash network
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
// t-Address support
rpc GetAddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
// Return the txids corresponding to the given t-address within the given block range
rpc GetTaddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
rpc GetTaddressBalance(AddressList) returns (Balance) {}
rpc GetTaddressBalanceStream(stream Address) returns (Balance) {}
// Return the compact transactions currently in the mempool; the results
// can be a few seconds out of date. If the Exclude list is empty, return
// all transactions; otherwise return all *except* those in the Exclude list
// (if any); this allows the client to avoid receiving transactions that it
// already has (from an earlier call to this rpc). The transaction IDs in the
// Exclude list can be shortened to any number of bytes to make the request
// more bandwidth-efficient; if two or more transactions in the mempool
// match a shortened txid, they are all sent (none is excluded). Transactions
// in the exclude list that don't exist in the mempool are ignored.
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
// Misc
// GetTreeState returns the note commitment tree state corresponding to the given block.
// See section 3.7 of the Zcash protocol specification. It returns several other useful
// values also (even though they can be obtained using GetBlock).
// The block can be specified by either height or hash.
rpc GetTreeState(BlockID) returns (TreeState) {}
rpc GetAddressUtxos(GetAddressUtxosArg) returns (GetAddressUtxosReplyList) {}
rpc GetAddressUtxosStream(GetAddressUtxosArg) returns (stream GetAddressUtxosReply) {}
// Return information about this lightwalletd instance and the blockchain
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
rpc GetCoinsupply(Empty) returns (Coinsupply) {}
}
// Testing-only
rpc Ping(Duration) returns (PingResponse) {}
}

4
sdk-lib/src/main/res/values/bools.xml

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

3
sdk-lib/src/main/res/values/strings.xml

@ -1,3 +0,0 @@
<resources>
<string name="sdk_test_message">Library linking is working!</string>
</resources>

28
sdk-lib/src/main/rust/lib.rs

@ -367,7 +367,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initBlocksT
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
height: jint,
height: jlong,
hash_string: JString<'_>,
time: jlong,
sapling_tree_string: JString<'_>,
@ -390,7 +390,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initBlocksT
hex::decode(utils::java_string_to_rust(&env, sapling_tree_string)).unwrap();
debug!("initializing blocks table with height {}", height);
match init_blocks_table(&db_data, height.try_into()?, hash, time, &sapling_tree) {
match init_blocks_table(&db_data, (height as u32).try_into()?, hash, time, &sapling_tree) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while initializing blocks table: {}", e)),
}
@ -673,7 +673,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_validateCom
db_cache: JString<'_>,
db_data: JString<'_>,
network_id: jint,
) -> jint {
) -> jlong {
let res = panic::catch_unwind(|| {
let network = parse_network(network_id as u32)?;
let block_db = block_db(&env, db_cache)?;
@ -689,7 +689,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_validateCom
match e {
SqliteClientError::BackendError(Error::InvalidChain(upper_bound, _)) => {
let upper_bound_u32 = u32::from(upper_bound);
Ok(upper_bound_u32 as i32)
Ok(upper_bound_u32 as i64)
}
_ => Err(format_err!("Error while validating chain: {}", e)),
}
@ -699,7 +699,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_validateCom
}
});
unwrap_exc_or(&env, res, 0)
unwrap_exc_or(&env, res, 0) as jlong
}
#[no_mangle]
@ -707,9 +707,9 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getNearestR
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
height: jint,
height: jlong,
network_id: jint,
) -> jint {
) -> jlong {
let res = panic::catch_unwind(|| {
if height < 100 {
Ok(height)
@ -720,11 +720,11 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getNearestR
Ok(Some(best_height)) => {
let first_unspent_note_height = u32::from(best_height);
Ok(std::cmp::min(
first_unspent_note_height as i32,
height as i32,
first_unspent_note_height as i64,
height as i64,
))
}
Ok(None) => Ok(height as i32),
Ok(None) => Ok(height as i64),
Err(e) => Err(format_err!(
"Error while getting nearest rewind height for {}: {}",
height,
@ -734,7 +734,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getNearestR
}
});
unwrap_exc_or(&env, res, -1)
unwrap_exc_or(&env, res, -1) as jlong
}
#[no_mangle]
@ -742,7 +742,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_rewindToHei
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
height: jint,
height: jlong,
network_id: jint,
) -> jboolean {
let res = panic::catch_unwind(|| {
@ -830,7 +830,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_clearUtxos(
_: JClass<'_>,
db_data: JString<'_>,
taddress: JString<'_>,
above_height: jint,
above_height: jlong,
network_id: jint,
) -> jint {
let res = panic::catch_unwind(|| {
@ -1153,7 +1153,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_shieldToAdd
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_branchIdForHeight(
env: JNIEnv<'_>,
_: JClass<'_>,
height: jint,
height: jlong,
network_id: jint,
) -> jlong {
let res = panic::catch_unwind(|| {

9
sdk-lib/src/test/java/cash/z/ecc/android/sdk/ext/ConversionsTest.kt

@ -1,5 +1,6 @@
package cash.z.ecc.android.sdk.ext
import cash.z.ecc.android.sdk.model.Zatoshi
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.math.BigDecimal
@ -9,7 +10,7 @@ internal class ConversionsTest {
@Test
fun `default right padding is 6`() {
assertEquals(1.13.toZec(6), 113000000L.convertZatoshiToZec())
assertEquals(1.13.toZec(6), Zatoshi(113000000L).convertZatoshiToZec())
assertEquals(1.13.toZec(6), 1.13.toZec())
}
@ -21,12 +22,12 @@ internal class ConversionsTest {
@Test
fun `toZecString defaults to 6 digits`() {
assertEquals("1.123457", 112345678L.convertZatoshiToZecString())
assertEquals("1.123457", Zatoshi(112345678L).convertZatoshiToZecString())
}
@Test
fun `toZecString uses banker's rounding`() {
assertEquals("1.123456", 112345650L.convertZatoshiToZecString())
assertEquals("1.123456", Zatoshi(112345650L).convertZatoshiToZecString())
}
@Test
@ -72,7 +73,7 @@ internal class ConversionsTest {
@Test
fun `toZecString zatoshi converts`() {
assertEquals("1.123456", 112345650L.convertZatoshiToZecString(6, 0))
assertEquals("1.123456", Zatoshi(112345650L).convertZatoshiToZecString(6, 0))
}
@Test

16
sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/ZatoshiTest.kt

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.model
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class ZatoshiTest {
@Test
@ -29,6 +30,21 @@ class ZatoshiTest {
assertEquals(Zatoshi(3), Zatoshi(4) - Zatoshi(1))
}
@Test
fun compare_equal() {
assertEquals(0, Zatoshi(1).compareTo(Zatoshi(1)))
}
@Test
fun compare_greater() {
assertTrue(Zatoshi(2) > Zatoshi(1))
}
@Test
fun compare_less() {
assertTrue(Zatoshi(1) < Zatoshi(2))
}
@Test
fun minus_fail() {
assertFailsWith<IllegalArgumentException> {

101
tools/detekt-baseline.xml

@ -2,16 +2,20 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ComplexCondition:CompactBlockProcessor.kt$CompactBlockProcessor$(null == lastScannedHeight &amp;&amp; targetHeight &lt; lastLocalBlock) || (null != lastScannedHeight &amp;&amp; targetHeight &lt; lastScannedHeight)</ID>
<ID>ComplexMethod:SdkSynchronizer.kt$SdkSynchronizer$private suspend fun refreshPendingTransactions()</ID>
<ID>ComplexMethod:SendFragment.kt$SendFragment$private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?)</ID>
<ID>ComplexMethod:Transactions.kt$ConfirmedTransaction$override fun equals(other: Any?): Boolean</ID>
<ID>ComplexMethod:Transactions.kt$PendingTransactionEntity$override fun equals(other: Any?): Boolean</ID>
<ID>DestructuringDeclarationWithTooManyEntries:TestnetIntegrationTest.kt$TestnetIntegrationTest$val (height, hash, time, tree) = runBlocking { CheckpointTool.loadNearest( context, synchronizer.network, saplingActivation + 1 ) }</ID>
<ID>EmptyCatchBlock:SdkSynchronizer.kt$SdkSynchronizer${ }</ID>
<ID>EmptyFunctionBlock:MainActivity.kt$MainActivity${ }</ID>
<ID>EmptyFunctionBlock:Placeholders.kt$SampleSpendingKeyProvider${ }</ID>
<ID>EmptyFunctionBlock:ReproduceZ2TFailureTest.kt$ReproduceZ2TFailureTest${ }</ID>
<ID>EmptyFunctionBlock:SampleCodeTest.kt$SampleCodeTest${ }</ID>
<ID>ForbiddenComment:BalancePrinterUtil.kt$BalancePrinterUtil$// TODO: clear the dataDb but leave the cacheDb</ID>
<ID>ForbiddenComment:CheckpointTool.kt$CheckpointTool$// TODO: If we ever add crash analytics hooks, this would be something to report</ID>
<ID>ForbiddenComment:CheckpointTool.kt$CheckpointTool$// TODO: potentially pull from shared preferences first</ID>
<ID>ForbiddenComment:CompactBlockDownloader.kt$CompactBlockDownloader$// TODO: cancel anything in flight</ID>
<ID>ForbiddenComment:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: add a concept of original checkpoint height to the processor. For now, derive it</ID>
<ID>ForbiddenComment:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons)</ID>
@ -39,25 +43,22 @@
<ID>ForbiddenComment:TestUtils.kt$* Use in place of `any()` to fix the issue with mockito `any` returning null (so you can't pass it to functions that * take a non-null param) * * TODO: perhaps submit this function to the mockito kotlin project because it allows the use of non-null 'any()'</ID>
<ID>ForbiddenComment:TestWallet.kt$TestWallet.Backups.DEFAULT$// TODO: get the proper birthday values for these wallets</ID>
<ID>ForbiddenComment:Transactions.kt$// TODO: test for off-by-one error here. Should we use &lt;= or &lt;</ID>
<ID>ForbiddenComment:WalletBirthdayTool.kt$WalletBirthdayTool$// TODO: If we ever add crash analytics hooks, this would be something to report</ID>
<ID>ForbiddenComment:WalletBirthdayTool.kt$WalletBirthdayTool$// TODO: potentially pull from shared preferences first</ID>
<ID>ForbiddenComment:WalletTransactionEncoder.kt$WalletTransactionEncoder$// TODO: if this error matches: Insufficient balance (have 0, need 1000 including fee)</ID>
<ID>FunctionParameterNaming:GetBlockFragment.kt$GetBlockFragment$_unused: View? = null</ID>
<ID>FunctionParameterNaming:GetBlockRangeFragment.kt$GetBlockRangeFragment$_unused: View</ID>
<ID>ImplicitDefaultLocale:BlockExt.kt$String.format("%02x", b)</ID>
<ID>ImplicitDefaultLocale:BlockExt.kt$String.format("%02x", this[i--])</ID>
<ID>ImplicitDefaultLocale:Twig.kt$TroubleshootingTwig.Companion$String.format("$tag %1\$tD %1\$tI:%1\$tM:%1\$tS.%1\$tN", System.currentTimeMillis())</ID>
<ID>InvalidPackageDeclaration:OpenForTesting.kt$package cash.z.ecc.android.sdk.annotation</ID>
<ID>LargeClass:CompactBlockProcessor.kt$CompactBlockProcessor</ID>
<ID>LongMethod:SdkSynchronizer.kt$SdkSynchronizer$private suspend fun refreshPendingTransactions()</ID>
<ID>LongParameterList:Initializer.kt$Initializer$( val context: Context, val rustBackend: RustBackend, val network: ZcashNetwork, val alias: String, val host: String, val port: Int, val viewingKeys: List&lt;UnifiedViewingKey>, val overwriteVks: Boolean, val birthday: WalletBirthday )</ID>
<ID>LongParameterList:Initializer.kt$Initializer.Config$( seed: ByteArray, birthdayHeight: Int? = null, network: ZcashNetwork, host: String = network.defaultHost, port: Int = network.defaultPort, alias: String = ZcashSdk.DEFAULT_ALIAS )</ID>
<ID>LongParameterList:Initializer.kt$Initializer.Config$( viewingKey: UnifiedViewingKey, birthdayHeight: Int? = null, network: ZcashNetwork, host: String = network.defaultHost, port: Int = network.defaultPort, alias: String = ZcashSdk.DEFAULT_ALIAS )</ID>
<ID>LongParameterList:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$( appContext: Context, pageSize: Int = 10, rustBackend: RustBackend, birthday: WalletBirthday, viewingKeys: List&lt;UnifiedViewingKey>, overwriteVks: Boolean = false )</ID>
<ID>LongParameterList:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$( appContext: Context, zcashNetwork: ZcashNetwork, pageSize: Int = 10, rustBackend: RustBackend, birthday: Checkpoint, viewingKeys: List&lt;UnifiedViewingKey>, overwriteVks: Boolean = false )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, account: Int, extsk: String, tsk: String, memo: ByteArray, spendParamsPath: String, outputParamsPath: String, networkId: Int )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, consensusBranchId: Long, account: Int, extsk: String, to: String, value: Long, memo: ByteArray, spendParamsPath: String, outputParamsPath: String, networkId: Int )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, height: Int, hash: String, time: Long, saplingTree: String, networkId: Int )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: Int, networkId: Int )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, height: Long, hash: String, time: Long, saplingTree: String, networkId: Int )</ID>
<ID>LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: Long, networkId: Int )</ID>
<ID>LongParameterList:RustBackendWelding.kt$RustBackendWelding$( consensusBranchId: Long, account: Int, extsk: String, to: String, value: Long, memo: ByteArray? = byteArrayOf() )</ID>
<ID>LongParameterList:RustBackendWelding.kt$RustBackendWelding$( tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: Int )</ID>
<ID>LongParameterList:RustBackendWelding.kt$RustBackendWelding$( tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: BlockHeight )</ID>
<ID>MagicNumber:BatchMetrics.kt$BatchMetrics$1000.0f</ID>
<ID>MagicNumber:BlockExt.kt$16</ID>
<ID>MagicNumber:BlockExt.kt$4</ID>
@ -83,7 +84,6 @@
<ID>MagicNumber:DemoConstants.kt$DemoConstants$0.000018</ID>
<ID>MagicNumber:DemoConstants.kt$DemoConstants$0.00017</ID>
<ID>MagicNumber:DemoConstants.kt$DemoConstants$1075590</ID>
<ID>MagicNumber:DemoConstants.kt$DemoConstants$968085</ID>
<ID>MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.&lt;no name provided>$3</ID>
<ID>MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.&lt;no name provided>$4</ID>
<ID>MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.&lt;no name provided>$5</ID>
@ -114,10 +114,6 @@
<ID>MagicNumber:UtxoViewHolder.kt$UtxoViewHolder$1000L</ID>
<ID>MagicNumber:WalletService.kt$1000L</ID>
<ID>MagicNumber:WalletService.kt$4.0</ID>
<ID>MagicNumber:WalletTypes.kt$ZcashNetwork.Mainnet$419_200</ID>
<ID>MagicNumber:WalletTypes.kt$ZcashNetwork.Mainnet$9067</ID>
<ID>MagicNumber:WalletTypes.kt$ZcashNetwork.Testnet$280_000</ID>
<ID>MagicNumber:WalletTypes.kt$ZcashNetwork.Testnet$9067</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$10</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$100</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$150</ID>
@ -128,24 +124,30 @@
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$512</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$600_000L</ID>
<ID>MagicNumber:ZcashSdk.kt$ZcashSdk$75_000L</ID>
<ID>MagicNumber:build.gradle.kts$16</ID>
<ID>MagicNumber:build.gradle.kts$21</ID>
<ID>MatchingDeclarationName:CurrencyFormatter.kt$Conversions</ID>
<ID>MaxLineLength:BatchMetrics.kt$BatchMetrics$class</ID>
<ID>MaxLineLength:BlockHeight.kt$BlockHeight.Companion$*</ID>
<ID>MaxLineLength:BranchIdTest.kt$BranchIdTest$assertEquals("Invalid branch ID for $networkName at height $height on ${rustBackend.network.networkName}", branchId, actual)</ID>
<ID>MaxLineLength:BranchIdTest.kt$BranchIdTest$assertEquals("Invalid branch Id Hex value for $networkName at height $height on ${rustBackend.network.networkName}", branchHex, clientBranch)</ID>
<ID>MaxLineLength:BranchIdTest.kt$BranchIdTest.Companion$val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight) }</ID>
<ID>MaxLineLength:BranchIdTest.kt$BranchIdTest.Companion$val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet, ZcashNetwork.Testnet.saplingActivationHeight) }</ID>
<ID>MaxLineLength:ChangeServiceTest.kt$ChangeServiceTest$"Exception was of the wrong type. Expected ${ChainInfoNotMatching::class.simpleName} but was ${caughtException!!::class.simpleName}"</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(clientBranch = clientBranch, serverBranch = info.consensusBranchId, networkName = network)</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$!info.matchingNetwork(network) -> MismatchedNetwork(clientNetwork = network, serverNetwork = info.chainName)</ID>
<ID>MaxLineLength:ChangeServiceTest.kt$ChangeServiceTest$downloader</ID>
<ID>MaxLineLength:CheckpointTool.kt$CheckpointTool$* @param treeFiles A list of files, sorted in descending order based on `int` value of the first part of the filename.</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$"ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!"</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$(lastScannedHeight.value - range.start.value) / (range.endInclusive.value - range.start.value + 1).toFloat() * 100.0f</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// Note: blocks are public information so it's okay to print them but, still, let's not unless we're debugging something</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons)</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// 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</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least 100 blocks away</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// 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.</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$internal suspend</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$oldestTransactionHeight = repository.receivedTransactions.first().lastOrNull()?.minedHeight ?: lowerBoundHeight</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$currentInfo.lastDownloadRange?.isEmpty() ?: true &amp;&amp; currentInfo.lastScanRange?.isEmpty() ?: true</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$if</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$lastScanRange</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$lowerBoundHeight + MAX_REORG_SIZE + 2</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$tempOldestTransactionHeight.value - tempOldestTransactionHeight.value.rem(ZcashSdk.MAX_REORG_SIZE) - ZcashSdk.MAX_REORG_SIZE.toLong()</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$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}).")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$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}).")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}")</ID>
@ -153,20 +155,15 @@
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Warning: An ${error::class.java.simpleName} was encountered while verifying setup but it was ignored by the onSetupErrorHandler. Ignoring message: ${error.message}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Warning: Fetching UTXOs is repeatedly failing! We will only try about ${(9 - failedUtxoFetches + 2) / 3} more times then give up for this session.")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because it already exists")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$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}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$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}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("batch scan complete! Total time: ${metrics.cumulativeTime} Total blocks measured: ${metrics.cumulativeItems} Cumulative bps: ${metrics.cumulativeIps.format()}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("batch scanned ($percent%): $lastScannedHeight/${range.last} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("block: $height\thash=${hash?.toHexReversed()} \tprevHash=${block?.prevHash?.toByteArray()?.toHexReversed()}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("batch scanned ($percent%): $lastScannedHeight/${range.endInclusive} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$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")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("validation failed at block ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}")</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val end = min((range.first + (i * DOWNLOAD_BATCH_SIZE)) - 1, range.last) // subtract 1 on the first value because the range is inclusive</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val errorMessage = "ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!"</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val lastDownloadedHeight = downloader.getLastDownloadedHeight().takeUnless { it &lt; network.saplingActivationHeight } ?: -1</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val originalCheckpoint = lowerBoundHeight + MAX_REORG_SIZE + 2 // add one because we already have the checkpoint. Add one again because we delete ABOVE the block</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val percentValue = (lastScannedHeight - range.first) / (range.last - range.first + 1).toFloat() * 100.0f</ID>
<ID>MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val summary = if (noWorkDone) "Nothing to process: no new blocks to download or scan" else "Done processing blocks"</ID>
<ID>MaxLineLength:ConsensusBranchId.kt$ConsensusBranchId.SPROUT$// 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)</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi$onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.toLong()).build())</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi$Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight.value).setCount(count).setNonce(nonce).build()</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi$onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.value).build())</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi$twig("resetting darksidewalletd with saplingActivation=$saplingActivationHeight branchId=$branchId chainName=$chainName")</ID>
<ID>MaxLineLength:DarksideApi.kt$DarksideApi.EmptyResponse$if (error != null) throw RuntimeException("Server responded with an error: $error caused by ${error?.cause}")</ID>
<ID>MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator$if</ID>
@ -186,6 +183,7 @@
<ID>MaxLineLength:DerivedDataDb.kt$TransactionDao$ /* we want all received txs except those that are change and all sent transactions (even those that haven't been mined yet). Note: every entry in the 'send_notes' table has a non-null value for 'address' */</ID>
<ID>MaxLineLength:DerivedDataDb.kt$TransactionDao$// delete the UTXOs because these are effectively cached and we don't have a good way of knowing whether they're spent</ID>
<ID>MaxLineLength:DerivedDataDb.kt$TransactionDao$suspend</ID>
<ID>MaxLineLength:Exceptions.kt$BirthdayException.ExactBirthdayNotFoundException$class</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException$ConfigurationException : CompactBlockProcessorException</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException$Disconnected : CompactBlockProcessorException</ID>
<ID>MaxLineLength:Exceptions.kt$CompactBlockProcessorException.EnhanceTransactionError$EnhanceTxDecryptError : EnhanceTransactionError</ID>
@ -203,6 +201,9 @@
<ID>MaxLineLength:GetAddressFragment.kt$GetAddressFragment$val zaddress = DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))</ID>
<ID>MaxLineLength:GetAddressFragment.kt$GetAddressFragment$viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }</ID>
<ID>MaxLineLength:GetBalanceFragment.kt$GetBalanceFragment$val viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() }</ID>
<ID>MaxLineLength:GetBlockFragment.kt$GetBlockFragment$val newHeight = min(binding.textBlockHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)</ID>
<ID>MaxLineLength:GetBlockRangeFragment.kt$GetBlockRangeFragment$val end = max(binding.textEndHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)</ID>
<ID>MaxLineLength:GetBlockRangeFragment.kt$GetBlockRangeFragment$val start = max(binding.textStartHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt"</ID>
<ID>MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt"</ID>
@ -231,10 +232,8 @@
<ID>MaxLineLength:InitializerTest.kt$InitializerTest$// assertEquals("Height should equal sapling activation height when defaultToOldestHeight is true", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)</ID>
<ID>MaxLineLength:InitializerTest.kt$InitializerTest$// assertEquals("Incorrect height used for import.", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height)</ID>
<ID>MaxLineLength:InitializerTest.kt$InitializerTest$// assertNotEquals("Height should not equal sapling activation height when defaultToOldestHeight is false", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, h)</ID>
<ID>MaxLineLength:LightWalletGrpcService.kt$LightWalletGrpcService$twig("getting channel isShutdown: ${channel.isShutdown} isTerminated: ${channel.isTerminated} getState: $state stateCount: $stateCount", -1)</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$binding.inputAddress.setText(DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext())))</ID>
<ID>MaxLineLength:LightWalletService.kt$LightWalletService$fun</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString())</ID>
<ID>MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight</ID>
<ID>MaxLineLength:MaintainedTest.kt$TestPurpose.DARKSIDE$* These tests require a running instance of [darksidewalletd](https://github.com/zcash/lightwalletd/blob/master/docs/darksidewalletd.md).</ID>
<ID>MaxLineLength:MultiAccountIntegrationTest.kt$// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l"</ID>
<ID>MaxLineLength:MultiAccountTest.kt$// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
@ -275,9 +274,9 @@
<ID>MaxLineLength:RustBackend.kt$RustBackend$// // serialize the list, send it over to rust and get back a serialized set of results that we parse out and return</ID>
<ID>MaxLineLength:RustBackend.kt$RustBackend$// override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList {</ID>
<ID>MaxLineLength:RustBackend.kt$RustBackend$throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds")</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest$"$info\n ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight"</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest$"is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false"</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest$assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height.toInt() == height)</ID>
<ID>MaxLineLength:RustBackendWelding.kt$RustBackendWelding$suspend fun clearUtxos(tAddress: String, aboveHeightInclusive: BlockHeight = BlockHeight(network.saplingActivationHeight.value)): Boolean</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest$"${wallet.endpoint} ${wallet.networkName} Lightwalletd is too far behind. Downloader height $downloaderHeight is more than 10 blocks behind block explorer height $expectedHeight"</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest$assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height == height.value)</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest.Companion$"zxviews1q0hxkupsqqqqpqzsffgrk2smjuccedua7zswf5e3rgtv3ga9nhvhjug670egshd6me53r5n083s2m9mf4va4z7t39ltd3wr7hawnjcw09eu85q0ammsg0tsgx24p4ma0uvr4p8ltx5laum2slh2whc23ctwlnxme9w4dw92kalwk5u4wyem8dynknvvqvs68ktvm8qh7nx9zg22xfc77acv8hk3qqll9k3x4v2fa26puu2939ea7hy4hh60ywma69xtqhcy4037ne8g2sg8sq"</ID>
<ID>MaxLineLength:SanityTest.kt$SanityTest.Companion$"zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063"</ID>
<ID>MaxLineLength:SdkSynchronizer.kt$DefaultSynchronizerFactory$// TODO [#242]: Don't hard code page size. It is a workaround for Uncaught Exception: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. and is probably related to FlowPagedList</ID>
@ -289,7 +288,6 @@
<ID>MaxLineLength:SetupTest.kt$SetupTest$val phrase = "still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread"</ID>
<ID>MaxLineLength:SetupTest.kt$SetupTest.Companion$private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt"</ID>
<ID>MaxLineLength:ShieldFundsSample.kt$ShieldFundsSample$val 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" // \"//\"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"//"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"</ID>
<ID>MaxLineLength:SmokeTest.kt$SmokeTest$"Wallet is using plaintext. This will cause problems for the test. Ensure that the `lightwalletd_allow_very_insecure_connections` resource value is false"</ID>
<ID>MaxLineLength:SmokeTest.kt$SmokeTest$Assert.assertEquals("Invalid CacheDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Cache.db", wallet.initializer.rustBackend.pathCacheDb)</ID>
<ID>MaxLineLength:SmokeTest.kt$SmokeTest$Assert.assertEquals("Invalid CacheDB params dir", "/data/user/0/cash.z.ecc.android.sdk.test/cache/params", wallet.initializer.rustBackend.pathParamsDir)</ID>
<ID>MaxLineLength:SmokeTest.kt$SmokeTest$Assert.assertEquals("Invalid DataDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Data.db", wallet.initializer.rustBackend.pathDataDb)</ID>
@ -315,24 +313,23 @@
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt"</ID>
<ID>MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet$suspend</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups$ALICE : Backups</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups$DEV_WALLET : Backups</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups$SAMPLE_WALLET : Backups</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.BOB$BOB</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.DEFAULT$DEFAULT</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.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"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.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"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.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"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.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"</ID>
<ID>MaxLineLength:TestWallet.kt$TestWallet.Backups.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"</ID>
<ID>MaxLineLength:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$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"</ID>
<ID>MaxLineLength:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$runBlocking { config.importWallet(seed, BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight), ZcashNetwork.Testnet, lightWalletEndpoint) }</ID>
<ID>MaxLineLength:TransactionViewHolder.kt$TransactionViewHolder$icon.setColorFilter(ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound))</ID>
<ID>MaxLineLength:Transactions.kt$if (latestHeight == null || latestHeight &lt; saplingActivationHeight || expiryHeight &lt; saplingActivationHeight) return false</ID>
<ID>MaxLineLength:Transactions.kt$if (latestHeight == null || latestHeight.value &lt; saplingActivationHeight.value || expiryHeight &lt; saplingActivationHeight.value) return false</ID>
<ID>MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$// Assert.assertTrue("Not enough funds to run sample. Expected at least $TX_VALUE Zatoshi but found $value. Try adding funds to $address", value >= TX_VALUE)</ID>
<ID>MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$// twig("FOUND utxo balance of total: ${walletBalance.totalZatoshi} available: ${walletBalance.availableZatoshi}")</ID>
<ID>MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$// walletA.send(TX_VALUE, walletA.transparentAddress, "${TransparentRestoreSample::class.java.simpleName} z->t")</ID>
<ID>MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$Assert.assertTrue("Not enough funds to run sample. Expected some Zatoshi but found ${tbalance.available}. Try adding funds to $address", tbalance.available.value > 0)</ID>
<ID>MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$val walletSandbox = TestWallet(TestWallet.Backups.SAMPLE_WALLET.seedPhrase, "WalletC", Testnet, startHeight = 1330190)</ID>
<ID>MaxLineLength:TransparentTest.kt$TransparentTest$assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPublicKey(uvk.extpub, network = network))</ID>
<ID>MaxLineLength:TransparentTest.kt$TransparentTest.Companion$const val PHRASE = "deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"</ID>
<ID>MaxLineLength:TransparentTest.kt$TransparentTest.Companion.ExpectedTestnet$override val zAddr = "ztestsapling1wn3tw9w5rs55x5yl586gtk72e8hcfdq8zsnjzcu8p7ghm8lrx54axc74mvm335q7lmy3g0sqje6"</ID>
<ID>MaxLineLength:Twig.kt$inline</ID>
<ID>MaxLineLength:WalletBirthdayTool.kt$WalletBirthdayTool$* @param treeFiles A list of files, sorted in descending order based on `int` value of the first part of the filename.</ID>
<ID>MaxLineLength:WalletService.kt$var duration = Math.pow(initialDelayMillis.toDouble(), (sequence.toDouble() / 4.0)).toLong() + Random.nextLong(1000L)</ID>
<ID>MaxLineLength:WalletService.kt$var sequence = 0 // count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much.</ID>
<ID>MaxLineLength:ZcashSdk.kt$ZcashSdk$*</ID>
@ -346,13 +343,11 @@
<ID>MayBeConst:DemoConstants.kt$DemoConstants$val sendAmount: Double = 0.000018</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$val sendAmount: Double = 0.00017</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$val utxoEndHeight: Int = 1075590</ID>
<ID>MayBeConst:DemoConstants.kt$DemoConstants$val utxoEndHeight: Int = 968085</ID>
<ID>MayBeConst:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m"</ID>
<ID>MayBeConst:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0"</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default amount of time, in milliseconds, to poll for new blocks. Typically, this should be about half the average * block time. */ val POLL_INTERVAL = 20_000L</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default attempts at retrying. */ val RETRIES = 5</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default number of blocks to rewind when a chain reorg is detected. This should be large enough to recover from the * reorg but smaller than the theoretical max reorg size of 100. */ val REWIND_DISTANCE = 10</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default size of batches of blocks to request from the compact block service. */ val DOWNLOAD_BATCH_SIZE = 100</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default size of batches of blocks to scan via librustzcash. The smaller this number the more granular information * can be provided about scan state. Unfortunately, it may also lead to a lot of overhead during scanning. */ val SCAN_BATCH_SIZE = 150</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Estimate of the time between blocks. */ val BLOCK_INTERVAL_MILLIS = 75_000L</ID>
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$/** * File name for the sapling output params */ val OUTPUT_PARAM_FILE_NAME = "sapling-output.params"</ID>
@ -368,7 +363,7 @@
<ID>MayBeConst:ZcashSdk.kt$ZcashSdk$val DEFAULT_ALIAS: String = "ZcashSdk"</ID>
<ID>PrintStackTrace:CompactBlockProcessor.kt$CompactBlockProcessor$t</ID>
<ID>PrintStackTrace:PersistentTransactionManager.kt$PersistentTransactionManager$t</ID>
<ID>ReturnCount:CurrencyFormatter.kt$ inline fun String?.safelyConvertToBigDecimal(): BigDecimal?</ID>
<ID>ReturnCount:CurrencyFormatter.kt$inline fun String?.safelyConvertToBigDecimal(): BigDecimal?</ID>
<ID>ReturnCount:MainActivity.kt$MainActivity$fun getClipboardText(): String?</ID>
<ID>SpreadOperator:Initializer.kt$Initializer.Config$( *DerivationTool.deriveUnifiedViewingKeys( seed, network, numberOfAccounts ) )</ID>
<ID>SpreadOperator:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$(*viewingKeys.toTypedArray())</ID>
@ -379,6 +374,7 @@
<ID>SwallowedException:Ext.kt$t: Throwable</ID>
<ID>SwallowedException:SdkSynchronizer.kt$SdkSynchronizer$t: Throwable</ID>
<ID>SwallowedException:SharedViewModel.kt$SharedViewModel$t: Throwable</ID>
<ID>TooGenericExceptionCaught:CheckpointTool.kt$CheckpointTool$t: Throwable</ID>
<ID>TooGenericExceptionCaught:CompactBlockDownloader.kt$CompactBlockDownloader$t: Throwable</ID>
<ID>TooGenericExceptionCaught:CompactBlockProcessor.kt$CompactBlockProcessor$e: Throwable</ID>
<ID>TooGenericExceptionCaught:CompactBlockProcessor.kt$CompactBlockProcessor$t: Throwable</ID>
@ -395,7 +391,6 @@
<ID>TooGenericExceptionCaught:SdkSynchronizer.kt$SdkSynchronizer$tError: Throwable</ID>
<ID>TooGenericExceptionCaught:SdkSynchronizer.kt$SdkSynchronizer$zError: Throwable</ID>
<ID>TooGenericExceptionCaught:SharedViewModel.kt$SharedViewModel$t: Throwable</ID>
<ID>TooGenericExceptionCaught:WalletBirthdayTool.kt$WalletBirthdayTool$t: Throwable</ID>
<ID>TooGenericExceptionCaught:WalletService.kt$t: Throwable</ID>
<ID>TooGenericExceptionCaught:WalletTransactionEncoder.kt$WalletTransactionEncoder$t: Throwable</ID>
<ID>TooGenericExceptionThrown:DarksideApi.kt$DarksideApi.EmptyResponse$throw RuntimeException("Server responded with an error: $error caused by ${error?.cause}")</ID>
@ -406,7 +401,6 @@
<ID>TooManyFunctions:HomeFragment.kt$HomeFragment : BaseDemoFragment</ID>
<ID>TooManyFunctions:Initializer.kt$Initializer$Companion : Erasable</ID>
<ID>TooManyFunctions:Initializer.kt$Initializer$Config</ID>
<ID>TooManyFunctions:LightWalletGrpcService.kt$LightWalletGrpcService : LightWalletService</ID>
<ID>TooManyFunctions:ListTransactionsFragment.kt$ListTransactionsFragment : BaseDemoFragment</ID>
<ID>TooManyFunctions:ListUtxosFragment.kt$ListUtxosFragment : BaseDemoFragment</ID>
<ID>TooManyFunctions:MainActivity.kt$MainActivity : AppCompatActivityOnPrimaryClipChangedListenerDrawerListener</ID>
@ -458,8 +452,13 @@
<ID>UnusedPrivateMember:SetupTest.kt$SetupTest.Companion$private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a"</ID>
<ID>UnusedPrivateMember:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$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"</ID>
<ID>UnusedPrivateMember:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$private const val targetHeight = 663250</ID>
<ID>UnusedPrivateMember:TransactionCounterUtil.kt$TransactionCounterUtil$private val network = ZcashNetwork.Mainnet</ID>
<ID>UseCheckOrError:FlowPagedListBuilder.kt$FlowPagedListBuilder$throw IllegalStateException("Unable to create executor based on dispatcher: $this")</ID>
<ID>UseCheckOrError:Placeholders.kt$SampleSpendingKeyProvider$throw IllegalStateException("This sample provider only supports the dummy seed")</ID>
<ID>UtilityClassWithPublicConstructor:DerivationTool.kt$DerivationTool</ID>
<ID>UtilityClassWithPublicConstructor:Placeholders.kt$SeedGenerator</ID>
<ID>UtilityClassWithPublicConstructor:SaplingParamTool.kt$SaplingParamTool</ID>
<ID>VariableNaming:ShieldFundsSample.kt$ShieldFundsSample$val 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" // \"//\"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"//"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"</ID>
<ID>VariableNaming:TransparentRestoreSample.kt$TransparentRestoreSample$val TX_VALUE = Zatoshi(ZcashSdk.MINERS_FEE.value / 2)</ID>
</CurrentIssues>
</SmellBaseline>

111
tools/detekt.yml

@ -46,6 +46,7 @@ output-reports:
# - 'TxtOutputReport'
# - 'XmlOutputReport'
# - 'HtmlOutputReport'
# - 'MdOutputReport'
comments:
active: true
@ -62,6 +63,9 @@ comments:
EndOfSentenceFormat:
active: false
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
KDocReferencesNonPublicProperty:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
OutdatedDocumentation:
active: false
matchTypeParameters: true
@ -133,6 +137,15 @@ complexity:
NestedBlockDepth:
active: true
threshold: 4
NestedScopeFunctions:
active: false
threshold: 1
functions:
- 'kotlin.apply'
- 'kotlin.run'
- 'kotlin.with'
- 'kotlin.let'
- 'kotlin.also'
ReplaceSafeCallChainWithRun:
active: false
StringLiteralDuplication:
@ -159,19 +172,19 @@ coroutines:
GlobalCoroutineUsage:
active: false
InjectDispatcher:
active: false
active: true
dispatcherNames:
- 'IO'
- 'Default'
- 'Unconfined'
RedundantSuspendModifier:
active: false
active: true
SleepInsteadOfDelay:
active: false
active: true
SuspendFunWithCoroutineScopeReceiver:
active: false
SuspendFunWithFlowReturnType:
active: false
active: true
empty-blocks:
active: true
@ -218,7 +231,7 @@ exceptions:
- 'hashCode'
- 'toString'
InstanceOfCheckForException:
active: false
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
NotImplementedDeclaration:
active: false
@ -283,58 +296,46 @@ naming:
active: true
BooleanPropertyNaming:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
allowedPattern: '^(is|has|are)'
ignoreOverridden: true
ClassNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
classPattern: '[A-Z][a-zA-Z0-9]*'
ConstructorParameterNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
EnumNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
forbiddenName: []
FunctionMaxLength:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
minimumFunctionNameLength: 3
FunctionNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
functionPattern: '[a-z][a-zA-Z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
ignoreAnnotated:
- 'Composable'
FunctionParameterNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
InvalidPackageDeclaration:
active: false
excludes: ['**/*.kts']
active: true
rootPackage: ''
requireRootInDeclaration: false
LambdaParameterNaming:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
parameterPattern: '[a-z][A-Za-z0-9]*|_'
MatchingDeclarationName:
active: true
@ -343,37 +344,30 @@ naming:
active: true
ignoreOverridden: true
NoNameShadowing:
active: false
active: true
NonBooleanPropertyPrefixedWithIs:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
ObjectPropertyNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
maximumVariableNameLength: 64
VariableMinLength:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
minimumVariableNameLength: 1
VariableNaming:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
@ -383,6 +377,9 @@ performance:
active: true
ArrayPrimitive:
active: true
CouldBeSequence:
active: false
threshold: 3
ForEachOnRange:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
@ -395,7 +392,7 @@ performance:
potential-bugs:
active: true
AvoidReferentialEquality:
active: false
active: true
forbiddenTypePatterns:
- 'kotlin.String'
CastToNullableType:
@ -405,7 +402,7 @@ potential-bugs:
DontDowncastCollectionTypes:
active: false
DoubleMutabilityForCollection:
active: false
active: true
mutableTypes:
- 'kotlin.collections.MutableList'
- 'kotlin.collections.MutableMap'
@ -428,9 +425,9 @@ potential-bugs:
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: false
active: true
IgnoredReturnValue:
active: false
active: true
restrictToAnnotatedMethods: true
returnValueAnnotations:
- '*.CheckResult'
@ -454,7 +451,7 @@ potential-bugs:
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
ignoreOnClassesPattern: ''
MapGetWithNotNullAssertionOperator:
active: false
active: true
MissingPackageDeclaration:
active: false
excludes: ['**/*.kts']
@ -474,7 +471,7 @@ potential-bugs:
UnnecessarySafeCall:
active: true
UnreachableCatchBlock:
active: false
active: true
UnreachableCode:
active: true
UnsafeCallOnNullableType:
@ -483,9 +480,9 @@ potential-bugs:
UnsafeCast:
active: true
UnusedUnaryOperator:
active: false
active: true
UselessPostfixExpression:
active: false
active: true
WrongEqualsTypeParameter:
active: true
@ -493,6 +490,9 @@ style:
active: true
CanBeNonNullable:
active: false
CascadingCallWrapping:
active: false
includeElvis: true
ClassOrdering:
active: false
CollapsibleIfStatements:
@ -503,7 +503,7 @@ style:
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: false
active: true
maxDestructuringEntries: 3
EqualsNullCall:
active: true
@ -512,7 +512,7 @@ style:
ExplicitCollectionElementAccessMethod:
active: false
ExplicitItLambdaParameter:
active: false
active: true
ExpressionBodySyntax:
active: false
includeLineWrapping: false
@ -539,8 +539,11 @@ style:
ignorePackages:
- '*.internal'
- '*.internal.*'
ForbiddenVoid:
ForbiddenSuppress:
active: false
rules: []
ForbiddenVoid:
active: true
ignoreOverridden: false
ignoreUsageInGenerics: false
FunctionOnlyReturningConstant:
@ -559,7 +562,7 @@ style:
maxJumpCount: 1
MagicNumber:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts']
ignoreNumbers:
- '-1'
- '0'
@ -579,6 +582,9 @@ style:
active: false
MandatoryBracesLoops:
active: false
MaxChainedCallsOnSameLine:
active: false
maxChainedCalls: 5
MaxLineLength:
active: true
maxLineLength: 120
@ -597,8 +603,10 @@ style:
active: false
NoTabs:
active: false
ObjectLiteralToLambda:
NullableBooleanCheck:
active: false
ObjectLiteralToLambda:
active: true
OptionalAbstractKeyword:
active: true
OptionalUnit:
@ -612,7 +620,7 @@ style:
RedundantExplicitType:
active: false
RedundantHigherOrderMapUsage:
active: false
active: true
RedundantVisibilityModifierRule:
active: false
ReturnCount:
@ -644,8 +652,10 @@ style:
active: false
UnnecessaryApply:
active: true
UnnecessaryFilter:
UnnecessaryBackticks:
active: false
UnnecessaryFilter:
active: true
UnnecessaryInheritance:
active: true
UnnecessaryInnerClass:
@ -664,13 +674,13 @@ style:
active: true
allowedNames: '(_|ignored|expected|serialVersionUID)'
UseAnyOrNoneInsteadOfFind:
active: false
active: true
UseArrayLiteralsInAnnotations:
active: false
active: true
UseCheckNotNull:
active: false
active: true
UseCheckOrError:
active: false
active: true
UseDataClass:
active: false
allowVars: false
@ -681,19 +691,20 @@ style:
UseIfInsteadOfWhen:
active: false
UseIsNullOrEmpty:
active: false
active: true
UseOrEmpty:
active: false
active: true
UseRequire:
active: false
active: true
UseRequireNotNull:
active: false
active: true
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: true
ignoreLateinitVar: false
WildcardImport:
active: true
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']

Loading…
Cancel
Save