Merge pull request #689 from zcash/merge-1.9.0-beta03

Merge v1.9.0-beta03
This commit is contained in:
str4d 2022-08-23 20:52:53 +01:00 committed by GitHub
commit ab883750c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 2335 additions and 1070 deletions

View File

@ -31,13 +31,13 @@ runs:
echo "org.gradle.daemon=false" >> ~/.gradle/gradle.properties
- name: Gradle Wrapper Cache
id: gradle-wrapper-cache
uses: actions/cache@0865c47f36e68161719c5b124609996bb5c40129
uses: actions/cache@a7c34adf76222e77931dedbf4a45b2e4648ced19
with:
path: ~/.gradle/wrapper
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles(format('{0}{1}', github.workspace, '/gradle/wrapper/gradle-wrapper.properties')) }}
- name: Gradle Dependency Cache
id: gradle-dependency-cache
uses: actions/cache@0865c47f36e68161719c5b124609996bb5c40129
uses: actions/cache@a7c34adf76222e77931dedbf4a45b2e4648ced19
with:
path: ~/.gradle/caches/modules-2
key: ${{ runner.os }}-gradle-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/gradle.properties')) }}
@ -48,7 +48,7 @@ runs:
# 2. Relying on the sha for an exact match so that the prime_cache job is re-used by all dependent jobs in a single workflow run
- name: Gradle Build Cache
id: gradle-build-cache
uses: actions/cache@0865c47f36e68161719c5b124609996bb5c40129
uses: actions/cache@a7c34adf76222e77931dedbf4a45b2e4648ced19
with:
path: |
~/.gradle/caches/build-cache-1
@ -59,7 +59,7 @@ runs:
${{ runner.os }}-gradle-build-
- name: Rust Cache
id: rust-cache
uses: actions/cache@0865c47f36e68161719c5b124609996bb5c40129
uses: actions/cache@a7c34adf76222e77931dedbf4a45b2e4648ced19
with:
path: |
sdk-lib/target

View File

@ -1,9 +1,7 @@
# Expected secrets
# MAVEN_CENTRAL_USERNAME - Username for Maven Central.
# MAVEN_CENTRAL_PASSWORD - Password for Maven Central.
# MAVEN_SIGNING_KEYRING_FILE_BASE64 - Base64 encoded GPG keyring file.
# MAVEN_SIGNING_KEY_ID - ID for the key in the GPG keyring file.
# MAVEN_SIGNING_PASSWORD - Password for the key in the GPG keyring file.
# MAVEN_SIGNING_KEY_ASCII - GPG key without a password which has ASCII-armored and then BASE64-encoded.
name: Deploy Release
@ -27,9 +25,26 @@ jobs:
timeout-minutes: 1
uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b
check_secrets:
environment: deployment
permissions:
contents: read
runs-on: ubuntu-latest
outputs:
has-secrets: ${{ steps.check_secrets.outputs.defined }}
steps:
- id: check_secrets
env:
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
MAVEN_SIGNING_KEY: ${{ secrets.MAVEN_SIGNING_KEY_ASCII }}
if: "${{ env.MAVEN_CENTRAL_USERNAME != '' && env.MAVEN_CENTRAL_PASSWORD != '' && env.MAVEN_SIGNING_KEY != '' }}"
run: echo "::set-output name=defined::true"
deploy_release:
environment: deployment
needs: validate_gradle_wrapper
needs: [validate_gradle_wrapper, check_secrets]
if: needs.check_secrets.outputs.has-secrets == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
@ -41,36 +56,24 @@ jobs:
id: setup
timeout-minutes: 30
uses: ./.github/actions/setup
- name: Export Maven Signing Key
env:
MAVEN_SIGNING_KEYRING_FILE_BASE64: ${{ secrets.MAVEN_SIGNING_KEYRING_FILE_BASE64 }}
GPG_KEY_PATH: ${{ format('{0}/keyring.gpg', env.home) }}
shell: bash
run: |
echo ${MAVEN_SIGNING_KEYRING_FILE_BASE64} | base64 --decode > ${GPG_KEY_PATH}
# While not strictly necessary, this sanity checks the build before attempting to upload.
# This adds minimal additional build time, since most of the work is cached and re-used
# in the next step.
- name: Deploy to Maven Local
timeout-minutes: 25
env:
ORG_GRADLE_PROJECT_IS_SNAPSHOT: true
ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED: false
ORG_GRADLE_PROJECT_IS_SNAPSHOT: false
ORG_GRADLE_PROJECT_ZCASH_ASCII_GPG_KEY: ${{ secrets.MAVEN_SIGNING_KEY_ASCII }}
run: |
./gradlew publishToMavenLocal --no-parallel
./gradlew publishReleasePublicationToMavenLocalRepository --no-parallel
# Note that GitHub Actions appears to have issues with environment variables that contain periods,
# so the GPG variables are done as command line arguments instead.
- name: Deploy to Maven Central
timeout-minutes: 8
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_IS_SNAPSHOT: false
ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED: true
GPG_KEY_PATH: ${{ format('{0}/keyring.gpg', env.home) }}
GPG_KEY_ID: ${{ secrets.MAVEN_SIGNING_KEY_ID }}
GPG_PASSWORD: ${{ secrets.MAVEN_SIGNING_PASSWORD }}
ORG_GRADLE_PROJECT_ZCASH_MAVEN_PUBLISH_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_ZCASH_MAVEN_PUBLISH_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_ZCASH_ASCII_GPG_KEY: ${{ secrets.MAVEN_SIGNING_KEY_ASCII }}
run: |
./gradlew publish -Psigning.secretKeyRingFile=$GPG_KEY_PATH -Psigning.keyId=$GPG_KEY_ID -Psigning.password=$GPG_PASSWORD --no-parallel
./gradlew closeAndReleaseRepository --no-parallel
./gradlew publishReleasePublicationToMavenCentralRepository --no-parallel
- name: Collect Artifacts
timeout-minutes: 1
if: ${{ always() }}

View File

@ -35,9 +35,24 @@ jobs:
timeout-minutes: 1
uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b
check_secrets:
environment: deployment
permissions:
contents: read
runs-on: ubuntu-latest
outputs:
has-secrets: ${{ steps.check_secrets.outputs.defined }}
steps:
- id: check_secrets
env:
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
if: "${{ env.MAVEN_CENTRAL_USERNAME != '' && env.MAVEN_CENTRAL_PASSWORD != '' }}"
run: echo "::set-output name=defined::true"
deploy_snapshot:
environment: deployment
needs: validate_gradle_wrapper
needs: [validate_gradle_wrapper, check_secrets]
runs-on: ubuntu-latest
permissions:
contents: read
@ -56,18 +71,16 @@ jobs:
timeout-minutes: 25
env:
ORG_GRADLE_PROJECT_IS_SNAPSHOT: true
ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED: false
run: |
./gradlew publishToMavenLocal --no-parallel
./gradlew publishReleasePublicationToMavenLocalRepository --no-parallel
- name: Deploy to Maven Central
timeout-minutes: 8
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_ZCASH_MAVEN_PUBLISH_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_ZCASH_MAVEN_PUBLISH_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_IS_SNAPSHOT: true
ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED: false
run: |
./gradlew publish --no-parallel
./gradlew publishReleasePublicationToMavenCentralRepository --no-parallel
- name: Collect Artifacts
timeout-minutes: 1
if: ${{ always() }}

View File

@ -1,56 +0,0 @@
# Although our CI builds should automatically close and release repositories after publishing, sometimes
# this process can fail. This GitHub Action allows a team member to manually unwedge this stuck deployment.
# Expected secrets
# MAVEN_CENTRAL_USERNAME - Username for Maven Central
# MAVEN_CENTRAL_PASSWORD - Password for Maven Central
name: Close and release repository
on:
workflow_dispatch:
inputs:
mavenCentralRepository:
description: 'Repository name to close'
required: true
concurrency: deploy_release
jobs:
validate_gradle_wrapper:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
timeout-minutes: 1
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
# Gradle Wrapper validation can be flaky
# https://github.com/gradle/wrapper-validation-action/issues/40
- name: Gradle Wrapper Validation
timeout-minutes: 1
uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b
unwedge:
environment: deployment
needs: validate_gradle_wrapper
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
timeout-minutes: 1
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
- name: Setup
id: setup
timeout-minutes: 30
uses: ./.github/actions/setup
- name: Close and release repository
timeout-minutes: 8
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_IS_SNAPSHOT: true
ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED: false
run: |
./gradlew closeAndReleaseRepository --repository="${{ github.event.inputs.mavenCentralRepository }}" --no-parallel

View File

@ -1,6 +1,11 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />

View File

@ -47,12 +47,23 @@ Change Log
- `DerivationTool.deriveUnifiedViewingKeys`
- `DerivationTool.validateUnifiedViewingKey`
1.9.0-beta01
Version 1.9.0-beta03
------------------------------------
- No changes; this release is a test of a new deployment process
Version 1.9.0-beta02
------------------------------------
- The SDK now stores database files in `no_backup/co.electricoin.zcash` folder instead of the `database` folder. **No action required from client app**.
Version 1.9.0-beta01
------------------------------------
- Split `ZcashNetwork` into `ZcashNetwork` and `LightWalletEndpoint` to decouple network and server configuration
- Gradle 7.5.1
- Updated checkpoints
Version 1.8.0-beta01
------------------------------------
- Enabled automated unit tests run on the CI server
- Added `BlockHeight` typesafe object to represent block heights
- Significantly reduced memory usage, fixing potential OutOfMemoryError during block download
- Kotlin 1.7.10

218
README.md
View File

@ -16,212 +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).
There are also more comprehensive sample walletes:
* [ECC Sample Wallet](https://github.com/zcash/zcash-android-wallet) — A basic sample application.
* [Secant Sample Wallet](https://github.com/zcash/secant-android-wallet) — A more modern codebase written in Compose. This repository is a work-in-progress and is not fully functional yet as of August 2022, although it will be our primary sample application in the future.
[Back to contents](#contents)
# Maintainers and Contributors
If you're building the SDK from source or modifying the SDK:
* [Setup.md](docs/Setup.md) to configure building from source
* [Architecture.md](docs/Architecture.md) to understand the high level architecture of the code
* [CI.md](docs/CI.md) to understand the Continuous Integration build scripts
* [PUBLISHING.md](docs/PUBLISHING.md) to understand our deployment process
## Checkpoints
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.
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
To update the checkpoints, see [Checkmate](https://github.com/zcash-hackworks/checkmate).
We generally recommend adding new checkpoints every few weeks. By convention, checkpoints are added in block increments of 10,000 which provides a reasonable tradeoff in terms of number of checkpoints versus performance.
There are two special checkpoints, one for sapling activation and another for orchard activation. These are mentioned because they don't follow the "round 10,000" rule.
* Sapling activation
* Mainnet: 419200
* Testnet: 280000
* Orchard activation
* Mainnet: 1687104
* Testnet: 1842420
## Publishing
Publishing instructions for maintainers of this repository can be found in [PUBLISHING.md](PUBLISHING.md)
[Back to contents](#contents)
# Known Issues
1. 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`
## 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.

View File

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

View File

@ -43,9 +43,9 @@ class InboundTxTests : ScopedTest() {
}
private fun addTransactions(targetHeight: BlockHeight, vararg txs: String) {
val overwriteBlockCount = 5
// val overwriteBlockCount = 5
chainMaker
// .stageEmptyBlocks(targetHeight, overwriteBlockCount)
// .stageEmptyBlocks(targetHeight, overwriteBlockCount)
.stageTransactions(targetHeight, *txs)
.applyTipHeight(targetHeight)
}

View File

@ -1,6 +1,6 @@
package cash.z.ecc.android.sdk.darkside.test
open class DarksideTest(name: String = javaClass.simpleName) : ScopedTest() {
open class DarksideTest : ScopedTest() {
val sithLord = DarksideTestCoordinator()
val validator = sithLord.validator

View File

@ -1,7 +1,6 @@
package cash.z.ecc.android.sdk.darkside.test
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.BlockHeight
@ -101,13 +100,13 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
twig("got processor status $it")
if (it == Synchronizer.Status.DISCONNECTED) {
twig("waiting a bit before giving up on connection...")
} else if (targetHeight != null && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
} else if (targetHeight != null && synchronizer.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 != null && (synchronizer as SdkSynchronizer).processor.getLastScannedHeight() < targetHeight) {
if (targetHeight != null && synchronizer.processor.getLastScannedHeight() < targetHeight) {
twig("switching status to DOWNLOADING because we're still waiting for height $targetHeight")
Synchronizer.Status.DOWNLOADING
} else {
@ -145,8 +144,8 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
fun validateHasBlock(height: BlockHeight) {
runBlocking {
assertTrue((synchronizer as SdkSynchronizer).findBlockHashAsHex(height) != null)
assertTrue((synchronizer as SdkSynchronizer).findBlockHash(height)?.size ?: 0 > 0)
assertTrue(synchronizer.findBlockHashAsHex(height) != null)
assertTrue(synchronizer.findBlockHash(height)?.size ?: 0 > 0)
}
}
@ -193,7 +192,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
}
fun validateBlockHash(height: BlockHeight, expectedHash: String) {
val hash = runBlocking { (synchronizer as SdkSynchronizer).findBlockHashAsHex(height) }
val hash = runBlocking { synchronizer.findBlockHashAsHex(height) }
assertEquals(expectedHash, hash)
}
@ -202,7 +201,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
}
fun validateTxCount(count: Int) {
val txCount = runBlocking { (synchronizer as SdkSynchronizer).getTransactionCount() }
val txCount = runBlocking { synchronizer.getTransactionCount() }
assertEquals("Expected $count transactions but found $txCount instead!", count, txCount)
}
@ -216,7 +215,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
}
}
suspend fun validateBalance(available: Long = -1, total: Long = -1, accountIndex: Int = 0) {
val balance = (synchronizer as SdkSynchronizer).processor.getBalanceInfo(accountIndex)
val balance = synchronizer.processor.getBalanceInfo(accountIndex)
if (available > 0) {
assertEquals("invalid available balance", available, balance.available)
}

View File

@ -6,6 +6,7 @@ import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.twig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
@ -19,6 +20,7 @@ import org.junit.Before
import org.junit.BeforeClass
import java.util.concurrent.TimeoutException
@OptIn(DelicateCoroutinesApi::class)
open class ScopedTest(val defaultTimeout: Long = 2000L) : DarksideTestPrerequisites() {
protected lateinit var testScope: CoroutineScope
@ -60,7 +62,7 @@ open class ScopedTest(val defaultTimeout: Long = 2000L) : DarksideTestPrerequisi
fun createScope() {
twig("======================= CLASS STARTED ===============================")
classScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(2, this.javaClass.simpleName)
SupervisorJob() + newFixedThreadPoolContext(2, this::class.java.simpleName)
)
}

View File

@ -12,9 +12,9 @@ class SimpleMnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
override fun nextMnemonic(seed: ByteArray): CharArray = MnemonicCode(seed).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
override fun nextMnemonicList(seed: ByteArray): List<CharArray> = MnemonicCode(seed).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
}

View File

@ -18,6 +18,7 @@ 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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
@ -34,6 +35,7 @@ import java.util.concurrent.TimeoutException
* A simple wallet that connects to testnet for integration testing. The intention is that it is
* easy to drive and nice to use.
*/
@OptIn(DelicateCoroutinesApi::class)
class TestWallet(
val seedPhrase: String,
val alias: String = "TestWallet",

View File

@ -10,9 +10,11 @@ plugins {
android {
defaultConfig {
applicationId = "cash.z.ecc.android.sdk.demoapp"
minSdk = 21 // Different from the SDK min
minSdk = 19
versionCode = 1
versionName = "1.0"
multiDexEnabled = true
vectorDrawables.useSupportLibrary = true
}
buildFeatures {
viewBinding = true
@ -63,6 +65,7 @@ dependencies {
// Android
implementation(libs.androidx.core)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.multidex)
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.navigation.ui)
implementation(libs.material)

View File

@ -1,20 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="5" by="lint 4.2.1" client="gradle" variant="all" version="4.2.1">
<issue
id="OldTargetApi"
message="Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the android.os.Build.VERSION_CODES javadoc for details."
errorLine1=" targetSdkVersion 29"
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="13"
column="9"/>
</issue>
<issues format="6" by="lint 7.2.2" type="baseline" client="gradle" dependencies="false" name="AGP (7.2.2)" variant="all" version="7.2.2">
<issue
id="UnusedAttribute"
message="Attribute `drawableTint` is only used in API level 23 and higher (current min is 21)"
message="Attribute `drawableTint` is only used in API level 23 and higher (current min is 19)"
errorLine1=" android:drawableTint=&quot;@color/colorPrimary&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
@ -23,6 +12,17 @@
column="9"/>
</issue>
<issue
id="UnusedAttribute"
message="Attribute `elevation` is only used in API level 21 and higher (current min is 19)"
errorLine1=" android:elevation=&quot;1dp&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/item_transaction.xml"
line="9"
column="5"/>
</issue>
<issue
id="FragmentTagUsage"
message="Replace the &lt;fragment> tag with FragmentContainerView."
@ -35,102 +35,146 @@
</issue>
<issue
id="GradleDependency"
message="A newer version of cash.z.ecc.android:kotlin-bip39 than 1.0.1 is available: 1.0.2"
errorLine1=" implementation &apos;cash.z.ecc.android:kotlin-bip39:1.0.1&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
id="RedundantLabel"
message="Redundant label can be removed"
errorLine1=" android:label=&quot;@string/app_name&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="61"
column="20"/>
file="src/main/AndroidManifest.xml"
line="16"
column="13"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.core:core-ktx than 1.3.2 is available: 1.6.0"
errorLine1=" implementation &apos;androidx.core:core-ktx:1.3.2&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
id="VectorDrawableCompat"
message="To use VectorDrawableCompat, you need to set `android.defaultConfig.vectorDrawables.useSupportLibrary = true` in `:demo-app/build.gradle`"
errorLine1=" app:srcCompat=&quot;@drawable/ic_floating_action&quot; />"
errorLine2=" ~~~~~~~~~~~~~">
<location
file="build.gradle"
line="64"
column="20"/>
file="src/main/res/layout/app_bar_main.xml"
line="32"
column="9"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.constraintlayout:constraintlayout than 2.0.4 is available: 2.1.0"
errorLine1=" implementation &apos;androidx.constraintlayout:constraintlayout:2.0.4&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
id="VectorDrawableCompat"
message="To use VectorDrawableCompat, you need to set `android.defaultConfig.vectorDrawables.useSupportLibrary = true` in `:demo-app/build.gradle`"
errorLine1=" app:srcCompat=&quot;@drawable/ic_receive&quot;"
errorLine2=" ~~~~~~~~~~~~~">
<location
file="build.gradle"
line="65"
column="20"/>
file="src/main/res/layout/item_transaction.xml"
line="21"
column="9"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.navigation:navigation-fragment-ktx than 2.3.1 is available: 2.3.5"
errorLine1=" implementation &apos;androidx.navigation:navigation-fragment-ktx:2.3.1&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:tint=&quot;?attr/colorControlNormal&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="66"
column="20"/>
file="src/main/res/drawable/ic_baseline_check_24.xml"
line="6"
column="19"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.navigation:navigation-ui-ktx than 2.3.1 is available: 2.3.5"
errorLine1=" implementation &apos;androidx.navigation:navigation-ui-ktx:2.3.1&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:fillColor=&quot;@android:color/white&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="67"
column="20"/>
file="src/main/res/drawable/ic_baseline_check_24.xml"
line="8"
column="26"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of com.google.android.material:material than 1.3.0-alpha03 is available: 1.5.0-alpha02"
errorLine1=" implementation &quot;com.google.android.material:material:1.3.0-alpha03&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:tint=&quot;?attr/colorControlNormal&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="68"
column="20"/>
file="src/main/res/drawable/ic_baseline_close_24.xml"
line="6"
column="19"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of junit:junit than 4.13 is available: 4.13.2"
errorLine1=" testImplementation &apos;junit:junit:4.13&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:fillColor=&quot;@android:color/white&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="69"
column="24"/>
file="src/main/res/drawable/ic_baseline_close_24.xml"
line="8"
column="26"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.test.ext:junit than 1.1.2 is available: 1.1.3"
errorLine1=" androidTestImplementation &apos;androidx.test.ext:junit:1.1.2&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:tint=&quot;?attr/colorControlNormal&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="70"
column="31"/>
file="src/main/res/drawable/ic_baseline_edit_24.xml"
line="6"
column="19"/>
</issue>
<issue
id="GradleDependency"
message="A newer version of androidx.test.espresso:espresso-core than 3.3.0 is available: 3.4.0"
errorLine1=" androidTestImplementation &apos;androidx.test.espresso:espresso-core:3.3.0&apos;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:fillColor=&quot;@android:color/white&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="build.gradle"
line="71"
column="31"/>
file="src/main/res/drawable/ic_baseline_edit_24.xml"
line="8"
column="26"/>
</issue>
<issue
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:tint=&quot;?attr/colorControlNormal&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_baseline_move_to_inbox_24.xml"
line="6"
column="19"/>
</issue>
<issue
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:fillColor=&quot;@android:color/white&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_baseline_move_to_inbox_24.xml"
line="8"
column="26"/>
</issue>
<issue
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:tint=&quot;?attr/colorControlNormal&quot;>"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_menu_balance.xml"
line="6"
column="19"/>
</issue>
<issue
id="VectorRaster"
message="Resource references will not work correctly in images generated for this vector icon for API &lt; 21; check generated icon to make sure it looks acceptable"
errorLine1=" android:fillColor=&quot;@android:color/black&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/drawable/ic_menu_balance.xml"
line="8"
column="26"/>
</issue>
<issue
@ -156,10 +200,14 @@
</issue>
<issue
id="ObsoleteSdkInt"
message="This folder configuration (`v21`) is unnecessary; `minSdkVersion` is 21. Merge all the resources in this folder into `values`.">
id="DataExtractionRules"
message="The attribute `android:allowBackup` is deprecated from Android 12 and higher and may be removed in future versions. Consider adding the attribute `android:dataExtractionRules` specifying an `@xml` resource which configures cloud backups and device transfers on Android 12 and higher."
errorLine1=" android:allowBackup=&quot;false&quot;"
errorLine2=" ~~~~~">
<location
file="src/main/res/values-v21"/>
file="src/main/AndroidManifest.xml"
line="8"
column="30"/>
</issue>
<issue
@ -363,66 +411,66 @@
<issue
id="SetTextI18n"
message="Do not concatenate text displayed with `setText`. Use resource string with placeholders."
errorLine1=" binding.textInfo.text = &quot;z-addr:\n$zaddress\n\n\nt-addr:\n$taddress&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
errorLine1=" binding.textInfo.text = &quot;z-addr:\n$zaddress\n\n\nt-addr:\n$taddress&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt"
line="41"
column="33"/>
line="48"
column="37"/>
</issue>
<issue
id="SetTextI18n"
message="String literal in `setText` can not be translated. Use Android resources instead."
errorLine1=" binding.textInfo.text = &quot;z-addr:\n$zaddress\n\n\nt-addr:\n$taddress&quot;"
errorLine2=" ~~~~~~~">
errorLine1=" binding.textInfo.text = &quot;z-addr:\n$zaddress\n\n\nt-addr:\n$taddress&quot;"
errorLine2=" ~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt"
line="41"
column="34"/>
line="48"
column="38"/>
</issue>
<issue
id="SetTextI18n"
message="String literal in `setText` can not be translated. Use Android resources instead."
errorLine1=" binding.textInfo.text = &quot;z-addr:\n$zaddress\n\n\nt-addr:\n$taddress&quot;"
errorLine2=" ~~~~~~~">
errorLine1=" binding.textInfo.text = &quot;z-addr:\n$zaddress\n\n\nt-addr:\n$taddress&quot;"
errorLine2=" ~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getaddress/GetAddressFragment.kt"
line="41"
column="58"/>
line="48"
column="62"/>
</issue>
<issue
id="SetTextI18n"
message="Do not concatenate text displayed with `setText`. Use resource string with placeholders."
errorLine1=" binding.textBalance.text = &quot;&quot;&quot;"
errorLine2=" ^">
errorLine1=" binding.textBalance.text = &quot;&quot;&quot;"
errorLine2=" ^">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt"
line="72"
column="40"/>
line="82"
column="36"/>
</issue>
<issue
id="SetTextI18n"
message="String literal in `setText` can not be translated. Use Android resources instead."
errorLine1=" Available balance: ${balance.availableZatoshi.convertZatoshiToZecString(12)}"
errorLine1=" Available balance: ${balance.available.convertZatoshiToZecString(12)}"
errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt"
line="73"
line="83"
column="1"/>
</issue>
<issue
id="SetTextI18n"
message="String literal in `setText` can not be translated. Use Android resources instead."
errorLine1=" Total balance: ${balance.totalZatoshi.convertZatoshiToZecString(12)}"
errorLine1=" Total balance: ${balance.total.convertZatoshiToZecString(12)}"
errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt"
line="74"
line="84"
column="1"/>
</issue>
@ -433,7 +481,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt"
line="81"
line="89"
column="35"/>
</issue>
@ -444,7 +492,7 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt"
line="81"
line="89"
column="36"/>
</issue>
@ -455,7 +503,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt"
line="83"
line="92"
column="41"/>
</issue>
@ -466,7 +514,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt"
line="91"
line="100"
column="39"/>
</issue>
@ -477,7 +525,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getbalance/GetBalanceFragment.kt"
line="91"
line="100"
column="40"/>
</issue>
@ -510,7 +558,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt"
line="92"
line="102"
column="33"/>
</issue>
@ -521,41 +569,41 @@
errorLine2=" ~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt"
line="92"
line="102"
column="34"/>
</issue>
<issue
id="SetTextI18n"
message="Do not concatenate text displayed with `setText`. Use resource string with placeholders."
errorLine1=" binding.textInfo.setText(&quot;Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey&quot;)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
errorLine1=" binding.textInfo.setText(&quot;Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey&quot;)"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt"
line="43"
column="34"/>
line="56"
column="38"/>
</issue>
<issue
id="SetTextI18n"
message="String literal in `setText` can not be translated. Use Android resources instead."
errorLine1=" binding.textInfo.setText(&quot;Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey&quot;)"
errorLine2=" ~~~~~~~~~~~~~">
errorLine1=" binding.textInfo.setText(&quot;Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey&quot;)"
errorLine2=" ~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt"
line="43"
column="35"/>
line="56"
column="39"/>
</issue>
<issue
id="SetTextI18n"
message="String literal in `setText` can not be translated. Use Android resources instead."
errorLine1=" binding.textInfo.setText(&quot;Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey&quot;)"
errorLine2=" ~~~~~~~~~~~~">
errorLine1=" binding.textInfo.setText(&quot;Spending Key:\n$spendingKey\n\nViewing Key:\n$viewingKey&quot;)"
errorLine2=" ~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt"
line="43"
column="66"/>
line="56"
column="70"/>
</issue>
<issue
@ -565,7 +613,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/home/HomeFragment.kt"
line="48"
line="42"
column="47"/>
</issue>
@ -576,7 +624,7 @@
errorLine2=" ~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/home/HomeFragment.kt"
line="48"
line="42"
column="48"/>
</issue>
@ -587,7 +635,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="82"
line="96"
column="54"/>
</issue>
@ -598,7 +646,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="82"
line="96"
column="55"/>
</issue>
@ -609,7 +657,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="86"
line="100"
column="46"/>
</issue>
@ -620,7 +668,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="86"
line="100"
column="47"/>
</issue>
@ -631,7 +679,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="91"
line="105"
column="35"/>
</issue>
@ -642,7 +690,7 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="91"
line="105"
column="36"/>
</issue>
@ -653,7 +701,7 @@
errorLine2=" ^">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="109"
line="123"
column="25"/>
</issue>
@ -664,30 +712,30 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="109"
line="123"
column="26"/>
</issue>
<issue
id="SetTextI18n"
message="Do not concatenate text displayed with `setText`. Use resource string with placeholders."
errorLine1=" &quot;or send funds to this address (tap the FAB to copy it):\n\n $address&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
errorLine1=" &quot;or send funds to this address (tap the FAB to copy it):\n\n $address&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="110"
column="33"/>
line="124"
column="25"/>
</issue>
<issue
id="SetTextI18n"
message="String literal in `setText` can not be translated. Use Android resources instead."
errorLine1=" &quot;or send funds to this address (tap the FAB to copy it):\n\n $address&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
errorLine1=" &quot;or send funds to this address (tap the FAB to copy it):\n\n $address&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt"
line="110"
column="34"/>
line="124"
column="26"/>
</issue>
<issue
@ -697,7 +745,7 @@
errorLine2=" ~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt"
line="96"
line="103"
column="36"/>
</issue>
@ -708,7 +756,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt"
line="145"
line="165"
column="39"/>
</issue>
@ -719,7 +767,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt"
line="218"
line="244"
column="56"/>
</issue>
@ -730,7 +778,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt"
line="218"
line="244"
column="57"/>
</issue>
@ -741,7 +789,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt"
line="222"
line="248"
column="48"/>
</issue>
@ -752,7 +800,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt"
line="222"
line="248"
column="49"/>
</issue>
@ -763,7 +811,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt"
line="227"
line="253"
column="35"/>
</issue>
@ -774,7 +822,7 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt"
line="227"
line="253"
column="36"/>
</issue>
@ -785,7 +833,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="114"
line="133"
column="35"/>
</issue>
@ -796,7 +844,7 @@
errorLine2=" ~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="114"
line="133"
column="36"/>
</issue>
@ -807,7 +855,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="117"
line="136"
column="41"/>
</issue>
@ -818,7 +866,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="125"
line="144"
column="39"/>
</issue>
@ -829,7 +877,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="125"
line="144"
column="40"/>
</issue>
@ -840,7 +888,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="133"
line="152"
column="56"/>
</issue>
@ -851,7 +899,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="133"
line="152"
column="57"/>
</issue>
@ -862,29 +910,29 @@
errorLine2=" ^">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="139"
line="158"
column="40"/>
</issue>
<issue
id="SetTextI18n"
message="String literal in `setText` can not be translated. Use Android resources instead."
errorLine1=" Available balance: ${balance.availableZatoshi.convertZatoshiToZecString(12)}"
errorLine1=" Available balance: ${balance?.available.convertZatoshiToZecString(12)}"
errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="140"
line="159"
column="1"/>
</issue>
<issue
id="SetTextI18n"
message="String literal in `setText` can not be translated. Use Android resources instead."
errorLine1=" Total balance: ${balance.totalZatoshi.convertZatoshiToZecString(12)}"
errorLine1=" Total balance: ${balance?.total.convertZatoshiToZecString(12)}"
errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="141"
line="160"
column="1"/>
</issue>
@ -895,7 +943,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="173"
line="192"
column="20"/>
</issue>
@ -906,7 +954,7 @@
errorLine2=" ~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="181"
line="200"
column="29"/>
</issue>
@ -917,7 +965,7 @@
errorLine2=" ~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="185"
line="204"
column="29"/>
</issue>
@ -928,7 +976,7 @@
errorLine2=" ~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="190"
line="209"
column="29"/>
</issue>
@ -939,7 +987,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt"
line="198"
line="217"
column="34"/>
</issue>

View File

@ -70,14 +70,14 @@ class SampleCodeTest {
)
}
assertEquals(1, spendingKeys.size)
log("Spending Key: ${spendingKeys?.get(0)}")
log("Spending Key: ${spendingKeys[0]}")
}
// ///////////////////////////////////////////////////
// Get Address
@Test fun getAddress() = runBlocking {
val address = synchronizer.getAddress()
assertFalse(address.isNullOrBlank())
assertFalse(address.isBlank())
log("Address: $address")
}

View File

@ -1,10 +1,10 @@
package cash.z.ecc.android.sdk.demoapp
import android.app.Application
import androidx.multidex.MultiDexApplication
import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
class App : Application() {
class App : MultiDexApplication() {
override fun onCreate() {
super.onCreate()

View File

@ -17,11 +17,11 @@ import com.google.android.material.snackbar.Snackbar
abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
/**
* Since the lightwalletservice is not a component that apps typically use, directly, we provide
* Since the lightWalletService is not a component that apps typically use, directly, we provide
* this from one place. Everything that can be done with the service can/should be done with the
* synchronizer because it wraps the service.
*/
val lightwalletService get() = mainActivity()?.lightwalletService
val lightWalletService get() = mainActivity()?.lightWalletService
// contains view information provided by the user
val sharedViewModel: SharedViewModel by activityViewModels()
@ -76,9 +76,8 @@ abstract class BaseDemoFragment<T : ViewBinding> : Fragment() {
* Convenience function to the given text to the clipboard.
*/
open fun copyToClipboard(text: String, description: String = "Copied to clipboard!") {
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)?.let { cm ->
cm.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text))
}
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)
.setPrimaryClip(ClipData.newPlainText("DemoAppClip", text))
toast(description)
}

View File

@ -41,7 +41,7 @@ class MainActivity :
* this object because it would utilize the synchronizer, instead, which exposes APIs that
* automatically sync with the server.
*/
var lightwalletService: LightWalletService? = null
var lightWalletService: LightWalletService? = null
private set
override fun onCreate(savedInstanceState: Bundle?) {
@ -53,8 +53,8 @@ class MainActivity :
setSupportActionBar(toolbar)
val fab: FloatingActionButton = findViewById(R.id.fab)
fab.setOnClickListener { view ->
onFabClicked(view)
fab.setOnClickListener {
onFabClicked()
}
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
val navView: NavigationView = findViewById(R.id.nav_view)
@ -78,7 +78,7 @@ class MainActivity :
override fun onDestroy() {
super.onDestroy()
lightwalletService?.shutdown()
lightWalletService?.shutdown()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -107,17 +107,17 @@ class MainActivity :
//
private fun initService() {
if (lightwalletService != null) {
lightwalletService?.shutdown()
if (lightWalletService != null) {
lightWalletService?.shutdown()
}
val network = ZcashNetwork.fromResources(applicationContext)
lightwalletService = LightWalletGrpcService.new(
lightWalletService = LightWalletGrpcService.new(
applicationContext,
LightWalletEndpoint.defaultForNetwork(network)
)
}
private fun onFabClicked(view: View) {
private fun onFabClicked() {
fabListener?.onActionButtonClicked()
}
@ -126,8 +126,10 @@ class MainActivity :
//
fun getClipboardText(): String? {
return with(clipboard) {
if (!hasPrimaryClip()) return null
with(clipboard) {
if (!hasPrimaryClip()) {
return null
}
return primaryClip!!.getItemAt(0)?.coerceToText(this@MainActivity)?.toString()
}
}

View File

@ -1,9 +1,9 @@
package cash.z.ecc.android.sdk.demoapp.demos.getblock
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import androidx.core.text.HtmlCompat
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
@ -26,10 +26,10 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
private fun setBlockHeight(blockHeight: BlockHeight) {
val blocks =
lightwalletService?.getBlockRange(blockHeight..blockHeight)
lightWalletService?.getBlockRange(blockHeight..blockHeight)
val block = blocks?.firstOrNull()
binding.textInfo.visibility = View.VISIBLE
binding.textInfo.text = Html.fromHtml(
binding.textInfo.text = HtmlCompat.fromHtml(
"""
<b>block height:</b> ${block?.height.withCommas()}
<br/><b>block time:</b> ${block?.time.toRelativeTime(requireApplicationContext())}
@ -37,10 +37,12 @@ class GetBlockFragment : BaseDemoFragment<FragmentGetBlockBinding>() {
<br/><b>hash:</b> ${block?.hash?.toByteArray()?.toHex()}
<br/><b>prevHash:</b> ${block?.prevHash?.toByteArray()?.toHex()}
${block?.vtxList.toHtml()}
""".trimIndent()
""".trimIndent(),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
}
@Suppress("UNUSED_PARAMETER")
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)

View File

@ -1,9 +1,9 @@
package cash.z.ecc.android.sdk.demoapp.demos.getblockrange
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.View
import androidx.core.text.HtmlCompat
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
@ -27,13 +27,13 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
private fun setBlockRange(blockRange: ClosedRange<BlockHeight>) {
val start = System.currentTimeMillis()
val blocks =
lightwalletService?.getBlockRange(blockRange)
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(
binding.textInfo.text = HtmlCompat.fromHtml(
blocks?.toList()?.run {
val count = size
val emptyCount = count { it.vtxCount == 0 }
@ -66,13 +66,15 @@ class GetBlockRangeFragment : BaseDemoFragment<FragmentGetBlockRangeBinding>() {
<br/><b>avg OUTs [per block / per TX]:</b> ${"%.1f / %.1f".format(outCount.toDouble() / (count - emptyCount), outCount.toDouble() / txCount)}
<br/><b>avg INs [per block / per TX]:</b> ${"%.1f / %.1f".format(inCount.toDouble() / (count - emptyCount), inCount.toDouble() / txCount)}
<br/><b>most shielded TXs:</b> ${if (maxTxs == null) "none" else "${maxTxs.vtxCount} in block ${maxTxs.height.withCommas()}"}
<br/><b>most shielded INs:</b> ${if (maxInTx == null) "none" else "${maxInTx.spendsCount} in block ${maxIns?.height.withCommas()} at tx index ${maxInTx.index}"}
<br/><b>most shielded OUTs:</b> ${if (maxOutTx == null) "none" else "${maxOutTx?.outputsCount} in block ${maxOuts?.height.withCommas()} at tx index ${maxOutTx?.index}"}
<br/><b>most shielded INs:</b> ${if (maxInTx == null) "none" else "${maxInTx.spendsCount} in block ${maxIns.height.withCommas()} at tx index ${maxInTx.index}"}
<br/><b>most shielded OUTs:</b> ${if (maxOutTx == null) "none" else "${maxOutTx.outputsCount} in block ${maxOuts.height.withCommas()} at tx index ${maxOutTx.index}"}
""".trimIndent()
} ?: "No blocks found in that range."
} ?: "No blocks found in that range.",
HtmlCompat.FROM_HTML_MODE_LEGACY
)
}
@Suppress("UNUSED_PARAMETER")
private fun onApply(_unused: View) {
val network = ZcashNetwork.fromResources(requireApplicationContext())
val start = max(binding.textStartHeight.text.toString().toLongOrNull() ?: network.saplingActivationHeight.value, network.saplingActivationHeight.value)

View File

@ -15,7 +15,7 @@ class GetLatestHeightFragment : BaseDemoFragment<FragmentGetLatestHeightBinding>
private fun displayLatestHeight() {
// note: this is a blocking call, a real app wouldn't do this on the main thread
// instead, a production app would leverage the synchronizer like in the other demos
binding.textInfo.text = lightwalletService?.getLatestBlockHeight().toString()
binding.textInfo.text = lightWalletService?.getLatestBlockHeight().toString()
}
//

View File

@ -2,6 +2,8 @@ package cash.z.ecc.android.sdk.demoapp.demos.getprivatekey
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
@ -61,9 +63,14 @@ class GetPrivateKeyFragment : BaseDemoFragment<FragmentGetPrivateKeyBinding>() {
// Android Lifecycle overrides
//
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
setup()
return view
}
override fun onResume() {

View File

@ -49,12 +49,14 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
mainActivity()?.removeClipboardListener()
}
@Suppress("UNUSED_PARAMETER")
private fun onEditSeedPhrase(unused: View) {
setEditShown(true)
binding.inputSeedPhrase.setText(sharedViewModel.seedPhrase.value)
binding.textLayoutSeedPhrase.helperText = ""
}
@Suppress("UNUSED_PARAMETER")
private fun onAcceptSeedPhrase(unused: View) {
if (applySeedPhrase()) {
setEditShown(false)
@ -62,10 +64,12 @@ class HomeFragment : BaseDemoFragment<FragmentHomeBinding>() {
}
}
@Suppress("UNUSED_PARAMETER")
private fun onCancelSeedPhrase(unused: View) {
setEditShown(false)
}
@Suppress("UNUSED_PARAMETER")
private fun onPasteSeedPhrase(unused: View) {
mainActivity()?.getClipboardText().let { clipboardText ->
binding.inputSeedPhrase.setText(clipboardText)

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.demoapp.demos.listtransactions
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import cash.z.ecc.android.bip39.Mnemonics
@ -33,7 +34,7 @@ import kotlinx.coroutines.runBlocking
class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBinding>() {
private lateinit var initializer: Initializer
private lateinit var synchronizer: Synchronizer
private lateinit var adapter: TransactionAdapter<ConfirmedTransaction>
private lateinit var adapter: TransactionAdapter
private lateinit var address: String
private var status: Synchronizer.Status? = null
private val isSynced get() = status == Synchronizer.Status.SYNCED
@ -134,9 +135,14 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
// Android Lifecycle overrides
//
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
setup()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

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

View File

@ -15,18 +15,18 @@ import java.util.Locale
/**
* Simple view holder for displaying confirmed transactions in the recyclerview.
*/
class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : RecyclerView.ViewHolder(itemView) {
class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val amountText = itemView.findViewById<TextView>(R.id.text_transaction_amount)
private val infoText = itemView.findViewById<TextView>(R.id.text_transaction_info)
private val timeText = itemView.findViewById<TextView>(R.id.text_transaction_timestamp)
private val icon = itemView.findViewById<ImageView>(R.id.image_transaction_type)
private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault())
fun bindTo(transaction: T?) {
fun bindTo(transaction: ConfirmedTransaction?) {
val isInbound = transaction?.toAddress.isNullOrEmpty()
amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString()
timeText.text =
if (transaction == null || transaction?.blockTimeInSeconds == 0L) "Pending"
if (transaction == null || transaction.blockTimeInSeconds == 0L) "Pending"
else formatter.format(transaction.blockTimeInSeconds * 1000L)
infoText.text = getMemoString(transaction)
@ -35,7 +35,7 @@ class TransactionViewHolder<T : ConfirmedTransaction>(itemView: View) : Recycler
icon.setColorFilter(ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound))
}
private fun getMemoString(transaction: T?): String {
private fun getMemoString(transaction: ConfirmedTransaction?): String {
return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo"
}
}

View File

@ -48,7 +48,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
private lateinit var seed: ByteArray
private lateinit var initializer: Initializer
private lateinit var synchronizer: Synchronizer
private lateinit var adapter: UtxoAdapter<ConfirmedTransaction>
private lateinit var adapter: UtxoAdapter
private val address: String = "t1RwbKka1CnktvAJ1cSqdn7c6PXWG4tZqgd"
private var status: Synchronizer.Status? = null
@ -86,7 +86,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
setup()
}
fun initUi() {
private fun initUi() {
binding.inputAddress.setText(address)
binding.inputRangeStart.setText(ZcashNetwork.fromResources(requireApplicationContext()).saplingActivationHeight.toString())
binding.inputRangeEnd.setText(getUxtoEndHeight(requireApplicationContext()).value.toString())
@ -99,7 +99,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
initTransactionUi()
}
fun downloadTransactions() {
private fun downloadTransactions() {
binding.textStatus.text = "loading..."
binding.textStatus.post {
val network = ZcashNetwork.fromResources(requireApplicationContext())
@ -114,7 +114,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
?: getUxtoEndHeight(requireApplicationContext()).value
var allStart = now
twig("loading transactions in range $startToUse..$endToUse")
val txids = lightwalletService?.getTAddressTransactions(
val txids = lightWalletService?.getTAddressTransactions(
addressToUse,
BlockHeight.new(network, startToUse)..BlockHeight.new(network, endToUse)
)
@ -131,18 +131,16 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
// twig("failed to decrypt and store transaction due to: $t")
// }
// }
}?.let { txData ->
}?.let { _ ->
// Disabled during migration to newer SDK version; this appears to have been
// leveraging non-public APIs in the SDK so perhaps should be removed
// val parseStart = now
// val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build()
// val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList)
// delta = now - parseStart
// updateStatus("parsed txs in ${delta}ms.")
// val parseStart = now
// val tList = LocalRpcTypes.TransactionDataList.newBuilder().addAllData(txData).build()
// val parsedTransactions = initializer.rustBackend.parseTransactionDataList(tList)
// delta = now - parseStart
// updateStatus("parsed txs in ${delta}ms.")
}
(synchronizer as SdkSynchronizer).refreshTransactions()
// val finalCount = (synchronizer as SdkSynchronizer).getTransactionCount()
// "found ${finalCount - initialCount} shielded outputs.
delta = now - allStart
updateStatus("Total time ${delta}ms.")
@ -199,7 +197,7 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
}
}
synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
// synchronizer.receivedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
// synchronizer.receivedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated)
} catch (t: Throwable) {
twig("failed to start the synchronizer!!! due to : $t")
}
@ -220,12 +218,12 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
adapter = UtxoAdapter()
binding.recyclerTransactions.adapter = adapter
// lifecycleScope.launch {
// // address = synchronizer.getAddress()
// synchronizer.receivedTransactions.onEach {
// onTransactionsUpdated(it)
// }.launchIn(this)
// }
// lifecycleScope.launch {
// address = synchronizer.getAddress()
// synchronizer.receivedTransactions.onEach {
// onTransactionsUpdated(it)
// }.launchIn(this)
// }
}
private fun startSynchronizer() {

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.demoapp.demos.send
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.bip39.Mnemonics
@ -162,6 +163,7 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
}
}
@Suppress("UNUSED_PARAMETER")
private fun onSend(unused: View) {
isSending = true
val amount = amountInput.text.toString().toDouble().convertZecToZatoshi()
@ -221,9 +223,14 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
// Android Lifecycle overrides
//
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
setup()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@ -5,7 +5,7 @@ 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.model.ZcashNetwork
import java.util.*
import java.util.Locale
fun ZcashNetwork.Companion.fromResources(context: Context): ZcashNetwork {
val networkNameFromResources = context.getString(R.string.network_name).lowercase(Locale.ROOT)

View File

@ -31,6 +31,7 @@ class SampleStorage(context: Context) {
* the SDK. This class delegates to the storage object. For demo purposes, we're using an insecure
* SampleStorage implementation but this can easily be swapped for a truly secure storage solution.
*/
@Suppress("deprecation")
class SampleStorageBridge(context: Context) {
private val delegate = SampleStorage(context.applicationContext)

View File

@ -19,9 +19,9 @@ class SimpleMnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
override fun nextMnemonic(seed: ByteArray): CharArray = MnemonicCode(seed).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
override fun nextMnemonicList(seed: ByteArray): List<CharArray> = MnemonicCode(seed).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
}

View File

@ -1 +1,35 @@
TODO
# Overview
From an app developer's perspective, this SDK will encapsulate the most complex aspects of using Zcash, freeing the developer to focus on UI and UX, rather than scanning blockchains and building commitment trees! Internally, the SDK is structured as follows:
![SDK Diagram](assets/sdk_diagram_final.png?raw=true "SDK Diagram")
Thankfully, the only thing an app developer has to be concerned with is the following:
![SDK Diagram Developer Perspective](assets/sdk_dev_pov_final.png?raw=true "SDK Diagram Dev PoV")
# Components
| Component | Summary |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **LightWalletService** | Service used for requesting compact blocks |
| **CompactBlockStore** | Stores compact blocks that have been downloaded from the `LightWalletService` |
| **CompactBlockProcessor** | Validates and scans the compact blocks in the `CompactBlockStore` for transaction details |
| **OutboundTransactionManager** | Creates, Submits and manages transactions for spending funds |
| **Initializer** | Responsible for all setup that must happen before synchronization can begin. Loads the rust library and helps initialize databases. |
| **DerivationTool**, **BirthdayTool** | Utilities for deriving keys, addresses and loading wallet checkpoints, called "birthdays." |
| **RustBackend** | Wraps and simplifies the rust library and exposes its functionality to the Kotlin SDK |
# Checkpoints
To improve the speed of syncing with the Zcash network, the SDK contains a series of embedded checkpoints. These should be updated periodically, as new transactions are added to the network. Checkpoints are stored under the [sdk-lib's assets](../sdk-lib/src/main/assets/co.electriccoin.zcash/checkpoint) directory as JSON files. Checkpoints for both mainnet and testnet are bundled into the SDK.
To update the checkpoints, see [Checkmate](https://github.com/zcash-hackworks/checkmate).
We generally recommend adding new checkpoints every few weeks. By convention, checkpoints are added in block increments of 10,000 which provides a reasonable tradeoff in terms of number of checkpoints versus performance.
There are two special checkpoints, one for sapling activation and another for orchard activation. These are mentioned because they don't follow the "round 10,000" rule.
* Sapling activation
* Mainnet: 419200
* Testnet: 280000
* Orchard activation
* Mainnet: 1687104
* Testnet: 1842420

110
docs/Consumers.md Normal file
View File

@ -0,0 +1,110 @@
# Configuring your build
Add flavors for testnet and mainnet. Since `productFlavors` cannot start with the word 'test' we recommend:
build.gradle
```groovy
flavorDimensions 'network'
productFlavors {
// would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test"
zcashtestnet {
dimension 'network'
matchingFallbacks = ['zcashtestnet', 'debug']
}
zcashmainnet {
dimension 'network'
matchingFallbacks = ['zcashmainnet', 'release']
}
}
```
build.gradle.kts
```kotlin
flavorDimensions.add("network")
productFlavors {
// would rather name them "testnet" and "mainnet" but product flavor names cannot start with the word "test"
create("zcashtestnet") {
dimension = "network"
matchingFallbacks.addAll(listOf("zcashtestnet", "debug"))
}
create("zcashmainnet") {
dimension = "network"
matchingFallbacks.addAll(listOf("zcashmainnet", "release"))
}
}
```
Resources
/src/main/res/values/bools.xml
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="zcash_is_testnet">false</bool>
</resources>
```
/src/zcashtestnet/res/values/bools.xml
```
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="zcash_is_testnet">true</bool>
</resources>
```
ZcashNetworkExt.kt
```
/**
* @return Zcash network determined from resources.
*/
fun ZcashNetwork.Companion.fromResources(context: Context) =
if (context.resources.getBoolean(R.bool.zcash_is_testnet)) {
ZcashNetwork.Testnet
} else {
ZcashNetwork.Mainnet
}
```
Add the SDK dependency:
```kotlin
implementation("cash.z.ecc.android:zcash-android-sdk:$LATEST_VERSION")
```
# Using the SDK
Start the [Synchronizer](-synchronizer/README.md)
```kotlin
synchronizer.start(this)
```
Get the wallet's address
```kotlin
synchronizer.getAddress()
// or alternatively
DerivationTool.deriveShieldedAddress(viewingKey)
```
Send funds to another address
```kotlin
synchronizer.sendToAddress(spendingKey, zatoshi, address, memo)
```
The [Synchronizer](-synchronizer/README.md) is the primary entrypoint for the SDK.
1. Start the [Synchronizer](-synchronizer/README.md)
2. Subscribe to wallet data
The [Synchronizer](-synchronizer/README.md) takes care of:
- Connecting to the light wallet server
- Downloading the latest compact blocks in a privacy-sensitive way
- Scanning and trial decrypting those blocks for shielded transactions related to the wallet
- Processing those related transactions into useful data for the UI
- Sending payments to a full node through [lightwalletd](https://github.com/zcash/lightwalletd)
- Monitoring sent payments for status updates

View File

@ -46,10 +46,14 @@ Because Gradle caches dependencies and because multiple snapshots can be deploye
## Releases
Production releases can be consumed using the instructions in the [README.MD](../README.md). Note that production releases can include alpha or beta designations.
Automated production releases still require a manual trigger. To do a production release:
1. Update the CHANGELOG and MIGRATIONS.md for any new changes since the last production release.
Automated production releases require a manual trigger of the GitHub action and a manual step inside the Sonatype dashboard. To do a production release:
1. Update the [CHANGELOG](../CHANGELOG.md) and [MIGRATIONS](../MIGRATIONS.md) for any new changes since the last production release.
1. Run the [release deployment](https://github.com/zcash/zcash-android-wallet-sdk/actions/workflows/deploy-release.yml).
1. Due to #535, release deployments are not fully automated. See the workaround steps in that issue and complete those steps.
1. Log into Maven Central and release the deployment.
1. Check the contents of the staging repository, to verify it looks correct
1. Close the staging repository
1. Wait a few minutes and refresh the page
1. Release the staging repository
1. Confirm deployment succeeded by modifying the [ECC Wallet](https://github.com/zcash/zcash-android-wallet) to consume the new SDK version.
1. Create a new Git tag for the new release in this repository.
1. Create a new pull request bumping the version to the next version (this ensures that the next merge to the main branch creates a snapshot under the next version number).
@ -59,33 +63,25 @@ See [ci.md](ci.md), which describes the continuous integration workflow for depl
## One time only
* Set up environment to [compile the SDK](https://github.com/zcash/zcash-android-wallet-sdk/#compiling-sources)
* Copy the GPG key to a directory with proper permissions (chmod 600). Note: If you'd like to quickly publish locally without subsequently publishing to Maven Central, configure a Gradle property `RELEASE_SIGNING_ENABLED=false`
* Create file `~/.gradle/gradle.properties` per the [instructions in this guide](https://proandroiddev.com/publishing-a-maven-artifact-3-3-step-by-step-instructions-to-mavencentral-publishing-bd661081645d)
* Create file `~/.gradle/gradle.properties`
* add your sonotype credentials with these properties
* `mavenCentralUsername`
* `mavenCentralPassword`
* point it to the GPG key with these properties
* `signing.keyId`
* `signing.password`
* `signing.secretKeyRingFile`
* `ZCASH_MAVEN_PUBLISH_USERNAME`
* `ZCASH_MAVEN_PUBLISH_PASSWORD`
* Point it to a passwordless GPG key that has been ASCII armored, then base64 encoded. Note this is only required for release builds. Snapshots do not require signing.
* `ZCASH_ASCII_GPG_KEY`
## Every time
1. Update the [build number](https://github.com/zcash/zcash-android-wallet-sdk/blob/main/gradle.properties) and the [CHANGELOG](https://github.com/zcash/zcash-android-wallet-sdk/blob/main/CHANGELOG.md). For release builds, suffix the Gradle invocations below with `-PIS_SNAPSHOT=false`.
1. Update the [build number](https://github.com/zcash/zcash-android-wallet-sdk/blob/main/gradle.properties) and the [CHANGELOG](../CHANGELOG.md). For release builds, suffix the Gradle invocations below with `-PIS_SNAPSHOT=false`.
3. Build locally
* This will install the files in your local maven repo at `~/.m2/repository/cash/z/ecc/android/`
```zsh
./gradlew publishToMavenLocal
```
4. Publish via the following command:
```zsh
# This uploads the file to sonotypes staging area
./gradlew publish --no-daemon --no-parallel
```
5. Deploy to maven central:
```zsh
# This closes the staging repository and releases it to the world
./gradlew closeAndReleaseRepository
./gradlew publishReleasePublicationToMavenLocalRepository
```
3. Publish via the following command:
1. Snapshot: `./gradlew publishReleasePublicationToMavenCentralRepository -PIS_SNAPSHOT=true`
2. Release
1. `./gradlew publishReleasePublicationToMavenCentralRepository -PIS_SNAPSHOT=false`
2. Log into the Sonatype portal to complete the process of closing and releasing the repository.
Note:
Our existing artifacts can be found here and here:

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 154 KiB

View File

Before

Width:  |  Height:  |  Size: 445 KiB

After

Width:  |  Height:  |  Size: 445 KiB

View File

@ -1,5 +1,5 @@
# Build
There are a variety of aspects to building the SDK and demo app. Although much of this ends up being tested automatically by the CI server, understanding these step can help when troubleshooting build issues. These test cases provide sanity checks that the build is not broken.
There are a variety of aspects to building the SDK and demo app. Although much of this ends up being tested automatically by the CI server, understanding these step can help when troubleshooting build issues. These test cases provide sanity checks that the build is not broken.
# Build
1. Run the assemble Gradle task, e.g. `./gradlew assemble`

View File

@ -0,0 +1,37 @@
# About
This manual test case provides information on how to manually test an implemented action of moving all of our databases files from default `/databases/` to preferred `/no_backup/co.electricoin.zcash` directory. The benefit of this approach is that the content `no_backup` folder is not part of automatic user data backup to user's cloud storage. Our databases can contain potentially big and sensitive data.
The move feature takes all related files (database file itself as well as `journal` and `wal` rollback files) and moves them only once on app start (before first database access) when a client app uses an updated version of this SDK.
# Prerequisite
- Installed Android Studio
- Ideally two emulators with min and max supported API level
- A working git client
- Cloned Zcash Android SDK repository
# Prepare steps
1. Install a previous version of the SDK and its demo-app to create database files in the original `database` folder
1. Switch back to commit **Bump version to 1.8.0-beta01 [3fda6da]** from Jul 11 2022 on the **Main** branch in your git client, or with this git command `git checkout 3fda6da1cae5b83174e5b1e020c91dfe95d93458`
2. Update dependencies lock (if needed) and sync Gradle files
3. Run the demo-app on selected emulator
4. Once it's opened go through the app to let the SDK create all the database files. Visit these screens step by step from the side menu:
1. Get Balance
2. List Transactions
3. List UTXOs
2. Open Device File Explorer from Android Studio bottom-left corner, select the same emulator device from the top drop-down menu
3. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/databases`
4. Verify there are `data.db`, `cache.db` and `utxos.db` files (their names can vary, depends on the current build variant). There can be several rollback files created.
# Move steps
1. Install the newer version of the SDK and its demo-app to the same device to check the database files move operation result
1. Switch to the latest commit on the **Main** branch in your git client
2. Update dependencies lock (if needed) and sync Gradle files
3. Run the demo-app on the same emulator device as previously
2. Once the app is opened, go to the Device File Explorer from Android Studio bottom-left corner again
3. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/databases` again, now there shouldn't be any files placed in the `database` folder
4. Go to `/data/data/cash.z.ecc.android.sdk.demoapp.mainnet/no_backup/co.electricoin.zcash`, which should be created automatically
5. Now verify there are the same files placed in the `no_backup/co.electricoin.zcash` folder as in `databases` were
6. To be sure everything is alright, just visit several screens from the side-menu and see no unexpected behavior
# Check result
Ideally run this test (Prepare and Move steps) for both emulators (Android SDK 21 and 31) to ensure the correct functionality on both Android version. There is a difference in implementation for these Android versions, but the result should be the same.

View File

@ -9,23 +9,23 @@ android.useAndroidX=true
android.builder.sdkDownload=true
# Publishing
## Configure these with command line arguments (`-PmavenCentralUsername=`), environment variables (`ORG_GRADLE_PROJECT_mavenCentralUsername`), or global ~/.gradle/gradle.properties
## Don't rename these; although we're consuming them directly for snapshot releases, they have special meaning in the com.vanniktech.maven.publish plugin and are used implicitly by it
mavenCentralUsername=
mavenCentralPassword=
ZCASH_MAVEN_PUBLISH_SNAPSHOT_URL=https://oss.sonatype.org/content/repositories/snapshots/
ZCASH_MAVEN_PUBLISH_RELEASE_URL=https://oss.sonatype.org/service/local/staging/deploy/maven2/
# Configures whether release is an unstable snapshot.
## Configure these with command line arguments (`-PZCASH_MAVEN_PUBLISH_USERNAME=`), environment variables (`ORG_GRADLE_PROJECT_ZCASH_MAVEN_PUBLISH_USERNAME`), or global ~/.gradle/gradle.properties
ZCASH_MAVEN_PUBLISH_USERNAME=
ZCASH_MAVEN_PUBLISH_PASSWORD=
# GPG key is only required if IS_SNAPSHOT is false
# GPG key is ASCII armored without a password, then Base64 encoded to escape the newlines
ZCASH_ASCII_GPG_KEY=
# Configures whether release is an unstable snapshot, therefore published to the snapshot repository.
IS_SNAPSHOT=true
RELEASE_SIGNING_ENABLED=false
# Required by the maven publishing plugin
SONATYPE_HOST=DEFAULT
LIBRARY_VERSION=1.9.0-beta01
LIBRARY_VERSION=1.9.0-beta03
# 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.
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=false
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true
# Optionally configure code coverage, as historically Jacoco has at times been buggy with respect to new Kotlin versions
IS_ANDROID_INSTRUMENTATION_TEST_COVERAGE_ENABLED=false
@ -63,8 +63,8 @@ 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
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
@ -72,7 +72,6 @@ FULLADLE_VERSION=0.17.4
GRADLE_VERSIONS_PLUGIN_VERSION=0.42.0
KTLINT_VERSION=0.46.1
KSP_VERSION=1.7.10-1.0.6
MAVEN_PUBLISH_GRADLE_PLUGIN=0.20.0
PROTOBUF_GRADLE_PLUGIN_VERSION=0.8.19
RUST_GRADLE_PLUGIN_VERSION=0.9.3
@ -90,10 +89,10 @@ 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.48.0
GRPC_VERSION=1.48.1
GSON_VERSION=2.9.0
GUAVA_VERSION=31.1-android
JACOCO_VERSION=0.8.8

View File

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

View File

@ -4,6 +4,7 @@ import com.google.protobuf.gradle.plugins
import com.google.protobuf.gradle.proto
import com.google.protobuf.gradle.protobuf
import com.google.protobuf.gradle.protoc
import java.util.Base64
plugins {
id("com.android.library")
@ -15,39 +16,114 @@ plugins {
id("org.jetbrains.dokka")
id("com.google.protobuf")
id("org.mozilla.rust-android-gradle.rust-android")
id("com.vanniktech.maven.publish.base")
id("wtf.emulator.gradle")
id("zcash-sdk.emulator-wtf-conventions")
id("maven-publish")
id("signing")
}
// Publishing information
val publicationVariant = "release"
val myVersion = project.property("LIBRARY_VERSION").toString()
val myArtifactId = "zcash-android-sdk"
val isSnapshot = project.property("IS_SNAPSHOT").toString().toBoolean()
val version = project.property("LIBRARY_VERSION").toString()
project.group = "cash.z.ecc.android"
project.version = if (isSnapshot) {
"$version-SNAPSHOT"
} else {
version
}
val myArtifactId = "zcash-android-sdk"
publishing {
publications {
publications.withType<MavenPublication>().all {
register<MavenPublication>("release") {
artifactId = myArtifactId
groupId = "cash.z.ecc.android"
version = if (isSnapshot) {
"$myVersion-SNAPSHOT"
} else {
myVersion
}
afterEvaluate {
from(components[publicationVariant])
}
pom {
name.set("Zcash Android Wallet SDK")
description.set("This lightweight SDK connects Android to Zcash, allowing third-party " +
"Android apps to send and receive shielded transactions easily, securely and privately.")
url.set("https://github.com/zcash/zcash-android-wallet-sdk/")
inceptionYear.set("2018")
scm {
url.set("https://github.com/zcash/zcash-android-wallet-sdk/")
connection.set("scm:git:git://github.com/zcash/zcash-android-wallet-sdk.git")
developerConnection.set("scm:git:ssh://git@github.com/zcash/zcash-android-wallet-sdk.git")
}
developers {
developer {
id.set("zcash")
name.set("Zcash")
url.set("https://github.com/zcash/")
}
}
licenses {
license {
name.set("The MIT License")
url.set("http://opensource.org/licenses/MIT")
distribution.set("repo")
}
}
}
}
}
repositories {
val mavenUrl = if (isSnapshot) {
project.property("ZCASH_MAVEN_PUBLISH_SNAPSHOT_URL").toString()
} else {
project.property("ZCASH_MAVEN_PUBLISH_RELEASE_URL").toString()
}
val mavenPublishUsername = project.property("ZCASH_MAVEN_PUBLISH_USERNAME").toString()
val mavenPublishPassword = project.property("ZCASH_MAVEN_PUBLISH_PASSWORD").toString()
mavenLocal {
name = "MavenLocal"
}
maven(mavenUrl) {
name = "MavenCentral"
credentials {
username = mavenPublishUsername
password = mavenPublishPassword
}
}
}
}
signing {
// Maven Central requires signing for non-snapshots
isRequired = !isSnapshot
val signingKey = run {
val base64EncodedKey = project.property("ZCASH_ASCII_GPG_KEY").toString()
if (base64EncodedKey.isNotEmpty()) {
val keyBytes = Base64.getDecoder().decode(base64EncodedKey)
String(keyBytes)
} else {
""
}
}
if (signingKey.isNotEmpty()) {
useInMemoryPgpKeys(signingKey, "")
}
sign(publishing.publications)
}
android {
useLibrary("android.test.runner")
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
argument("room.schemaLocation", "$projectDir/schemas")
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
consumerProguardFiles("proguard-consumer.txt")
@ -78,7 +154,7 @@ android {
kotlinOptions {
// Tricky: fix: By default, the kotlin_module name will not include the version (in classes.jar/META-INF). Instead it has a colon, which breaks compilation on Windows. This is one way to set it explicitly to the proper value. See https://github.com/zcash/zcash-android-wallet-sdk/issues/222 for more info.
freeCompilerArgs += listOf("-module-name", "$myArtifactId-${project.version}_release")
freeCompilerArgs += listOf("-module-name", "$myArtifactId-${myVersion}_release")
}
packagingOptions {
@ -102,53 +178,12 @@ android {
baseline = File("lint-baseline.xml")
}
// Handled by com.vanniktech.maven.publish.AndroidSingleVariantLibrary below
// publishing {
// singleVariant("release") {
// withSourcesJar()
// withJavadocJar()
// }
// }
}
mavenPublishing {
publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.DEFAULT)
signAllPublications()
pom {
name.set("Zcash Android Wallet SDK")
description.set("This lightweight SDK connects Android to Zcash, allowing third-party Android " +
"apps to send and receive shielded transactions easily, securely and privately.")
url.set("https://github.com/zcash/zcash-android-wallet-sdk/")
inceptionYear.set("2018")
scm {
url.set("https://github.com/zcash/zcash-android-wallet-sdk/")
connection.set("scm:git:git://github.com/zcash/zcash-android-wallet-sdk.git")
developerConnection.set("scm:git:ssh://git@github.com/zcash/zcash-android-wallet-sdk.git")
}
developers {
developer {
id.set("zcash")
name.set("Zcash")
url.set("https://github.com/zcash/")
}
}
licenses {
license {
name.set("The MIT License")
url.set("http://opensource.org/licenses/MIT")
distribution.set("repo")
}
publishing {
singleVariant(publicationVariant) {
withSourcesJar()
withJavadocJar()
}
}
configure(
com.vanniktech.maven.publish.AndroidSingleVariantLibrary(
"release",
sourcesJar = true,
publishJavadocJar = true
)
)
}
allOpen {
@ -264,6 +299,8 @@ dependencies {
androidTestImplementation(libs.androidx.test.junit)
androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.coroutines.okhttp)
androidTestImplementation(libs.kotlin.test)
androidTestImplementation(libs.kotlinx.coroutines.test)
// used by 'ru.gildor.corutines.okhttp.await' (to make simple suspended requests) and breaks on versions higher than 3.8.0
androidTestImplementation(libs.okhttp)

View File

@ -83,8 +83,8 @@ class AssetTest {
assertEquals(
"File: ${it.filename}",
CheckpointTool.checkpointHeightFromFilename(network, it.filename),
jsonObject.getInt("height")
CheckpointTool.checkpointHeightFromFilename(network, it.filename).value,
jsonObject.getLong("height")
)
// In the future, additional validation of the JSON can be added
@ -94,9 +94,9 @@ class AssetTest {
private data class JsonFile(val jsonObject: JSONObject, val filename: String)
companion object {
fun listAssets(network: ZcashNetwork) = runBlocking {
fun listAssets(network: ZcashNetwork): Array<String>? = runBlocking {
CheckpointTool.listCheckpointDirectoryContents(
ApplicationProvider.getApplicationContext<Context>(),
ApplicationProvider.getApplicationContext(),
CheckpointTool.checkpointDirectory(network)
)
}

View File

@ -0,0 +1,39 @@
package cash.z.ecc.android.sdk.db
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.internal.AndroidApiVersion
import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb
import cash.z.ecc.android.sdk.test.getAppContext
import cash.z.ecc.fixture.DatabaseNameFixture
import cash.z.ecc.fixture.DatabasePathFixture
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import java.io.File
class CommonDatabaseBuilderTest {
@Test
@SmallTest
fun proper_database_name_used_test() {
val dbDirectory = File(DatabasePathFixture.new())
val dbFileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_PENDING_TRANSACTIONS_NAME)
val dbFile = File(dbDirectory, dbFileName)
val db = commonDatabaseBuilder(
getAppContext(),
PendingTransactionDb::class.java,
dbFile
).build()
assertNotNull(db)
val expectedDbName = if (AndroidApiVersion.isAtLeastO_MR1) {
dbFileName
} else {
dbFile.absolutePath
}
assertEquals(expectedDbName, db.openHelper.databaseName)
}
}

View File

@ -0,0 +1,239 @@
package cash.z.ecc.android.sdk.db
import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.test.getAppContext
import cash.z.ecc.fixture.DatabaseNameFixture
import cash.z.ecc.fixture.DatabasePathFixture
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.io.File
class DatabaseCoordinatorTest {
private val dbCoordinator = DatabaseCoordinator.getInstance(getAppContext())
@Before
fun clear_test_files() {
val databaseDir = DatabasePathFixture.new(baseFolderPath = DatabasePathFixture.DATABASE_DIR_PATH)
val noBackupDir = DatabasePathFixture.new(baseFolderPath = DatabasePathFixture.NO_BACKUP_DIR_PATH)
File(databaseDir).deleteRecursively()
File(noBackupDir).deleteRecursively()
}
// Sanity check of the database coordinator instance and its thread-safe implementation. Our aim
// here is to run two jobs in parallel (the second one runs immediately after the first was started)
// to test mutex implementation and correct DatabaseCoordinator function call result.
@Test
@SmallTest
fun mutex_test() = runTest {
var testResult: File? = null
launch {
delay(1000)
testResult = dbCoordinator.cacheDbFile(
DatabaseNameFixture.TEST_DB_NETWORK,
DatabaseNameFixture.TEST_DB_ALIAS
)
}
val job2 = launch {
delay(1001)
testResult = dbCoordinator.cacheDbFile(
ZcashNetwork.Mainnet,
"TestZcashSdk"
)
}
advanceTimeBy(1002)
job2.join().also {
assertTrue(testResult != null)
assertTrue(testResult!!.absolutePath.isNotEmpty())
assertTrue(testResult!!.absolutePath.contains(ZcashNetwork.Mainnet.networkName))
assertTrue(testResult!!.absolutePath.contains("TestZcashSdk"))
}
}
@FlakyTest
@Test
@MediumTest
fun mutex_stress_test() {
// We run the mutex test multiple times sequentially to catch a possible problem.
for (x in 0..9) {
mutex_test()
}
}
@Test
@SmallTest
fun database_cache_file_creation_test() = runTest {
val directory = File(DatabasePathFixture.new())
val fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_CACHE_NAME)
val expectedFilePath = File(directory, fileName).path
dbCoordinator.cacheDbFile(
DatabaseNameFixture.TEST_DB_NETWORK,
DatabaseNameFixture.TEST_DB_ALIAS
).also { resultFile ->
assertEquals(expectedFilePath, resultFile.absolutePath)
}
}
@Test
@SmallTest
fun database_data_file_creation_test() = runTest {
val directory = File(DatabasePathFixture.new())
val fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_DATA_NAME)
val expectedFilePath = File(directory, fileName).path
dbCoordinator.dataDbFile(
DatabaseNameFixture.TEST_DB_NETWORK,
DatabaseNameFixture.TEST_DB_ALIAS
).also { resultFile ->
assertEquals(expectedFilePath, resultFile.absolutePath)
}
}
@Test
@SmallTest
fun database_transactions_file_creation_test() = runTest {
val directory = File(DatabasePathFixture.new())
val fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_PENDING_TRANSACTIONS_NAME)
val expectedFilePath = File(directory, fileName).path
dbCoordinator.pendingTransactionsDbFile(
DatabaseNameFixture.TEST_DB_NETWORK,
DatabaseNameFixture.TEST_DB_ALIAS
).also { resultFile ->
assertEquals(expectedFilePath, resultFile.absolutePath)
}
}
@Test
@SmallTest
fun database_files_move_test() = runTest {
val parentFile = File(
DatabasePathFixture.new(
baseFolderPath = DatabasePathFixture.DATABASE_DIR_PATH,
internalPath = ""
)
)
val originalDbFile = getEmptyFile(
parent = parentFile,
fileName = DatabaseNameFixture.newDb(
name = DatabaseCoordinator.DB_CACHE_NAME_LEGACY,
alias = DatabaseCoordinator.ALIAS_LEGACY
)
)
val originalDbJournalFile = getEmptyFile(
parent = parentFile,
fileName = DatabaseNameFixture.newDbJournal(
name = DatabaseCoordinator.DB_CACHE_NAME_LEGACY,
alias = DatabaseCoordinator.ALIAS_LEGACY
)
)
val originalDbWalFile = getEmptyFile(
parent = parentFile,
fileName = DatabaseNameFixture.newDbWal(
name = DatabaseCoordinator.DB_CACHE_NAME_LEGACY,
alias = DatabaseCoordinator.ALIAS_LEGACY
)
)
val expectedDbFile = File(
DatabasePathFixture.new(),
DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_CACHE_NAME)
)
val expectedDbJournalFile = File(
DatabasePathFixture.new(),
DatabaseNameFixture.newDbJournal(name = DatabaseCoordinator.DB_CACHE_NAME)
)
val expectedDbWalFile = File(
DatabasePathFixture.new(),
DatabaseNameFixture.newDbWal(name = DatabaseCoordinator.DB_CACHE_NAME)
)
assertTrue(originalDbFile.exists())
assertTrue(originalDbJournalFile.exists())
assertTrue(originalDbWalFile.exists())
assertFalse(expectedDbFile.exists())
assertFalse(expectedDbJournalFile.exists())
assertFalse(expectedDbWalFile.exists())
dbCoordinator.cacheDbFile(
DatabaseNameFixture.TEST_DB_NETWORK,
DatabaseNameFixture.TEST_DB_ALIAS
).also { resultFile ->
assertTrue(resultFile.exists())
assertEquals(expectedDbFile.absolutePath, resultFile.absolutePath)
assertTrue(expectedDbFile.exists())
assertTrue(expectedDbJournalFile.exists())
assertTrue(expectedDbWalFile.exists())
assertFalse(originalDbFile.exists())
assertFalse(originalDbJournalFile.exists())
assertFalse(originalDbWalFile.exists())
}
}
private fun getEmptyFile(parent: File, fileName: String): File {
return File(parent, fileName).apply {
assertTrue(parentFile != null)
parentFile!!.mkdirs()
assertTrue(parentFile!!.exists())
createNewFile()
assertTrue(exists())
}
}
@Test
@SmallTest
fun delete_database_files_test() = runTest {
val parentFile = File(
DatabasePathFixture.new(
baseFolderPath = DatabasePathFixture.NO_BACKUP_DIR_PATH,
internalPath = DatabasePathFixture.INTERNAL_DATABASE_PATH
)
)
val dbFile = getEmptyFile(
parent = parentFile,
fileName = DatabaseNameFixture.newDb(name = DatabaseCoordinator.DB_CACHE_NAME)
)
val dbJournalFile = getEmptyFile(
parent = parentFile,
fileName = DatabaseNameFixture.newDbJournal(name = DatabaseCoordinator.DB_CACHE_NAME)
)
val dbWalFile = getEmptyFile(
parent = parentFile,
fileName = DatabaseNameFixture.newDbWal(name = DatabaseCoordinator.DB_CACHE_NAME)
)
assertTrue(dbFile.exists())
assertTrue(dbJournalFile.exists())
assertTrue(dbWalFile.exists())
dbCoordinator.deleteDatabases(DatabaseNameFixture.TEST_DB_NETWORK, DatabaseNameFixture.TEST_DB_ALIAS).also {
assertFalse(dbFile.exists())
assertFalse(dbJournalFile.exists())
assertFalse(dbWalFile.exists())
}
}
}

View File

@ -0,0 +1,40 @@
package cash.z.ecc.android.sdk.db
import android.os.Build
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.internal.NoBackupContextWrapper
import cash.z.ecc.android.sdk.test.getAppContext
import cash.z.ecc.fixture.DatabaseNameFixture
import cash.z.ecc.fixture.DatabasePathFixture
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O_MR1)
class NoBackupContextWrapperTest {
private val databaseParentDir = File(DatabasePathFixture.new())
private val noBackupContextWrapper = NoBackupContextWrapper(getAppContext(), databaseParentDir)
@Test
@SmallTest
fun get_context_test() {
assertTrue(noBackupContextWrapper.applicationContext is NoBackupContextWrapper)
assertTrue(noBackupContextWrapper.baseContext is NoBackupContextWrapper)
}
@Test
@SmallTest
fun get_database_path_test() {
val testDbPath = File(DatabasePathFixture.new(), DatabaseNameFixture.newDb()).absolutePath
val testDbFile = noBackupContextWrapper.getDatabasePath(testDbPath)
testDbFile.absolutePath.also {
assertTrue(it.isNotEmpty())
assertTrue(it.contains(DatabaseNameFixture.TEST_DB_NAME))
assertTrue(it.contains(DatabasePathFixture.NO_BACKUP_DIR_PATH))
assertTrue(it.contains(DatabasePathFixture.INTERNAL_DATABASE_PATH))
}
}
}

View File

@ -8,6 +8,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import ru.gildor.coroutines.okhttp.await
import kotlin.test.assertNotNull
fun Initializer.Config.seedPhrase(seedPhrase: String, network: ZcashNetwork) {
runBlocking { setSeed(SimpleMnemonics().toSeed(seedPhrase.toCharArray()), network) }
@ -21,6 +22,7 @@ object BlockExplorer {
.build()
val result = client.newCall(request).await()
val body = result.body?.string()
assertNotNull(body, "Body can not be null.")
return JSONObject(body).getJSONArray("data").getJSONObject(0).getLong("id")
}
}

View File

@ -2,16 +2,20 @@ 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.db.DatabaseCoordinator
import cash.z.ecc.android.sdk.ext.BlockExplorer
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 cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import kotlin.test.DefaultAsserter.assertEquals
import kotlin.test.DefaultAsserter.assertTrue
// TODO [#650]: https://github.com/zcash/zcash-android-wallet-sdk/issues/650
/**
* This test is intended to run to make sure that basic things are functional and pinpoint what is
@ -22,7 +26,7 @@ import org.junit.runners.Parameterized
class SanityTest(
private val wallet: TestWallet,
private val encoding: String,
private val birthday: Int
private val birthday: BlockHeight
) {
@ -31,20 +35,23 @@ class SanityTest(
@Test
fun testFilePaths() {
assertEquals(
assertTrue(
"$name has invalid DataDB file",
"/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_${networkName}_Data.db",
wallet.initializer.rustBackend.pathDataDb
wallet.initializer.rustBackend.dataDbFile.absolutePath.endsWith(
"no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_DATA_NAME}"
)
)
assertEquals(
assertTrue(
"$name has invalid CacheDB file",
"/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_${networkName}_Cache.db",
wallet.initializer.rustBackend.pathCacheDb
wallet.initializer.rustBackend.cacheDbFile.absolutePath.endsWith(
"no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_CACHE_NAME}"
)
)
assertEquals(
assertTrue(
"$name has invalid CacheDB params dir",
"/data/user/0/cash.z.ecc.android.sdk.test/cache/params",
wallet.initializer.rustBackend.pathParamsDir
wallet.initializer.rustBackend.pathParamsDir.endsWith(
"cache/params"
)
)
}
@ -70,8 +77,15 @@ class SanityTest(
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()
// Fetch height directly because the synchronizer hasn't started, yet. Then we test the
// result, only if there is no server communication problem.
val downloaderHeight = runCatching {
return@runCatching wallet.service.getLatestBlockHeight()
}.onFailure {
twig(it)
}.getOrElse { return@runBlocking }
assertTrue(
"${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
@ -81,9 +95,18 @@ class SanityTest(
@Test
fun testSingleBlockDownload() = runBlocking {
// fetch block directly because the synchronizer hasn't started, yet
// Fetch height directly because the synchronizer hasn't started, yet. Then we test the
// result, only if there is no server communication problem.
val height = BlockHeight.new(wallet.network, 1_000_000)
val block = wallet.service.getBlockRange(height..height).first()
val block = runCatching {
return@runCatching wallet.service.getBlockRange(height..height).first()
}.onFailure {
twig(it)
}.getOrElse { return@runBlocking }
runCatching {
wallet.service.getLatestBlockHeight()
}.getOrNull() ?: return@runBlocking
assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height == height.value)
}
@ -95,13 +118,13 @@ class SanityTest(
arrayOf(
TestWallet(TestWallet.Backups.SAMPLE_WALLET),
"zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063",
1320000
BlockHeight.new(ZcashNetwork.Testnet, 1330000)
),
// Mainnet wallet
arrayOf(
TestWallet(TestWallet.Backups.SAMPLE_WALLET, ZcashNetwork.Mainnet),
"zxviews1q0hxkupsqqqqpqzsffgrk2smjuccedua7zswf5e3rgtv3ga9nhvhjug670egshd6me53r5n083s2m9mf4va4z7t39ltd3wr7hawnjcw09eu85q0ammsg0tsgx24p4ma0uvr4p8ltx5laum2slh2whc23ctwlnxme9w4dw92kalwk5u4wyem8dynknvvqvs68ktvm8qh7nx9zg22xfc77acv8hk3qqll9k3x4v2fa26puu2939ea7hy4hh60ywma69xtqhcy4037ne8g2sg8sq",
1000000
BlockHeight.new(ZcashNetwork.Mainnet, 1000000)
)
)
}

View File

@ -4,9 +4,11 @@ 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.db.DatabaseCoordinator
import cash.z.ecc.android.sdk.util.TestWallet
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Ignore
import org.junit.Test
@ -20,19 +22,36 @@ class SmokeTest {
@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)
Assert.assertEquals("Invalid CacheDB file", "/data/user/0/cash.z.ecc.android.sdk.test/databases/TestWallet_testnet_Cache.db", wallet.initializer.rustBackend.pathCacheDb)
Assert.assertEquals("Invalid CacheDB params dir", "/data/user/0/cash.z.ecc.android.sdk.test/cache/params", wallet.initializer.rustBackend.pathParamsDir)
assertTrue(
"Invalid DataDB file",
wallet.initializer.rustBackend.dataDbFile.absolutePath.endsWith(
"no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_DATA_NAME}"
)
)
assertTrue(
"Invalid CacheDB file",
wallet.initializer.rustBackend.cacheDbFile.absolutePath.endsWith(
"no_backup/co.electricoin.zcash/TestWallet_testnet_${DatabaseCoordinator.DB_CACHE_NAME}"
)
)
assertTrue(
"Invalid CacheDB params dir",
wallet.initializer.rustBackend.pathParamsDir.endsWith("cache/params")
)
}
@Test
fun testBirthday() {
Assert.assertEquals("Invalid birthday height", 1_320_000, wallet.initializer.checkpoint.height)
assertEquals(
"Invalid birthday height",
1_330_000,
wallet.initializer.checkpoint.height.value
)
}
@Test
fun testViewingKeys() {
Assert.assertEquals("Invalid encoding", "zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063", wallet.initializer.viewingKeys[0].encoding)
assertEquals("Invalid encoding", "zxviewtestsapling1qv0ue89kqqqqpqqyt4cl5wvssx4wqq30e5m948p07dnwl9x3u75vvnzvjwwpjkrf8yk2gva0kkxk9p8suj4xawlzw9pajuxgap83wykvsuyzfrm33a2p2m4jz2205kgzx0l2lj2kyegtnuph6crkyvyjqmfxut84nu00wxgrstu5fy3eu49nzl8jzr4chmql4ysgg2t8htn9dtvxy8c7wx9rvcerqsjqm6lqln9syk3g8rr3xpy3l4nj0kawenzpcdtnv9qmy98vdhqzaf063", wallet.initializer.viewingKeys[0].encoding)
}
// This test takes an extremely long time

View File

@ -25,12 +25,12 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import java.util.concurrent.CountDownLatch
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class TestnetIntegrationTest : ScopedTest() {
@ -50,7 +50,7 @@ class TestnetIntegrationTest : ScopedTest() {
@Test
fun testLoadBirthday() {
val (height, hash, time, tree) = runBlocking {
val (height) = runBlocking {
CheckpointTool.loadNearest(
context,
synchronizer.network,
@ -81,8 +81,8 @@ class TestnetIntegrationTest : ScopedTest() {
}
assertTrue(
"No funds available when we expected a balance greater than zero!",
availableBalance!!.value > 0
availableBalance!!.value > 0,
"No funds available when we expected a balance greater than zero!"
)
}
@ -105,7 +105,7 @@ class TestnetIntegrationTest : ScopedTest() {
ZcashSdk.MINERS_FEE,
toAddress,
"first mainnet tx from the SDK"
).filter { it?.isSubmitSuccess() == true }.onFirst {
).filter { it.isSubmitSuccess() }.onFirst {
log("DONE SENDING!!!")
}
log("returning true from sendFunds")

View File

@ -1,6 +1,8 @@
package cash.z.ecc.android.sdk.integration.service
import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
@ -32,6 +34,7 @@ import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.Spy
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(AndroidJUnit4::class)
@SmallTest
@ -69,14 +72,28 @@ class ChangeServiceTest : ScopedTest() {
@Test
fun testSanityCheck() {
val result = service.getLatestBlockHeight()
// Test the result, only if there is no server communication problem.
val result = runCatching {
return@runCatching service.getLatestBlockHeight()
}.onFailure {
twig(it)
}.getOrElse { return }
assertTrue(result > network.saplingActivationHeight)
}
@Test
fun testCleanSwitch() = runBlocking {
downloader.changeService(otherService)
val result = downloader.downloadBlockRange(BlockHeight.new(network, 900_000)..BlockHeight.new(network, 901_000))
// Test the result, only if there is no server communication problem.
val result = runCatching {
downloader.changeService(otherService)
return@runCatching downloader.downloadBlockRange(
BlockHeight.new(network, 900_000)..BlockHeight.new(network, 901_000)
)
}.onFailure {
twig(it)
}.getOrElse { return@runBlocking }
assertEquals(1_001, result)
}
@ -111,9 +128,17 @@ class ChangeServiceTest : ScopedTest() {
@Test
fun testSwitchToInvalidServer() = runBlocking {
var caughtException: Throwable? = null
downloader.changeService(LightWalletGrpcService.new(context, LightWalletEndpoint("invalid.lightwalletd", 9087, true))) {
caughtException = it
}
// the test can continue only if there is no server communication problem
if (caughtException is StatusException) {
twig("Server communication problem while testing.")
return@runBlocking
}
assertNotNull("Using an invalid host should generate an exception.", caughtException)
assertTrue(
"Exception was of the wrong type.",
@ -124,9 +149,17 @@ class ChangeServiceTest : ScopedTest() {
@Test
fun testSwitchToTestnetFails() = runBlocking {
var caughtException: Throwable? = null
downloader.changeService(LightWalletGrpcService.new(context, LightWalletEndpoint.Testnet)) {
caughtException = it
}
// the test can continue only if there is no server communication problem
if (caughtException is StatusException) {
twig("Server communication problem while testing.")
return@runBlocking
}
assertNotNull("Using an invalid host should generate an exception.", caughtException)
assertTrue(
"Exception was of the wrong type. Expected ${ChainInfoNotMatching::class.simpleName} but was ${caughtException!!::class.simpleName}",

View File

@ -39,10 +39,10 @@ class SaplingParamToolTest {
}
@Test
fun testOnlySpendFileExits() = runBlocking {
fun output_file_exists() = runBlocking {
// Given
SaplingParamTool.fetchParams(cacheDir)
File("$cacheDir/${ZcashSdk.OUTPUT_PARAM_FILE_NAME}").delete()
File(cacheDir, ZcashSdk.OUTPUT_PARAM_FILE_NAME).delete()
// When
val result = SaplingParamTool.validate(cacheDir)
@ -52,10 +52,10 @@ class SaplingParamToolTest {
}
@Test
fun testOnlyOutputOFileExits() = runBlocking {
fun param_file_exists() = runBlocking {
// Given
SaplingParamTool.fetchParams(cacheDir)
File("$cacheDir/${ZcashSdk.SPEND_PARAM_FILE_NAME}").delete()
File(cacheDir, ZcashSdk.SPEND_PARAM_FILE_NAME).delete()
// When
val result = SaplingParamTool.validate(cacheDir)

View File

@ -13,6 +13,8 @@ import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.test.ScopedTest
import cash.z.ecc.fixture.DatabaseNameFixture
import cash.z.ecc.fixture.DatabasePathFixture
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.stub
import kotlinx.coroutines.cancel
@ -22,16 +24,17 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import java.io.File
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@MaintainedTest(TestPurpose.REGRESSION)
@RunWith(AndroidJUnit4::class)
@ -42,27 +45,35 @@ class PersistentTransactionManagerTest : ScopedTest() {
@Mock lateinit var mockService: LightWalletService
val pendingDbName = "PersistentTxMgrTest_Pending.db"
val dataDbName = "PersistentTxMgrTest_Data.db"
private val pendingDbFile = File(
DatabasePathFixture.new(),
DatabaseNameFixture.newDb(name = "PersistentTxMgrTest_Pending.db")
).apply {
assertTrue(parentFile != null)
parentFile!!.mkdirs()
assertTrue(parentFile!!.exists())
createNewFile()
assertTrue(exists())
}
private lateinit var manager: OutboundTransactionManager
@Before
fun setup() {
initMocks()
deleteDb()
manager = PersistentTransactionManager(context, mockEncoder, mockService, pendingDbName)
manager = PersistentTransactionManager(context, mockEncoder, mockService, pendingDbFile)
}
private fun deleteDb() {
context.getDatabasePath(pendingDbName).delete()
pendingDbFile.deleteRecursively()
}
private fun initMocks() {
MockitoAnnotations.initMocks(this)
MockitoAnnotations.openMocks(this)
mockEncoder.stub {
onBlocking {
createTransaction(any(), any(), any(), any(), any())
}.thenAnswer { invocation ->
}.thenAnswer {
runBlocking {
delay(200)
EncodedTransaction(byteArrayOf(1, 2, 3), byteArrayOf(8, 9), 5_000_000)
@ -89,7 +100,7 @@ class PersistentTransactionManagerTest : ScopedTest() {
txFlow.drop(2).onEach {
twig("found tx: $it")
assertTrue("Expected the encoded tx to be cancelled but it wasn't", it.isCancelled())
assertTrue(it.isCancelled(), "Expected the encoded tx to be cancelled but it wasn't")
twig("found it to be successfully cancelled")
testScope.cancel()
}.launchIn(testScope).join()
@ -101,16 +112,16 @@ class PersistentTransactionManagerTest : ScopedTest() {
assertFalse(tx.isCancelled())
manager.cancel(tx.id)
tx = manager.findById(tx.id)!!
assertTrue("Transaction was not cancelled", tx.isCancelled())
assertTrue(tx.isCancelled(), "Transaction was not cancelled")
}
@Test
fun testAbort() = runBlocking {
var tx: PendingTransaction? = manager.initSpend(Zatoshi(1234), "a", "b", 0)
assertNotNull(tx)
manager.abort(tx!!)
manager.abort(tx)
tx = manager.findById(tx.id)
assertNull("Transaction was not removed from the DB", tx)
assertNull(tx, "Transaction was not removed from the DB")
}
companion object {

View File

@ -49,16 +49,16 @@ class BranchIdTest internal constructor(
val mainnetBackend = runBlocking { RustBackend.init("", "", "", ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight) }
return listOf(
// Mainnet Cases
arrayOf("Sapling", 419_200, 1991772603L, "76b809bb", mainnetBackend),
arrayOf("Blossom", 653_600, 733220448L, "2bb40e60", mainnetBackend),
arrayOf("Heartwood", 903_000, 4122551051L, "f5b9230b", mainnetBackend),
arrayOf("Canopy", 1_046_400, 3925833126L, "e9ff75a6", mainnetBackend),
arrayOf("Sapling", BlockHeight.new(ZcashNetwork.Mainnet, 419_200), 1991772603L, "76b809bb", mainnetBackend),
arrayOf("Blossom", BlockHeight.new(ZcashNetwork.Mainnet, 653_600), 733220448L, "2bb40e60", mainnetBackend),
arrayOf("Heartwood", BlockHeight.new(ZcashNetwork.Mainnet, 903_000), 4122551051L, "f5b9230b", mainnetBackend),
arrayOf("Canopy", BlockHeight.new(ZcashNetwork.Mainnet, 1_046_400), 3925833126L, "e9ff75a6", mainnetBackend),
// Testnet Cases
arrayOf("Sapling", 280_000, 1991772603L, "76b809bb", testnetBackend),
arrayOf("Blossom", 584_000, 733220448L, "2bb40e60", testnetBackend),
arrayOf("Heartwood", 903_800, 4122551051L, "f5b9230b", testnetBackend),
arrayOf("Canopy", 1_028_500, 3925833126L, "e9ff75a6", testnetBackend)
arrayOf("Sapling", BlockHeight.new(ZcashNetwork.Testnet, 280_000), 1991772603L, "76b809bb", testnetBackend),
arrayOf("Blossom", BlockHeight.new(ZcashNetwork.Testnet, 584_000), 733220448L, "2bb40e60", testnetBackend),
arrayOf("Heartwood", BlockHeight.new(ZcashNetwork.Testnet, 903_800), 4122551051L, "f5b9230b", testnetBackend),
arrayOf("Canopy", BlockHeight.new(ZcashNetwork.Testnet, 1_028_500), 3925833126L, "e9ff75a6", testnetBackend)
)
}
}

View File

@ -0,0 +1,11 @@
package cash.z.ecc.android.sdk.test
import android.content.Context
import androidx.annotation.StringRes
import androidx.test.core.app.ApplicationProvider
fun getAppContext(): Context = ApplicationProvider.getApplicationContext()
fun getStringResource(@StringRes resId: Int) = getAppContext().getString(resId)
fun getStringResourceWithArgs(@StringRes resId: Int, vararg formatArgs: String) = getAppContext().getString(resId, *formatArgs)

View File

@ -6,6 +6,7 @@ import cash.z.ecc.android.sdk.internal.TroubleshootingTwig
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.twig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
@ -19,6 +20,7 @@ import org.junit.Before
import org.junit.BeforeClass
import java.util.concurrent.TimeoutException
@OptIn(DelicateCoroutinesApi::class)
open class ScopedTest(val defaultTimeout: Long = 2000L) {
protected lateinit var testScope: CoroutineScope
@ -60,7 +62,7 @@ open class ScopedTest(val defaultTimeout: Long = 2000L) {
fun createScope() {
twig("======================= CLASS STARTED ===============================")
classScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(2, this.javaClass.simpleName)
SupervisorJob() + newFixedThreadPoolContext(2, this::class.java.simpleName)
)
}

View File

@ -4,6 +4,7 @@ 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.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.CheckpointTool.IS_FALLBACK_ON_FAILURE
import kotlinx.coroutines.runBlocking
@ -16,7 +17,10 @@ class CheckpointToolTest {
@Test
@SmallTest
fun birthday_height_from_filename() {
assertEquals(123, CheckpointTool.checkpointHeightFromFilename(ZcashNetwork.Mainnet, "123.json"))
assertEquals(
BlockHeight.new(ZcashNetwork.Mainnet, 1_230_000),
CheckpointTool.checkpointHeightFromFilename(ZcashNetwork.Mainnet, "1230000.json")
)
}
@Test

View File

@ -12,9 +12,9 @@ class SimpleMnemonics : MnemonicPlugin {
override fun fullWordList(languageCode: String) = Mnemonics.getCachedWords(Locale.ENGLISH.language)
override fun nextEntropy(): ByteArray = WordCount.COUNT_24.toEntropy()
override fun nextMnemonic(): CharArray = MnemonicCode(WordCount.COUNT_24).chars
override fun nextMnemonic(entropy: ByteArray): CharArray = MnemonicCode(entropy).chars
override fun nextMnemonic(seed: ByteArray): CharArray = MnemonicCode(seed).chars
override fun nextMnemonicList(): List<CharArray> = MnemonicCode(WordCount.COUNT_24).words
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> = MnemonicCode(entropy).words
override fun nextMnemonicList(seed: ByteArray): List<CharArray> = MnemonicCode(seed).words
override fun toSeed(mnemonic: CharArray): ByteArray = MnemonicCode(mnemonic).toSeed()
override fun toWordList(mnemonic: CharArray): List<CharArray> = MnemonicCode(mnemonic).words
}

View File

@ -18,6 +18,7 @@ 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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
@ -34,6 +35,7 @@ import java.util.concurrent.TimeoutException
* A simple wallet that connects to testnet for integration testing. The intention is that it is
* easy to drive and nice to use.
*/
@OptIn(DelicateCoroutinesApi::class)
class TestWallet(
val seedPhrase: String,
val alias: String = "TestWallet",

View File

@ -0,0 +1,31 @@
package cash.z.ecc.fixture
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
import cash.z.ecc.android.sdk.model.ZcashNetwork
object DatabaseNameFixture {
const val TEST_DB_NAME = "empty.db"
const val TEST_DB_JOURNAL_NAME_SUFFIX = DatabaseCoordinator.DATABASE_FILE_JOURNAL_SUFFIX
const val TEST_DB_WAL_NAME_SUFFIX = DatabaseCoordinator.DATABASE_FILE_WAL_SUFFIX
const val TEST_DB_ALIAS = "zcash_sdk"
val TEST_DB_NETWORK = ZcashNetwork.Testnet
internal fun newDb(
name: String = TEST_DB_NAME,
alias: String = TEST_DB_ALIAS,
network: String = TEST_DB_NETWORK.networkName
) = "${alias}_${network}_$name"
internal fun newDbJournal(
name: String = TEST_DB_NAME,
alias: String = TEST_DB_ALIAS,
network: String = TEST_DB_NETWORK.networkName
) = "${alias}_${network}_$name-$TEST_DB_JOURNAL_NAME_SUFFIX"
internal fun newDbWal(
name: String = TEST_DB_NAME,
alias: String = TEST_DB_ALIAS,
network: String = TEST_DB_NETWORK.networkName
) = "${alias}_${network}_$name-$TEST_DB_WAL_NAME_SUFFIX"
}

View File

@ -0,0 +1,29 @@
package cash.z.ecc.fixture
import cash.z.ecc.android.sdk.internal.Files
import cash.z.ecc.android.sdk.internal.ext.getDatabasePathSuspend
import cash.z.ecc.android.sdk.internal.ext.getNoBackupFilesDirCompat
import cash.z.ecc.android.sdk.test.getAppContext
import kotlinx.coroutines.runBlocking
import java.io.File
object DatabasePathFixture {
val NO_BACKUP_DIR_PATH: String = runBlocking {
getAppContext().getNoBackupFilesDirCompat().absolutePath
}
val DATABASE_DIR_PATH: String = runBlocking {
getAppContext().getDatabasePathSuspend("temporary.db").parentFile.let { parentFile ->
assert(parentFile != null) { "Failed to create database folder." }
parentFile!!.mkdirs()
assert(parentFile.exists()) { "Failed to check database folder." }
parentFile.absolutePath
}
}
const val INTERNAL_DATABASE_PATH = Files.NO_BACKUP_SUBDIRECTORY
internal fun new(
baseFolderPath: String = NO_BACKUP_DIR_PATH,
internalPath: String = INTERNAL_DATABASE_PATH
) = File(baseFolderPath, internalPath).absolutePath
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1752500",
"hash": "00000000002721ca5e87699661dc2c259ae14e5ccea0252dbfbd3ecd4d972fa7",
"time": 1658950269,
"saplingTree": "01a3ef62b2119b06263f4ddf3bb35429b91b0c2d5432c2e82381d56ea5e4c8d227001801f1165a0d96afd18c88b0a3198c9b12b3b388e2d0cc3915e7743a7c783510552f00000000000001996cb567e9a871f88f38fbdf9e027da6d9e45c2830a3014b960d684cdc592553000000000001f1275a28ffa603047a39513672a52253bbde6e453fba60b742f790959614bc560162ea4359404b3b43fb50798b3758be7eda6a32fcdbbed2def4c269cbbc362010010629c8231ea9ed1f21dff7b13fddd79400605d1a71dad22765a11dab4514950200019687efd54c9ee6d74165788e1120e8095ff48f7b5cc0a9e67954cf83b04de07001c97f6223a9bc66019e50af01f00e4768212c1ae1c42dc3d82944e2348de15137012acafd61cb88a77c098a05f8a6eec00930b6a28e1b4748c0fde540cd39edfa47000000017e16ac72afb29d36b6ba9cead439f560ec9c25e60a178552a69a4748e7edf51e",
"orchardTree": "01e4954081f6ae3df79b22ec63ae6b6a8af9b9d8dbbd1c4ac894187e54ff18e820001f01520234b92baf9ba6ceb5636e8e694deaf34fcdc2b9a6c61c67889ea8ffd58605000000000000000001f8be16a220acf8ba291b9b9bf14c1bbb3541c08d5a8a89f6205bf890a18ac93401130cfb41380fdd7836985e2c4c488fdc3d1d1bd4390f350f0e1b8a448f47ac1c012bcbdd308beca04006b18928c4418aad2b3650677289b1b45ea5a21095c5310301100ed4d0a8a440b03f1254ce1efb2d82a94cf001cffa0e7fd6ada813a2688b240130a69de998b87aebcd4c556503a45e559a422ecfbdf2f0e6318a8427e41a7b09017676cfe97afff13797f82f8d631bd29edde424854139c64ab8241e2a2331551401da371f0d3294843fd8f645019a04c07607342c70cf4cc793068355eaccdd671601bc79f0119f97113775379bf58f3f5d9d122766909b797947127b28248ff0720501c1eb3aa1717c2a696ce0aba6c5b5bda44c4eda5cf69ae376cc8b334a2c23fb2b0001374feb2041bfd423c6cc3e064ee2b4705748a082836d39dd723515357fb06e300000000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1755000",
"hash": "00000000000aa431fa339ff48bd7ac55f31346f14407362884d5a04674e50e74",
"time": 1659139285,
"saplingTree": "01487aeb1b6ed4701c648e9ef247028ba2e94cd8d236c9bec59e5fb7022ef2b3660185ffc2003a288dde5dbfeb6fcedd3715386cb0b2ce71a10a546fdd25a3d4055d18000141a3a594e42cedd19855d8dbedd7ded0928962a40af5a504592a2bcbb648c100016871d23a7420a11788d2c0608f6d987b73771ff1d85bb4866343b5875f079b6600015979ac055368ed68e7f26aaf7c688d24af4f96274e2f455855f13c1678c2a93001774ce40898fe539ac97e91980ebe1a5085be10239eda6d0104738c819fad864801f1f47fa4eae37e6e1fd478673c43c8adde64dc9cf2281ad8a9c1624805b9385901a0951dffa15b626e4899f376c899245882f88c4ffede8cd7a242de51edfffa6600016bc4ff27516858b8cf13b7675fd90d56bde42f8571ed52e0731c7037dcea013b0001a5f05d22e67265d52a04314b17e58f7fc50933aea0ed5e430a020353a5ebe12401b311bd6baa175de69b09ad3ba0b03ef75c9eb74461c4f4f52120df856e15051f014ee0e948878a90bcb7278e4037bf44ef0325fea828c937ffa613ab946433301b0159a7b450b3e2fa85c1de2385f6e3b77298cda110ad590836972b26fc6ef2ed5a01aff9dd23237bcabbedfdc9a4241ee3c471e0b4d8a860658d5aa35e8a0594a9210001352446c36ff71e79bbed92275d7572cf1445e90155e7dbee94d37aab1858cf280134867611fb40f23a94fa1517e620cb3c4cab5b6d88fff002009e4e9c86dc5c3d01f5b866a0e1a8aa712824cef26261a7ee5a3f98747d1da413f86c681e319c423a01570f01e74ac0b816bdfb55ddec96081fe3da2557abf406ed30ccf93a20ae5f680000017e16ac72afb29d36b6ba9cead439f560ec9c25e60a178552a69a4748e7edf51e",
"orchardTree": "01f7f982f1e5bb2607adb498641aad65b0b886285c9d0354216fa9f0f0d97f442c001f01dc1c327185e61b049ff03531f9c8cb3818f7924a15b8cf325967801f0117442500000134e73998cd29120070cef7d8e8235a075528725ad9210a4e3d0565b40789561d000000000001f8be16a220acf8ba291b9b9bf14c1bbb3541c08d5a8a89f6205bf890a18ac93401130cfb41380fdd7836985e2c4c488fdc3d1d1bd4390f350f0e1b8a448f47ac1c012bcbdd308beca04006b18928c4418aad2b3650677289b1b45ea5a21095c5310301100ed4d0a8a440b03f1254ce1efb2d82a94cf001cffa0e7fd6ada813a2688b240130a69de998b87aebcd4c556503a45e559a422ecfbdf2f0e6318a8427e41a7b09017676cfe97afff13797f82f8d631bd29edde424854139c64ab8241e2a2331551401da371f0d3294843fd8f645019a04c07607342c70cf4cc793068355eaccdd671601bc79f0119f97113775379bf58f3f5d9d122766909b797947127b28248ff0720501c1eb3aa1717c2a696ce0aba6c5b5bda44c4eda5cf69ae376cc8b334a2c23fb2b0001374feb2041bfd423c6cc3e064ee2b4705748a082836d39dd723515357fb06e300000000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1757500",
"hash": "0000000000d151ac883834ea09e1817a48b6efa7f1950dd1fb1a381ab0e739a0",
"time": 1659327728,
"saplingTree": "016fa42ca011082d89168ca57bfb93c2caea59b8004a525fe1f09654c207f83d1d001800000147ed8dad3a86c353e902744e326f89701ee5c8282d97bb4f45b92a1c2a4c1c0e000001f6322d8bcb120e73f0138c460dd361b3949163645769df3ec842600e7fc5b60200000001ff715e9ec1bb54c821000b7ed62b9271375ca9d8ca11b16bc6662de4e9fe865400000001a91d7f61ad8158e6da534285fe0ff445ed2779adf2d12be5b256a14170f7a70101e25cf57ced229ea089d5732d8356bb8ca19d14a4ada376d543fa977f5a7513190001954941e529339317ce9598695b6acf0a01d3d35be8f578363c4de5c8ee68c95300016562c6b48c77ce7f673740755ebabf8c607b30cb926f9c314e616ba79cd7675901ce54921528804b9297bef103d267fdc4038c64380fcb740287e56f71731178440001a330b2cb242c3abf253345e76375cfc2c71c61e474cb97c547336577050cbf5c00017e16ac72afb29d36b6ba9cead439f560ec9c25e60a178552a69a4748e7edf51e",
"orchardTree": "013e8aeee6cce0c450b9a21f54977033f617f5d14c7b66023f8e1f0bbf2877212f001f0001a7a78f2cf30e0c514b85b53ce2a799f28f263a36f4ee8b7e48fc0fb5b987183a0162a250fa9f3b9f0d743805986b31ebd7d762c38a72433fef358a057f49b67e370001d589f1512354126e95efad28fa7da3b7f62dfe1967910d80114c6410bbe795190000000001f8be16a220acf8ba291b9b9bf14c1bbb3541c08d5a8a89f6205bf890a18ac93401130cfb41380fdd7836985e2c4c488fdc3d1d1bd4390f350f0e1b8a448f47ac1c012bcbdd308beca04006b18928c4418aad2b3650677289b1b45ea5a21095c5310301100ed4d0a8a440b03f1254ce1efb2d82a94cf001cffa0e7fd6ada813a2688b240130a69de998b87aebcd4c556503a45e559a422ecfbdf2f0e6318a8427e41a7b09017676cfe97afff13797f82f8d631bd29edde424854139c64ab8241e2a2331551401da371f0d3294843fd8f645019a04c07607342c70cf4cc793068355eaccdd671601bc79f0119f97113775379bf58f3f5d9d122766909b797947127b28248ff0720501c1eb3aa1717c2a696ce0aba6c5b5bda44c4eda5cf69ae376cc8b334a2c23fb2b0001374feb2041bfd423c6cc3e064ee2b4705748a082836d39dd723515357fb06e300000000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1760000",
"hash": "0000000001283529e3acf4f17948e87044a1dee9f5324a2be4bfdb1f9965adff",
"time": 1659516733,
"saplingTree": "01f51fb3642308c649edea46c6e0ed7084c3626f6ff7bf7fde8afeffe1528d0c3e0018000001827d8ce3486ac3843ea6cfd4e836176eadb7ef3e9bb10958f2a4c978dfb959560001663b4da07a3508de750b1eb1d2c1e41ee27117ea4229064fdc9320a4d1bfd63501dd8c2c119c75c7f0897c2805ccb54f33d9b46967e7d67f2ee62ad4134b25512f000001cd26ccdbef4a6edcb9ebf6af3b1f8fba4de56dfc472fc8ba4b4fa1f56aa6ef21011ac156b267e3700c8d6ecf50f2015ffa6b0deca83ef568e150818b2901b54a310001c85edfba92c0f619db0a88d2879275c3dc99890e289a0487fde46329cb40a67201bb05c3fac5068ecf7c879d05e0e2870fef6db2429925fd187e2fd38c7bccc15f0000019abbee9b4817684fc2c5272627af712e1f2fe971bfd4bc143a26b22b623d996b010f1004c6b806e1764c6b08b0b8433a7f339e7217611e9ce5421d17e21f1d350701b3e97c3870c26ce324e2290569cc520b46fbf78e8e44994f55fd4f7d0697804d0001703f0e4cb0cd39846b1f0fb99e58ef8079bf6199626f9585eff0d01fb4d18155011a455e02dd258c6a97a9b81756269fce9ea96b661896561192f4537cc3d10a4901a330b2cb242c3abf253345e76375cfc2c71c61e474cb97c547336577050cbf5c00017e16ac72afb29d36b6ba9cead439f560ec9c25e60a178552a69a4748e7edf51e",
"orchardTree": "01e256b39f1f747be0c9a8d5961583805db49080e05e379565db98e9b9648f890001c96aebc9cded8c7bd0f66f0f5c92c1aa077f871d4efb45c8662913afd514c02f1f01f3261c161e1594f19cc5c8dda4a3374ae4a66a25392a3dc0318c64aff72a7b32016a94ff2145b4a9250d7eac080ba7423d272e3bcbdef26cb96df78169e7c56c340000000001b1d090970cbc6cd3cc3dacd48b84185d0f97b2db04b7d270074935d1e30ba628000001f8be16a220acf8ba291b9b9bf14c1bbb3541c08d5a8a89f6205bf890a18ac93401130cfb41380fdd7836985e2c4c488fdc3d1d1bd4390f350f0e1b8a448f47ac1c012bcbdd308beca04006b18928c4418aad2b3650677289b1b45ea5a21095c5310301100ed4d0a8a440b03f1254ce1efb2d82a94cf001cffa0e7fd6ada813a2688b240130a69de998b87aebcd4c556503a45e559a422ecfbdf2f0e6318a8427e41a7b09017676cfe97afff13797f82f8d631bd29edde424854139c64ab8241e2a2331551401da371f0d3294843fd8f645019a04c07607342c70cf4cc793068355eaccdd671601bc79f0119f97113775379bf58f3f5d9d122766909b797947127b28248ff0720501c1eb3aa1717c2a696ce0aba6c5b5bda44c4eda5cf69ae376cc8b334a2c23fb2b0001374feb2041bfd423c6cc3e064ee2b4705748a082836d39dd723515357fb06e300000000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1762500",
"hash": "00000000017a7c3843f21669e8e4bd8bdaf0fad5f5dcf629ab04987b42e1aa17",
"time": 1659704759,
"saplingTree": "01fbcd6aa2451a2992dc3ebebbc7e342bf170e2d7ccfcf47f4123536c2f6b1564000180001b31b22d8e3fa25cbcda26117e9c49a9ac0be890ae2d96259c40d170209cd4f69000000000109bc787aaa3cb1dd53b9a6312a5319f65375b57bff779305ca4fecf22ed73b19019cc4f6765546053057cb46c04b13c3491a2845463c8e1c0b459861c3d041ae1201e5ded1df5bb1dfd04c1fa03339f2e860029d0524d4e877f578dcfe3ab33fcf230001c23df56312273b93b9e894e52dd21e6f4355e1b501431202b777a23e1784f06401440d1e33e0eb0ef54b953de0083cc94a21e119edc07f73912f33955a5ce94c1800000109f62a95f9c37785745abe209233d080c1e9d258e9b828f3afc85e698797275c00016835e0b829511b303ad2fdb06f00e2cbc90d0a9a0a7bfecb12d6d794c716f70f00000186384c1f717045cd48e98de8b5d9da6047b0821f6c9c6c485a92a520dbacfb100000018ed21e6b0098dd6f0902efde81f9d5da8d7068e8dcf01ab1c015f8f5936c2d5f017e16ac72afb29d36b6ba9cead439f560ec9c25e60a178552a69a4748e7edf51e",
"orchardTree": "018d8550d51fce8201a8ba611925d247a6434ec4051d754e97761ee2ebb177361b0120227a75d363c060d205fddb48b693b9de061dc224b7291210532295f623950c1f01ecbd7de037092436013cfe17b59249c719e72fea3e0e24082bcf46837048d02d000195f8d67fe58751cb04828837810d6442023a55ed677b7ac6d2572dc443e5d51d0001cd69a36b954858a9cde0629c6ec31bb735aa80d20947bcf25dd182db7492b92d0001b1d090970cbc6cd3cc3dacd48b84185d0f97b2db04b7d270074935d1e30ba628000001f8be16a220acf8ba291b9b9bf14c1bbb3541c08d5a8a89f6205bf890a18ac93401130cfb41380fdd7836985e2c4c488fdc3d1d1bd4390f350f0e1b8a448f47ac1c012bcbdd308beca04006b18928c4418aad2b3650677289b1b45ea5a21095c5310301100ed4d0a8a440b03f1254ce1efb2d82a94cf001cffa0e7fd6ada813a2688b240130a69de998b87aebcd4c556503a45e559a422ecfbdf2f0e6318a8427e41a7b09017676cfe97afff13797f82f8d631bd29edde424854139c64ab8241e2a2331551401da371f0d3294843fd8f645019a04c07607342c70cf4cc793068355eaccdd671601bc79f0119f97113775379bf58f3f5d9d122766909b797947127b28248ff0720501c1eb3aa1717c2a696ce0aba6c5b5bda44c4eda5cf69ae376cc8b334a2c23fb2b0001374feb2041bfd423c6cc3e064ee2b4705748a082836d39dd723515357fb06e300000000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "main",
"height": "1765000",
"hash": "00000000010881061abd953e3cae22cef9ac0d59d52a7fc41fa84d6c7656763e",
"time": 1659893089,
"saplingTree": "01c82bfd8fb955801a6c73bf26c999c7588eb33b362a68d898013f827e9ae0fb6a015e4715bab32e13c2bd88b32f9397dedbb115f3b4b9f056aeab068bcacb599659180167619283e7272dd833c45bb2f9ccc486c388a203e0a8a53213a84de27d66854501a0557fe4b8693ca7db4d1de647c987873f3e231bc8aa75892d3cd3d6c6755c0e01f7cc73cb43aa1d6a367f41eef0211173dd062da372e977ed0c692e59fbcbc93e000109a80a49995974f38fa8f73acdad1f7e6bd4919bc126309ce437d2ea4f3ab70c00000001ec676047263b15efba4d8e63b4f37ddbda791cbc588957e0671341aa3720084301dbfc2451fe34384638a3432143865f5d7b8b2a7e2e7a66cd53a5f7c299fb0a1d01cc9f79025fa31f9f5ed10802ea7c4544e23fa10013eea892329c2675b0eca20c01c3a0c40c83e182112e7a2ab8327b9898acbd3aefa31da43b47a39a36db6ef74f0000018a81193c32003895922db0e63e73c50cd71cac65107ee32d6232ecdd332f585b00017685c6cb28cae3ebd2b5f1dd5722b53718dda65c8e7abb38f55b180ef1ffdb37015fff3290c15bc580315673e71ce9ebc18ea8562083db38dd61f2d2bd37ff99550001e9254e1c420d982ba56e67ecebf5e4b1921f588b3a165ba65f9d8241d26bb32301d85f6214920d267663a855587995121330148f79681d1d0e1dd9cd3cd0fefa4500018ed21e6b0098dd6f0902efde81f9d5da8d7068e8dcf01ab1c015f8f5936c2d5f017e16ac72afb29d36b6ba9cead439f560ec9c25e60a178552a69a4748e7edf51e",
"orchardTree": "01366cfbdde62aae1022f45c2e7ab97a13dbc62e349292b5fe66af232d7852200e001f0000012542127304cf2a12e2786e2c48bfac68bb1e1832f30748fd5faae64d4009382e000001ea4f1bf4431dc71f69560969ed1b1896331139f6c2abb30d31f96d699c182a0101b1d090970cbc6cd3cc3dacd48b84185d0f97b2db04b7d270074935d1e30ba628000001f8be16a220acf8ba291b9b9bf14c1bbb3541c08d5a8a89f6205bf890a18ac93401130cfb41380fdd7836985e2c4c488fdc3d1d1bd4390f350f0e1b8a448f47ac1c012bcbdd308beca04006b18928c4418aad2b3650677289b1b45ea5a21095c5310301100ed4d0a8a440b03f1254ce1efb2d82a94cf001cffa0e7fd6ada813a2688b240130a69de998b87aebcd4c556503a45e559a422ecfbdf2f0e6318a8427e41a7b09017676cfe97afff13797f82f8d631bd29edde424854139c64ab8241e2a2331551401da371f0d3294843fd8f645019a04c07607342c70cf4cc793068355eaccdd671601bc79f0119f97113775379bf58f3f5d9d122766909b797947127b28248ff0720501c1eb3aa1717c2a696ce0aba6c5b5bda44c4eda5cf69ae376cc8b334a2c23fb2b0001374feb2041bfd423c6cc3e064ee2b4705748a082836d39dd723515357fb06e300000000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "test",
"height": "1980000",
"hash": "002ed4cb2b1ff42fb3c0a2cc937fa7c6593c73c0c8688f3ce6c398419a4c0af3",
"time": 1659290352,
"saplingTree": "0119c3970a5842d338e1d00d789ddabaab0b128b5485779674a6f2ff4350c2382d00100115573ba41e1eba171f1f752a0c87798a426f5f7ba329f7cab8929c37c5f77d270000000001a75a362c331628986b65647791374e7db0bbdbb2b871673fb1e6c0f11d7b791a000160a5822bf1407657b82e959214634ec4c36a092bc1ab7615286058d8bc06152c01d878f6e29835fd77030d6c7395e85231d74c3a4d72659c8f301d65dfc2f3c51f018bd9ca6b4e00aac24d384b8d85b32481e0b4335ab3de5a0dea5c61aa32366153019aa3b71d9ce27ab88add19fda2e50caf313de20a04c6b72f5fbe1783980431730122c55fdffb446e39b73f1606907b2889d18b01ac818a0cbd4b2c661ad6a5a170000117ddeb3a5f8d2f6b2d0a07f28f01ab25e03a05a9319275bb86d72fcaef6fc01501f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39",
"orchardTree": "01e9e5bbb3887195a57aae9be6a4fe8c6c3f43fe800c8650eb9937aa5a14e24219011c3fa06971b6adc1b915ec0f83c43cd6869f3026ca45ac68c2665d9f8654c4301f01f7cf6110e572e1946cf525c03905cb3129e477ba772ab5052be8a5ac97f2a911000001c8c190d39cf8b3d2e1728e9597f2c0124f94c7d29c10d9a25d094bee38a8150f016ae9f45036c0ce6ac3f523e0887a3195a02cacef78a2e49ac150adb7dc7b701d00016460d41653df12bbe8c6584d2aa56ae658f02f8ee202ed9238dcfd48bd89e51401bfae99e1684aa4131806b36d697f2c07df94fc5d4a85a1b740ce5b174f9eb12b01a89b33f0d7233cc0e1e80799d98de525c05caf198398a352f8a3bbcfd3c61f3c0000016fc552915a0d5bc5c0c0cdf29453edf081d9a2de396535e6084770c38dcff838019518d88883e466a41ca67d6b986739fb2f601d77bb957398ed899de70b2a9f0801cd4871c1f545e7f5d844cc65fb00b8a162e316c3d1a435b00c435032b732c4280000000000000000000000000000000000"
}

View File

@ -0,0 +1,8 @@
{
"network": "test",
"height": "1990000",
"hash": "004167f8881a1e0d6407ddebaa77dd2ddb9d5d979f5a489cbb31f6797ac83e35",
"time": 1659943879,
"saplingTree": "013fd1d8c97e8c58705795887d6b2b95f241bc7b7f5dc7465ec7efcee9c481e45a0010015735fc78e731a7806d90cbe18cc8329954998f8e6ce0518c1237647b25c3c8540001d894fa58e452130e147ad6cdee0bb5435723fd90f405e76ade170723606aa43a01cf26d83db78c952aa29219071d11711b55efa4253bb1040987967a2922da684b0000012233753a4db3b97f44b3bb4d77034fd77c8e5c7938092724cb4a3bf3491bc1180160a5822bf1407657b82e959214634ec4c36a092bc1ab7615286058d8bc06152c01d878f6e29835fd77030d6c7395e85231d74c3a4d72659c8f301d65dfc2f3c51f018bd9ca6b4e00aac24d384b8d85b32481e0b4335ab3de5a0dea5c61aa32366153019aa3b71d9ce27ab88add19fda2e50caf313de20a04c6b72f5fbe1783980431730122c55fdffb446e39b73f1606907b2889d18b01ac818a0cbd4b2c661ad6a5a170000117ddeb3a5f8d2f6b2d0a07f28f01ab25e03a05a9319275bb86d72fcaef6fc01501f08f39275112dd8905b854170b7f247cf2df18454d4fa94e6e4f9320cca05f24011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39",
"orchardTree": "01c86718d75ab663fe7a0b73f1fbaeb623205e632b50232b3e4e0aa642a2ee432f01b7ae647c07586477c57919f3d8511e5637d0be5bd79682abeefb4e2f4301cc141f000001c829fce51dc638a0b5027c8693ea0699cd65aa80b9e7fa36e61600a1a5de68300001e9507f2da9342ede27e7a548c176167e823b5447f7b1ffd13ffd3881724e2a22000156e5e2a74ac07ec0441aa4a435babf8536b4cf072a88b8f6e3be2f9c97c9fd0901dbcd13b6969c227ffb50565a2fa3659fd6691718d96b691e5d0aae952b8d4e1c0001b6fd291e9d6068bc24e99aefe49f8f29836ed1223deabc23871f1a1288f9240300016fc552915a0d5bc5c0c0cdf29453edf081d9a2de396535e6084770c38dcff838019518d88883e466a41ca67d6b986739fb2f601d77bb957398ed899de70b2a9f0801cd4871c1f545e7f5d844cc65fb00b8a162e316c3d1a435b00c435032b732c4280000000000000000000000000000000000"
}

View File

@ -1,11 +1,10 @@
package cash.z.ecc.android.sdk
import android.content.Context
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
import cash.z.ecc.android.sdk.exception.InitializerException
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
@ -16,13 +15,12 @@ import cash.z.ecc.android.sdk.tool.CheckpointTool
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
/**
* Simplified Initializer focused on starting from a ViewingKey.
*/
@Suppress("LongParameterList")
@Suppress("LongParameterList", "unused")
class Initializer private constructor(
val context: Context,
internal val rustBackend: RustBackend,
@ -149,12 +147,10 @@ class Initializer private constructor(
/**
* Set the server and the network property at the same time to prevent them from getting out
* of sync. Ultimately, this determines which host a synchronizer will use in order to
* connect to lightwalletd. In most cases, the default host is sufficient but an override
* can be provided. The host cannot be changed without explicitly setting the network.
* connect to lightwalletd.
*
* @param network the Zcash network to use. Either testnet or mainnet.
* @param host the lightwalletd host to use.
* @param port the lightwalletd port to use.
* @param lightWalletEndpoint the light wallet endpoint to use.
*/
fun setNetwork(
network: ZcashNetwork,
@ -315,6 +311,7 @@ class Initializer private constructor(
block: (Config) -> Unit
) = new(appContext, onCriticalErrorHandler, Config(block))
@Suppress("UNUSED_PARAMETER")
suspend fun new(
context: Context,
onCriticalErrorHandler: ((Throwable?) -> Boolean)?,
@ -380,9 +377,11 @@ class Initializer private constructor(
alias: String,
blockHeight: BlockHeight
): RustBackend {
val coordinator = DatabaseCoordinator.getInstance(context)
return RustBackend.init(
cacheDbPath(context, network, alias),
dataDbPath(context, network, alias),
coordinator.cacheDbFile(network, alias).absolutePath,
coordinator.dataDbFile(network, alias).absolutePath,
File(context.getCacheDirSuspend(), "params").absolutePath,
network,
blockHeight
@ -407,88 +406,7 @@ class Initializer private constructor(
appContext: Context,
network: ZcashNetwork,
alias: String
): Boolean {
val cacheDeleted = deleteDb(cacheDbPath(appContext, network, alias))
val dataDeleted = deleteDb(dataDbPath(appContext, network, alias))
return dataDeleted || cacheDeleted
}
//
// Path Helpers
//
/**
* Returns the path to the cache database that would correspond to the given alias.
*
* @param appContext the application context
* @param network the network associated with the data in the cache database.
* @param alias the alias to convert into a database path
*/
private suspend fun cacheDbPath(
appContext: Context,
network: ZcashNetwork,
alias: String
): String =
aliasToPath(appContext, network, alias, ZcashSdk.DB_CACHE_NAME)
/**
* Returns the path to the data database that would correspond to the given alias.
* @param appContext the application context
* * @param network the network associated with the data in the database.
* @param alias the alias to convert into a database path
*/
private suspend fun dataDbPath(
appContext: Context,
network: ZcashNetwork,
alias: String
): String =
aliasToPath(appContext, network, alias, ZcashSdk.DB_DATA_NAME)
private suspend fun aliasToPath(
appContext: Context,
network: ZcashNetwork,
alias: String,
dbFileName: String
): String {
val parentDir: String =
appContext.getDatabasePathSuspend("unused.db").parentFile?.absolutePath
?: throw InitializerException.DatabasePathException
val prefix = if (alias.endsWith('_')) alias else "${alias}_"
return File(parentDir, "$prefix${network.networkName}_$dbFileName").absolutePath
}
/**
* Delete a database and it's potential journal file at the given path.
*
* @param filePath the path of the db to erase.
* @return true when a file exists at the given path and was deleted.
*/
private suspend fun deleteDb(filePath: String): Boolean {
// just try the journal file. Doesn't matter if it's not there.
delete("$filePath-journal")
return delete(filePath)
}
/**
* Delete the file at the given path.
*
* @param filePath the path of the file to erase.
* @return true when a file exists at the given path and was deleted.
*/
private suspend fun delete(filePath: String): Boolean {
return File(filePath).let {
withContext(SdkDispatchers.DATABASE_IO) {
if (it.exists()) {
twig("Deleting ${it.name}!")
it.delete()
true
} else {
false
}
}
}
}
): Boolean = DatabaseCoordinator.getInstance(appContext).deleteDatabases(network, alias)
}
}

View File

@ -17,6 +17,7 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanned
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanning
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Stopped
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Validating
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.hasRawTransactionId
import cash.z.ecc.android.sdk.db.entity.isCancelled
@ -62,7 +63,6 @@ import io.grpc.ManagedChannel
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
@ -96,7 +96,7 @@ import kotlin.coroutines.EmptyCoroutineContext
* @property processor saves the downloaded compact blocks to the cache and then scans those blocks for
* data related to this wallet.
*/
@ExperimentalCoroutinesApi
@OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class)
@FlowPreview
class SdkSynchronizer internal constructor(
private val storage: TransactionRepository,
@ -109,6 +109,8 @@ class SdkSynchronizer internal constructor(
private val _saplingBalances = MutableStateFlow<WalletBalance?>(null)
private val _transparentBalances = MutableStateFlow<WalletBalance?>(null)
// TODO [#288]: Remove Deprecated Usage of ConflatedBroadcastChannel
// TODO [#288]: https://github.com/zcash/zcash-android-wallet-sdk/issues/288
private val _status = ConflatedBroadcastChannel<Synchronizer.Status>(DISCONNECTED)
/**
@ -171,6 +173,9 @@ class SdkSynchronizer internal constructor(
* processor is finished scanning, the synchronizer updates transaction and balance info and
* then emits a [SYNCED] status.
*/
// TODO [#658] Replace ComputableFlow and asFlow() obsolete Coroutine usage
// TODO [#658] https://github.com/zcash/zcash-android-wallet-sdk/issues/658
@Suppress("DEPRECATION")
override val status = _status.asFlow()
/**
@ -414,6 +419,7 @@ class SdkSynchronizer internal constructor(
twig("Synchronizer onReady complete. Processor start has exited!")
}
@Suppress("UNUSED_PARAMETER")
private fun onCriticalError(unused: CoroutineContext?, error: Throwable) {
twig("********")
twig("******** ERROR: $error")
@ -641,8 +647,9 @@ class SdkSynchronizer internal constructor(
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
if (encodedTx.isCancelled()) {
twig("[cleanup] this tx has been cancelled so we will cleanup instead of submitting")
if (cleanupCancelledTx(encodedTx)) refreshAllBalances()
encodedTx
if (cleanupCancelledTx(encodedTx)) {
refreshAllBalances()
}
} else {
txManager.submit(encodedTx)
}
@ -673,8 +680,9 @@ class SdkSynchronizer internal constructor(
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
if (encodedTx.isCancelled()) {
twig("[cleanup] this shielding tx has been cancelled so we will cleanup instead of submitting")
if (cleanupCancelledTx(encodedTx)) refreshAllBalances()
encodedTx
if (cleanupCancelledTx(encodedTx)) {
refreshAllBalances()
}
} else {
txManager.submit(encodedTx)
}
@ -685,8 +693,8 @@ class SdkSynchronizer internal constructor(
txManager.monitorById(it.id)
}.distinctUntilChanged()
override suspend fun refreshUtxos(address: String, startHeight: BlockHeight): Int? {
return processor.refreshUtxos(address, startHeight)
override suspend fun refreshUtxos(tAddr: String, since: BlockHeight): Int? {
return processor.refreshUtxos(tAddr, since)
}
override suspend fun getTransparentBalance(tAddr: String): WalletBalance {
@ -785,7 +793,11 @@ object DefaultSynchronizerFactory {
)
fun defaultBlockStore(initializer: Initializer): CompactBlockStore =
CompactBlockDbStore.new(initializer.context, initializer.network, initializer.rustBackend.pathCacheDb)
CompactBlockDbStore.new(
initializer.context,
initializer.network,
initializer.rustBackend.cacheDbFile
)
fun defaultService(initializer: Initializer): LightWalletService =
LightWalletGrpcService.new(initializer.context, initializer.lightWalletEndpoint)
@ -800,12 +812,23 @@ object DefaultSynchronizerFactory {
blockStore: CompactBlockStore
): CompactBlockDownloader = CompactBlockDownloader(service, blockStore)
fun defaultTxManager(
suspend fun defaultTxManager(
initializer: Initializer,
encoder: TransactionEncoder,
service: LightWalletService
): OutboundTransactionManager =
PersistentTransactionManager(initializer.context, encoder, service)
): OutboundTransactionManager {
val databaseFile = DatabaseCoordinator.getInstance(initializer.context).pendingTransactionsDbFile(
initializer.network,
initializer.alias
)
return PersistentTransactionManager(
initializer.context,
encoder,
service,
databaseFile
)
}
fun defaultProcessor(
initializer: Initializer,

View File

@ -76,6 +76,7 @@ import kotlin.math.roundToInt
* in when considering initial range to download. In most cases, this should be the birthday height
* of the current wallet--the height before which we do not need to scan for transactions.
*/
@OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class)
@OpenForTesting
class CompactBlockProcessor internal constructor(
val downloader: CompactBlockDownloader,
@ -126,6 +127,8 @@ class CompactBlockProcessor internal constructor(
)
)
// TODO [#288]: Remove Deprecated Usage of ConflatedBroadcastChannel
// TODO [#288]: https://github.com/zcash/zcash-android-wallet-sdk/issues/288
private val _state: ConflatedBroadcastChannel<State> = ConflatedBroadcastChannel(Initialized)
private val _progress = ConflatedBroadcastChannel(0)
private val _processorInfo =
@ -161,18 +164,27 @@ class CompactBlockProcessor internal constructor(
* The flow of state values so that a wallet can monitor the state of this class without needing
* to poll.
*/
// TODO [#658] Replace ComputableFlow and asFlow() obsolete Coroutine usage
// TODO [#658] https://github.com/zcash/zcash-android-wallet-sdk/issues/658
@Suppress("DEPRECATION")
val state = _state.asFlow()
/**
* The flow of progress values so that a wallet can monitor how much downloading remains
* without needing to poll.
*/
// TODO [#658] Replace ComputableFlow and asFlow() obsolete Coroutine usage
// TODO [#658] https://github.com/zcash/zcash-android-wallet-sdk/issues/658
@Suppress("DEPRECATION")
val progress = _progress.asFlow()
/**
* The flow of detailed processorInfo like the range of blocks that shall be downloaded and
* scanned. This gives the wallet a lot of insight into the work of this processor.
*/
// TODO [#658] Replace ComputableFlow and asFlow() obsolete Coroutine usage
// TODO [#658] https://github.com/zcash/zcash-android-wallet-sdk/issues/658
@Suppress("DEPRECATION")
val processorInfo = _processorInfo.asFlow()
/**
@ -393,12 +405,8 @@ class CompactBlockProcessor internal constructor(
}
}
newTxs?.onEach { newTransaction ->
if (newTransaction == null) {
twig("somehow, new transaction was null!!!")
} else {
enhance(newTransaction)
}
newTxs.onEach { newTransaction ->
enhance(newTransaction)
}
twig("Done enhancing transaction details")
BlockProcessingResult.Success
@ -603,7 +611,7 @@ class CompactBlockProcessor internal constructor(
return BlockProcessingResult.NoBlocksToProcess
}
Twig.sprout("validating")
twig("validating blocks in range $range in db: ${(rustBackend as RustBackend).pathCacheDb}")
twig("validating blocks in range $range in db: ${(rustBackend as RustBackend).cacheDbFile.absolutePath}")
val result = rustBackend.validateCombinedChain()
Twig.clip("validating")
@ -864,6 +872,7 @@ class CompactBlockProcessor internal constructor(
* when we unexpectedly lose server connection or are waiting for an event to happen on the
* chain. We can pass this desire along now and later figure out how to handle it, privately.
*/
@Suppress("UNUSED_PARAMETER")
private fun calculatePollInterval(fastIntervalDesired: Boolean = false): Long {
val interval = POLL_INTERVAL
val now = System.currentTimeMillis()
@ -1119,7 +1128,7 @@ class CompactBlockProcessor internal constructor(
}
private fun Service.LightdInfo.matchingNetwork(network: String): Boolean {
fun String.toId() = toLowerCase(Locale.US).run {
fun String.toId() = lowercase(Locale.US).run {
when {
contains("main") -> "mainnet"
contains("test") -> "testnet"
@ -1140,7 +1149,6 @@ class CompactBlockProcessor internal constructor(
twig("$name MUTEX: releasing lock", -1)
}
}
twig("$name MUTEX: withLock complete", -1)
}
}

View File

@ -0,0 +1,416 @@
package cash.z.ecc.android.sdk.db
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.room.Room
import androidx.room.RoomDatabase
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.AndroidApiVersion
import cash.z.ecc.android.sdk.internal.Files
import cash.z.ecc.android.sdk.internal.LazyWithArgument
import cash.z.ecc.android.sdk.internal.NoBackupContextWrapper
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
import cash.z.ecc.android.sdk.internal.ext.getDatabasePathSuspend
import cash.z.ecc.android.sdk.internal.ext.renameToSuspend
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.model.ZcashNetwork
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
/**
* Wrapper class for various SDK databases operations. It always guaranties exclusive access to
* provided operations.
*
* @param context the application context
*/
@SuppressWarnings("TooManyFunctions")
internal class DatabaseCoordinator private constructor(context: Context) {
/*
* This implementation is thread-safe but is not multi-process safe.
*
* The mutex helps to ensure that two instances of the SDK being initialized in the same
* process do not have conflicts with regard to moving the databases around. However if an
* application decides to use multiple processes this could cause a problem during the one-time
* the database path migration.
*/
private val applicationContext = context.applicationContext
private val createFileMutex = Mutex()
private val deleteFileMutex = Mutex()
companion object {
@VisibleForTesting
internal const val DB_DATA_NAME_LEGACY = "Data.db" // $NON-NLS
const val DB_DATA_NAME = "data.sqlite3" // $NON-NLS
@VisibleForTesting
internal const val DB_CACHE_NAME_LEGACY = "Cache.db" // $NON-NLS
const val DB_CACHE_NAME = "cache.sqlite3" // $NON-NLS
@VisibleForTesting
internal const val DB_PENDING_TRANSACTIONS_NAME_LEGACY = "PendingTransactions.db" // $NON-NLS
const val DB_PENDING_TRANSACTIONS_NAME = "pending_transactions.sqlite3" // $NON-NLS
const val DATABASE_FILE_JOURNAL_SUFFIX = "journal" // $NON-NLS
const val DATABASE_FILE_WAL_SUFFIX = "wal" // $NON-NLS
@VisibleForTesting
internal const val ALIAS_LEGACY = "ZcashSdk" // $NON-NLS
private val lazy =
LazyWithArgument<Context, DatabaseCoordinator> { DatabaseCoordinator(it) }
fun getInstance(context: Context) = lazy.getInstance(context)
}
/**
* Returns the file of the Cache database that would correspond to the given alias
* and network attributes.
*
* @param network the network associated with the data in the cache database.
* @param alias the alias to convert into a database path
*
* @return the Cache database file
*/
internal suspend fun cacheDbFile(
network: ZcashNetwork,
alias: String
): File {
val dbLocationsPair = prepareDbFiles(
applicationContext,
network,
alias,
DB_CACHE_NAME_LEGACY,
DB_CACHE_NAME
)
createFileMutex.withLock {
return checkAndMoveDatabaseFiles(
dbLocationsPair.first,
dbLocationsPair.second
)
}
}
/**
* Returns the file of the Data database that would correspond to the given alias
* and network attributes.
*
* @param network the network associated with the data in the database.
* @param alias the alias to convert into a database path
*
* @return the Data database file
*/
internal suspend fun dataDbFile(
network: ZcashNetwork,
alias: String
): File {
val dbLocationsPair = prepareDbFiles(
applicationContext,
network,
alias,
DB_DATA_NAME_LEGACY,
DB_DATA_NAME
)
createFileMutex.withLock {
return checkAndMoveDatabaseFiles(
dbLocationsPair.first,
dbLocationsPair.second
)
}
}
/**
* Returns the file of the PendingTransaction database that would correspond to the given
* alias and network attributes. As the originally created file was called just
* PendingTransactions.db, we choose slightly different approach, but it also leads to
* original database files migration with additional renaming too.
*
* @param network the network associated with the data in the database.
* @param alias the alias to convert into a database path
*
* @return the PendingTransaction database file
*/
internal suspend fun pendingTransactionsDbFile(
network: ZcashNetwork,
alias: String
): File {
val legacyLocationDbFile = newDatabaseFilePointer(
null,
null,
DB_PENDING_TRANSACTIONS_NAME_LEGACY,
getDatabaseParentDir(applicationContext)
)
val preferredLocationDbFile = newDatabaseFilePointer(
network,
alias,
DB_PENDING_TRANSACTIONS_NAME,
Files.getZcashNoBackupSubdirectory(applicationContext)
)
createFileMutex.withLock {
return checkAndMoveDatabaseFiles(
legacyLocationDbFile,
preferredLocationDbFile
)
}
}
/**
* Function for common deletion of Data and Cache database files. It also checks and deletes
* additional journal and wal files, if they exist.
*
* @param network the network associated with the data in the database.
* @param alias the alias to convert into a database path
*/
internal suspend fun deleteDatabases(
network: ZcashNetwork,
alias: String
): Boolean {
deleteFileMutex.withLock {
val dataDeleted = deleteDatabase(
dataDbFile(network, alias)
)
val cacheDeleted = deleteDatabase(
cacheDbFile(network, alias)
)
return dataDeleted || cacheDeleted
}
}
/**
* This helper function prepares a legacy (i.e. previously created) database file, as well
* as the preferred (i.e. newly created) file for subsequent use (and eventually move).
*
* Note: the database file placed under the fake no_backup folder for devices with Android SDK
* level lower than 21.
*
* @param appContext the application context
* @param network the network associated with the data in the database.
* @param alias the alias to convert into a database path
* @param databaseName the name of the new database file
*/
private suspend fun prepareDbFiles(
appContext: Context,
network: ZcashNetwork,
alias: String,
databaseNameLegacy: String,
databaseName: String
): Pair<File, File> {
// Here we change the alias to be lowercase and underscored only if we work with the default
// Zcash alias, otherwise we need to keep an SDK caller alias the same to avoid the database
// files move breakage.
val aliasLegacy = if (ZcashSdk.DEFAULT_ALIAS == alias) {
ALIAS_LEGACY
} else {
alias
}
val legacyLocationDbFile = newDatabaseFilePointer(
network,
aliasLegacy,
databaseNameLegacy,
getDatabaseParentDir(appContext)
)
val preferredLocationDbFile = newDatabaseFilePointer(
network,
alias,
databaseName,
Files.getZcashNoBackupSubdirectory(appContext)
)
return Pair(
legacyLocationDbFile,
preferredLocationDbFile
)
}
/**
* This function do actual database file move or simply validate the file and return it.
* From the Android SDK level 21 it places database files into no_backup folder, as it does
* not allow automatic backup. On older APIs it places database files into databases folder,
* which allows automatic backup. It also copies database files between these two folders,
* if older folder usage is detected.
*
* @param legacyLocationDbFile the previously used file location
* @param preferredLocationDbFile the newly used file location
*/
private suspend fun checkAndMoveDatabaseFiles(
legacyLocationDbFile: File,
preferredLocationDbFile: File
): File {
var resultDbFile = preferredLocationDbFile
// check if the move wasn't already performed and if it's needed
if (!preferredLocationDbFile.existsSuspend() && legacyLocationDbFile.existsSuspend()) {
// We check the move operation result and fallback to the legacy file, if
// anything went wrong.
if (!moveDatabaseFile(legacyLocationDbFile, preferredLocationDbFile)) {
resultDbFile = legacyLocationDbFile
}
}
return resultDbFile
}
/**
* The purpose of this function is to move database files between the old location (given by
* the legacyLocationDbFile parameter) and the new location (given by preferredLocationDbFile).
* The actual move operation is performed with the renameTo function, which simply renames
* a file path and persists the metadata information. The mechanism deals with the additional
* database files -journal and -wal too, if they exist.
*
* @param legacyLocationDbFile the previously used file location (rename from)
* @param preferredLocationDbFile the newly used file location (rename to)
*/
private suspend fun moveDatabaseFile(
legacyLocationDbFile: File,
preferredLocationDbFile: File
): Boolean {
val filesToBeRenamed = mutableListOf<Pair<File, File>>().apply {
add(Pair(legacyLocationDbFile, preferredLocationDbFile))
}
// add journal database file, if exists
val journalSuffixedDbFile = File(
"${legacyLocationDbFile.absolutePath}-$DATABASE_FILE_JOURNAL_SUFFIX"
)
if (journalSuffixedDbFile.existsSuspend()) {
filesToBeRenamed.add(
Pair(
journalSuffixedDbFile,
File(
"${preferredLocationDbFile.absolutePath}-$DATABASE_FILE_JOURNAL_SUFFIX"
)
)
)
}
// add wal database file, if exists
val walSuffixedDbFile = File(
"${legacyLocationDbFile.absolutePath}-$DATABASE_FILE_WAL_SUFFIX"
)
if (walSuffixedDbFile.existsSuspend()) {
filesToBeRenamed.add(
Pair(
walSuffixedDbFile,
File(
"${preferredLocationDbFile.absolutePath}-$DATABASE_FILE_WAL_SUFFIX"
)
)
)
}
return runCatching {
return@runCatching filesToBeRenamed.all {
it.first.renameToSuspend(it.second)
}
}.onFailure {
twig("Failed while renaming database files with: $it")
}.getOrDefault(false)
}
/**
* This function returns previously used database folder path (i.e. databases). The databases
* folder is deprecated now, as it allows automatic data backup, which is not permitted for
* our database files.
*
* @param appContext the application context
*/
private suspend fun getDatabaseParentDir(appContext: Context): File {
return appContext.getDatabasePathSuspend("unused.db").parentFile
?: throw InitializerException.DatabasePathException
}
/**
* Simple helper function, which prepares a database file object by input parameters. It does
* not create the file, just determines the file path.
*
* @param network the network associated with the data in the database.
* @param alias the alias to convert into a database path
* @param dbFileName the name of the new database file
* @param parentDir the name of the parent directory, in which the file should be placed
*/
private fun newDatabaseFilePointer(
network: ZcashNetwork?,
alias: String?,
dbFileName: String,
parentDir: File
): File {
val aliasPrefix = if (alias == null) {
""
} else if (alias.endsWith('_')) {
alias
} else {
"${alias}_"
}
val networkPrefix = network?.networkName ?: ""
return if (aliasPrefix.isNotEmpty()) {
File(parentDir, "$aliasPrefix${networkPrefix}_$dbFileName")
} else {
File(parentDir, dbFileName)
}
}
/**
* Delete a database and its potential journal and wal file at the given path.
*
* The rollback journal (or newer wal) file is a temporary file used to implement atomic commit
* and rollback capabilities in SQLite.
*
* @param file the path of the db to erase.
* @return true when a file exists at the given path and was deleted.
*/
private suspend fun deleteDatabase(file: File): Boolean {
// Just try the journal and wal files too. Doesn't matter if they're not there.
File("${file.absolutePath}-$DATABASE_FILE_JOURNAL_SUFFIX").deleteSuspend()
File("${file.absolutePath}-$DATABASE_FILE_WAL_SUFFIX").deleteSuspend()
return file.deleteSuspend()
}
}
/**
* The purpose of this function is to provide Room.Builder via a static Room.databaseBuilder with
* an injection of our NoBackupContextWrapper to override the behavior of getDatabasePath() for
* Android SDK level 27 and higher and regular Context class for the Android SDK level 26 and lower.
*
* Note: ideally we'd make this extension function or override the Room.databaseBuilder function,
* but it's not possible, as it's a static function on Room class, which does not allow its
* instantiation.
*
* @param context
* @param klass The database class.
* @param databaseFile The database file.
* @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
*/
internal fun <T : RoomDatabase?> commonDatabaseBuilder(
context: Context,
klass: Class<T>,
databaseFile: File
): RoomDatabase.Builder<T> {
return if (AndroidApiVersion.isAtLeastO_MR1) {
Room.databaseBuilder(
NoBackupContextWrapper(
context,
databaseFile.parentFile ?: throw InitializerException.DatabasePathException
),
klass,
databaseFile.name
)
} else {
Room.databaseBuilder(
context,
klass,
databaseFile.absolutePath
)
}
}

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.RoomWarnings
@Entity(
tableName = "received_notes",
@ -25,6 +26,7 @@ import androidx.room.ForeignKey
)
]
)
@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD)
data class Received(
@ColumnInfo(name = "id_note")
val id: Int? = 0,

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.RoomWarnings
@Entity(
tableName = "sent_notes",
@ -19,6 +20,7 @@ import androidx.room.ForeignKey
)
]
)
@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD)
data class Sent(
@ColumnInfo(name = "id_note")
val id: Int? = 0,

View File

@ -5,6 +5,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import androidx.room.RoomWarnings
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.Zatoshi
@ -23,6 +24,7 @@ import cash.z.ecc.android.sdk.model.Zatoshi
)
]
)
@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD)
data class TransactionEntity(
@ColumnInfo(name = "id_tx")
val id: Long?,

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.RoomWarnings
@Entity(
tableName = "utxos",
@ -15,6 +16,7 @@ import androidx.room.ForeignKey
)
]
)
@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD)
data class Utxo(
@ColumnInfo(name = "id_utxo")
val id: Long? = 0L,

View File

@ -10,8 +10,8 @@ class BatchMetrics(val range: ClosedRange<BlockHeight>, val batchSize: Int, priv
private var batchStartTime = 0L
private var batchEndTime = 0L
private var rangeSize = range.endInclusive.value - range.start.value + 1
private inline fun now() = System.currentTimeMillis()
private inline fun ips(blocks: Long, time: Long) = 1000.0f * blocks / time
private fun now() = System.currentTimeMillis()
private fun ips(blocks: Long, time: Long) = 1000.0f * blocks / time
val isComplete get() = completedBatches * batchSize >= rangeSize
val isBatchComplete get() = batchEndTime > batchStartTime
@ -29,8 +29,6 @@ class BatchMetrics(val range: ClosedRange<BlockHeight>, val batchSize: Int, priv
fun endBatch() {
completedBatches++
batchEndTime = now()
onMetricComplete?.let {
it.invoke(this, isComplete)
}
onMetricComplete?.invoke(this, isComplete)
}
}

View File

@ -24,7 +24,7 @@ enum class ConsensusBranchId(val displayName: String, val id: Long, val hexId: S
fun fromId(id: Long): ConsensusBranchId? = values().firstOrNull { it.id == id }
fun fromHex(hex: String): ConsensusBranchId? = values().firstOrNull { branch ->
hex.toLowerCase(Locale.US).replace("_", "").replaceFirst("0x", "").let { sanitized ->
hex.lowercase(Locale.US).replace("_", "").replaceFirst("0x", "").let { sanitized ->
branch.hexId.equals(sanitized, true)
}
}

View File

@ -18,18 +18,18 @@ object ZcashSdk {
/**
* The theoretical maximum number of blocks in a reorg, due to other bottlenecks in the protocol design.
*/
val MAX_REORG_SIZE = 100
const val MAX_REORG_SIZE = 100
/**
* The maximum length of a memo.
*/
val MAX_MEMO_SIZE = 512
const val MAX_MEMO_SIZE = 512
/**
* The amount of blocks ahead of the current height where new transactions are set to expire. This value is controlled
* by the rust backend but it is helpful to know what it is set to and should be kept in sync.
*/
val EXPIRY_OFFSET = 20
const val EXPIRY_OFFSET = 20
/**
* Default size of batches of blocks to request from the compact block service.
@ -43,60 +43,57 @@ object 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
const val SCAN_BATCH_SIZE = 150
/**
* 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
const val POLL_INTERVAL = 20_000L
/**
* Estimate of the time between blocks.
*/
val BLOCK_INTERVAL_MILLIS = 75_000L
const val BLOCK_INTERVAL_MILLIS = 75_000L
/**
* Default attempts at retrying.
*/
val RETRIES = 5
const val RETRIES = 5
/**
* The default maximum amount of time to wait during retry backoff intervals. Failed loops will never wait longer than
* this before retyring.
*/
val MAX_BACKOFF_INTERVAL = 600_000L
const val MAX_BACKOFF_INTERVAL = 600_000L
/**
* 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
val DB_DATA_NAME = "Data.db"
val DB_CACHE_NAME = "Cache.db"
const val REWIND_DISTANCE = 10
/**
* File name for the sappling spend params
*/
val SPEND_PARAM_FILE_NAME = "sapling-spend.params"
const val SPEND_PARAM_FILE_NAME = "sapling-spend.params"
/**
* File name for the sapling output params
*/
val OUTPUT_PARAM_FILE_NAME = "sapling-output.params"
const val OUTPUT_PARAM_FILE_NAME = "sapling-output.params"
/**
* The Url that is used by default in zcashd.
* We'll want to make this externally configurable, rather than baking it into the SDK but
* this will do for now, since we're using a cloudfront URL that already redirects.
*/
val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
const val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
/**
* The default memo to use when shielding transparent funds.
*/
val DEFAULT_SHIELD_FUNDS_MEMO_PREFIX = "shielding:"
const val DEFAULT_SHIELD_FUNDS_MEMO_PREFIX = "shielding:"
val DEFAULT_ALIAS: String = "ZcashSdk"
const val DEFAULT_ALIAS: String = "zcash_sdk"
}

View File

@ -0,0 +1,57 @@
package cash.z.ecc.android.sdk.internal
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.IntRange
internal object AndroidApiVersion {
/**
* @param sdk SDK version number to test against the current environment.
* @return `true` if [android.os.Build.VERSION.SDK_INT] is greater than or equal to
* [sdk].
*/
@ChecksSdkIntAtLeast(parameter = 0)
fun isAtLeast(@IntRange(from = Build.VERSION_CODES.BASE.toLong()) sdk: Int): Boolean {
return Build.VERSION.SDK_INT >= sdk
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.LOLLIPOP)
val isAtLeastL = isAtLeast(Build.VERSION_CODES.LOLLIPOP)
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.M)
val isAtLeastM = isAtLeast(Build.VERSION_CODES.M)
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N)
val isAtLeastN = isAtLeast(Build.VERSION_CODES.N)
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
val isAtLeastO = isAtLeast(Build.VERSION_CODES.O)
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O_MR1)
val isAtLeastO_MR1 = isAtLeast(Build.VERSION_CODES.O_MR1)
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
val isAtLeastP = isAtLeast(Build.VERSION_CODES.P)
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
val isAtLeastQ = isAtLeast(Build.VERSION_CODES.Q)
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
val isAtLeastR = isAtLeast(Build.VERSION_CODES.R)
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
val isAtLeastS = isAtLeast(Build.VERSION_CODES.S)
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
val isAtLeastT = isAtLeast(Build.VERSION_CODES.TIRAMISU)
/**
* This property indicates a preview version of the current device Android SDK. It works only on
* Android SDK 23 and later, on the previous SDK versions its value is always false.
*/
val isPreview = if (isAtLeastM) {
0 != Build.VERSION.PREVIEW_SDK_INT
} else {
false
}
}

View File

@ -0,0 +1,47 @@
package cash.z.ecc.android.sdk.internal
import android.content.Context
import cash.z.ecc.android.sdk.internal.ext.canWriteSuspend
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
import cash.z.ecc.android.sdk.internal.ext.getNoBackupFilesDirCompat
import cash.z.ecc.android.sdk.internal.ext.mkdirsSuspend
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
/**
* Because the filesystem is a shared resource, this declares the filenames that the SDK is using
* in one centralized place.
*/
internal object Files {
/**
* Subdirectory under the Android "no backup" directory which is owned by the SDK.
*/
const val NO_BACKUP_SUBDIRECTORY = "co.electricoin.zcash" // $NON-NLS
private val accessMutex = Mutex()
/**
* @return Subdirectory of the "no_backup" directory that is owned by the SDK. The returned
* directory will exist when this method returns.
*
* As we use a suspend version of the file operations here, we protect the operations with mutex
* to prevent multiple threads to invoke the function at the same time.
*/
suspend fun getZcashNoBackupSubdirectory(context: Context): File {
val dir = File(context.getNoBackupFilesDirCompat(), NO_BACKUP_SUBDIRECTORY)
accessMutex.withLock {
if (!dir.existsSuspend()) {
if (!dir.mkdirsSuspend()) {
error("${dir.absolutePath} directory does not exist and could not be created")
}
}
if (!dir.canWriteSuspend()) {
error("${dir.absolutePath} directory is not writable")
}
}
return dir
}
}

View File

@ -0,0 +1,33 @@
package cash.z.ecc.android.sdk.internal
/**
* Implements a lazy singleton pattern with an input argument.
*
* This class is thread-safe.
*/
internal class LazyWithArgument<in Input, out Output>(private val deferredCreator: ((Input) -> Output)) {
@Volatile
private var singletonInstance: Output? = null
private val intrinsicLock = Any()
fun getInstance(input: Input): Output {
/*
* Double-checked idiom for lazy initialization, Effective Java 2nd edition page 283.
*/
var localSingletonInstance = singletonInstance
if (null == localSingletonInstance) {
synchronized(intrinsicLock) {
localSingletonInstance = singletonInstance
if (null == localSingletonInstance) {
localSingletonInstance = deferredCreator(input)
singletonInstance = localSingletonInstance
}
}
}
return localSingletonInstance!!
}
}

View File

@ -0,0 +1,47 @@
package cash.z.ecc.android.sdk.internal
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import androidx.annotation.RequiresApi
import java.io.File
/**
* A context class wrapper used for building our database classes. The advantage of this implementation
* is that we can control actions run on this context class. This is supposed to be used only for
* Android SDK level 27 and higher. The Room's underlying SQLite has a different implementation of
* SQLiteOpenHelper#getDatabaseLocked() and possibly other methods for Android SDK level 26 and lower.
* Which at the end call ContextImpl#openOrCreateDatabase(), instead of the overridden getDatabasePath(),
* and thus is not suitable for this custom context wrapper class.
*
* @param context
* @param parentDir The directory in which is the database file placed.
* @return Wrapped context class.
*/
@RequiresApi(Build.VERSION_CODES.O_MR1)
internal class NoBackupContextWrapper(
context: Context,
private val parentDir: File
) : ContextWrapper(context.applicationContext) {
/**
* Overriding this function gives us ability to control the result database file location.
*
* @param name Database file name.
* @return File located under no_backup/co.electricoin.zcash directory.
*/
override fun getDatabasePath(name: String): File {
twig("Database: $name in directory: ${parentDir.absolutePath}")
return File(parentDir, name)
}
override fun getApplicationContext(): Context {
// Prevent breakout
return this
}
override fun getBaseContext(): Context {
// Prevent breakout
return this
}
}

View File

@ -2,6 +2,9 @@ package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.ext.deleteRecursivelySuspend
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
import cash.z.ecc.android.sdk.internal.ext.mkdirsSuspend
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
@ -133,7 +136,3 @@ class SaplingParamTool {
}
}
}
suspend fun File.existsSuspend() = withContext(Dispatchers.IO) { exists() }
suspend fun File.mkdirsSuspend() = withContext(Dispatchers.IO) { mkdirs() }
suspend fun File.deleteRecursivelySuspend() = withContext(Dispatchers.IO) { deleteRecursively() }

View File

@ -1,8 +1,8 @@
package cash.z.ecc.android.sdk.internal.block
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
import cash.z.ecc.android.sdk.db.commonDatabaseBuilder
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
@ -11,6 +11,7 @@ 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 java.io.File
/**
* An implementation of CompactBlockStore that persists information to a database in the given
@ -45,23 +46,27 @@ class CompactBlockDbStore private constructor(
companion object {
/**
* @param appContext the application context. This is used for creating the database.
* @property dbPath the absolute path to the database.
* @property databaseFile the database file.
*/
fun new(
appContext: Context,
zcashNetwork: ZcashNetwork,
dbPath: String
databaseFile: File
): CompactBlockDbStore {
val cacheDb = createCompactBlockCacheDb(appContext.applicationContext, dbPath)
val cacheDb = createCompactBlockCacheDb(appContext.applicationContext, databaseFile)
return CompactBlockDbStore(zcashNetwork, cacheDb)
}
private fun createCompactBlockCacheDb(
appContext: Context,
dbPath: String
databaseFile: File
): CompactBlockDb {
return Room.databaseBuilder(appContext, CompactBlockDb::class.java, dbPath)
return commonDatabaseBuilder(
appContext,
CompactBlockDb::class.java,
databaseFile
)
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
// this is a simple cache of blocks. destroying the db should be benign
.fallbackToDestructiveMigration()

View File

@ -5,6 +5,7 @@ import androidx.room.Dao
import androidx.room.Database
import androidx.room.Query
import androidx.room.RoomDatabase
import androidx.room.RoomWarnings
import androidx.room.Transaction
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@ -302,6 +303,7 @@ interface TransactionDao {
LIMIT :limit
"""
)
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
fun getSentTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ConfirmedTransaction>
/**
@ -328,6 +330,7 @@ interface TransactionDao {
LIMIT :limit
"""
)
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
fun getReceivedTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ConfirmedTransaction>
/**
@ -430,7 +433,6 @@ interface TransactionDao {
var success = false
try {
var hasInitialMatch = false
var hasFinalMatch = true
twig("[cleanup] cleanupCancelledTx starting...")
findUnminedTransactionIds(rawTransactionId).also {
twig("[cleanup] cleanupCancelledTx found ${it.size} matching transactions to cleanup")
@ -438,7 +440,7 @@ interface TransactionDao {
hasInitialMatch = true
removeInvalidOutboundTransaction(transactionId)
}
hasFinalMatch = findMatchingTransactionId(rawTransactionId) != null
val hasFinalMatch = findMatchingTransactionId(rawTransactionId) != null
success = hasInitialMatch && !hasFinalMatch
twig("[cleanup] cleanupCancelledTx Done. success? $success")
} catch (t: Throwable) {

View File

@ -1,11 +1,51 @@
package cash.z.ecc.android.sdk.internal.ext
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import cash.z.ecc.android.sdk.internal.AndroidApiVersion
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
suspend fun Context.getDatabasePathSuspend(fileName: String) =
internal suspend fun Context.getDatabasePathSuspend(fileName: String) =
withContext(Dispatchers.IO) { getDatabasePath(fileName) }
suspend fun Context.getCacheDirSuspend() =
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
internal suspend fun Context.getNoBackupFilesDirSuspend() =
withContext(Dispatchers.IO) { noBackupFilesDir }
internal suspend fun Context.getCacheDirSuspend() =
withContext(Dispatchers.IO) { cacheDir }
internal suspend fun Context.getFilesDirSuspend() =
withContext(Dispatchers.IO) { filesDir }
internal suspend fun Context.getDataDirCompatSuspend() =
withContext(Dispatchers.IO) { ContextCompat.getDataDir(this@getDataDirCompatSuspend) }
private const val FAKE_NO_BACKUP_FOLDER = "no_backup" // $NON-NLS
/**
* @return Path to the no backup folder, with fallback behavior for API < 21.
*/
internal suspend fun Context.getNoBackupFilesDirCompat(): File {
val dir = if (AndroidApiVersion.isAtLeastL) {
getNoBackupFilesDirSuspend()
} else {
File(getDataDirCompatSuspend(), FAKE_NO_BACKUP_FOLDER)
}
if (!dir.existsSuspend()) {
if (!dir.mkdirsSuspend()) {
error("no_backup directory does not exist and could not be created")
}
}
if (!dir.canWriteSuspend()) {
error("${dir.absolutePath} directory is not writable")
}
return dir
}

View File

@ -28,11 +28,11 @@ internal inline fun <R> tryWarn(
} catch (t: Throwable) {
val shouldThrowAnyway = (
unlessContains != null &&
(t.message?.toLowerCase()?.contains(unlessContains.toLowerCase()) == true)
(t.message?.lowercase()?.contains(unlessContains.lowercase()) == true)
) ||
(
ifContains != null &&
(t.message?.toLowerCase()?.contains(ifContains.toLowerCase()) == false)
(t.message?.lowercase()?.contains(ifContains.lowercase()) == false)
)
if (shouldThrowAnyway) {
throw t

View File

@ -6,4 +6,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
suspend fun File.deleteSuspend() = withContext(Dispatchers.IO) { delete() }
internal suspend fun File.deleteSuspend() = withContext(Dispatchers.IO) { delete() }
internal suspend fun File.existsSuspend() = withContext(Dispatchers.IO) { exists() }
internal suspend fun File.mkdirsSuspend() = withContext(Dispatchers.IO) { mkdirs() }
internal suspend fun File.canWriteSuspend() = withContext(Dispatchers.IO) { canWrite() }
internal suspend fun File.renameToSuspend(dest: File) = withContext(Dispatchers.IO) { renameTo(dest) }
suspend fun File.deleteRecursivelySuspend() = withContext(Dispatchers.IO) { deleteRecursively() }

View File

@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal.ext.android
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ObsoleteCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
@ -12,6 +13,9 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
/* Adapted from ComputableLiveData */
// TODO [#658] https://github.com/zcash/zcash-android-wallet-sdk/issues/658
@Suppress("DEPRECATION")
@OptIn(ObsoleteCoroutinesApi::class)
abstract class ComputableFlow<T>(dispatcher: CoroutineDispatcher = Dispatchers.IO) {
private val computationScope: CoroutineScope = CoroutineScope(dispatcher + SupervisorJob())
private val computationChannel: ConflatedBroadcastChannel<T> = ConflatedBroadcastChannel()

View File

@ -49,8 +49,7 @@ fun <Key, Value> DataSource.Factory<Key, Value>.toFlowPagedList(
*
* @see FlowPagedListBuilder
*/
@SuppressLint("RestrictedApi")
inline fun <Key, Value> DataSource.Factory<Key, Value>.toFlowPagedList(
fun <Key, Value> DataSource.Factory<Key, Value>.toFlowPagedList(
pageSize: Int,
initialLoadKey: Key? = null,
boundaryCallback: PagedList.BoundaryCallback<Value>? = null,

View File

@ -1,6 +1,5 @@
package cash.z.ecc.android.sdk.internal.ext.android
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import androidx.paging.Config
@ -29,7 +28,7 @@ class FlowPagedListBuilder<Key, Value>(
* Creates a FlowPagedListBuilder with required parameters.
*
* @param dataSourceFactory DataSource factory providing DataSource generations.
* @param config Paging configuration.
* @param pageSize List page size.
*/
constructor(dataSourceFactory: DataSource.Factory<Key, Value>, pageSize: Int) : this(
dataSourceFactory,
@ -44,7 +43,6 @@ class FlowPagedListBuilder<Key, Value>(
*
* @return The Flow of PagedLists
*/
@SuppressLint("RestrictedApi")
fun build(): Flow<List<Value>> {
return object : ComputableFlow<List<Value>>(fetchContext) {
private lateinit var dataSource: DataSource<Key, Value>
@ -56,6 +54,7 @@ class FlowPagedListBuilder<Key, Value>(
var initializeKey = initialLoadKey
if (::list.isInitialized) {
twig("list is initialized")
@Suppress("UNCHECKED_CAST")
initializeKey = list.lastKey as Key
}

View File

@ -2,8 +2,8 @@ package cash.z.ecc.android.sdk.internal.transaction
import android.content.Context
import androidx.paging.PagedList
import androidx.room.Room
import androidx.room.RoomDatabase
import cash.z.ecc.android.sdk.db.commonDatabaseBuilder
import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.SdkDispatchers
@ -21,6 +21,7 @@ import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import java.io.File
/**
* Example of a repository that leverages the Room paging library to return a [PagedList] of
@ -123,7 +124,7 @@ internal class PagedTransactionRepository private constructor(
): PagedTransactionRepository {
initMissingDatabases(rustBackend, birthday, viewingKeys)
val db = buildDatabase(appContext.applicationContext, rustBackend.pathDataDb)
val db = buildDatabase(appContext.applicationContext, rustBackend.dataDbFile)
applyKeyMigrations(rustBackend, overwriteVks, viewingKeys)
return PagedTransactionRepository(zcashNetwork, db, pageSize)
@ -132,9 +133,13 @@ internal class PagedTransactionRepository private constructor(
/**
* Build the database and apply migrations.
*/
private suspend fun buildDatabase(context: Context, databasePath: String): DerivedDataDb {
private suspend fun buildDatabase(context: Context, databaseFile: File): DerivedDataDb {
twig("Building dataDb and applying migrations")
return Room.databaseBuilder(context, DerivedDataDb::class.java, databasePath)
return commonDatabaseBuilder(
context,
DerivedDataDb::class.java,
databaseFile
)
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.setQueryExecutor(SdkExecutors.DATABASE_IO)
.setTransactionExecutor(SdkExecutors.DATABASE_IO)
@ -146,6 +151,7 @@ internal class PagedTransactionRepository private constructor(
.build().also {
// TODO: document why we do this. My guess is to catch database issues early or to trigger migrations--I forget why it was added but there was a good reason?
withContext(SdkDispatchers.DATABASE_IO) {
// TODO [#649]: https://github.com/zcash/zcash-android-wallet-sdk/issues/649
it.openHelper.writableDatabase.beginTransaction()
it.openHelper.writableDatabase.endTransaction()
}
@ -173,7 +179,7 @@ internal class PagedTransactionRepository private constructor(
private suspend fun maybeCreateDataDb(rustBackend: RustBackend) {
tryWarn("Warning: did not create dataDb. It probably already exists.") {
rustBackend.initDataDb()
twig("Initialized wallet for first run file: ${rustBackend.pathDataDb}")
twig("Initialized wallet for first run file: ${rustBackend.dataDbFile}")
}
}
@ -192,7 +198,7 @@ internal class PagedTransactionRepository private constructor(
rustBackend.initBlocksTable(checkpoint)
twig("seeded the database with sapling tree at height ${checkpoint.height}")
}
twig("database file: ${rustBackend.pathDataDb}")
twig("database file: ${rustBackend.dataDbFile}")
}
/**

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