From 2225f65ee74965ad7b2fe6dfb093ab59ff3a5416 Mon Sep 17 00:00:00 2001 From: fekt Date: Tue, 29 Nov 2022 20:19:12 -0500 Subject: [PATCH] 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 --- .gitattributes | 2 + .gitignore | 1 + .idea/.gitignore | 3 - CHANGELOG.md | 14 +- MIGRATIONS.md | 20 +- README.md | 217 +----- build-conventions/build.gradle.kts | 26 +- build-conventions/buildscript-gradle.lockfile | 60 +- build-conventions/gradle.lockfile | 163 ++--- ...cash-sdk.dependency-conventions.gradle.kts | 13 +- .../sdk/darkside/reorgs/InboundTxTests.kt | 14 +- .../sdk/darkside/reorgs/ReorgSetupTest.kt | 6 +- .../sdk/darkside/reorgs/ReorgSmallTest.kt | 7 +- .../android/sdk/darkside/test/DarksideApi.kt | 36 +- .../darkside/test/DarksideTestCoordinator.kt | 68 +- .../android/sdk/darkside/test/TestWallet.kt | 72 +- .../sdk/sample/demoapp/SampleCodeTest.kt | 20 +- .../z/ecc/android/sdk/demoapp/MainActivity.kt | 10 +- .../demos/getaddress/GetAddressFragment.kt | 2 +- .../demos/getbalance/GetBalanceFragment.kt | 16 +- .../demos/getblock/GetBlockFragment.kt | 11 +- .../getblockrange/GetBlockRangeFragment.kt | 24 +- .../getprivatekey/GetPrivateKeyFragment.kt | 2 +- .../ListTransactionsFragment.kt | 22 +- .../demos/listutxos/ListUtxosFragment.kt | 66 +- .../sdk/demoapp/demos/send/SendFragment.kt | 16 +- .../android/sdk/demoapp/util/NetworkExt.kt | 18 +- .../ecc/android/sdk/demoapp/DemoConstants.kt | 4 +- .../ecc/android/sdk/demoapp/DemoConstants.kt | 2 +- docs/Architecture.md | 36 +- docs/Consumers.md | 110 +++ docs/Setup.md | 6 +- {assets => docs/assets}/build-variants.png | Bin {assets => docs/assets}/ndk-window.png | Bin {assets => docs/assets}/sdk-manager-icon.png | Bin {assets => docs/assets}/sdk_dev_pov_final.png | Bin {assets => docs/assets}/sdk_diagram_final.png | Bin gradle.properties | 35 +- gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 6 + gradlew.bat | 14 +- sdk-lib/Cargo.toml | 20 +- .../java/cash/z/ecc/android/sdk/AssetTest.kt | 13 +- .../z/ecc/android/sdk/ext/TestExtensions.kt | 6 +- .../ecc/android/sdk/integration/SanityTest.kt | 33 +- .../ecc/android/sdk/integration/SmokeTest.kt | 13 +- .../sdk/integration/TestnetIntegrationTest.kt | 22 +- .../integration/service/ChangeServiceTest.kt | 20 +- .../android/sdk/internal/CheckpointTest.kt | 47 ++ .../sdk/internal/WalletBirthdayTest.kt | 30 - .../z/ecc/android/sdk/jni/BranchIdTest.kt | 15 +- .../z/ecc/android/sdk/jni/TransparentTest.kt | 2 +- .../android/sdk/sample/ShieldFundsSample.kt | 2 +- .../sdk/sample/TransparentRestoreSample.kt | 27 +- ...thdayToolTest.kt => CheckpointToolTest.kt} | 17 +- .../android/sdk/util/AddressGeneratorUtil.kt | 3 +- .../android/sdk/util/BalancePrinterUtil.kt | 19 +- .../ecc/android/sdk/util/DataDbScannerUtil.kt | 10 +- .../cash/z/ecc/android/sdk/util/TestWallet.kt | 72 +- .../sdk/util/TransactionCounterUtil.kt | 21 +- ...irthdayFixture.kt => CheckpointFixture.kt} | 27 +- .../checkpoint/mainnet/1150000.json | 4 +- .../checkpoint/mainnet/1160000.json | 7 + .../checkpoint/testnet/1000000.json | 7 - .../checkpoint/testnet/1150000.json | 7 + .../cash/z/ecc/android/sdk/Initializer.kt | 99 ++- .../cash/z/ecc/android/sdk/SdkSynchronizer.kt | 76 +-- .../cash/z/ecc/android/sdk/Synchronizer.kt | 43 +- .../sdk/block/CompactBlockProcessor.kt | 628 +++++++++++------- .../sdk/db/entity/CompactBlockEntity.kt | 4 +- .../ecc/android/sdk/db/entity/Transactions.kt | 56 +- .../z/ecc/android/sdk/exception/Exceptions.kt | 166 ++--- .../z/ecc/android/sdk/ext/BatchMetrics.kt | 11 +- .../ecc/android/sdk/ext/ConsensusBranchId.kt | 7 +- .../cash/z/ecc/android/sdk/ext/ZcashSdk.kt | 5 +- .../ecc/android/sdk/internal/CheckpointExt.kt | 46 ++ .../z/ecc/android/sdk/internal/IsEmpty.kt | 5 + .../cash/z/ecc/android/sdk/internal/Twig.kt | 1 + .../android/sdk/internal/WalletBirthdayExt.kt | 37 -- .../sdk/internal/block/CompactBlockDbStore.kt | 28 +- .../internal/block/CompactBlockDownloader.kt | 6 +- .../sdk/internal/block/CompactBlockStore.kt | 10 +- .../android/sdk/internal/db/CompactBlockDb.kt | 19 +- .../android/sdk/internal/db/DerivedDataDb.kt | 14 +- .../sdk/internal/db/PendingTransactionDb.kt | 4 +- .../android/sdk/internal/model/Checkpoint.kt | 22 + .../service/LightWalletGrpcService.kt | 190 +++--- .../internal/service/LightWalletService.kt | 13 +- .../transaction/PagedTransactionRepository.kt | 44 +- .../PersistentTransactionManager.kt | 9 +- .../transaction/TransactionEncoder.kt | 7 +- .../transaction/TransactionManager.kt | 5 +- .../transaction/TransactionRepository.kt | 11 +- .../transaction/WalletTransactionEncoder.kt | 19 +- .../cash/z/ecc/android/sdk/jni/RustBackend.kt | 119 ++-- .../ecc/android/sdk/jni/RustBackendWelding.kt | 29 +- .../z/ecc/android/sdk/model/BlockHeight.kt | 67 ++ .../android/sdk/model/LightWalletEndpoint.kt | 5 + .../sdk/model/LightWalletEndpointExt.kt | 44 ++ .../z/ecc/android/sdk/model/WalletBalance.kt | 28 + .../cash/z/ecc/android/sdk/model/Zatoshi.kt | 4 +- .../z/ecc/android/sdk/model/ZcashNetwork.kt | 40 ++ ...alletBirthdayTool.kt => CheckpointTool.kt} | 72 +- .../z/ecc/android/sdk/tool/DerivationTool.kt | 2 +- .../z/ecc/android/sdk/type/WalletTypes.kt | 62 -- sdk-lib/src/main/proto/darkside.proto | 6 +- sdk-lib/src/main/proto/service.proto | 172 +++-- sdk-lib/src/main/res/values/bools.xml | 4 - sdk-lib/src/main/res/values/strings.xml | 3 - sdk-lib/src/main/rust/lib.rs | 28 +- .../z/ecc/android/sdk/ext/ConversionsTest.kt | 9 +- .../z/ecc/android/sdk/model/ZatoshiTest.kt | 16 + tools/detekt-baseline.xml | 101 ++- tools/detekt.yml | 111 ++-- 115 files changed, 2333 insertions(+), 1764 deletions(-) create mode 100644 .gitattributes delete mode 100644 .idea/.gitignore create mode 100644 docs/Consumers.md rename {assets => docs/assets}/build-variants.png (100%) rename {assets => docs/assets}/ndk-window.png (100%) rename {assets => docs/assets}/sdk-manager-icon.png (100%) rename {assets => docs/assets}/sdk_dev_pov_final.png (100%) rename {assets => docs/assets}/sdk_diagram_final.png (100%) create mode 100644 sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/CheckpointTest.kt delete mode 100644 sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/WalletBirthdayTest.kt rename sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/{WalletBirthdayToolTest.kt => CheckpointToolTest.kt} (72%) rename sdk-lib/src/androidTest/java/cash/z/ecc/fixture/{WalletBirthdayFixture.kt => CheckpointFixture.kt} (70%) create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1160000.json delete mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1000000.json create mode 100644 sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1150000.json create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/CheckpointExt.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/IsEmpty.kt delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/WalletBirthdayExt.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/Checkpoint.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/BlockHeight.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpoint.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpointExt.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletBalance.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/ZcashNetwork.kt rename sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/{WalletBirthdayTool.kt => CheckpointTool.kt} (68%) delete mode 100644 sdk-lib/src/main/res/values/bools.xml delete mode 100644 sdk-lib/src/main/res/values/strings.xml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore index 62a11d5..fdd8263 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ DecompileChecker.kt backup-dbs/ *.db .DS_Store +sdk-lib/Cargo.lock diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a34faa..4dc5e38 100644 --- a/CHANGELOG.md +++ b/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 ------------------------------------ diff --git a/MIGRATIONS.md b/MIGRATIONS.md index d5ce811..0bb7132 100644 --- a/MIGRATIONS.md +++ b/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. diff --git a/README.md b/README.md index 5ce2b30..da5b7be 100644 --- a/README.md +++ b/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. diff --git a/build-conventions/build.gradle.kts b/build-conventions/build.gradle.kts index a992379..ebd2392 100644 --- a/build-conventions/build.gradle.kts +++ b/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 diff --git a/build-conventions/buildscript-gradle.lockfile b/build-conventions/buildscript-gradle.lockfile index 504107c..8f2c701 100644 --- a/build-conventions/buildscript-gradle.lockfile +++ b/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= diff --git a/build-conventions/gradle.lockfile b/build-conventions/gradle.lockfile index dbe2cb9..11b99c8 100644 --- a/build-conventions/gradle.lockfile +++ b/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 diff --git a/build-conventions/src/main/kotlin/zcash-sdk.dependency-conventions.gradle.kts b/build-conventions/src/main/kotlin/zcash-sdk.dependency-conventions.gradle.kts index bbdc79a..e55997c 100644 --- a/build-conventions/src/main/kotlin/zcash-sdk.dependency-conventions.gradle.kts +++ b/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 { diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/InboundTxTests.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/InboundTxTests.kt index 34f7f96..36215e6 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/InboundTxTests.kt +++ b/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() diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSetupTest.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSetupTest.kt index 105dca8..50af386 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSetupTest.kt +++ b/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() { diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSmallTest.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSmallTest.kt index ae837ff..9a3b45f 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/reorgs/ReorgSmallTest.kt +++ b/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" diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideApi.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideApi.kt index cb9ad27..4f10d49 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideApi.kt +++ b/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?, tipHeight: Int) { + fun stageTransactions(txs: Iterator?, 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 { var completed = false diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt index 24652d4..92bd4de 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/DarksideTestCoordinator.kt +++ b/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 { + fun validateLatestHeight(height: BlockHeight) = runBlocking { val info = synchronizer.processorInfo.first() val networkBlockHeight = info.networkBlockHeight assertTrue( @@ -157,41 +160,44 @@ class DarksideTestCoordinator(val wallet: TestWallet) { ) } - fun validateMinHeightDownloaded(minHeight: Int) = runBlocking { + fun validateMinHeightDownloaded(minHeight: BlockHeight) = runBlocking { 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 { + fun validateMinHeightScanned(minHeight: BlockHeight) = runBlocking { 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 { + fun validateMaxHeightScanned(maxHeight: BlockHeight) = runBlocking { 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" } diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt index 275405c..f783530 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt +++ b/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) + ), ; } } diff --git a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt b/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt index ffaf4a8..60ae52f 100644 --- a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt +++ b/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 { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt index 61e6bc8..cfae5f1 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/MainActivity.kt +++ b/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) { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt index 6f45b4b..df754ca 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt +++ b/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 diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt index cb41bf8..c287926 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt +++ b/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() { // 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() { 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 { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt index 4c494d3..161a186 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblock/GetBlockFragment.kt +++ b/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() { - 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() { } 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") } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt index c96ae82..f1d0c48 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt +++ b/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() { - private fun setBlockRange(blockRange: IntRange) { + private fun setBlockRange(blockRange: ClosedRange) { 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() { 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() { } 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() { 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) } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt index ac863fe..6e07a3c 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt +++ b/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 /** diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt index 33296f4..f8e8a5e 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt +++ b/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() { // 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() { 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() { } } } - */ private val now get() = System.currentTimeMillis() @@ -159,7 +179,12 @@ class ListUtxosFragment : BaseDemoFragment() { 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() { } } } + + @Suppress("MagicNumber") + private fun getUxtoEndHeight(context: Context): BlockHeight { + return BlockHeight.new(ZcashNetwork.fromResources(context), 968085L) + } } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt index 4a005c4..620166c 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt +++ b/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() { 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) diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/NetworkExt.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/NetworkExt.kt index 9c8bc76..4018557 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/util/NetworkExt.kt +++ b/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") + } +} diff --git a/demo-app/src/zcashmainnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt b/demo-app/src/zcashmainnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt index eed743a..c95a317 100644 --- a/demo-app/src/zcashmainnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt +++ b/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 = diff --git a/demo-app/src/zcashtestnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt b/demo-app/src/zcashtestnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt index c23dfc1..a56c3e4 100644 --- a/demo-app/src/zcashtestnet/java/cash/z/ecc/android/sdk/demoapp/DemoConstants.kt +++ b/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 diff --git a/docs/Architecture.md b/docs/Architecture.md index 30404ce..50fc76e 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -1 +1,35 @@ -TODO \ No newline at end of file +# 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 \ No newline at end of file diff --git a/docs/Consumers.md b/docs/Consumers.md new file mode 100644 index 0000000..4b4f48f --- /dev/null +++ b/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 +``` + + + false + + +``` + +/src/zcashtestnet/res/values/bools.xml +``` + + + true + +``` + +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 \ No newline at end of file diff --git a/docs/Setup.md b/docs/Setup.md index 0b6141d..666e6c0 100644 --- a/docs/Setup.md +++ b/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) diff --git a/assets/build-variants.png b/docs/assets/build-variants.png similarity index 100% rename from assets/build-variants.png rename to docs/assets/build-variants.png diff --git a/assets/ndk-window.png b/docs/assets/ndk-window.png similarity index 100% rename from assets/ndk-window.png rename to docs/assets/ndk-window.png diff --git a/assets/sdk-manager-icon.png b/docs/assets/sdk-manager-icon.png similarity index 100% rename from assets/sdk-manager-icon.png rename to docs/assets/sdk-manager-icon.png diff --git a/assets/sdk_dev_pov_final.png b/docs/assets/sdk_dev_pov_final.png similarity index 100% rename from assets/sdk_dev_pov_final.png rename to docs/assets/sdk_dev_pov_final.png diff --git a/assets/sdk_diagram_final.png b/docs/assets/sdk_diagram_final.png similarity index 100% rename from assets/sdk_diagram_final.png rename to docs/assets/sdk_diagram_final.png diff --git a/gradle.properties b/gradle.properties index 813a8c5..0fd71fa 100644 --- a/gradle.properties +++ b/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 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4d4fb3f96a785543079b8df6723c946b..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 10197 zcmaKS1ymhDwk=#NxVyW%y9U<)A-Dv)xI0|j{UX8L-JRg>5ZnnKAh;%chM6~S-g^K4 z>eZ{yK4;gd>gwvXs=Id8Jk-J}R4pT911;+{Jp9@aiz6!p1Oz9z&_kGLA%J5%3Ih@0 zQ|U}%$)3u|G`jIfPzMVfcWs?jV2BO^*3+q2><~>3j+Z`^Z%=;19VWg0XndJ zwJ~;f4$;t6pBKaWn}UNO-wLCFHBd^1)^v%$P)fJk1PbK5<;Z1K&>k~MUod6d%@Bq9 z>(44uiaK&sdhwTTxFJvC$JDnl;f}*Q-^01T508(8{+!WyquuyB7R!d!J)8Ni0p!cV6$CHsLLy6}7C zYv_$eD;)@L)tLj0GkGpBoa727hs%wH$>EhfuFy{_8Q8@1HI%ZAjlpX$ob{=%g6`Ox zLzM!d^zy`VV1dT9U9(^}YvlTO9Bf8v^wMK37`4wFNFzW?HWDY(U(k6@tp(crHD)X5>8S-# zW1qgdaZa*Sh6i%60e1+hty}34dD%vKgb?QmQiZ=-j+isA4={V_*R$oGN#j|#ia@n6 zuZx4e2Xx?^lUwYFn2&Tmbx0qA3Z8;y+zKoeQu;~k~FZGy!FU_TFxYd!Ck;5QvMx9gj5fI2@BLNp~Ps@ zf@k<&Q2GS5Ia9?_D?v~$I%_CLA4x~eiKIZ>9w^c#r|vB?wXxZ(vXd*vH(Fd%Me8p( z=_0)k=iRh%8i`FYRF>E97uOFTBfajv{IOz(7CU zv0Gd84+o&ciHlVtY)wn6yhZTQQO*4Mvc#dxa>h}82mEKKy7arOqU$enb9sgh#E=Lq zU;_RVm{)30{bw+|056%jMVcZRGEBSJ+JZ@jH#~DvaDQm92^TyUq=bY*+AkEakpK>8 zB{)CkK48&nE5AzTqT;WysOG|!y}5fshxR8Ek(^H6i>|Fd&wu?c&Q@N9ZrJ=?ABHI! z`*z8D`w=~AJ!P-9M=T}f`;76$qZRllB&8#9WgbuO$P7lVqdX1=g*t=7z6!0AQ^ux_ z9rcfUv^t}o_l-ZE+TqvqFsA*~W<^78!k;~!i8(eS+(+@u8FxK+Q7;mHZ<1}|4m<}vh@p`t%|@eM_J(P% zI>M7C)Ir{l|J;$G_EGGEhbP4?6{sYzMqBv+x95N&YWFH6UcE@b}B?q)G*4<4mR@sy1#vPnLMK51tb#ED(8TA1nE zYfhK7bo1!R5WJF$5Y?zG21)6+_(_5oSX9sGIW;(O&S?Rh(nydNQYzKjjJ54aDJ-1F zrJ=np8LsN?%?Rt7f~3aAX!2E{`fh_pb?2(;HOB3W+I*~A>W%iY+v45+^e$cE10fA} zXPvw9=Bd+(;+!rl)pkYj0HGB}+3Z!Mr;zr%gz~c-hFMv8b2VRE2R$8V=_XE zq$3=|Yg05(fmwrJ)QK2ptB4no`Y8Dg_vK2QDc6-6sXRQ5k78-+cPi-fH}vpgs|Ive zE=m*XNVs?EWgiNI!5AcD*3QMW)R`EqT!f0e1%hERO&?AT7HWnSf5@#AR{OGuXG3Zb zCnVWg7h|61lGV3k+>L<#d>)InG>ETn1DbOHCfztqzQ_fBiaUt@q6VMy={Fe-w#~2- z0?*f|z$zgjI9>+JVICObBaK=pU}AEOd@q(8d?j7zQFD@=6t`|KmolTr2MfBI$;EGh zD%W0cA_d#V6Lb$us5yIG(|d>r-QleC4;%hEu5W9hyY zY#+ESY&v`8(&mC~?*|e5WEhC!YU2>m_}`K+q9)a(d$bsS<=YkyZGp}YA%TXw>@abA zS_poVPoN+?<6?DAuCNt&5SHV(hp56PJ})swwVFZFXM->F zc|0c8<$H_OV%DR|y7e+s$12@Ac8SUClPg8_O9sTUjpv%6Jsn5vsZCg>wL+db4c+{+ zsg<#wOuV4jeOq`veckdi-1`dz;gvL)bZeH|D*x=8UwRU5&8W1@l>3$)8WzET0%;1J zM3(X<7tKK&9~kWRI{&FmwY5Gg!b5f4kI_vSm)H1#>l6M+OiReDXC{kPy!`%Ecq-+3yZTk=<` zm)pE6xum5q0Qkd#iny0Q-S}@I0;mDhxf>sX)Oiv)FdsAMnpx%oe8OQ`m%Xeozdzx!C1rQR>m1c_}+J4x)K}k{G zo68;oGG&Ox7w^-m7{g4a7NJu-B|~M;oIH~~#`RyUNm##feZH;E?pf}nshmoiIY52n z%pc%lnU4Q#C=RUz)RU6}E_j4#)jh<&a%JyJj$Fufc#&COaxFHtl}zJUGNLBu3~_@1 zn9F^JO9);Duxo&i@>X(kbYga1i>6p1fca8FzQ0>((Lb-aPUbC*d~a03V$y;*RBY!R ziEJ2IF^FjrvO}0Uy{cMn%u<+P5U!UO>pm9#ZYL5i6|xSC+np7IH$GfXs&uI;y4as@ z&AzJh>(S2?3PKKgab3Z(`xbx(C#46XIvVcW8eG_DjT~}Yz_8PWZ`uf6^Xr=vkvL_` zqmvfgJL+Zc`;iq~iP?%@G7}~fal-zqxa0yNyHBJJ5M)9bI>7S_cg?Ya&p(I)C5Ef4 zZ>YAF6x|U=?ec?g*|f2g5Tw3PgxaM_bi_5Az9MO$;_Byw(2d}2%-|bg4ShdQ;)Z|M z4K|tFv)qx*kKGKoyh!DQY<{n&UmAChq@DJrQP>EY7g1JF(ih*D8wCVWyQ z5Jj^|-NVFSh5T0vd1>hUvPV6?=`90^_)t(L9)XOW7jeP45NyA2lzOn&QAPTl&d#6P zSv%36uaN(9i9WlpcH#}rmiP#=L0q(dfhdxvFVaOwM;pY;KvNQ9wMyUKs6{d}29DZQ z{H3&Sosr6)9Z+C>Q5)iHSW~gGoWGgK-0;k~&dyr-bA3O|3PCNzgC?UKS_B=^i8Ri^ zd_*_qI4B07Cayq|p4{`U_E_P=K`N_~{F|+-+`sCgcNxs`%X!$=(?l2aAW}0M=~COb zf19oe^iuAUuDEf)4tgv<=WRPpK@IjToNNC*#&Ykw!)aqWU4h#|U@(cG_=Qx+&xt~a zvCz~Ds3F71dsjNLkfM%TqdVNu=RNMOzh7?b+%hICbFlOAPphrYy>7D-e7{%o_kPFn z;T!?ilE-LcKM0P(GKMseEeW57Vs`=FF}(y@^pQl;rL3fHs8icmA+!6YJt&8 ztSF?%Un35qkv>drkks&BNTJv~xK?vD;aBkp7eIkDYqn+G0%;sT4FcwAoO+vke{8CO z0d76sgg$CannW5T#q`z~L4id)9BCKRU0A!Z-{HpXr)QJrd9@iJB+l32Ql)Z}*v(St zE)Vp=BB=DDB4Pr}B(UHNe31<@!6d{U?XDoxJ@S)9QM)2L%SA0x^~^fb=bdsBy!uh& zU?M_^kvnt%FZzm+>~bEH{2o?v&Iogs`1t-b+Ml`J!ZPS(46YQJKxWE81O$HE5w;** z|8zM%bp`M7J8)4;%DqH`wVTmM0V@D}xd%tRE3_6>ioMJxyi5Hkb>85muF81&EY!73ei zA3e<#ug||EZJ=1GLXNJ)A z791&ge#lF;GVX6IU?iw0jX^1bYaU?+x{zPlpyX6zijyn*nEdZ$fxxkl!a-~*P3bkf zPd*pzu~3GBYkR_>ET`5UM^>>zTV>5m>)f=az{d0sg6a8VzUtXy$ZS?h#Gk-CA?7)c zI%Vu9DN6XSDQn6;?n9`>l$q&>s?K)R8*OsmI+$L_m z_~E`}w694Z*`Xk3Ne=497Si~=RWRqCM?6=88smrxle#s*W znwhTRsMRmg?37GLJ-)%nDZA7r$YG849j8mJWir1bWBy& zZPneYojSbooC8U@tkO`bWx4%E5*;p#Q^1^S3lsfy7(6A{jL0`A__0vm?>xC%1y8_m z57FfWr^@YG2I1K7MGYuYd>JC}@sT2n^rkrY3w%~$J$Y~HSoOHn?zpR$ zjLj_bq@Yj8kd~DXHh30KVbz@K)0S;hPKm+S&-o%IG+@x@MEcrxW2KFh;z^4dJDZix zGRGe&lQD$p)0JVF4NRgGYuh0bYLy)BCy~sbS3^b3 zHixT<%-Vwbht|25T{3^Hk;qZ^3s!OOgljHs+EIf~C%=_>R5%vQI4mQR9qOXThMXlU zS|oSH>0PjnCakb*js2{ObN`}%HYsT6=%(xA| znpUtG_TJ08kHgm5l@G|t?4E3tG2fq?wNtIp*Vqrb{9@bo^~Rx7+J&OnayrX`LDcF~ zd@0m0ZJ#Z@=T>4kTa5e2FjI&5c(F7S{gnRPoGpu9eIqrtSvnT_tk$8T)r%YwZw!gK zj*k@cG)V&@t+mtDi37#>LhVGTfRA^p%x0d#_P|Mktz3*KOoLIqFm`~KGoDDD4OOxe z?}ag_c08u%vu=5Vx=~uoS8Q;}+R2~?Uh|m-+`-2kDo$d6T!nD*hc#dB(*R{LXV=zo z`PJP0V=O!@3l-bw+d`X6(=@fq=4O#ETa8M^fOvO4qja9o3e8ANc9$sI=A4$zUut~w z4+JryRkI{9qWxU1CCMM$@Aj=6)P+z?vqa=UCv_4XyVNoBD{Xb~Oi4cjjhm8fRD!*U z2)zaS;AI78^Wq+5mDInKiMz|z#K`2emQfNH*U;{9^{NqSMVoq?RSo43<8YpJM^+W$ zxy!A5>5Zl16Vi#?nAYywu3w_=KWnd3*QetocWt`3pK67>)ZVwnT3h zbPdD&MZkD?q=-N`MpCCwpM74L+Tr1aa)zJ)8G;(Pg51@U&5W>aNu9rA`bh{vgfE={ zdJ>aKc|2Ayw_bop+dK?Y5$q--WM*+$9&3Q9BBiwU8L<-`T6E?ZC`mT0b}%HR*LPK} z!MCd_Azd{36?Y_>yN{U1w5yrN8q`z(Vh^RnEF+;4b|2+~lfAvPT!`*{MPiDioiix8 zY*GdCwJ{S(5(HId*I%8XF=pHFz<9tAe;!D5$Z(iN#jzSql4sqX5!7Y?q4_%$lH zz8ehZuyl0K=E&gYhlfFWabnSiGty$>md|PpU1VfaC5~kskDnZX&Yu}?-h;OSav=8u z=e3Yq=mi$4A|sB-J00;1d{Sd1+!v0NtU((Nz2;PFFlC}V{@p&4wGcVhU&nI($RAS! zwXn7)?8~1J3*4+VccRSg5JS<(bBhBM&{ELMD4C_NTpvzboH!{Zr*%HP;{UqxI#g&7 zOAqPSW5Qus$8-xtTvD%h{Tw<2!XR(lU54LZG{)Cah*LZbpJkA=PMawg!O>X@&%+5XiyeIf91n2E*hl$k-Y(3iW*E}Mz-h~H~7S9I1I zR#-j`|Hk?$MqFhE4C@=n!hN*o5+M%NxRqP+aLxDdt=wS6rAu6ECK*;AB%Nyg0uyAv zO^DnbVZZo*|Ef{nsYN>cjZC$OHzR_*g%T#oF zCky9HJS;NCi=7(07tQXq?V8I&OA&kPlJ_dfSRdL2bRUt;tA3yKZRMHMXH&#W@$l%-{vQd7y@~i*^qnj^`Z{)V$6@l&!qP_y zg2oOd!Wit#)2A~w-eqw3*Mbe)U?N|q6sXw~E~&$!!@QYX4b@%;3=>)@Z#K^`8~Aki z+LYKJu~Y$;F5%_0aF9$MsbGS9Bz2~VUG@i@3Fi2q(hG^+Ia44LrfSfqtg$4{%qBDM z_9-O#3V+2~W$dW0G)R7l_R_vw(KSkC--u&%Rs^Io&*?R=`)6BN64>6>)`TxyT_(Rd zUn+aIl1mPa#Jse9B3`!T=|e!pIp$(8ZOe0ao?nS7o?oKlj zypC-fMj1DHIDrh1unUI1vp=-Fln;I9e7Jvs3wj*^_1&W|X} zZSL|S|Bb@CV*YC_-T&2!Ht3b6?)d`tHOP?rA;;t#zaXa0Sc;vGnV0BLIf8f-r{QHh z*Zp`4_ItlOR7{u(K+!p_oLDmaAkNag*l4#29F2b_A*0oz0T|#-&f*;c#<`^)(W@gm z#k9k=t%u8<+C1fNUA{Fh7~wgPrEZZ#(6aBI%6bR4RO(e1(ZocjoDek4#MTgZD>1NG zy9~yoZfWYfwe&S-(zk4o6q6o?2*~DOrJ(%5wSnEJMVOKCzHd z=Yhm+HLzoDl{P*Ybro7@sk1!Ez3`hE+&qr7Rw^2glw^M(b(NS2!F|Q!mi|l~lF94o z!QiV)Q{Z>GO5;l1y!$O)=)got;^)%@v#B!ZEVQy1(BJApHr5%Zh&W|gweD+%Ky%CO ztr45vR*y(@*Dg_Qw5v~PJtm^@Lyh*zRuT6~(K+^HWEF{;R#L$vL2!_ndBxCtUvZ(_ zauI7Qq}ERUWjr&XW9SwMbU>*@p)(cuWXCxRK&?ZoOy>2VESII53iPDP64S1pl{NsC zD;@EGPxs&}$W1;P6BB9THF%xfoLX|4?S;cu@$)9OdFst-!A7T{(LXtdNQSx!*GUSIS_lyI`da8>!y_tpJb3Zuf0O*;2y?HCfH z5QT6@nL|%l3&u4;F!~XG9E%1YwF*Fgs5V&uFsx52*iag(?6O|gYCBY3R{qhxT-Etb zq(E%V=MgQnuDGEKOGsmBj9T0-nmI%zys8NSO>gfJT4bP>tI>|ol@ zDt(&SUKrg%cz>AmqtJKEMUM;f47FEOFc%Bbmh~|*#E zDd!Tl(wa)ZZIFwe^*)4>{T+zuRykc3^-=P1aI%0Mh}*x7%SP6wD{_? zisraq`Las#y-6{`y@CU3Ta$tOl|@>4qXcB;1bb)oH9kD6 zKym@d$ zv&PZSSAV1Gwwzqrc?^_1+-ZGY+3_7~a(L+`-WdcJMo>EWZN3%z4y6JyF4NR^urk`c z?osO|J#V}k_6*9*n2?j+`F{B<%?9cdTQyVNm8D}H~T}?HOCXt%r7#2hz97Gx#X%62hyaLbU z_ZepP0<`<;eABrHrJAc!_m?kmu#7j}{empH@iUIEk^jk}^EFwO)vd7NZB=&uk6JG^ zC>xad8X$h|eCAOX&MaX<$tA1~r|hW?-0{t4PkVygTc`yh39c;&efwY(-#;$W)+4Xb z$XFsdG&;@^X`aynAMxsq)J#KZXX!sI@g~YiJdHI~r z$4mj_?S29sIa4c$z)19JmJ;Uj?>Kq=0XuH#k#};I&-6zZ_&>)j>UR0XetRO!-sjF< zd_6b1A2vfi++?>cf}s{@#BvTD|a%{9si7G}T+8ZnwuA z1k8c%lgE<-7f~H`cqgF;qZ|$>R-xNPA$25N1WI3#n%gj}4Ix}vj|e=x)B^roGQpB) zO+^#nO2 zjzJ9kHI6nI5ni&V_#5> z!?<7Qd9{|xwIf4b0bRc;zb}V4>snRg6*wl$Xz`hRDN8laL5tg&+@Dv>U^IjGQ}*=XBnXWrwTy;2nX?<1rkvOs#u(#qJ=A zBy>W`N!?%@Ay=upXFI}%LS9bjw?$h)7Dry0%d}=v0YcCSXf9nnp0tBKT1eqZ-4LU` zyiXglKRX)gtT0VbX1}w0f2ce8{$WH?BQm@$`ua%YP8G@<$n13D#*(Yd5-bHfI8!on zf5q4CPdgJLl;BqIo#>CIkX)G;rh|bzGuz1N%rr+5seP${mEg$;uQ3jC$;TsR&{IX< z;}7j3LnV+xNn^$F1;QarDf6rNYj7He+VsjJk6R@0MAkcwrsq4?(~`GKy|mgkfkd1msc2>%B!HpZ~HOzj}kl|ZF(IqB=D6ZTVcKe=I7)LlAI=!XU?J*i#9VXeKeaG zwx_l@Z(w`)5Cclw`6kQKlS<;_Knj)^Dh2pL`hQo!=GPOMR0iqEtx12ORLpN(KBOm5 zontAH5X5!9WHS_=tJfbACz@Dnkuw|^7t=l&x8yb2a~q|aqE_W&0M|tI7@ilGXqE)MONI8p67OiQGqKEQWw;LGga=ZM1;{pSw1jJK_y$vhY6 ztFrV7-xf>lbeKH1U)j3R=?w*>(Yh~NNEPVmeQ8n}0x01$-o z2Jyjn+sXhgOz>AzcZ zAbJZ@f}MBS0lLKR=IE{z;Fav%tcb+`Yi*!`HTDPqSCsFr>;yt^^&SI2mhKJ8f*%ji zz%JkZGvOn{JFn;)5jf^21AvO-9nRzsg0&CPz;OEn07`CfT@gK4abFBT$Mu?8fCcscmRkK+ zbAVJZ~#_a z{|(FFX}~8d3;DW8zuY9?r#Dt>!aD>} zlYw>D7y#eDy+PLZ&XKIY&Df0hsLDDi(Yrq8O==d30RchrUw8a=Eex>Dd?)3+k=}Q> z-b85lun-V$I}86Vg#l1S@1%=$2BQD5_waAZKQfJ${3{b2SZ#w1u+jMr{dJMvI|Og= zpQ9D={XK|ggbe04zTUd}iF{`GO1dV%zWK~?sM9OM(= zVK9&y4F^w1WFW{$qi|xQk0F`@HG8oLI5|5$j~ci9xTMT69v5KS-Yym--raU5kn2#C z<~5q^Bf0rTXVhctG2%&MG(cUGaz(gC(rcG~>qgO$W6>!#NOVQJ;pIYe-lLy(S=HgI zPh;lkL$l+FfMHItHnw_^bj8}CKM19t(C_2vSrhX2$K@-gFlH};#C?1;kk&U1L%4S~ zR^h%h+O1WE7DI$~dly?-_C7>(!E`~#REJ~Xa7lyrB$T!`&qYV5QreAa^aKr%toUJR zPWh)J3iD`(P6BI5k$oE$us#%!4$>`iH2p-88?WV0M$-K)JDibvA4 zpef%_*txN$Ei3=Lt(BBxZ&mhl|mUz-z*OD1=r9nfN zc5vOMFWpi>K=!$6f{eb?5Ru4M3o;t9xLpry|C%j~`@$f)OFB5+xo8XM8g&US@UU-sB|dAoc20y(F@=-2Ggp_`SWjEb#>IG^@j zuQK}e^>So#W2%|-)~K!+)wdU#6l>w5wnZt2pRL5Dz#~N`*UyC9tYechBTc2`@(OI# zNvcE*+zZZjU-H`QOITK^tZwOyLo)ZCLk>>Wm+flMsr5X{A<|m`Y281n?8H_2Fkz5}X?i%Rfm5s+n`J zDB&->=U+LtOIJ|jdYXjQWSQZFEs>Rm{`knop4Sq)(}O_@gk{14y51)iOcGQ5J=b#e z2Yx^6^*F^F7q_m-AGFFgx5uqyw6_4w?yKCJKDGGprWyekr;X(!4CnM5_5?KgN=3qCm03 z##6k%kIU5%g!cCL(+aK>`Wd;dZ4h$h_jb7n?nqx5&o9cUJfr%h#m4+Bh)>HodKcDcsXDXwzJ3jR(sSFqWV(OKHC*cV8;;&bH=ZI0YbW3PgIHwTjiWy z?2MXWO2u0RAEEq(zv9e%Rsz|0(OKB?_3*kkXwHxEuazIZ7=JhaNV*P~hv57q55LoebmJpfHXA@yuS{Esg+ z*C}0V-`x^=0nOa@SPUJek>td~tJ{U1T&m)~`FLp*4DF77S^{|0g%|JIqd-=5)p6a` zpJOsEkKT(FPS@t^80V!I-YJbLE@{5KmVXjEq{QbCnir%}3 zB)-J379=wrBNK6rbUL7Mh^tVmQYn-BJJP=n?P&m-7)P#OZjQoK0{5?}XqJScV6>QX zPR>G{xvU_P;q!;S9Y7*07=Z!=wxIUorMQP(m?te~6&Z0PXQ@I=EYhD*XomZ^z;`Os z4>Uh4)Cg2_##mUa>i1Dxi+R~g#!!i{?SMj%9rfaBPlWj_Yk)lCV--e^&3INB>I?lu z9YXCY5(9U`3o?w2Xa5ErMbl5+pDVpu8v+KJzI9{KFk1H?(1`_W>Cu903Hg81vEX32l{nP2vROa1Fi!Wou0+ZX7Rp`g;B$*Ni3MC-vZ`f zFTi7}c+D)!4hz6NH2e%%t_;tkA0nfkmhLtRW%){TpIqD_ev>}#mVc)<$-1GKO_oK8 zy$CF^aV#x7>F4-J;P@tqWKG0|D1+7h+{ZHU5OVjh>#aa8+V;6BQ)8L5k9t`>)>7zr zfIlv77^`Fvm<)_+^z@ac%D&hnlUAFt8!x=jdaUo{)M9Ar;Tz5Dcd_|~Hl6CaRnK3R zYn${wZe8_BZ0l0c%qbP}>($jsNDay>8+JG@F!uV4F;#zGsBP0f$f3HqEHDz_sCr^q z1;1}7KJ9&`AX2Qdav1(nNzz+GPdEk5K3;hGXe{Hq13{)c zZy%fFEEH#nlJoG{f*M^#8yXuW%!9svN8ry-Vi7AOFnN~r&D`%6d#lvMXBgZkX^vFj z;tkent^62jUr$Cc^@y31Lka6hS>F?1tE8JW$iXO*n9CQMk}D*At3U(-W1E~z>tG?> z5f`5R5LbrhRNR8kv&5d9SL7ke2a*Xr)Qp#75 z6?-p035n2<7hK;sb>t9GAwG4{9v~iEIG>}7B5zcCgZhu$M0-z8?eUO^E?g)md^XT_ z2^~-u$yak>LBy(=*GsTj6p<>b5PO&un@5hGCxpBQlOB3DpsItKZRC*oXq-r{u}Wb; z&ko>#fbnl2Z;o@KqS-d6DTeCG?m1 z&E>p}SEc*)SD&QjZbs!Csjx~0+$@ekuzV_wAalnQvX3a^n~3ui)|rDO+9HW|JPEeBGP4 z)?zcZ<8qv47`EWA*_X~H^vr(lP|f%=%cWFM;u)OFHruKT<~?>5Y8l?56>&;=WdZU# zZEK4-C8s-3zPMA^&y~e*9z)!ZJghr3N^pJa2A$??Xqx-BR*TytGYor&l8Q+^^r%Yq02xay^f#;;wO6K7G!v>wRd6531WnDI~h$PN( z+4#08uX?r&zVKsQ;?5eBX=FxsXaGyH4Gth4a&L|{8LnNCHFr1M{KjJ!BfBS_aiy-E zxtmNcXq3}WTwQ7Dq-9YS5o758sT(5b`Sg-NcH>M9OH1oW6&sZ@|GYk|cJI`vm zO<$~q!3_$&GfWetudRc*mp8)M)q7DEY-#@8w=ItkApfq3sa)*GRqofuL7)dafznKf zLuembr#8gm*lIqKH)KMxSDqbik*B(1bFt%3Vv|ypehXLCa&wc7#u!cJNlUfWs8iQ` z$66(F=1fkxwg745-8_eqV>nWGY3DjB9gE23$R5g&w|C{|xvT@7j*@aZNB199scGchI7pINb5iyqYn)O=yJJX)Ca3&Ca+{n<=1w|(|f0)h<9gs$pVSV<<9Og-V z8ki@nKwE)x)^wmHBMk?mpMT=g{S#^8W|>&rI#Ceh;9za}io0k@0JxiCqi-jHlxbt3 zjJA?RihhRvhk6%G5-D{ePh1jare*fQS<328P-DcVAxPTrw=n6k?C6EV75f}cnBRPT zMYDqqKu(ND&aOtc!QRV`vzJSVxx8i~WB#5Ml{b#eQqNnSi7l-bS-`ITW<^zyYQA(b zbj4SuRK>q9o`_v%+C=S?h>2e4!66Ij(P5{7Uz$3u6YJJC$W%EoBa{-(=tQ|y1vov%ZkXVOV z##_UVg4V^4ne#4~<-1DkJqkKqgT+E_=&4Ue&eQ-JC+gi?7G@d6= zximz{zE)WW{b@QCJ!7l&N5x=dXS?$5RBU-VvN4Uec-GHK&jPa&P2z+qDdLhIB+HU) zu0CW&uLvE^4I5xtK-$+oe|58)7m6*PO%Xt<+-XEA%jG_BEachkF3e@pn?tl!`8lOF zbi2QOuNXX)YT*MCYflILO{VZ*9GiC%R4FO20zMK?p+&aCMm2oeMK7(aW=UDzr=AO0 z$5mJ%=qRsR8rZ>_YsL+vi{3*J_9Kzq(;ZwRj+4_f0-*wbkSMPWahX#Fj_a8BnrhJ6 zo^ZZ?Vah1@&6#r=JkuaYDBdp;J3@ii+CHM&@9*er&#P}$@wI$bfrH)&c!*|nkvhf%^*Y6b%dKz%QBSIo@U z{?V^qEs4`q<8@n+u8YiB^sc@6g>TncG<|GsmC3egwE6aO=EwLr~3-2 zNr`+)`i+-83?|1Xy0^8ps&pb}YT?w1eWVnC9Ps1=KM;Rw)bH6O!7Did1NwpnqVPZc z*%Qo~qkDL>@^<^fmIBtx$WUWQiNtAB2x-LO^BB=|w~-zTnJNEdm1Ou(?8PF&U88X@ z#8rdaTd||)dG^uJw~N_-%!XNbuAyh4`>Shea=pSj0TqP+w4!`nxsmVSv02kb`DBr% zyX=e>5IJ3JYPtdbCHvKMdhXUO_*E9jc_?se7%VJF#&ZaBD;7+eFN3x+hER7!u&`Wz z7zMvBPR4y`*$a250KYjFhAKS%*XG&c;R-kS0wNY1=836wL6q02mqx;IPcH(6ThA@2 zXKQF|9H>6AW$KUF#^A%l6y5{fel77_+cR_zZ0(7=6bmNXABv}R!B-{(E^O6Y?ZS)n zs1QEmh_Fm7p}oRyT3zxUNr4UV8NGs+2b8|4shO$OGFj3D&7_e?#yDi=TTe%$2QbG5 zk<;q7aQ;p!M-Osm{vFdmXZ@!z9uWh!;*%>(vTRggufuUGP9Hols@vhx z73pn$3u2;vzRvnXuT&$Os7J@6y12*j!{ix%3B4YU1466ItmJs0NsU(4ZYRYh7wEA6q{b*Hs6@k~ zi7Yq@Ax!et0cUMTvk7P%ym){MHpcliHEI~e3HP0NV=}7;xFv#IC?a<=`>~j_sk{e> z7vg-tK*p83HZ0=QK@ zRIHo^r{D8&Ms-^WZp+6US_Quqjh$Q66W^1}=Uz&XJ8AQE9&2}P zY|FXZzZ|0IiaBd2qdt6dIjQr(ZMIOU%NG1F&fu6Po9m^?BvLhI6T0R!H2d8;U(&p2 zYA|MFscMqcO(ye~Jp?F;0>Ke+5hzVr?aBNe>GsGgr$XrpS9uajN2kNQ3o$V5rp0T( z0$6TJC;3)26SNG#XcX7l^MKTn$ga?6r4Jzfb%ZgA(Zbwit0$kY=avSnI$@Gk%+^pu zS5mHrcRS8LFPC*uVWH4DDD1pY$H8N>X?KIJZuZ2SvTqc5Nr0GHdD8TCJcd$zIhOdC zZX0ErnsozQh;t^==4zTfrZO421AL?)O)l#GSxU#|LTTg4#&yeK=^w#;q63!Nv~1(@ zs^-RNRuF&qgcr+bIzc@7$h9L;_yjdifE*$j0Q&Np=1AuHL--zdkv@}`1 zo~LlDl_YAq*z?vmr4M`GjDkl9?p|-tl(DtX76oZv25_DtZutLS9Ez!5~p?th@4 zyc_uax4W#<(#)LMkvo)yp|5tKsC2=p#6PyhpH|449T<9Zdk|%CAb5cw?fhvQtBO&7 zpQ9$24yLqPHP;$N&fe2wm%8qdctwIna<3SwGtQA3{C77s%CW%LYxtK(SBGustL0<( zu~U9r0UOkr(c{OJxZS0Ntu3+cJlF7R`7k-Bsa&q?9Ae5{{|o~?cM+T7{lB1^#vT8R z?>c9fNWey`1dKDY%F3d2O*8^qYhjlB8*7HMKE<*=(A`{>=1%s1}Pm&#_t1xy!FkPk@%SMEka2@*= zxDuM|vJJ5s+xgDls{>*o!7eOcs|xuVBPWX&+y5vEiADK%hi`#Dbd>;;Pbk2H4*-X&R?_-6ZEutSd8hC+sSjhIo z;D(j4P;2EVpEj#UF7IjM6PC+X$C5T&=nL`*!*hm9U)#O?>wqOgC>jXKN3Slk_yaQX zLf|4D8T4k|wHW`;#ZQVocNF|3izi0sOqXzi7@KlYC3CXBG`94wD;tMI1bj|8Vm zY}9`VI9!plSfhAal$M_HlaYOVNU?9Z#0<$o?lXXbX3O(l_?f)i3_~r+GcO-x#+x^X zfsZl0>Rj2iP1rsT;+b;Mr? z4Vu&O)Q5ru4j;qaSP5gA{az@XTS1NpT0d9Xhl_FkkRpcEGA0(QQ~YMh#&zwDUkNzm z6cgkdgl9W{iL6ArJ1TQHqnQ^SQ1WGu?FT|93$Ba}mPCH~!$3}0Y0g zcoG%bdTd$bmBx9Y<`Jc+=Cp4}c@EUfjiz;Rcz101p z=?#i$wo>gBE9|szaZMt-d4nUIhBnYRuBVyx+p?5#aZQgUe(!ah`J#l1$%bl5avL27 zU2~@V`3Ic&!?FhDX@Cw!R4%xtWark#p8DLT)HCZ?VJxf^yr@AD*!ERK3#L$E^*Yr? zzN&uF9Roh4rP+r`Z#7U$tzl6>k!b~HgM$C<_crP=vC>6=q{j?(I}!9>g3rJU(&){o z`R^E*9%+kEa8H_fkD9VT7(Fks&Y-RcHaUJYf-|B+eMXMaRM;{FKRiTB>1(=Iij4k1(X__|WqAd-~t#2@UQ}Z&<1Th0azdXfoll!dd)6>1miA z!&=6sDJm=e$?L&06+Q3`D-HNSkK-3$3DdZMX-6Xjn;wd#9A{~ur!2NcX>(qY_oZL0~H7dnQ9sgLe!W>~2|RSW7|hWn<({Pg*xF$%B-!rKe^_R_vc z(LO!0agxxP;FWPV({8#lEv$&&GVakGus=@!3YVG`y^AO1m{2%Np;>HNA1e{=?ra1C}H zAwT0sbwG|!am;fl?*_t^^#yLDXZ*Nx)_FqueZi0c-G~omtpHW0Cu)mEJ`Z1X8brq$ z%vK##b~o*^b&Hz!hgrD=^6P8}aW40lhzMLB5T5*v`1QH?+L~-@CDi3+C@nRf2{7UE zyDIe{@LKw`Eu=Z%6<<_=#V|yxJIKiq_N?ZJ_v0$c)N4l07ZV_mIXG}glfBSPivOhw z-~+9GdckSpMBNR9eR`Y|9_)sXS+u_OiQ%!9rE(2AFjoxN8lk16Sb~^Sq6kRoEp3yD(mm`HsYIXcag_EAB8MHc}nahxVVUTts~U9P|f;7Ul$_` zStR4v&P4q_$KXOEni$lkxy8=9w8G&47VY0oDb^+jT+>ARe3NHUg~St`$RDxY)?;_F znqTujR&chZd2qHF7y8D$4&E3+e@J~!X3&BW4BF(Ebp#TEjrd+9SU!)j;qH+ZkL@AW z?J6Mj}v0_+D zH0qlbzCkHf|EZ`6c>5ig5NAFF%|La%M-}g(7&}Vx8K)qg30YD;H!S!??{;YivzrH0 z(M%2*b_S-)yh&Aiqai)GF^c!<1Xemj|13>dZ_M#)41SrP;OEMaRJ)bCeX*ZT7W`4Y zQ|8L@NHpD@Tf(5>1U(s5iW~Zdf7$@pAL`a3X@YUv1J>q-uJ_(Dy5nYTCUHC}1(dlI zt;5>DLcHh&jbysqt?G01MhXI3!8wgf){Hv}=0N|L$t8M#L7d6WscO8Om2|NBz2Ga^ zs86y%x$H18)~akOWD7@em7)ldlWgb?_sRN>-EcYQO_}aX@+b$dR{146>{kXWP4$nN{V0_+|3{Lt|8uX_fhKh~i{(x%cj*PU$i{PO(5$uA? zQzO>a6oPj-TUk&{zq?JD2MNb6Mf~V3g$ra+PB;ujLJ2JM(a7N*b`y{MX--!fAd}5C zF$D_b8S;+Np(!cW)(hnv5b@@|EMt*RLKF*wy>ykFhEhlPN~n_Bj>LT9B^_yj>z#fx z3JuE4H&?Cc!;G@}E*3k`HK#8ag`yE3Z1)5JUlSua%qkF zkTu|<9{w9OSi$qr)WD#7EzITnch=xnR63E*d~WGvi*Co9BBE?ETHud;!Z)7&wz+l6 zuKODYG1>I1U#a%&(GNJ`AqRfg=H!BtSl+_;CEeufF-#+*2EMMz-22@>18=8PH{PHd z);mN=aR0MPF>eutLiS#-AOX>#2%+pTGEOj!j4L(m0~&xR=0+g#HNpno6@veLhJp}e zyNVC$a>4;!9&iGvU_dj&xbKt@^t6r%f^)+}eV^suRTLP52+BVs0kOLwg6n`=NUv50E7My8XQUh?y%mW62OT1pMrKI3Q(r`7vU&@93=G~A?b(^pvC-8x=bSk zZ60BQR96WB1Z@9Df(M1IQh+YrU8sEjB=Tc2;(zBn-pete*icZE|M&Uc+oHg`|1o`g zH~m+k=D$o);{Rs)b<9Zo|9_Z6L6QHLNki(N>Dw^^i1LITprZeeqIaT#+)fw)PlllU zldphHC)t!0Gf(i9zgVm>`*TbmITF zH1FZ4{wrjRCx{t^26VK_2srZuWuY*EMAsMrJYFFCH35Ky7bq8<0K|ey2wHnrFMZyr z&^yEgX{{3i@&iE5>xKZ{Ads36G3a!i50D!C4?^~cLB<<|fc1!XN(HJRM)H^21sEs%vv+Mu0h*HkLHaEffMwc0n6)JhNXY#M5w@iO@dfXY z0c6dM2a4Hd1SA*#qYj@jK}uVgAZdaBj8t6uuhUNe>)ne9vfd#C6qLV9+@Q7{MnF#0 zJ7fd-ivG_~u3bVvOzpcw1u~ZSp8-kl(sunnX>L~*K-ByWDM2E8>;Si6kn^58AZQxI xVa^It*?521mj4+UJO?7%w*+`EfEcU=@KhDx-s^WzP+ae~{CgHDE&XryzW}Nww%-5% diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1730841..5116c5b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/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 diff --git a/gradlew b/gradlew index 1b6c787..a69d9cb 100644 --- a/gradlew +++ b/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. diff --git a/gradlew.bat b/gradlew.bat index 107acd3..f127cfd 100644 --- a/gradlew.bat +++ b/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 diff --git a/sdk-lib/Cargo.toml b/sdk-lib/Cargo.toml index 1e047f7..8335d94 100644 --- a/sdk-lib/Cargo.toml +++ b/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"] diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/AssetTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/AssetTest.kt index 8814685..342e1e9 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/AssetTest.kt +++ b/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?) { files?.map { filename -> - val filePath = "${WalletBirthdayTool.birthdayDirectory(network)}/$filename" + val filePath = "${CheckpointTool.checkpointDirectory(network)}/$filename" ApplicationProvider.getApplicationContext().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(), - WalletBirthdayTool.birthdayDirectory(network) + CheckpointTool.checkpointDirectory(network) ) } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/TestExtensions.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/TestExtensions.kt index 07b400d..3692532 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/ext/TestExtensions.kt +++ b/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") } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt index 1f28f4c..699b7e0 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt +++ b/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 { diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt index 670dc7b..6b80d7c 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt +++ b/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 diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt index d0f2664..f4f56f4 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt +++ b/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 diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/service/ChangeServiceTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/service/ChangeServiceTest.kt index 898e296..96dccb0 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/service/ChangeServiceTest.kt +++ b/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() 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) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/CheckpointTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/CheckpointTest.kt new file mode 100644 index 0000000..6382a02 --- /dev/null +++ b/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) + } + } +} diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/WalletBirthdayTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/WalletBirthdayTest.kt deleted file mode 100644 index 3a6a3d8..0000000 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/WalletBirthdayTest.kt +++ /dev/null @@ -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) - } - } -} diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/BranchIdTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/BranchIdTest.kt index 063b7b3..1a0f385 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/BranchIdTest.kt +++ b/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), ) } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/TransparentTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/TransparentTest.kt index 901a882..5111223 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/jni/TransparentTest.kt +++ b/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 diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt index a2fc436..b876496 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/ShieldFundsSample.kt +++ b/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 diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt index 0057c64..35daef8 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/sample/TransparentRestoreSample.kt +++ b/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 { - 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 { - 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) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/WalletBirthdayToolTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/CheckpointToolTest.kt similarity index 72% rename from sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/WalletBirthdayToolTest.kt rename to sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/CheckpointToolTest.kt index d9fa6ac..8446d13 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/tool/WalletBirthdayToolTest.kt +++ b/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() 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() 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) } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/AddressGeneratorUtil.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/AddressGeneratorUtil.kt index b282085..faba2d7 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/AddressGeneratorUtil.kt +++ b/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 diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt index 2493940..782bdc3 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/BalancePrinterUtil.kt +++ b/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 } /* diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt index 7f8cb6c..94603bc 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/DataDbScannerUtil.kt +++ b/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 ) } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt index 1bbfd14..e296dfc 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt +++ b/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) + ), ; } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TransactionCounterUtil.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TransactionCounterUtil.kt index 2a46935..5c9607d 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TransactionCounterUtil.kt +++ b/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() - 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() 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) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/WalletBirthdayFixture.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/CheckpointFixture.kt similarity index 70% rename from sdk-lib/src/androidTest/java/cash/z/ecc/fixture/WalletBirthdayFixture.kt rename to sdk-lib/src/androidTest/java/cash/z/ecc/fixture/CheckpointFixture.kt index b1c2585..a7781fc 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/WalletBirthdayFixture.kt +++ b/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() diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1150000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1150000.json index a471e7d..f96dc5f 100644 --- a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1150000.json +++ b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1150000.json @@ -3,5 +3,5 @@ "height": "1150000", "hash": "0000000650e627bd7da6868f14070aff8fdbd31ef7125fe77851976ed3adfc54", "time": 1668316308, - "saplingtree": "012e058162e4e6bc9553c413134b66e5e89cd63c330fc557060878c623fdedc63d01533dbef6ad6b226c7138bef1e1961ca170510a91fdb1ff5972e1cd79c347401c150168979af907639a39b427d83d4602fd22867faffb0942cbb11955dac680aab85901d8d396d94feb0cc78cbec422c6fd09834c4031a1ffdcbef04178827add5193160102e9b3fecaf39d40e5e63ef3253d1ac5aee4141bdf26763de74033d6016f5955019f59fdd4a570ad22980428caea7b5fa61f55b52e2e6fbb29600eee53d31ed35f017c5ad297c1e83430ce9d3768fd38e74bd06757b67e2917b34d0f3f763803f32600000000015c1016fb7c68d85099ce8423d6446c2ea3d77a63b5ab6044f6eb0c024ddb0d5a01627b5eae7588998ef2645fe8be1ec3227d560828956b7df00632b26784c4f80a0000012d8bdb15bce00ab0c8bd332355a100d9db356ac05fb97412b479214dcefea331000001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13" -} \ No newline at end of file + "saplingTree": "012e058162e4e6bc9553c413134b66e5e89cd63c330fc557060878c623fdedc63d01533dbef6ad6b226c7138bef1e1961ca170510a91fdb1ff5972e1cd79c347401c150168979af907639a39b427d83d4602fd22867faffb0942cbb11955dac680aab85901d8d396d94feb0cc78cbec422c6fd09834c4031a1ffdcbef04178827add5193160102e9b3fecaf39d40e5e63ef3253d1ac5aee4141bdf26763de74033d6016f5955019f59fdd4a570ad22980428caea7b5fa61f55b52e2e6fbb29600eee53d31ed35f017c5ad297c1e83430ce9d3768fd38e74bd06757b67e2917b34d0f3f763803f32600000000015c1016fb7c68d85099ce8423d6446c2ea3d77a63b5ab6044f6eb0c024ddb0d5a01627b5eae7588998ef2645fe8be1ec3227d560828956b7df00632b26784c4f80a0000012d8bdb15bce00ab0c8bd332355a100d9db356ac05fb97412b479214dcefea331000001a5e10b312a666ed313eb0db76bfc977430ec6b463f944816c43cd82d42181d1f000001708c9850eb440b259f233187662c5228804cb4500263949301b6fac8f6428f2301d6f84c424acdb1d10f8cef641662e0f63f954f07fe6199d504a61979c9ba3e13" +} diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1160000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/mainnet/1160000.json new file mode 100644 index 0000000..007e578 --- /dev/null +++ b/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" +} \ No newline at end of file diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1000000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1000000.json deleted file mode 100644 index 1951648..0000000 --- a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1000000.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "network": "test", - "height": "1000000", - "hash": "00000002e920ff85f57a164e0637d12147e95428b4da8ca2f0dde4192f5df829", - "time": 1657012340, - "saplingTree": "014e4c1dba2b623864148a9dc4828fc67ee7f231907425be3695949749092e845b01ebe665e0198710c608da05a8a7964700642cbfd9b7bcac005ed967df32357e6a140001688ee9449b2d45afe7fb411f19c58f56269a43967fc1d1fdb9fd28edc2344016018746e37c9fd6c64662e26f2e146427a69ed3800280a9124c4d8c19f25a2aa155011b980b27687b29f85d315c87db08ea5559cc812159a838240b45bd88fd545059000171bc61ad6eacc00f6252a2898470e6a6391311db651b443e74f1f506e35ae61b011ca124f40831dd203b8d4eb8386f58b593d503168df44aa2eed6f4e3fdacd51000000001216607336029bd6f322d3decb8dd7ae1f8df1760b240030f1879b7aa3b4c646e0001f529692b1ee5845a6e0681b2efcc66342586397c79b09cdefa957aa1b0e614310156e2babf0dca08c8b1991c00a5d74d740e7d0c4b95099065016719e93455833a018d57c3859e298989e2eae8e1d8f9135944eef930e3ad20330e0de0541aacc94801f2cec17739de7e1476938f895b1a6381b36ec44ccdbbac2eeb60be43be6f815e01a8d81d60d7de99c1e45988bc29029102ab653c13b490ee0133dc739bf63a971601437aa93f8bebde50ea70fd8c7b60fe826aa6892fd6a9c6d72a60a7f8d12bea58000108a67f0c370f350b9179a081f2fc8d33b62e01e729419860b5ae143cbbcd2769" -} \ No newline at end of file diff --git a/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1150000.json b/sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint/testnet/1150000.json new file mode 100644 index 0000000..f96dc5f --- /dev/null +++ b/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" +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt index 90b4107..b3b4882 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Initializer.kt +++ b/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, 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 = 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 ) } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index a6a88ab..b7b635f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/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(null) + private val _orchardBalances = MutableStateFlow(null) private val _saplingBalances = MutableStateFlow(null) - //private val _transparentBalances = MutableStateFlow(null) + private val _transparentBalances = MutableStateFlow(null) private val _status = ConflatedBroadcastChannel(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 = processor.networkHeight + override val networkHeight: StateFlow = 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?, 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, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index 24fc71d..218b180 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/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 + val networkHeight: StateFlow /** * A stream of balance values for the orchard pool. Includes the available and total balance. */ - //val orchardBalances: StateFlow + val orchardBalances: StateFlow /** * 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 + val transparentBalances: StateFlow /* 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. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt index 09c34d2..115d3d0 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt +++ b/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 = 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(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?): 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): 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, tAddress: String, startHeight: Int): Int = withContext(IO) { + internal suspend fun processUtxoResult( + result: List, + 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(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?) = + withContext(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?): 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?): 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? = currentInfo.lastScanRange, + lastDownloadRange: ClosedRange? = 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 { + 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?) : 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?, + val lastScanRange: ClosedRange? ) { /** @@ -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 } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/CompactBlockEntity.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/CompactBlockEntity.kt index e22e441..50fd949 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/CompactBlockEntity.kt +++ b/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 } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt index 77278b9..f2180cd 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt +++ b/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 { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt index 5d41d5a..d40fe7c 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt +++ b/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" + diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/BatchMetrics.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/BatchMetrics.kt index eb58ecd..95235ca 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/BatchMetrics.kt +++ b/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, 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) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ConsensusBranchId.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ConsensusBranchId.kt index dcc62a4..e410928 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ConsensusBranchId.kt +++ b/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 diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdk.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdk.kt index e87184c..7292295 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/ext/ZcashSdk.kt +++ b/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 diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/CheckpointExt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/CheckpointExt.kt new file mode 100644 index 0000000..c526e0e --- /dev/null +++ b/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") + } + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/IsEmpty.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/IsEmpty.kt new file mode 100644 index 0000000..9acee3c --- /dev/null +++ b/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?.isEmpty() = this?.isEmpty() ?: true diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Twig.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Twig.kt index 7e855f0..4eebb1f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/Twig.kt +++ b/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 = CopyOnWriteArraySet() } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/WalletBirthdayExt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/WalletBirthdayExt.kt deleted file mode 100644 index 20b647a..0000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/WalletBirthdayExt.kt +++ /dev/null @@ -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") - } - } -} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDbStore.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDbStore.kt index f7697ad..061b75e 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDbStore.kt +++ b/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) = - cacheDao.insert(result.map { CompactBlockEntity(it.height.toInt(), it.toByteArray()) }) + override suspend fun write(result: Sequence) = + 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( diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDownloader.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDownloader.kt index f3c0f36..92fdc51 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDownloader.kt +++ b/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): 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) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockStore.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockStore.kt index 9305887..ba7123e 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockStore.kt +++ b/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) + suspend fun write(result: Sequence): 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. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CompactBlockDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CompactBlockDb.kt index c50bec0..6fcdbc9 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CompactBlockDb.kt +++ b/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) + @Transaction + suspend fun insert(blocks: Sequence): 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? } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt index efd8031..0d67821 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt +++ b/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 + suspend fun findAllTransactionsByRange(blockRangeStart: Long, blockRangeEnd: Long = blockRangeStart, limit: Int = Int.MAX_VALUE): List // 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 + suspend fun findExpiredTxs(lastheight: Long): List } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/PendingTransactionDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/PendingTransactionDb.kt index 904040e..302a707 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/PendingTransactionDb.kt +++ b/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?) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/Checkpoint.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/Checkpoint.kt new file mode 100644 index 0000000..d42acfb --- /dev/null +++ b/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 +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletGrpcService.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletGrpcService.kt index 2fac8db..b2d5c9d 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletGrpcService.kt +++ b/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 { - if (heightRange.isEmpty()) return listOf() + override fun getBlockRange(heightRange: ClosedRange): Sequence { + 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 { 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 ): List { 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 Iterator.toList(): List = - mutableListOf().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.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 Iterator.toList(): List = + mutableListOf().apply { + while (hasNext()) { + this@apply += next() + } + } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletService.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletService.kt index 8f6bd13..d8a48e7 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/service/LightWalletService.kt +++ b/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 - */ + fun fetchUtxos(tAddress: String, startHeight: BlockHeight): List /** * 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 + fun getBlockRange(heightRange: ClosedRange): Sequence /** * 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 - */ + fun getTAddressTransactions(tAddress: String, blockHeightRange: ClosedRange): List /** * Reconnect to the same or a different server. This is useful when the connection is diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt index 1e9c1c5..9d845f8 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt +++ b/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 = - transactions.findAllTransactionsByRange(blockHeightRange.first, blockHeightRange.last) + override suspend fun findNewTransactions(blockHeightRange: ClosedRange): List = + 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, 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 ) { 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}") } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt index d2eadc0..a5e73a7 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt +++ b/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 { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt index f070e50..0760bc5 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt +++ b/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. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt index fee13d4..d098c1f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt +++ b/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. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt index 303965a..1c1e482 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt +++ b/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 + suspend fun findNewTransactions(blockHeightRange: ClosedRange): List /** * 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 diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt index 5c10c2a..db8d995 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt +++ b/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) { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt index 71231f7..1515d57 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackend.kt +++ b/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 diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt index eff3c34..6b6bffe 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/jni/RustBackendWelding.kt +++ b/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 diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/BlockHeight.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/BlockHeight.kt new file mode 100644 index 0000000..d9bb541 --- /dev/null +++ b/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 { + 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 + } + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpoint.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpoint.kt new file mode 100644 index 0000000..076d2b2 --- /dev/null +++ b/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 +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpointExt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/LightWalletEndpointExt.kt new file mode 100644 index 0000000..f7b647e --- /dev/null +++ b/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 + ) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletBalance.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/WalletBalance.kt new file mode 100644 index 0000000..a2c4e16 --- /dev/null +++ b/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 + ) +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zatoshi.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zatoshi.kt index d0216d1..7384277 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Zatoshi.kt +++ b/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 { 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. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/ZcashNetwork.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/ZcashNetwork.kt new file mode 100644 index 0000000..d3a8ecd --- /dev/null +++ b/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") + } + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/WalletBirthdayTool.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/CheckpointTool.kt similarity index 68% rename from sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/WalletBirthdayTool.kt rename to sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/CheckpointTool.kt index f125cf7..dc56871 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/WalletBirthdayTool.kt +++ b/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.sortDescending() = - apply { sortByDescending { birthdayHeight(it) } } + private fun Array.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 { - 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 - ): 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, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt index 9ab1b98..21596a7 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/tool/DerivationTool.kt +++ b/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 { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt index 30afe18..2862336 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/type/WalletTypes.kt +++ b/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 } - } -} diff --git a/sdk-lib/src/main/proto/darkside.proto b/sdk-lib/src/main/proto/darkside.proto index 096e9ca..a1e0bc9 100644 --- a/sdk-lib/src/main/proto/darkside.proto +++ b/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; } diff --git a/sdk-lib/src/main/proto/service.proto b/sdk-lib/src/main/proto/service.proto index b1e9d9b..d0a7085 100644 --- a/sdk-lib/src/main/proto/service.proto +++ b/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) {} -} \ No newline at end of file + // Testing-only + rpc Ping(Duration) returns (PingResponse) {} +} diff --git a/sdk-lib/src/main/res/values/bools.xml b/sdk-lib/src/main/res/values/bools.xml deleted file mode 100644 index c75e660..0000000 --- a/sdk-lib/src/main/res/values/bools.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - true - diff --git a/sdk-lib/src/main/res/values/strings.xml b/sdk-lib/src/main/res/values/strings.xml deleted file mode 100644 index 2ecbf2e..0000000 --- a/sdk-lib/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Library linking is working! - diff --git a/sdk-lib/src/main/rust/lib.rs b/sdk-lib/src/main/rust/lib.rs index f4a583a..21836ac 100644 --- a/sdk-lib/src/main/rust/lib.rs +++ b/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(|| { diff --git a/sdk-lib/src/test/java/cash/z/ecc/android/sdk/ext/ConversionsTest.kt b/sdk-lib/src/test/java/cash/z/ecc/android/sdk/ext/ConversionsTest.kt index 885f470..76a5bdb 100644 --- a/sdk-lib/src/test/java/cash/z/ecc/android/sdk/ext/ConversionsTest.kt +++ b/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 diff --git a/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/ZatoshiTest.kt b/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/ZatoshiTest.kt index 7427bbb..03de567 100644 --- a/sdk-lib/src/test/java/cash/z/ecc/android/sdk/model/ZatoshiTest.kt +++ b/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 { diff --git a/tools/detekt-baseline.xml b/tools/detekt-baseline.xml index 593d0ff..04e33d2 100644 --- a/tools/detekt-baseline.xml +++ b/tools/detekt-baseline.xml @@ -2,16 +2,20 @@ + ComplexCondition:CompactBlockProcessor.kt$CompactBlockProcessor$(null == lastScannedHeight && targetHeight < lastLocalBlock) || (null != lastScannedHeight && targetHeight < lastScannedHeight) ComplexMethod:SdkSynchronizer.kt$SdkSynchronizer$private suspend fun refreshPendingTransactions() ComplexMethod:SendFragment.kt$SendFragment$private fun onPendingTxUpdated(pendingTransaction: PendingTransaction?) ComplexMethod:Transactions.kt$ConfirmedTransaction$override fun equals(other: Any?): Boolean ComplexMethod:Transactions.kt$PendingTransactionEntity$override fun equals(other: Any?): Boolean + DestructuringDeclarationWithTooManyEntries:TestnetIntegrationTest.kt$TestnetIntegrationTest$val (height, hash, time, tree) = runBlocking { CheckpointTool.loadNearest( context, synchronizer.network, saplingActivation + 1 ) } EmptyCatchBlock:SdkSynchronizer.kt$SdkSynchronizer${ } EmptyFunctionBlock:MainActivity.kt$MainActivity${ } EmptyFunctionBlock:Placeholders.kt$SampleSpendingKeyProvider${ } EmptyFunctionBlock:ReproduceZ2TFailureTest.kt$ReproduceZ2TFailureTest${ } EmptyFunctionBlock:SampleCodeTest.kt$SampleCodeTest${ } ForbiddenComment:BalancePrinterUtil.kt$BalancePrinterUtil$// TODO: clear the dataDb but leave the cacheDb + ForbiddenComment:CheckpointTool.kt$CheckpointTool$// TODO: If we ever add crash analytics hooks, this would be something to report + ForbiddenComment:CheckpointTool.kt$CheckpointTool$// TODO: potentially pull from shared preferences first ForbiddenComment:CompactBlockDownloader.kt$CompactBlockDownloader$// TODO: cancel anything in flight ForbiddenComment:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: add a concept of original checkpoint height to the processor. For now, derive it ForbiddenComment:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons) @@ -39,25 +43,22 @@ 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()' ForbiddenComment:TestWallet.kt$TestWallet.Backups.DEFAULT$// TODO: get the proper birthday values for these wallets ForbiddenComment:Transactions.kt$// TODO: test for off-by-one error here. Should we use <= or < - ForbiddenComment:WalletBirthdayTool.kt$WalletBirthdayTool$// TODO: If we ever add crash analytics hooks, this would be something to report - ForbiddenComment:WalletBirthdayTool.kt$WalletBirthdayTool$// TODO: potentially pull from shared preferences first ForbiddenComment:WalletTransactionEncoder.kt$WalletTransactionEncoder$// TODO: if this error matches: Insufficient balance (have 0, need 1000 including fee) FunctionParameterNaming:GetBlockFragment.kt$GetBlockFragment$_unused: View? = null FunctionParameterNaming:GetBlockRangeFragment.kt$GetBlockRangeFragment$_unused: View ImplicitDefaultLocale:BlockExt.kt$String.format("%02x", b) ImplicitDefaultLocale:BlockExt.kt$String.format("%02x", this[i--]) ImplicitDefaultLocale:Twig.kt$TroubleshootingTwig.Companion$String.format("$tag %1\$tD %1\$tI:%1\$tM:%1\$tS.%1\$tN", System.currentTimeMillis()) + InvalidPackageDeclaration:OpenForTesting.kt$package cash.z.ecc.android.sdk.annotation + LargeClass:CompactBlockProcessor.kt$CompactBlockProcessor LongMethod:SdkSynchronizer.kt$SdkSynchronizer$private suspend fun refreshPendingTransactions() - 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<UnifiedViewingKey>, val overwriteVks: Boolean, val birthday: WalletBirthday ) - 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 ) - 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 ) - LongParameterList:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$( appContext: Context, pageSize: Int = 10, rustBackend: RustBackend, birthday: WalletBirthday, viewingKeys: List<UnifiedViewingKey>, overwriteVks: Boolean = false ) + LongParameterList:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$( appContext: Context, zcashNetwork: ZcashNetwork, pageSize: Int = 10, rustBackend: RustBackend, birthday: Checkpoint, viewingKeys: List<UnifiedViewingKey>, overwriteVks: Boolean = false ) LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, account: Int, extsk: String, tsk: String, memo: ByteArray, spendParamsPath: String, outputParamsPath: String, networkId: Int ) 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 ) - LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, height: Int, hash: String, time: Long, saplingTree: String, networkId: Int ) - LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: Int, networkId: Int ) + LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, height: Long, hash: String, time: Long, saplingTree: String, networkId: Int ) + LongParameterList:RustBackend.kt$RustBackend.Companion$( dbDataPath: String, tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: Long, networkId: Int ) LongParameterList:RustBackendWelding.kt$RustBackendWelding$( consensusBranchId: Long, account: Int, extsk: String, to: String, value: Long, memo: ByteArray? = byteArrayOf() ) - LongParameterList:RustBackendWelding.kt$RustBackendWelding$( tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: Int ) + LongParameterList:RustBackendWelding.kt$RustBackendWelding$( tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: BlockHeight ) MagicNumber:BatchMetrics.kt$BatchMetrics$1000.0f MagicNumber:BlockExt.kt$16 MagicNumber:BlockExt.kt$4 @@ -83,7 +84,6 @@ MagicNumber:DemoConstants.kt$DemoConstants$0.000018 MagicNumber:DemoConstants.kt$DemoConstants$0.00017 MagicNumber:DemoConstants.kt$DemoConstants$1075590 - MagicNumber:DemoConstants.kt$DemoConstants$968085 MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.<no name provided>$3 MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.<no name provided>$4 MagicNumber:DerivedDataDb.kt$DerivedDataDb.Companion.<no name provided>$5 @@ -114,10 +114,6 @@ MagicNumber:UtxoViewHolder.kt$UtxoViewHolder$1000L MagicNumber:WalletService.kt$1000L MagicNumber:WalletService.kt$4.0 - MagicNumber:WalletTypes.kt$ZcashNetwork.Mainnet$419_200 - MagicNumber:WalletTypes.kt$ZcashNetwork.Mainnet$9067 - MagicNumber:WalletTypes.kt$ZcashNetwork.Testnet$280_000 - MagicNumber:WalletTypes.kt$ZcashNetwork.Testnet$9067 MagicNumber:ZcashSdk.kt$ZcashSdk$10 MagicNumber:ZcashSdk.kt$ZcashSdk$100 MagicNumber:ZcashSdk.kt$ZcashSdk$150 @@ -128,24 +124,30 @@ MagicNumber:ZcashSdk.kt$ZcashSdk$512 MagicNumber:ZcashSdk.kt$ZcashSdk$600_000L MagicNumber:ZcashSdk.kt$ZcashSdk$75_000L - MagicNumber:build.gradle.kts$16 - MagicNumber:build.gradle.kts$21 MatchingDeclarationName:CurrencyFormatter.kt$Conversions MaxLineLength:BatchMetrics.kt$BatchMetrics$class + MaxLineLength:BlockHeight.kt$BlockHeight.Companion$* MaxLineLength:BranchIdTest.kt$BranchIdTest$assertEquals("Invalid branch ID for $networkName at height $height on ${rustBackend.network.networkName}", branchId, actual) MaxLineLength:BranchIdTest.kt$BranchIdTest$assertEquals("Invalid branch Id Hex value for $networkName at height $height on ${rustBackend.network.networkName}", branchHex, clientBranch) + MaxLineLength:BranchIdTest.kt$BranchIdTest.Companion$val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight) } + MaxLineLength:BranchIdTest.kt$BranchIdTest.Companion$val testnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Testnet, ZcashNetwork.Testnet.saplingActivationHeight) } MaxLineLength:ChangeServiceTest.kt$ChangeServiceTest$"Exception was of the wrong type. Expected ${ChainInfoNotMatching::class.simpleName} but was ${caughtException!!::class.simpleName}" - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$!info.matchingConsensusBranchId(clientBranch) -> MismatchedBranch(clientBranch = clientBranch, serverBranch = info.consensusBranchId, networkName = network) - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$!info.matchingNetwork(network) -> MismatchedNetwork(clientNetwork = network, serverNetwork = info.chainName) + MaxLineLength:ChangeServiceTest.kt$ChangeServiceTest$downloader + 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. + MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$"ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!" + MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$(lastScannedHeight.value - range.start.value) / (range.endInclusive.value - range.start.value + 1).toFloat() * 100.0f 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 MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons) 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 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 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. - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$internal suspend - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$oldestTransactionHeight = repository.receivedTransactions.first().lastOrNull()?.minedHeight ?: lowerBoundHeight + MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$currentInfo.lastDownloadRange?.isEmpty() ?: true && currentInfo.lastScanRange?.isEmpty() ?: true + MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$if + MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$lastScanRange + MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$lowerBoundHeight + MAX_REORG_SIZE + 2 + MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$tempOldestTransactionHeight.value - tempOldestTransactionHeight.value.rem(ZcashSdk.MAX_REORG_SIZE) - ZcashSdk.MAX_REORG_SIZE.toLong() MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig - 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}).") + 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}).") MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Also clearing block cache back to $targetHeight. These rewound blocks will download in the next scheduled scan") MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Rewinding from $lastScannedHeight to requested height: $height using target height: $targetHeight with last local block: $lastLocalBlock") MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("The next block block: ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}") @@ -153,20 +155,15 @@ 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}") 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.") MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because it already exists") - 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}") + 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}") MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("batch scan complete! Total time: ${metrics.cumulativeTime} Total blocks measured: ${metrics.cumulativeItems} Cumulative bps: ${metrics.cumulativeIps.format()}") - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("batch scanned ($percent%): $lastScannedHeight/${range.last} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps") - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("block: $height\thash=${hash?.toHexReversed()} \tprevHash=${block?.prevHash?.toByteArray()?.toHexReversed()}") + MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("batch scanned ($percent%): $lastScannedHeight/${range.endInclusive} | ${metrics.batchTime}ms, ${metrics.batchItems}blks, ${metrics.batchIps.format()}bps") + MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("found $missingBlockCount missing blocks, downloading in $batches batches of $DOWNLOAD_BATCH_SIZE...") 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") MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$twig("validation failed at block ${errorInfo.errorHeight} which had hash ${errorInfo.actualPrevHash} but the expected hash was ${errorInfo.expectedPrevHash}") - 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 - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val errorMessage = "ERROR: unable to resolve reorg at height $result after ${consecutiveChainErrors.get()} correction attempts!" - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val lastDownloadedHeight = downloader.getLastDownloadedHeight().takeUnless { it < network.saplingActivationHeight } ?: -1 - 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 - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val percentValue = (lastScannedHeight - range.first) / (range.last - range.first + 1).toFloat() * 100.0f - MaxLineLength:CompactBlockProcessor.kt$CompactBlockProcessor$val summary = if (noWorkDone) "Nothing to process: no new blocks to download or scan" else "Done processing blocks" 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) - MaxLineLength:DarksideApi.kt$DarksideApi$onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.toLong()).build()) + MaxLineLength:DarksideApi.kt$DarksideApi$Darkside.DarksideEmptyBlocks.newBuilder().setHeight(startHeight.value).setCount(count).setNonce(nonce).build() + MaxLineLength:DarksideApi.kt$DarksideApi$onNext(it.newBuilderForType().setData(it.data).setHeight(tipHeight.value).build()) MaxLineLength:DarksideApi.kt$DarksideApi$twig("resetting darksidewalletd with saplingActivation=$saplingActivationHeight branchId=$branchId chainName=$chainName") MaxLineLength:DarksideApi.kt$DarksideApi.EmptyResponse$if (error != null) throw RuntimeException("Server responded with an error: $error caused by ${error?.cause}") MaxLineLength:DarksideTestCoordinator.kt$DarksideTestCoordinator$if @@ -186,6 +183,7 @@ 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' */ 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 MaxLineLength:DerivedDataDb.kt$TransactionDao$suspend + MaxLineLength:Exceptions.kt$BirthdayException.ExactBirthdayNotFoundException$class MaxLineLength:Exceptions.kt$CompactBlockProcessorException$ConfigurationException : CompactBlockProcessorException MaxLineLength:Exceptions.kt$CompactBlockProcessorException$Disconnected : CompactBlockProcessorException MaxLineLength:Exceptions.kt$CompactBlockProcessorException.EnhanceTransactionError$EnhanceTxDecryptError : EnhanceTransactionError @@ -203,6 +201,9 @@ MaxLineLength:GetAddressFragment.kt$GetAddressFragment$val zaddress = DerivationTool.deriveShieldedAddress(seed, ZcashNetwork.fromResources(requireApplicationContext())) MaxLineLength:GetAddressFragment.kt$GetAddressFragment$viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() } MaxLineLength:GetBalanceFragment.kt$GetBalanceFragment$val viewingKey = runBlocking { DerivationTool.deriveUnifiedViewingKeys(seed, ZcashNetwork.fromResources(requireApplicationContext())).first() } + MaxLineLength:GetBlockFragment.kt$GetBlockFragment$val newHeight = min(binding.textBlockHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value) + MaxLineLength:GetBlockRangeFragment.kt$GetBlockRangeFragment$val end = max(binding.textEndHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value) + MaxLineLength:GetBlockRangeFragment.kt$GetBlockRangeFragment$val start = max(binding.textStartHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value) MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/0821a89be7f2fc1311792c3fa1dd2171a8cdfb2effd98590cbd5ebcdcfcf491f.txt" MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/15a677b6770c5505fb47439361d3d3a7c21238ee1a6874fdedad18ae96850590.txt" MaxLineLength:InboundTxTests.kt$InboundTxTests.Companion$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/recv/4dcc95dd0a2f1f51bd64bb9f729b423c6de1690664a1b6614c75925e781662f7.txt" @@ -231,10 +232,8 @@ MaxLineLength:InitializerTest.kt$InitializerTest$// assertEquals("Height should equal sapling activation height when defaultToOldestHeight is true", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height) MaxLineLength:InitializerTest.kt$InitializerTest$// assertEquals("Incorrect height used for import.", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height) MaxLineLength:InitializerTest.kt$InitializerTest$// assertNotEquals("Height should not equal sapling activation height when defaultToOldestHeight is false", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, h) - MaxLineLength:LightWalletGrpcService.kt$LightWalletGrpcService$twig("getting channel isShutdown: ${channel.isShutdown} isTerminated: ${channel.isTerminated} getState: $state stateCount: $stateCount", -1) - MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$binding.inputAddress.setText(DerivationTool.deriveTransparentAddress(seed, ZcashNetwork.fromResources(requireApplicationContext()))) + MaxLineLength:LightWalletService.kt$LightWalletService$fun MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString()) - MaxLineLength:ListUtxosFragment.kt$ListUtxosFragment$val startToUse = binding.inputRangeStart.text.toString().toIntOrNull() ?: ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight MaxLineLength:MaintainedTest.kt$TestPurpose.DARKSIDE$* These tests require a running instance of [darksidewalletd](https://github.com/zcash/lightwalletd/blob/master/docs/darksidewalletd.md). MaxLineLength:MultiAccountIntegrationTest.kt$// private val secondKey = "zxviews1q0w208wwqqqqpqyxp978kt2qgq5gcyx4er907zhczxpepnnhqn0a47ztefjnk65w2573v7g5fd3hhskrg7srpxazfvrj4n2gm4tphvr74a9xnenpaxy645dmuqkevkjtkf5jld2f7saqs3xyunwquhksjpqwl4zx8zj73m8gk2d5d30pck67v5hua8u3chwtxyetmzjya8jdjtyn2aum7au0agftfh5q9m4g596tev9k365s84jq8n3laa5f4palt330dq0yede053sdyfv6l" MaxLineLength:MultiAccountTest.kt$// private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt" @@ -275,9 +274,9 @@ 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 MaxLineLength:RustBackend.kt$RustBackend$// override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList { 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") - 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" - 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" - MaxLineLength:SanityTest.kt$SanityTest$assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height.toInt() == height) + MaxLineLength:RustBackendWelding.kt$RustBackendWelding$suspend fun clearUtxos(tAddress: String, aboveHeightInclusive: BlockHeight = BlockHeight(network.saplingActivationHeight.value)): Boolean + 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" + MaxLineLength:SanityTest.kt$SanityTest$assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height == height.value) MaxLineLength:SanityTest.kt$SanityTest.Companion$"zxviews1q0hxkupsqqqqpqzsffgrk2smjuccedua7zswf5e3rgtv3ga9nhvhjug670egshd6me53r5n083s2m9mf4va4z7t39ltd3wr7hawnjcw09eu85q0ammsg0tsgx24p4ma0uvr4p8ltx5laum2slh2whc23ctwlnxme9w4dw92kalwk5u4wyem8dynknvvqvs68ktvm8qh7nx9zg22xfc77acv8hk3qqll9k3x4v2fa26puu2939ea7hy4hh60ywma69xtqhcy4037ne8g2sg8sq" MaxLineLength:SanityTest.kt$SanityTest.Companion$"zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063" 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 @@ -289,7 +288,6 @@ 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" MaxLineLength:SetupTest.kt$SetupTest.Companion$private const val blocksUrl = "https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/basic-reorg/before-reorg.txt" 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" - 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" 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) 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) 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) @@ -315,24 +313,23 @@ MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/sent/ff6ea36765dc29793775c7aa71de19fca039c5b5b873a0497866e9c4bc48af01.txt" MaxLineLength:TestExtensions.kt$Transactions$"https://raw.githubusercontent.com/zcash-hackworks/darksidewalletd-test-data/master/transactions/t-shielded-spend.txt" MaxLineLength:TestWallet.kt$TestWallet$suspend - MaxLineLength:TestWallet.kt$TestWallet.Backups$ALICE : Backups - MaxLineLength:TestWallet.kt$TestWallet.Backups$DEV_WALLET : Backups - MaxLineLength:TestWallet.kt$TestWallet.Backups$SAMPLE_WALLET : Backups - MaxLineLength:TestWallet.kt$TestWallet.Backups.BOB$BOB - MaxLineLength:TestWallet.kt$TestWallet.Backups.DEFAULT$DEFAULT + 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" + 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" + 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" + 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" + 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" 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" + MaxLineLength:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$runBlocking { config.importWallet(seed, BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight), ZcashNetwork.Testnet, lightWalletEndpoint) } MaxLineLength:TransactionViewHolder.kt$TransactionViewHolder$icon.setColorFilter(ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound)) - MaxLineLength:Transactions.kt$if (latestHeight == null || latestHeight < saplingActivationHeight || expiryHeight < saplingActivationHeight) return false + MaxLineLength:Transactions.kt$if (latestHeight == null || latestHeight.value < saplingActivationHeight.value || expiryHeight < saplingActivationHeight.value) return false 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) MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$// twig("FOUND utxo balance of total: ${walletBalance.totalZatoshi} available: ${walletBalance.availableZatoshi}") MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$// walletA.send(TX_VALUE, walletA.transparentAddress, "${TransparentRestoreSample::class.java.simpleName} z->t") 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) - MaxLineLength:TransparentRestoreSample.kt$TransparentRestoreSample$val walletSandbox = TestWallet(TestWallet.Backups.SAMPLE_WALLET.seedPhrase, "WalletC", Testnet, startHeight = 1330190) MaxLineLength:TransparentTest.kt$TransparentTest$assertEquals(expected.tAddr, DerivationTool.deriveTransparentAddressFromPublicKey(uvk.extpub, network = network)) 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" MaxLineLength:TransparentTest.kt$TransparentTest.Companion.ExpectedTestnet$override val zAddr = "ztestsapling1wn3tw9w5rs55x5yl586gtk72e8hcfdq8zsnjzcu8p7ghm8lrx54axc74mvm335q7lmy3g0sqje6" MaxLineLength:Twig.kt$inline - 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. MaxLineLength:WalletService.kt$var duration = Math.pow(initialDelayMillis.toDouble(), (sequence.toDouble() / 4.0)).toLong() + Random.nextLong(1000L) 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. MaxLineLength:ZcashSdk.kt$ZcashSdk$* @@ -346,13 +343,11 @@ MayBeConst:DemoConstants.kt$DemoConstants$val sendAmount: Double = 0.000018 MayBeConst:DemoConstants.kt$DemoConstants$val sendAmount: Double = 0.00017 MayBeConst:DemoConstants.kt$DemoConstants$val utxoEndHeight: Int = 1075590 - MayBeConst:DemoConstants.kt$DemoConstants$val utxoEndHeight: Int = 968085 MayBeConst:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m" MayBeConst:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0" 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 MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default attempts at retrying. */ val RETRIES = 5 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 - MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Default size of batches of blocks to request from the compact block service. */ val DOWNLOAD_BATCH_SIZE = 100 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 MayBeConst:ZcashSdk.kt$ZcashSdk$/** * Estimate of the time between blocks. */ val BLOCK_INTERVAL_MILLIS = 75_000L MayBeConst:ZcashSdk.kt$ZcashSdk$/** * File name for the sapling output params */ val OUTPUT_PARAM_FILE_NAME = "sapling-output.params" @@ -368,7 +363,7 @@ MayBeConst:ZcashSdk.kt$ZcashSdk$val DEFAULT_ALIAS: String = "ZcashSdk" PrintStackTrace:CompactBlockProcessor.kt$CompactBlockProcessor$t PrintStackTrace:PersistentTransactionManager.kt$PersistentTransactionManager$t - ReturnCount:CurrencyFormatter.kt$ inline fun String?.safelyConvertToBigDecimal(): BigDecimal? + ReturnCount:CurrencyFormatter.kt$inline fun String?.safelyConvertToBigDecimal(): BigDecimal? ReturnCount:MainActivity.kt$MainActivity$fun getClipboardText(): String? SpreadOperator:Initializer.kt$Initializer.Config$( *DerivationTool.deriveUnifiedViewingKeys( seed, network, numberOfAccounts ) ) SpreadOperator:PagedTransactionRepository.kt$PagedTransactionRepository.Companion$(*viewingKeys.toTypedArray()) @@ -379,6 +374,7 @@ SwallowedException:Ext.kt$t: Throwable SwallowedException:SdkSynchronizer.kt$SdkSynchronizer$t: Throwable SwallowedException:SharedViewModel.kt$SharedViewModel$t: Throwable + TooGenericExceptionCaught:CheckpointTool.kt$CheckpointTool$t: Throwable TooGenericExceptionCaught:CompactBlockDownloader.kt$CompactBlockDownloader$t: Throwable TooGenericExceptionCaught:CompactBlockProcessor.kt$CompactBlockProcessor$e: Throwable TooGenericExceptionCaught:CompactBlockProcessor.kt$CompactBlockProcessor$t: Throwable @@ -395,7 +391,6 @@ TooGenericExceptionCaught:SdkSynchronizer.kt$SdkSynchronizer$tError: Throwable TooGenericExceptionCaught:SdkSynchronizer.kt$SdkSynchronizer$zError: Throwable TooGenericExceptionCaught:SharedViewModel.kt$SharedViewModel$t: Throwable - TooGenericExceptionCaught:WalletBirthdayTool.kt$WalletBirthdayTool$t: Throwable TooGenericExceptionCaught:WalletService.kt$t: Throwable TooGenericExceptionCaught:WalletTransactionEncoder.kt$WalletTransactionEncoder$t: Throwable TooGenericExceptionThrown:DarksideApi.kt$DarksideApi.EmptyResponse$throw RuntimeException("Server responded with an error: $error caused by ${error?.cause}") @@ -406,7 +401,6 @@ TooManyFunctions:HomeFragment.kt$HomeFragment : BaseDemoFragment TooManyFunctions:Initializer.kt$Initializer$Companion : Erasable TooManyFunctions:Initializer.kt$Initializer$Config - TooManyFunctions:LightWalletGrpcService.kt$LightWalletGrpcService : LightWalletService TooManyFunctions:ListTransactionsFragment.kt$ListTransactionsFragment : BaseDemoFragment TooManyFunctions:ListUtxosFragment.kt$ListUtxosFragment : BaseDemoFragment TooManyFunctions:MainActivity.kt$MainActivity : AppCompatActivityOnPrimaryClipChangedListenerDrawerListener @@ -458,8 +452,13 @@ UnusedPrivateMember:SetupTest.kt$SetupTest.Companion$private const val lastBlockHash = "2fc7b4682f5ba6ba6f86e170b40f0aa9302e1d3becb2a6ee0db611ff87835e4a" 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" UnusedPrivateMember:TestnetIntegrationTest.kt$TestnetIntegrationTest.Companion$private const val targetHeight = 663250 + UnusedPrivateMember:TransactionCounterUtil.kt$TransactionCounterUtil$private val network = ZcashNetwork.Mainnet + UseCheckOrError:FlowPagedListBuilder.kt$FlowPagedListBuilder$throw IllegalStateException("Unable to create executor based on dispatcher: $this") + UseCheckOrError:Placeholders.kt$SampleSpendingKeyProvider$throw IllegalStateException("This sample provider only supports the dummy seed") UtilityClassWithPublicConstructor:DerivationTool.kt$DerivationTool UtilityClassWithPublicConstructor:Placeholders.kt$SeedGenerator UtilityClassWithPublicConstructor:SaplingParamTool.kt$SaplingParamTool + 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" + VariableNaming:TransparentRestoreSample.kt$TransparentRestoreSample$val TX_VALUE = Zatoshi(ZcashSdk.MINERS_FEE.value / 2) diff --git a/tools/detekt.yml b/tools/detekt.yml index dd92c93..5734002 100644 --- a/tools/detekt.yml +++ b/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/**']