Merge branch 'fast-spendability'
This commit is contained in:
commit
4d99ad10ac
|
@ -199,7 +199,7 @@ jobs:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Build and test
|
- name: Build and test
|
||||||
timeout-minutes: 25
|
timeout-minutes: 30
|
||||||
run: |
|
run: |
|
||||||
./gradlew test
|
./gradlew test
|
||||||
- name: Collect Artifacts
|
- name: Collect Artifacts
|
||||||
|
@ -292,7 +292,7 @@ jobs:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Build and test
|
- name: Build and test
|
||||||
timeout-minutes: 25
|
timeout-minutes: 30
|
||||||
env:
|
env:
|
||||||
ORG_GRADLE_PROJECT_ZCASH_EMULATOR_WTF_API_KEY: ${{ secrets.EMULATOR_WTF_API_KEY }}
|
ORG_GRADLE_PROJECT_ZCASH_EMULATOR_WTF_API_KEY: ${{ secrets.EMULATOR_WTF_API_KEY }}
|
||||||
run: |
|
run: |
|
||||||
|
@ -336,7 +336,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
keytool -genkey -v -keystore $SIGNING_KEY_PATH -keypass android -storepass android -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 100000 -dname "CN=, OU=, O=Test, L=, S=, C=" -noprompt
|
keytool -genkey -v -keystore $SIGNING_KEY_PATH -keypass android -storepass android -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 100000 -dname "CN=, OU=, O=Test, L=, S=, C=" -noprompt
|
||||||
- name: Build
|
- name: Build
|
||||||
timeout-minutes: 30
|
timeout-minutes: 35
|
||||||
env:
|
env:
|
||||||
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PATH: ${{ format('{0}/release.jks', env.home) }}
|
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PATH: ${{ format('{0}/release.jks', env.home) }}
|
||||||
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: android
|
ORG_GRADLE_PROJECT_ZCASH_RELEASE_KEYSTORE_PASSWORD: android
|
||||||
|
@ -395,7 +395,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: Demo app release binaries
|
name: Demo app release binaries
|
||||||
- name: Robo test
|
- name: Robo test
|
||||||
timeout-minutes: 15
|
timeout-minutes: 20
|
||||||
env:
|
env:
|
||||||
# Path depends on `release_build` job, plus path of `Download a single artifact` step
|
# Path depends on `release_build` job, plus path of `Download a single artifact` step
|
||||||
BINARIES_ZIP_PATH: binaries.zip
|
BINARIES_ZIP_PATH: binaries.zip
|
||||||
|
|
|
@ -51,6 +51,7 @@ captures/
|
||||||
.idea/workspace.xml
|
.idea/workspace.xml
|
||||||
.idea/protoeditor.xml
|
.idea/protoeditor.xml
|
||||||
.idea/appInsightsSettings.xml
|
.idea/appInsightsSettings.xml
|
||||||
|
.idea/migrations.xml
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
# Keystore files
|
# Keystore files
|
||||||
|
|
56
CHANGELOG.md
56
CHANGELOG.md
|
@ -1,6 +1,58 @@
|
||||||
# Change Log
|
# Changelog
|
||||||
|
All notable changes to this library will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.0.0-rc.1] - 2023-09-12
|
||||||
|
|
||||||
|
### Notable Changes
|
||||||
|
|
||||||
|
- `CompactBlockProcessor` now processes compact blocks from the lightwalletd
|
||||||
|
server using the **Spend-before-Sync** algorithm, which allows scanning of
|
||||||
|
wallet blocks to be performed in arbitrary order and optimized to make it
|
||||||
|
possible to spend received notes without waiting for synchronization to be
|
||||||
|
complete. This feature shortens the time until a wallet's spendable balance
|
||||||
|
can be used.
|
||||||
|
- The block synchronization mechanism is additionally about one-third faster
|
||||||
|
thanks to an optimized `CompactBlockProcessor.SYNC_BATCH_SIZE` (issue **#1206**).
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `CompactBlockProcessor.ProcessorInfo.lastSyncHeight` no longer had a
|
||||||
|
well-defined meaning after implementation of the **SpendBeforeSync**
|
||||||
|
synchronization algorithm and has been removed.
|
||||||
|
`CompactBlockProcessor.ProcessorInfo.overallSyncRange` provides related
|
||||||
|
information.
|
||||||
|
- `CompactBlockProcessor.ProcessorInfo.isSyncing`. Use `Synchronizer.status` instead.
|
||||||
|
- `CompactBlockProcessor.ProcessorInfo.syncProgress`. Use `Synchronizer.progress` instead.
|
||||||
|
- `alsoClearBlockCache` parameter from rewind functions of `Synchronizer` and
|
||||||
|
`CompactBlockProcessor`, as it has no effect on the current behaviour of
|
||||||
|
these functions.
|
||||||
|
- Internally, we removed access to the shared block table from the Kotlin
|
||||||
|
layer, which resulted in eliminating these APIs:
|
||||||
|
- `SdkSynchronizer.findBlockHash()`
|
||||||
|
- `SdkSynchronizer.findBlockHashAsHex()`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `CompactBlockProcessor.quickRewind()` and `CompactBlockProcessor.rewindToNearestHeight()`
|
||||||
|
now might fail due to internal changes in getting scanned height. Thus, these
|
||||||
|
functions now return `Boolean` results.
|
||||||
|
- `Synchronizer.new()` and `PersistableWallet` APIs require a new
|
||||||
|
`walletInitMode` parameter of type `WalletInitMode`, which describes wallet
|
||||||
|
initialization mode. See related function and sealed class documentation.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `Synchronizer.getMemos()` now correctly returns a flow of strings for sent
|
||||||
|
and received transactions. Issue **#1154**.
|
||||||
|
- `CompactBlockProcessor` now triggers transaction polling while block
|
||||||
|
synchronization is in progress as expected. Clients will be notified shortly
|
||||||
|
after every new transaction is discovered via `Synchronizer.transactions`
|
||||||
|
API. Issue **#1170**.
|
||||||
|
|
||||||
|
## [1.21.0-beta01]
|
||||||
|
|
||||||
## 1.21.0-beta01
|
|
||||||
Note: This is the last _1.x_ version release. The upcoming version _2.0_ brings the **Spend-before-Sync** feature,
|
Note: This is the last _1.x_ version release. The upcoming version _2.0_ brings the **Spend-before-Sync** feature,
|
||||||
which speeds up discovering the wallet's spendable balance.
|
which speeds up discovering the wallet's spendable balance.
|
||||||
|
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2017-2021 Electric Coin Company
|
Copyright (c) 2017-2023 Electric Coin Company
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -53,3 +53,9 @@ Note that we aim for the main branch of this repository to be stable and releasa
|
||||||
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. 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. During builds, a warning will be printed that says "Unable to detect AGP versions for included builds. All projects in the build should use the same AGP version." This can be safely ignored. The version under build-conventions is the same as the version used elsewhere in the application.
|
||||||
1. Android Studio will warn about the Gradle checksum. This is a [known issue](https://github.com/gradle/gradle/issues/9361) and can be safely ignored.
|
1. Android Studio will warn about the Gradle checksum. This is a [known issue](https://github.com/gradle/gradle/issues/9361) and can be safely ignored.
|
||||||
|
|
||||||
|
## Unstable Features
|
||||||
|
### Spend-before-Sync compact blocks synchronization algorithm
|
||||||
|
`CompactBlockProcessor` now processes compact blocks from the lightwalletd server in non-linear order with the
|
||||||
|
**Spend-before-Sync** algorithm. This feature speeds up discovering the wallet's spendable balance. Please note that
|
||||||
|
this new block synchronization algorithm is still under development.
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,19 +12,21 @@ rust-version = "1.60"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
failure = "0.1"
|
failure = "0.1"
|
||||||
hdwallet = "0.3.1"
|
hdwallet = "0.4"
|
||||||
hdwallet-bitcoin = "0.3"
|
hdwallet-bitcoin = "0.4"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
jni = { version = "0.20", default-features = false }
|
jni = { version = "0.20", default-features = false }
|
||||||
|
prost = "0.12"
|
||||||
|
rusqlite = "0.29"
|
||||||
schemer = "0.2"
|
schemer = "0.2"
|
||||||
secp256k1 = "0.21"
|
secp256k1 = "0.26"
|
||||||
secrecy = "0.8"
|
secrecy = "0.8"
|
||||||
zcash_address = "0.2"
|
zcash_address = "0.3"
|
||||||
zcash_client_backend = { version = "0.9", features = ["transparent-inputs", "unstable"] }
|
zcash_client_backend = { version = "=0.10.0-rc.2", features = ["transparent-inputs", "unstable"] }
|
||||||
zcash_client_sqlite = { version = "0.7.1", features = ["transparent-inputs", "unstable"] }
|
zcash_client_sqlite = { version = "=0.8.0-rc.3", features = ["transparent-inputs", "unstable"] }
|
||||||
zcash_primitives = "0.11"
|
zcash_primitives = "=0.13.0-rc.1"
|
||||||
zcash_proofs = "0.11"
|
zcash_proofs = "=0.13.0-rc.1"
|
||||||
orchard = { version = "0.4", default-features = false }
|
orchard = { version = "0.6", default-features = false }
|
||||||
|
|
||||||
# Initialization
|
# Initialization
|
||||||
rayon = "1.7"
|
rayon = "1.7"
|
||||||
|
@ -36,7 +38,7 @@ tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
|
|
||||||
# Conditional access to newer NDK features
|
# Conditional access to newer NDK features
|
||||||
dlopen2 = "0.4"
|
dlopen2 = "0.6"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
||||||
## Uncomment this to test librustzcash changes locally
|
## Uncomment this to test librustzcash changes locally
|
||||||
|
|
|
@ -104,6 +104,9 @@ dependencies {
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
testImplementation(libs.kotlin.test)
|
||||||
|
|
||||||
androidTestImplementation(libs.androidx.multidex)
|
androidTestImplementation(libs.androidx.multidex)
|
||||||
androidTestImplementation(libs.androidx.test.runner)
|
androidTestImplementation(libs.androidx.test.runner)
|
||||||
androidTestImplementation(libs.androidx.test.junit)
|
androidTestImplementation(libs.androidx.test.junit)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package cash.z.ecc.android.sdk.internal.jni
|
package cash.z.ecc.android.sdk.internal.jni
|
||||||
|
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertContentEquals
|
import kotlin.test.assertContentEquals
|
||||||
|
@ -15,7 +14,6 @@ class RustDerivationToolTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
fun create_spending_key_does_not_mutate_passed_bytes() = runTest {
|
fun create_spending_key_does_not_mutate_passed_bytes() = runTest {
|
||||||
val bytesOne = Mnemonics.MnemonicCode(SEED_PHRASE).toEntropy()
|
val bytesOne = Mnemonics.MnemonicCode(SEED_PHRASE).toEntropy()
|
||||||
val bytesTwo = Mnemonics.MnemonicCode(SEED_PHRASE).toEntropy()
|
val bytesTwo = Mnemonics.MnemonicCode(SEED_PHRASE).toEntropy()
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package cash.z.ecc.android.sdk.internal
|
package cash.z.ecc.android.sdk.internal
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniScanProgress
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniScanRange
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot
|
||||||
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
|
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
|
||||||
import java.lang.RuntimeException
|
|
||||||
import kotlin.jvm.Throws
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contract defining the exposed capabilities of the Rust backend.
|
* Contract defining the exposed capabilities of the Rust backend.
|
||||||
|
@ -25,37 +26,34 @@ interface Backend {
|
||||||
to: String,
|
to: String,
|
||||||
value: Long,
|
value: Long,
|
||||||
memo: ByteArray? = byteArrayOf()
|
memo: ByteArray? = byteArrayOf()
|
||||||
): Long
|
): ByteArray
|
||||||
|
|
||||||
suspend fun shieldToAddress(
|
suspend fun shieldToAddress(
|
||||||
account: Int,
|
account: Int,
|
||||||
unifiedSpendingKey: ByteArray,
|
unifiedSpendingKey: ByteArray,
|
||||||
memo: ByteArray? = byteArrayOf()
|
memo: ByteArray? = byteArrayOf()
|
||||||
): Long
|
): ByteArray
|
||||||
|
|
||||||
suspend fun decryptAndStoreTransaction(tx: ByteArray)
|
suspend fun decryptAndStoreTransaction(tx: ByteArray)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param keys A list of UFVKs to initialize the accounts table with
|
* Sets up the internal structure of the data database.
|
||||||
|
*
|
||||||
|
* If `seed` is `null`, database migrations will be attempted without it.
|
||||||
|
*
|
||||||
|
* @return 0 if successful, 1 if the seed must be provided in order to execute the requested migrations, or -1
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
* @throws RuntimeException as a common indicator of the operation failure
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
*/
|
*/
|
||||||
@Throws(RuntimeException::class)
|
@Throws(RuntimeException::class)
|
||||||
suspend fun initAccountsTable(vararg keys: String)
|
suspend fun initDataDb(seed: ByteArray?): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws RuntimeException as a common indicator of the operation failure
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
*/
|
*/
|
||||||
@Throws(RuntimeException::class)
|
@Throws(RuntimeException::class)
|
||||||
suspend fun initBlocksTable(
|
suspend fun createAccount(seed: ByteArray, treeState: ByteArray, recoverUntil: Long?): JniUnifiedSpendingKey
|
||||||
checkpointHeight: Long,
|
|
||||||
checkpointHash: String,
|
|
||||||
checkpointTime: Long,
|
|
||||||
checkpointSaplingTree: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun initDataDb(seed: ByteArray?): Int
|
|
||||||
|
|
||||||
suspend fun createAccount(seed: ByteArray): JniUnifiedSpendingKey
|
|
||||||
|
|
||||||
fun isValidShieldedAddr(addr: String): Boolean
|
fun isValidShieldedAddr(addr: String): Boolean
|
||||||
|
|
||||||
|
@ -71,6 +69,10 @@ interface Backend {
|
||||||
|
|
||||||
suspend fun listTransparentReceivers(account: Int): List<String>
|
suspend fun listTransparentReceivers(account: Int): List<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
@Throws(RuntimeException::class)
|
||||||
suspend fun getBalance(account: Int): Long
|
suspend fun getBalance(account: Int): Long
|
||||||
|
|
||||||
fun getBranchIdForHeight(height: Long): Long
|
fun getBranchIdForHeight(height: Long): Long
|
||||||
|
@ -79,14 +81,12 @@ interface Backend {
|
||||||
* @throws RuntimeException as a common indicator of the operation failure
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
*/
|
*/
|
||||||
@Throws(RuntimeException::class)
|
@Throws(RuntimeException::class)
|
||||||
suspend fun getReceivedMemoAsUtf8(idNote: Long): String?
|
suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int): String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws RuntimeException as a common indicator of the operation failure
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
*/
|
*/
|
||||||
@Throws(RuntimeException::class)
|
@Throws(RuntimeException::class)
|
||||||
suspend fun getSentMemoAsUtf8(idNote: Long): String?
|
|
||||||
|
|
||||||
suspend fun getVerifiedBalance(account: Int): Long
|
suspend fun getVerifiedBalance(account: Int): Long
|
||||||
|
|
||||||
suspend fun getNearestRewindHeight(height: Long): Long
|
suspend fun getNearestRewindHeight(height: Long): Long
|
||||||
|
@ -101,7 +101,57 @@ interface Backend {
|
||||||
* @throws RuntimeException as a common indicator of the operation failure
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
*/
|
*/
|
||||||
@Throws(RuntimeException::class)
|
@Throws(RuntimeException::class)
|
||||||
suspend fun scanBlocks(limit: Long?)
|
suspend fun putSaplingSubtreeRoots(
|
||||||
|
startIndex: Long,
|
||||||
|
roots: List<JniSubtreeRoot>,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
@Throws(RuntimeException::class)
|
||||||
|
suspend fun updateChainTip(height: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the height to which the wallet has been fully scanned.
|
||||||
|
*
|
||||||
|
* This is the height for which the wallet has fully trial-decrypted this and all
|
||||||
|
* preceding blocks above the wallet's birthday height.
|
||||||
|
*
|
||||||
|
* @return The height to which the wallet has been fully scanned, or Null if no blocks have been scanned.
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
suspend fun getFullyScannedHeight(): Long?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum height that the wallet has scanned.
|
||||||
|
*
|
||||||
|
* If the wallet is fully synced, this will be equivalent to `getFullyScannedHeight`;
|
||||||
|
* otherwise the maximal scanned height is likely to be greater than the fully scanned
|
||||||
|
* height due to the fact that out-of-order scanning can leave gaps.
|
||||||
|
*
|
||||||
|
* @return The maximum height that the wallet has scanned, or Null if no blocks have been scanned.
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
suspend fun getMaxScannedHeight(): Long?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
@Throws(RuntimeException::class)
|
||||||
|
suspend fun getScanProgress(): JniScanProgress?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
@Throws(RuntimeException::class)
|
||||||
|
suspend fun suggestScanRanges(): List<JniScanRange>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
@Throws(RuntimeException::class)
|
||||||
|
suspend fun scanBlocks(fromHeight: Long, limit: Long)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws RuntimeException as a common indicator of the operation failure
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
@ -112,18 +162,12 @@ interface Backend {
|
||||||
/**
|
/**
|
||||||
* @return The latest height in the CompactBlock cache metadata DB, or Null if no blocks have been cached.
|
* @return The latest height in the CompactBlock cache metadata DB, or Null if no blocks have been cached.
|
||||||
*/
|
*/
|
||||||
suspend fun getLatestHeight(): Long?
|
suspend fun getLatestCacheHeight(): Long?
|
||||||
|
|
||||||
suspend fun findBlockMetadata(height: Long): JniBlockMeta?
|
suspend fun findBlockMetadata(height: Long): JniBlockMeta?
|
||||||
|
|
||||||
suspend fun rewindBlockMetadataToHeight(height: Long)
|
suspend fun rewindBlockMetadataToHeight(height: Long)
|
||||||
|
|
||||||
/**
|
|
||||||
* @param limit The limit provides an efficient way how to restrict the portion of blocks, which will be validated.
|
|
||||||
* @return Null if successful. If an error occurs, the height will be the blockheight where the error was detected.
|
|
||||||
*/
|
|
||||||
suspend fun validateCombinedChainOrErrorHeight(limit: Long?): Long?
|
|
||||||
|
|
||||||
suspend fun getVerifiedTransparentBalance(address: String): Long
|
suspend fun getVerifiedTransparentBalance(address: String): Long
|
||||||
suspend fun getTotalTransparentBalance(address: String): Long
|
suspend fun getTotalTransparentBalance(address: String): Long
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
@file:Suppress("ktlint:standard:filename")
|
||||||
|
|
||||||
|
package cash.z.ecc.android.sdk.internal.ext
|
||||||
|
|
||||||
|
fun Long.isInUIntRange(): Boolean {
|
||||||
|
return this >= 0L && this <= UInt.MAX_VALUE.toLong()
|
||||||
|
}
|
|
@ -5,6 +5,9 @@ import cash.z.ecc.android.sdk.internal.SdkDispatchers
|
||||||
import cash.z.ecc.android.sdk.internal.ext.deleteRecursivelySuspend
|
import cash.z.ecc.android.sdk.internal.ext.deleteRecursivelySuspend
|
||||||
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
|
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
|
||||||
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniScanProgress
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniScanRange
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot
|
||||||
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
|
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -66,42 +69,17 @@ class RustBackend private constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createAccount(seed: ByteArray): JniUnifiedSpendingKey {
|
override suspend fun createAccount(
|
||||||
|
seed: ByteArray,
|
||||||
|
treeState: ByteArray,
|
||||||
|
recoverUntil: Long?
|
||||||
|
): JniUnifiedSpendingKey {
|
||||||
return withContext(SdkDispatchers.DATABASE_IO) {
|
return withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
createAccount(
|
createAccount(
|
||||||
dataDbFile.absolutePath,
|
dataDbFile.absolutePath,
|
||||||
seed,
|
seed,
|
||||||
networkId = networkId
|
treeState,
|
||||||
)
|
recoverUntil ?: -1,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param keys A list of UFVKs to initialize the accounts table with
|
|
||||||
*/
|
|
||||||
override suspend fun initAccountsTable(vararg keys: String) {
|
|
||||||
return withContext(SdkDispatchers.DATABASE_IO) {
|
|
||||||
initAccountsTableWithKeys(
|
|
||||||
dataDbFile.absolutePath,
|
|
||||||
keys,
|
|
||||||
networkId = networkId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun initBlocksTable(
|
|
||||||
checkpointHeight: Long,
|
|
||||||
checkpointHash: String,
|
|
||||||
checkpointTime: Long,
|
|
||||||
checkpointSaplingTree: String,
|
|
||||||
) {
|
|
||||||
return withContext(SdkDispatchers.DATABASE_IO) {
|
|
||||||
initBlocksTable(
|
|
||||||
dataDbFile.absolutePath,
|
|
||||||
checkpointHeight,
|
|
||||||
checkpointHash,
|
|
||||||
checkpointTime,
|
|
||||||
checkpointSaplingTree,
|
|
||||||
networkId = networkId
|
networkId = networkId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -154,20 +132,12 @@ class RustBackend private constructor(
|
||||||
return longValue
|
return longValue
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getReceivedMemoAsUtf8(idNote: Long) =
|
override suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int) =
|
||||||
withContext(SdkDispatchers.DATABASE_IO) {
|
withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
getReceivedMemoAsUtf8(
|
getMemoAsUtf8(
|
||||||
dataDbFile.absolutePath,
|
dataDbFile.absolutePath,
|
||||||
idNote,
|
txId,
|
||||||
networkId = networkId
|
outputIndex,
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getSentMemoAsUtf8(idNote: Long) =
|
|
||||||
withContext(SdkDispatchers.DATABASE_IO) {
|
|
||||||
getSentMemoAsUtf8(
|
|
||||||
dataDbFile.absolutePath,
|
|
||||||
idNote,
|
|
||||||
networkId = networkId
|
networkId = networkId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -180,9 +150,9 @@ class RustBackend private constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLatestHeight() =
|
override suspend fun getLatestCacheHeight() =
|
||||||
withContext(SdkDispatchers.DATABASE_IO) {
|
withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
val height = getLatestHeight(fsBlockDbRoot.absolutePath)
|
val height = getLatestCacheHeight(fsBlockDbRoot.absolutePath)
|
||||||
|
|
||||||
if (-1L == height) {
|
if (-1L == height) {
|
||||||
null
|
null
|
||||||
|
@ -207,22 +177,6 @@ class RustBackend private constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun validateCombinedChainOrErrorHeight(limit: Long?) =
|
|
||||||
withContext(SdkDispatchers.DATABASE_IO) {
|
|
||||||
val validationResult = validateCombinedChain(
|
|
||||||
dbCachePath = fsBlockDbRoot.absolutePath,
|
|
||||||
dbDataPath = dataDbFile.absolutePath,
|
|
||||||
limit = limit ?: -1,
|
|
||||||
networkId = networkId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-1L == validationResult) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
validationResult
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getVerifiedTransparentBalance(address: String): Long =
|
override suspend fun getVerifiedTransparentBalance(address: String): Long =
|
||||||
withContext(SdkDispatchers.DATABASE_IO) {
|
withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
getVerifiedTransparentBalance(
|
getVerifiedTransparentBalance(
|
||||||
|
@ -264,12 +218,79 @@ class RustBackend private constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun scanBlocks(limit: Long?) {
|
override suspend fun putSaplingSubtreeRoots(
|
||||||
|
startIndex: Long,
|
||||||
|
roots: List<JniSubtreeRoot>,
|
||||||
|
) = withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
|
putSaplingSubtreeRoots(
|
||||||
|
dataDbFile.absolutePath,
|
||||||
|
startIndex,
|
||||||
|
roots.toTypedArray(),
|
||||||
|
networkId = networkId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateChainTip(height: Long) =
|
||||||
|
withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
|
updateChainTip(
|
||||||
|
dataDbFile.absolutePath,
|
||||||
|
height,
|
||||||
|
networkId = networkId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getFullyScannedHeight() =
|
||||||
|
withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
|
val height = getFullyScannedHeight(
|
||||||
|
dataDbFile.absolutePath,
|
||||||
|
networkId = networkId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-1L == height) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMaxScannedHeight() =
|
||||||
|
withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
|
val height = getMaxScannedHeight(
|
||||||
|
dataDbFile.absolutePath,
|
||||||
|
networkId = networkId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-1L == height) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getScanProgress(): JniScanProgress? =
|
||||||
|
withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
|
getScanProgress(
|
||||||
|
dataDbFile.absolutePath,
|
||||||
|
networkId = networkId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun suggestScanRanges(): List<JniScanRange> {
|
||||||
|
return withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
|
suggestScanRanges(
|
||||||
|
dataDbFile.absolutePath,
|
||||||
|
networkId = networkId
|
||||||
|
).asList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun scanBlocks(fromHeight: Long, limit: Long) {
|
||||||
return withContext(SdkDispatchers.DATABASE_IO) {
|
return withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
scanBlocks(
|
scanBlocks(
|
||||||
fsBlockDbRoot.absolutePath,
|
fsBlockDbRoot.absolutePath,
|
||||||
dataDbFile.absolutePath,
|
dataDbFile.absolutePath,
|
||||||
limit ?: -1,
|
fromHeight,
|
||||||
|
limit,
|
||||||
networkId = networkId
|
networkId = networkId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -290,7 +311,7 @@ class RustBackend private constructor(
|
||||||
to: String,
|
to: String,
|
||||||
value: Long,
|
value: Long,
|
||||||
memo: ByteArray?
|
memo: ByteArray?
|
||||||
): Long = withContext(SdkDispatchers.DATABASE_IO) {
|
): ByteArray = withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
createToAddress(
|
createToAddress(
|
||||||
dataDbFile.absolutePath,
|
dataDbFile.absolutePath,
|
||||||
unifiedSpendingKey,
|
unifiedSpendingKey,
|
||||||
|
@ -308,7 +329,7 @@ class RustBackend private constructor(
|
||||||
account: Int,
|
account: Int,
|
||||||
unifiedSpendingKey: ByteArray,
|
unifiedSpendingKey: ByteArray,
|
||||||
memo: ByteArray?
|
memo: ByteArray?
|
||||||
): Long {
|
): ByteArray {
|
||||||
return withContext(SdkDispatchers.DATABASE_IO) {
|
return withContext(SdkDispatchers.DATABASE_IO) {
|
||||||
shieldToAddress(
|
shieldToAddress(
|
||||||
dataDbFile.absolutePath,
|
dataDbFile.absolutePath,
|
||||||
|
@ -403,25 +424,13 @@ class RustBackend private constructor(
|
||||||
private external fun initDataDb(dbDataPath: String, seed: ByteArray?, networkId: Int): Int
|
private external fun initDataDb(dbDataPath: String, seed: ByteArray?, networkId: Int): Int
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun initAccountsTableWithKeys(
|
private external fun createAccount(
|
||||||
dbDataPath: String,
|
dbDataPath: String,
|
||||||
ufvks: Array<out String>,
|
seed: ByteArray,
|
||||||
|
treeState: ByteArray,
|
||||||
|
recoverUntil: Long,
|
||||||
networkId: Int
|
networkId: Int
|
||||||
)
|
): JniUnifiedSpendingKey
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Suppress("LongParameterList")
|
|
||||||
private external fun initBlocksTable(
|
|
||||||
dbDataPath: String,
|
|
||||||
height: Long,
|
|
||||||
hash: String,
|
|
||||||
time: Long,
|
|
||||||
saplingTree: String,
|
|
||||||
networkId: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
private external fun createAccount(dbDataPath: String, seed: ByteArray, networkId: Int): JniUnifiedSpendingKey
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun getCurrentAddress(
|
private external fun getCurrentAddress(
|
||||||
|
@ -439,8 +448,7 @@ class RustBackend private constructor(
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun listTransparentReceivers(dbDataPath: String, account: Int, networkId: Int): Array<String>
|
private external fun listTransparentReceivers(dbDataPath: String, account: Int, networkId: Int): Array<String>
|
||||||
|
|
||||||
fun validateUnifiedSpendingKey(bytes: ByteArray) =
|
fun validateUnifiedSpendingKey(bytes: ByteArray) = isValidSpendingKey(bytes)
|
||||||
isValidSpendingKey(bytes)
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun isValidSpendingKey(bytes: ByteArray): Boolean
|
private external fun isValidSpendingKey(bytes: ByteArray): Boolean
|
||||||
|
@ -465,16 +473,10 @@ class RustBackend private constructor(
|
||||||
): Long
|
): Long
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun getReceivedMemoAsUtf8(
|
private external fun getMemoAsUtf8(
|
||||||
dbDataPath: String,
|
dbDataPath: String,
|
||||||
idNote: Long,
|
txId: ByteArray,
|
||||||
networkId: Int
|
outputIndex: Int,
|
||||||
): String?
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
private external fun getSentMemoAsUtf8(
|
|
||||||
dbDataPath: String,
|
|
||||||
dNote: Long,
|
|
||||||
networkId: Int
|
networkId: Int
|
||||||
): String?
|
): String?
|
||||||
|
|
||||||
|
@ -485,7 +487,7 @@ class RustBackend private constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun getLatestHeight(dbCachePath: String): Long
|
private external fun getLatestCacheHeight(dbCachePath: String): Long
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun findBlockMetadata(
|
private external fun findBlockMetadata(
|
||||||
|
@ -499,14 +501,6 @@ class RustBackend private constructor(
|
||||||
height: Long
|
height: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
private external fun validateCombinedChain(
|
|
||||||
dbCachePath: String,
|
|
||||||
dbDataPath: String,
|
|
||||||
limit: Long,
|
|
||||||
networkId: Int
|
|
||||||
): Long
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun getNearestRewindHeight(
|
private external fun getNearestRewindHeight(
|
||||||
dbDataPath: String,
|
dbDataPath: String,
|
||||||
|
@ -521,10 +515,50 @@ class RustBackend private constructor(
|
||||||
networkId: Int
|
networkId: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun putSaplingSubtreeRoots(
|
||||||
|
dbDataPath: String,
|
||||||
|
startIndex: Long,
|
||||||
|
roots: Array<JniSubtreeRoot>,
|
||||||
|
networkId: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun updateChainTip(
|
||||||
|
dbDataPath: String,
|
||||||
|
height: Long,
|
||||||
|
networkId: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun getFullyScannedHeight(
|
||||||
|
dbDataPath: String,
|
||||||
|
networkId: Int
|
||||||
|
): Long
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun getMaxScannedHeight(
|
||||||
|
dbDataPath: String,
|
||||||
|
networkId: Int
|
||||||
|
): Long
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun getScanProgress(
|
||||||
|
dbDataPath: String,
|
||||||
|
networkId: Int
|
||||||
|
): JniScanProgress?
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private external fun suggestScanRanges(
|
||||||
|
dbDataPath: String,
|
||||||
|
networkId: Int
|
||||||
|
): Array<JniScanRange>
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun scanBlocks(
|
private external fun scanBlocks(
|
||||||
dbCachePath: String,
|
dbCachePath: String,
|
||||||
dbDataPath: String,
|
dbDataPath: String,
|
||||||
|
fromHeight: Long,
|
||||||
limit: Long,
|
limit: Long,
|
||||||
networkId: Int
|
networkId: Int
|
||||||
)
|
)
|
||||||
|
@ -548,7 +582,7 @@ class RustBackend private constructor(
|
||||||
outputParamsPath: String,
|
outputParamsPath: String,
|
||||||
networkId: Int,
|
networkId: Int,
|
||||||
useZip317Fees: Boolean
|
useZip317Fees: Boolean
|
||||||
): Long
|
): ByteArray
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
@ -560,7 +594,7 @@ class RustBackend private constructor(
|
||||||
outputParamsPath: String,
|
outputParamsPath: String,
|
||||||
networkId: Int,
|
networkId: Int,
|
||||||
useZip317Fees: Boolean
|
useZip317Fees: Boolean
|
||||||
): Long
|
): ByteArray
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private external fun branchIdForHeight(height: Long, networkId: Int): Long
|
private external fun branchIdForHeight(height: Long, networkId: Int): Long
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package cash.z.ecc.android.sdk.internal.model
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.isInUIntRange
|
||||||
import co.electriccoin.lightwallet.client.model.CompactBlockUnsafe
|
import co.electriccoin.lightwallet.client.model.CompactBlockUnsafe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,20 +24,18 @@ class JniBlockMeta(
|
||||||
init {
|
init {
|
||||||
// We require some of the parameters below to be in the range of unsigned integer, because of the Rust layer
|
// We require some of the parameters below to be in the range of unsigned integer, because of the Rust layer
|
||||||
// implementation.
|
// implementation.
|
||||||
require(UINT_RANGE.contains(height)) {
|
require(height.isInUIntRange()) {
|
||||||
"Height $height is outside of allowed range $UINT_RANGE"
|
"Height $height is outside of allowed UInt range"
|
||||||
}
|
}
|
||||||
require(UINT_RANGE.contains(saplingOutputsCount)) {
|
require(saplingOutputsCount.isInUIntRange()) {
|
||||||
"SaplingOutputsCount $saplingOutputsCount is outside of allowed range $UINT_RANGE"
|
"SaplingOutputsCount $saplingOutputsCount is outside of allowed UInt range"
|
||||||
}
|
}
|
||||||
require(UINT_RANGE.contains(orchardOutputsCount)) {
|
require(orchardOutputsCount.isInUIntRange()) {
|
||||||
"SaplingOutputsCount $orchardOutputsCount is outside of allowed range $UINT_RANGE"
|
"SaplingOutputsCount $orchardOutputsCount is outside of allowed UInt range"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val UINT_RANGE = 0.toLong()..UInt.MAX_VALUE.toLong()
|
|
||||||
|
|
||||||
fun new(block: CompactBlockUnsafe): JniBlockMeta {
|
fun new(block: CompactBlockUnsafe): JniBlockMeta {
|
||||||
return JniBlockMeta(
|
return JniBlockMeta(
|
||||||
height = block.height,
|
height = block.height,
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves as cross layer (Kotlin, Rust) communication class.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException unless (numerator is nonnegative, denominator is
|
||||||
|
* positive, and the represented ratio is in the range 0.0 to 1.0 inclusive).
|
||||||
|
* @param numerator the numerator of the progress ratio
|
||||||
|
* @param denominator the denominator of the progress ratio
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
class JniScanProgress(
|
||||||
|
val numerator: Long,
|
||||||
|
val denominator: Long
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
require(numerator >= 0L) {
|
||||||
|
"Numerator $numerator is outside of allowed range [0, Long.MAX_VALUE]"
|
||||||
|
}
|
||||||
|
require(denominator >= 1L) {
|
||||||
|
"Denominator $denominator is outside of allowed range [1, Long.MAX_VALUE]"
|
||||||
|
}
|
||||||
|
require(numerator.toFloat().div(denominator) >= 0f) {
|
||||||
|
"Result of ${numerator.toFloat()}/$denominator is outside of allowed range"
|
||||||
|
}
|
||||||
|
require(numerator.toFloat().div(denominator) <= 1f) {
|
||||||
|
"Result of ${numerator.toFloat()}/$denominator is outside of allowed range"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.isInUIntRange
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves as cross layer (Kotlin, Rust) communication class.
|
||||||
|
*
|
||||||
|
* @param startHeight the minimum height in the range (inclusive) - although it's type Long, it needs to be a UInt
|
||||||
|
* @param endHeight the maximum height in the range (exclusive) - although it's type Long, it needs to be a UInt
|
||||||
|
* @param priority the priority of the range for scanning
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
class JniScanRange(
|
||||||
|
val startHeight: Long,
|
||||||
|
val endHeight: Long,
|
||||||
|
val priority: Long
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
// We require some of the parameters below to be in the range of unsigned integer, because of the Rust layer
|
||||||
|
// implementation.
|
||||||
|
require(startHeight.isInUIntRange()) {
|
||||||
|
"Height $startHeight is outside of allowed UInt range"
|
||||||
|
}
|
||||||
|
require(endHeight.isInUIntRange()) {
|
||||||
|
"Height $endHeight is outside of allowed UInt range"
|
||||||
|
}
|
||||||
|
require(endHeight >= startHeight) {
|
||||||
|
"End height $endHeight must be greater than start height $startHeight."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.isInUIntRange
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves as cross layer (Kotlin, Rust) communication class.
|
||||||
|
*
|
||||||
|
* @param rootHash the subtree's root hash
|
||||||
|
* @param completingBlockHeight the block height in which the subtree was completed - although it's type Long, it needs
|
||||||
|
* to be in UInt range
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
class JniSubtreeRoot(
|
||||||
|
val rootHash: ByteArray,
|
||||||
|
val completingBlockHeight: Long
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
// We require some of the parameters below to be in the range of unsigned integer,
|
||||||
|
// because of the Rust layer implementation.
|
||||||
|
require(completingBlockHeight.isInUIntRange()) {
|
||||||
|
"Height $completingBlockHeight is outside of allowed UInt range"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun new(rootHash: ByteArray, completingBlockHeight: Long): JniSubtreeRoot {
|
||||||
|
return JniSubtreeRoot(
|
||||||
|
rootHash = rootHash,
|
||||||
|
completingBlockHeight = completingBlockHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
use std::num::NonZeroU32;
|
||||||
use std::panic;
|
use std::panic;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
|
@ -11,34 +11,40 @@ use jni::{
|
||||||
sys::{jboolean, jbyteArray, jint, jlong, jobject, jobjectArray, jstring, JNI_FALSE, JNI_TRUE},
|
sys::{jboolean, jbyteArray, jint, jlong, jobject, jobjectArray, jstring, JNI_FALSE, JNI_TRUE},
|
||||||
JNIEnv,
|
JNIEnv,
|
||||||
};
|
};
|
||||||
|
use prost::Message;
|
||||||
use schemer::MigratorError;
|
use schemer::MigratorError;
|
||||||
use secrecy::{ExposeSecret, SecretVec};
|
use secrecy::{ExposeSecret, SecretVec};
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::reload;
|
use tracing_subscriber::reload;
|
||||||
use zcash_address::{ToAddress, ZcashAddress};
|
use zcash_address::{ToAddress, ZcashAddress};
|
||||||
|
use zcash_client_backend::data_api::{
|
||||||
|
scanning::{ScanPriority, ScanRange},
|
||||||
|
AccountBirthday, NoteId, Ratio, ShieldedProtocol,
|
||||||
|
};
|
||||||
use zcash_client_backend::keys::{DecodingError, UnifiedSpendingKey};
|
use zcash_client_backend::keys::{DecodingError, UnifiedSpendingKey};
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
address::{RecipientAddress, UnifiedAddress},
|
address::{RecipientAddress, UnifiedAddress},
|
||||||
data_api::{
|
data_api::{
|
||||||
chain::{self, scan_cached_blocks, validate_chain},
|
chain::{scan_cached_blocks, CommitmentTreeRoot},
|
||||||
wallet::{
|
wallet::{
|
||||||
decrypt_and_store_transaction, input_selection::GreedyInputSelector,
|
decrypt_and_store_transaction, input_selection::GreedyInputSelector,
|
||||||
shield_transparent_funds, spend,
|
shield_transparent_funds, spend,
|
||||||
},
|
},
|
||||||
WalletRead, WalletWrite,
|
WalletCommitmentTrees, WalletRead, WalletWrite,
|
||||||
},
|
},
|
||||||
encoding::AddressCodec,
|
encoding::AddressCodec,
|
||||||
fees::DustOutputPolicy,
|
fees::DustOutputPolicy,
|
||||||
keys::{Era, UnifiedFullViewingKey},
|
keys::{Era, UnifiedFullViewingKey},
|
||||||
|
proto::service::TreeState,
|
||||||
wallet::{OvkPolicy, WalletTransparentOutput},
|
wallet::{OvkPolicy, WalletTransparentOutput},
|
||||||
zip321::{Payment, TransactionRequest},
|
zip321::{Payment, TransactionRequest},
|
||||||
};
|
};
|
||||||
use zcash_client_sqlite::chain::init::init_blockmeta_db;
|
use zcash_client_sqlite::chain::init::init_blockmeta_db;
|
||||||
use zcash_client_sqlite::{
|
use zcash_client_sqlite::{
|
||||||
chain::BlockMeta,
|
chain::BlockMeta,
|
||||||
wallet::init::{init_accounts_table, init_blocks_table, init_wallet_db, WalletMigrationError},
|
wallet::init::{init_wallet_db, WalletMigrationError},
|
||||||
FsBlockDb, NoteId, WalletDb,
|
FsBlockDb, WalletDb,
|
||||||
};
|
};
|
||||||
use zcash_primitives::consensus::Network::{MainNetwork, TestNetwork};
|
use zcash_primitives::consensus::Network::{MainNetwork, TestNetwork};
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
|
@ -46,9 +52,11 @@ use zcash_primitives::{
|
||||||
consensus::{BlockHeight, BranchId, Network, Parameters},
|
consensus::{BlockHeight, BranchId, Network, Parameters},
|
||||||
legacy::{Script, TransparentAddress},
|
legacy::{Script, TransparentAddress},
|
||||||
memo::{Memo, MemoBytes},
|
memo::{Memo, MemoBytes},
|
||||||
|
merkle_tree::HashSer,
|
||||||
|
sapling,
|
||||||
transaction::{
|
transaction::{
|
||||||
components::{amount::NonNegativeAmount, Amount, OutPoint, TxOut},
|
components::{amount::NonNegativeAmount, Amount, OutPoint, TxOut},
|
||||||
Transaction,
|
Transaction, TxId,
|
||||||
},
|
},
|
||||||
zip32::{AccountId, DiversifierIndex},
|
zip32::{AccountId, DiversifierIndex},
|
||||||
};
|
};
|
||||||
|
@ -68,7 +76,8 @@ mod zip317 {
|
||||||
pub(super) use zcash_primitives::transaction::fees::zip317::*;
|
pub(super) use zcash_primitives::transaction::fees::zip317::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ANCHOR_OFFSET: u32 = 10;
|
const ANCHOR_OFFSET_U32: u32 = 10;
|
||||||
|
const ANCHOR_OFFSET: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(ANCHOR_OFFSET_U32) };
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
fn print_debug_state() {
|
fn print_debug_state() {
|
||||||
|
@ -84,7 +93,7 @@ fn wallet_db<P: Parameters>(
|
||||||
env: &JNIEnv<'_>,
|
env: &JNIEnv<'_>,
|
||||||
params: P,
|
params: P,
|
||||||
db_data: JString<'_>,
|
db_data: JString<'_>,
|
||||||
) -> Result<WalletDb<P>, failure::Error> {
|
) -> Result<WalletDb<rusqlite::Connection, P>, failure::Error> {
|
||||||
WalletDb::for_path(utils::java_string_to_rust(&env, db_data), params)
|
WalletDb::for_path(utils::java_string_to_rust(&env, db_data), params)
|
||||||
.map_err(|e| format_err!("Error opening wallet database connection: {}", e))
|
.map_err(|e| format_err!("Error opening wallet database connection: {}", e))
|
||||||
}
|
}
|
||||||
|
@ -252,16 +261,32 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_cr
|
||||||
_: JClass<'_>,
|
_: JClass<'_>,
|
||||||
db_data: JString<'_>,
|
db_data: JString<'_>,
|
||||||
seed: jbyteArray,
|
seed: jbyteArray,
|
||||||
|
treestate: jbyteArray,
|
||||||
|
recover_until: jlong,
|
||||||
network_id: jint,
|
network_id: jint,
|
||||||
) -> jobject {
|
) -> jobject {
|
||||||
|
use zcash_client_backend::data_api::BirthdayError;
|
||||||
|
|
||||||
let res = panic::catch_unwind(|| {
|
let res = panic::catch_unwind(|| {
|
||||||
let network = parse_network(network_id as u32)?;
|
let network = parse_network(network_id as u32)?;
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let mut db_data = wallet_db(&env, network, db_data)?;
|
||||||
let seed = SecretVec::new(env.convert_byte_array(seed).unwrap());
|
let seed = SecretVec::new(env.convert_byte_array(seed).unwrap());
|
||||||
|
let treestate = TreeState::decode(&env.convert_byte_array(treestate).unwrap()[..])
|
||||||
|
.map_err(|e| format_err!("Invalid TreeState: {}", e))?;
|
||||||
|
let recover_until = recover_until.try_into().ok();
|
||||||
|
|
||||||
let mut db_ops = db_data.get_update_ops()?;
|
let birthday =
|
||||||
let (account, usk) = db_ops
|
AccountBirthday::from_treestate(treestate, recover_until).map_err(|e| match e {
|
||||||
.create_account(&seed)
|
BirthdayError::HeightInvalid(e) => {
|
||||||
|
format_err!("Invalid TreeState: Invalid height: {}", e)
|
||||||
|
}
|
||||||
|
BirthdayError::Decode(e) => {
|
||||||
|
format_err!("Invalid TreeState: Invalid frontier encoding: {}", e)
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (account, usk) = db_data
|
||||||
|
.create_account(&seed, birthday)
|
||||||
.map_err(|e| format_err!("Error while initializing accounts: {}", e))?;
|
.map_err(|e| format_err!("Error while initializing accounts: {}", e))?;
|
||||||
|
|
||||||
encode_usk(&env, account, usk)
|
encode_usk(&env, account, usk)
|
||||||
|
@ -269,52 +294,6 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_cr
|
||||||
unwrap_exc_or(&env, res, ptr::null_mut())
|
unwrap_exc_or(&env, res, ptr::null_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialises the data database with the given set of unified full viewing keys.
|
|
||||||
///
|
|
||||||
/// This should only be used in special cases for implementing wallet recovery; prefer
|
|
||||||
/// `RustBackend.createAccount` for normal account creation purposes.
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initAccountsTableWithKeys(
|
|
||||||
env: JNIEnv<'_>,
|
|
||||||
_: JClass<'_>,
|
|
||||||
db_data: JString<'_>,
|
|
||||||
ufvks_arr: jobjectArray,
|
|
||||||
network_id: jint,
|
|
||||||
) -> jboolean {
|
|
||||||
let res = panic::catch_unwind(|| {
|
|
||||||
let network = parse_network(network_id as u32)?;
|
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
|
||||||
// TODO: avoid all this unwrapping and also surface errors, better
|
|
||||||
let count = env.get_array_length(ufvks_arr).unwrap();
|
|
||||||
let ufvks = (0..count)
|
|
||||||
.map(|i| env.get_object_array_element(ufvks_arr, i))
|
|
||||||
.map(|jstr| utils::java_string_to_rust(&env, jstr.unwrap().into()))
|
|
||||||
.map(|ufvkstr| {
|
|
||||||
UnifiedFullViewingKey::decode(&network, &ufvkstr).map_err(|e| {
|
|
||||||
if e.starts_with("UFVK is for network") {
|
|
||||||
let (network_name, other) = if network == TestNetwork {
|
|
||||||
("testnet", "mainnet")
|
|
||||||
} else {
|
|
||||||
("mainnet", "testnet")
|
|
||||||
};
|
|
||||||
format_err!("Error: Wrong network! Unable to decode viewing key for {}. Check whether this is a key for {}.", network_name, other)
|
|
||||||
} else {
|
|
||||||
format_err!("Invalid Unified Full Viewing Key: {}", e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.enumerate() // TODO: Pass account IDs across the FFI.
|
|
||||||
.map(|(i, res)| res.map(|ufvk| (AccountId::from(i as u32), ufvk)))
|
|
||||||
.collect::<Result<HashMap<_,_>, _>>()?;
|
|
||||||
|
|
||||||
match init_accounts_table(&db_data, &ufvks) {
|
|
||||||
Ok(()) => Ok(JNI_TRUE),
|
|
||||||
Err(e) => Err(format_err!("Error while initializing accounts: {}", e)),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
unwrap_exc_or(&env, res, JNI_FALSE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Derives and returns a unified spending key from the given seed for the given account ID.
|
/// Derives and returns a unified spending key from the given seed for the given account ID.
|
||||||
///
|
///
|
||||||
/// Returns the newly created [ZIP 316] account identifier, along with the binary encoding
|
/// Returns the newly created [ZIP 316] account identifier, along with the binary encoding
|
||||||
|
@ -472,48 +451,6 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivation
|
||||||
unwrap_exc_or(&env, res, ptr::null_mut())
|
unwrap_exc_or(&env, res, ptr::null_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initBlocksTable(
|
|
||||||
env: JNIEnv<'_>,
|
|
||||||
_: JClass<'_>,
|
|
||||||
db_data: JString<'_>,
|
|
||||||
height: jlong,
|
|
||||||
hash_string: JString<'_>,
|
|
||||||
time: jlong,
|
|
||||||
sapling_tree_string: JString<'_>,
|
|
||||||
network_id: jint,
|
|
||||||
) -> jboolean {
|
|
||||||
let res = panic::catch_unwind(|| {
|
|
||||||
let network = parse_network(network_id as u32)?;
|
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
|
||||||
let hash = {
|
|
||||||
let mut hash = hex::decode(utils::java_string_to_rust(&env, hash_string)).unwrap();
|
|
||||||
hash.reverse();
|
|
||||||
BlockHash::from_slice(&hash)
|
|
||||||
};
|
|
||||||
let time = if time >= 0 && time <= jlong::from(u32::max_value()) {
|
|
||||||
time as u32
|
|
||||||
} else {
|
|
||||||
return Err(format_err!("time argument must fit in a u32"));
|
|
||||||
};
|
|
||||||
let sapling_tree =
|
|
||||||
hex::decode(utils::java_string_to_rust(&env, sapling_tree_string)).unwrap();
|
|
||||||
|
|
||||||
debug!("initializing blocks table with height {}", height);
|
|
||||||
match init_blocks_table(
|
|
||||||
&db_data,
|
|
||||||
(height as u32).try_into()?,
|
|
||||||
hash,
|
|
||||||
time,
|
|
||||||
&sapling_tree,
|
|
||||||
) {
|
|
||||||
Ok(()) => Ok(JNI_TRUE),
|
|
||||||
Err(e) => Err(format_err!("Error while initializing blocks table: {}", e)),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
unwrap_exc_or(&env, res, JNI_FALSE)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getCurrentAddress(
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getCurrentAddress(
|
||||||
env: JNIEnv<'_>,
|
env: JNIEnv<'_>,
|
||||||
|
@ -720,27 +657,20 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_ge
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let db_data = wallet_db(&env, network, db_data)?;
|
||||||
let account = AccountId::from(u32::try_from(accountj)?);
|
let account = AccountId::from(u32::try_from(accountj)?);
|
||||||
|
|
||||||
// We query the unverified balance including unmined transactions. Shielded notes
|
if let Some(wallet_summary) = db_data
|
||||||
// in unmined transactions are never spendable, but this ensures that the balance
|
.get_wallet_summary(0)
|
||||||
// reported to users does not drop temporarily in a way that they don't expect.
|
.map_err(|e| format_err!("Error while fetching balance: {}", e))?
|
||||||
// `getVerifiedBalance` requires `ANCHOR_OFFSET` confirmations, which means it
|
{
|
||||||
// always shows a spendable balance.
|
wallet_summary
|
||||||
let min_confirmations = 0;
|
.account_balances()
|
||||||
|
.get(&account)
|
||||||
(&db_data)
|
.ok_or_else(|| format_err!("Unknown account"))
|
||||||
.get_target_and_anchor_heights(min_confirmations)
|
.map(|balances| Amount::from(balances.sapling_balance.total()).into())
|
||||||
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
|
} else {
|
||||||
.and_then(|opt_anchor| {
|
// `None` means that the caller has not yet called `updateChainTip` on a
|
||||||
opt_anchor
|
// brand-new wallet, so we can assume the balance is zero.
|
||||||
.map(|(_, a)| a)
|
Ok(0)
|
||||||
.ok_or(format_err!("Anchor height not available; scan required."))
|
}
|
||||||
})
|
|
||||||
.and_then(|anchor| {
|
|
||||||
(&db_data)
|
|
||||||
.get_balance_at(account, anchor)
|
|
||||||
.map_err(|e| format_err!("Error while fetching verified balance: {}", e))
|
|
||||||
})
|
|
||||||
.map(|amount| amount.into())
|
|
||||||
});
|
});
|
||||||
unwrap_exc_or(&env, res, -1)
|
unwrap_exc_or(&env, res, -1)
|
||||||
}
|
}
|
||||||
|
@ -797,16 +727,14 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_ge
|
||||||
let addr = utils::java_string_to_rust(&env, address);
|
let addr = utils::java_string_to_rust(&env, address);
|
||||||
let taddr = TransparentAddress::decode(&network, &addr).unwrap();
|
let taddr = TransparentAddress::decode(&network, &addr).unwrap();
|
||||||
|
|
||||||
// We select all transparent funds including unmined coins, as that is the same
|
let min_confirmations = NonZeroU32::new(1).unwrap();
|
||||||
// set of UTXOs we shield from.
|
|
||||||
let min_confirmations = 0;
|
|
||||||
|
|
||||||
let amount = (&db_data)
|
let amount = (&db_data)
|
||||||
.get_target_and_anchor_heights(min_confirmations)
|
.get_target_and_anchor_heights(min_confirmations)
|
||||||
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
|
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
|
||||||
.and_then(|opt_anchor| {
|
.and_then(|opt_anchor| {
|
||||||
opt_anchor
|
opt_anchor
|
||||||
.map(|(_, a)| a)
|
.map(|(target, _)| target) // Include unconfirmed funds.
|
||||||
.ok_or(format_err!("Anchor height not available; scan required."))
|
.ok_or(format_err!("Anchor height not available; scan required."))
|
||||||
})
|
})
|
||||||
.and_then(|anchor| {
|
.and_then(|anchor| {
|
||||||
|
@ -838,70 +766,49 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_ge
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let db_data = wallet_db(&env, network, db_data)?;
|
||||||
let account = AccountId::from(u32::try_from(account)?);
|
let account = AccountId::from(u32::try_from(account)?);
|
||||||
|
|
||||||
(&db_data)
|
if let Some(wallet_summary) = db_data
|
||||||
.get_target_and_anchor_heights(ANCHOR_OFFSET)
|
.get_wallet_summary(ANCHOR_OFFSET_U32)
|
||||||
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
|
.map_err(|e| format_err!("Error while fetching verified balance: {}", e))?
|
||||||
.and_then(|opt_anchor| {
|
{
|
||||||
opt_anchor
|
wallet_summary
|
||||||
.map(|(_, a)| a)
|
.account_balances()
|
||||||
.ok_or(format_err!("Anchor height not available; scan required."))
|
.get(&account)
|
||||||
})
|
.ok_or_else(|| format_err!("Unknown account"))
|
||||||
.and_then(|anchor| {
|
.map(|balances| Amount::from(balances.sapling_balance.spendable_value).into())
|
||||||
(&db_data)
|
} else {
|
||||||
.get_balance_at(account, anchor)
|
// `None` means that the caller has not yet called `updateChainTip` on a
|
||||||
.map_err(|e| format_err!("Error while fetching verified balance: {}", e))
|
// brand-new wallet, so we can assume the balance is zero.
|
||||||
})
|
Ok(0)
|
||||||
.map(|amount| amount.into())
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
unwrap_exc_or(&env, res, -1)
|
unwrap_exc_or(&env, res, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getReceivedMemoAsUtf8(
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getMemoAsUtf8(
|
||||||
env: JNIEnv<'_>,
|
env: JNIEnv<'_>,
|
||||||
_: JClass<'_>,
|
_: JClass<'_>,
|
||||||
db_data: JString<'_>,
|
db_data: JString<'_>,
|
||||||
id_note: jlong,
|
txid_bytes: jbyteArray,
|
||||||
|
output_index: jint,
|
||||||
network_id: jint,
|
network_id: jint,
|
||||||
) -> jstring {
|
) -> jstring {
|
||||||
let res = panic::catch_unwind(|| {
|
let res = panic::catch_unwind(|| {
|
||||||
let network = parse_network(network_id as u32)?;
|
let network = parse_network(network_id as u32)?;
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let db_data = wallet_db(&env, network, db_data)?;
|
||||||
|
|
||||||
let memo = (&db_data)
|
let txid_bytes = env.convert_byte_array(txid_bytes)?;
|
||||||
.get_memo(NoteId::ReceivedNoteId(id_note))
|
let txid = TxId::read(&txid_bytes[..])?;
|
||||||
.map_err(|e| format_err!("An error occurred retrieving the memo, {}", e))
|
let output_index = u16::try_from(output_index)?;
|
||||||
.and_then(|memo| match memo {
|
|
||||||
Memo::Empty => Ok("".to_string()),
|
|
||||||
Memo::Text(memo) => Ok(memo.into()),
|
|
||||||
_ => Err(format_err!("This memo does not contain UTF-8 text")),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let output = env.new_string(memo).expect("Couldn't create Java string!");
|
|
||||||
Ok(output.into_raw())
|
|
||||||
});
|
|
||||||
unwrap_exc_or(&env, res, ptr::null_mut())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getSentMemoAsUtf8(
|
|
||||||
env: JNIEnv<'_>,
|
|
||||||
_: JClass<'_>,
|
|
||||||
db_data: JString<'_>,
|
|
||||||
id_note: jlong,
|
|
||||||
network_id: jint,
|
|
||||||
) -> jstring {
|
|
||||||
let res = panic::catch_unwind(|| {
|
|
||||||
let network = parse_network(network_id as u32)?;
|
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
|
||||||
|
|
||||||
let memo = (&db_data)
|
let memo = (&db_data)
|
||||||
.get_memo(NoteId::SentNoteId(id_note))
|
.get_memo(NoteId::new(txid, ShieldedProtocol::Sapling, output_index))
|
||||||
.map_err(|e| format_err!("An error occurred retrieving the memo, {}", e))
|
.map_err(|e| format_err!("An error occurred retrieving the memo, {}", e))
|
||||||
.and_then(|memo| match memo {
|
.and_then(|memo| match memo {
|
||||||
Memo::Empty => Ok("".to_string()),
|
Some(Memo::Empty) => Ok("".to_string()),
|
||||||
Memo::Text(memo) => Ok(memo.into()),
|
Some(Memo::Text(memo)) => Ok(memo.into()),
|
||||||
|
None => Err(format_err!("Memo not available")),
|
||||||
_ => Err(format_err!("This memo does not contain UTF-8 text")),
|
_ => Err(format_err!("This memo does not contain UTF-8 text")),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -984,7 +891,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_wr
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getLatestHeight(
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getLatestCacheHeight(
|
||||||
env: JNIEnv<'_>,
|
env: JNIEnv<'_>,
|
||||||
_: JClass<'_>,
|
_: JClass<'_>,
|
||||||
fsblockdb_root: JString<'_>,
|
fsblockdb_root: JString<'_>,
|
||||||
|
@ -1051,44 +958,6 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_re
|
||||||
unwrap_exc_or(&env, res, ())
|
unwrap_exc_or(&env, res, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_validateCombinedChain(
|
|
||||||
env: JNIEnv<'_>,
|
|
||||||
_: JClass<'_>,
|
|
||||||
db_cache: JString<'_>,
|
|
||||||
db_data: JString<'_>,
|
|
||||||
limit: jlong,
|
|
||||||
network_id: jint,
|
|
||||||
) -> jlong {
|
|
||||||
let res = panic::catch_unwind(|| {
|
|
||||||
let network = parse_network(network_id as u32)?;
|
|
||||||
let block_db = block_db(&env, db_cache)?;
|
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
|
||||||
let validate_limit = u32::try_from(limit).ok();
|
|
||||||
|
|
||||||
let validate_from = (&db_data)
|
|
||||||
.get_max_height_hash()
|
|
||||||
.map_err(|e| format_err!("Error while validating chain: {}", e))?;
|
|
||||||
|
|
||||||
let val_res = validate_chain(&block_db, validate_from, validate_limit);
|
|
||||||
|
|
||||||
if let Err(e) = val_res {
|
|
||||||
match e {
|
|
||||||
chain::error::Error::Chain(e) => {
|
|
||||||
let upper_bound_u32 = u32::from(e.at_height());
|
|
||||||
Ok(upper_bound_u32 as i64)
|
|
||||||
}
|
|
||||||
_ => Err(format_err!("Error while validating chain: {:?}", e)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// All blocks are valid, so "highest invalid block height" is below genesis.
|
|
||||||
Ok(-1)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
unwrap_exc_or(&env, res, 0) as jlong
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getNearestRewindHeight(
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getNearestRewindHeight(
|
||||||
env: JNIEnv<'_>,
|
env: JNIEnv<'_>,
|
||||||
|
@ -1135,8 +1004,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_re
|
||||||
) -> jboolean {
|
) -> jboolean {
|
||||||
let res = panic::catch_unwind(|| {
|
let res = panic::catch_unwind(|| {
|
||||||
let network = parse_network(network_id as u32)?;
|
let network = parse_network(network_id as u32)?;
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let mut db_data = wallet_db(&env, network, db_data)?;
|
||||||
let mut db_data = db_data.get_update_ops()?;
|
|
||||||
|
|
||||||
let height = BlockHeight::try_from(height)?;
|
let height = BlockHeight::try_from(height)?;
|
||||||
db_data
|
db_data
|
||||||
|
@ -1148,23 +1016,247 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_re
|
||||||
unwrap_exc_or(&env, res, JNI_FALSE)
|
unwrap_exc_or(&env, res, JNI_FALSE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_sapling_subtree_root(
|
||||||
|
env: &JNIEnv<'_>,
|
||||||
|
obj: JObject<'_>,
|
||||||
|
) -> Result<CommitmentTreeRoot<sapling::Node>, failure::Error> {
|
||||||
|
let long_as_u32 = |name| -> Result<u32, failure::Error> {
|
||||||
|
Ok(u32::try_from(env.get_field(obj, name, "J")?.j()?)?)
|
||||||
|
};
|
||||||
|
|
||||||
|
fn byte_array(
|
||||||
|
env: &JNIEnv<'_>,
|
||||||
|
obj: JObject<'_>,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Vec<u8>, failure::Error> {
|
||||||
|
let field = env.get_field(obj, name, "[B")?.l()?.into_raw();
|
||||||
|
Ok(env.convert_byte_array(field)?[..].try_into()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CommitmentTreeRoot::from_parts(
|
||||||
|
BlockHeight::from_u32(long_as_u32("completingBlockHeight")?),
|
||||||
|
sapling::Node::read(&byte_array(env, obj, "rootHash")?[..])?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_putSaplingSubtreeRoots(
|
||||||
|
env: JNIEnv<'_>,
|
||||||
|
_: JClass<'_>,
|
||||||
|
db_data: JString<'_>,
|
||||||
|
start_index: jlong,
|
||||||
|
roots: jobjectArray,
|
||||||
|
network_id: jint,
|
||||||
|
) -> jboolean {
|
||||||
|
let res = panic::catch_unwind(|| {
|
||||||
|
let network = parse_network(network_id as u32)?;
|
||||||
|
let mut db_data = wallet_db(&env, network, db_data)?;
|
||||||
|
|
||||||
|
let start_index = if start_index >= 0 {
|
||||||
|
start_index as u64
|
||||||
|
} else {
|
||||||
|
return Err(format_err!("Start index must be nonnegative."));
|
||||||
|
};
|
||||||
|
let roots = {
|
||||||
|
let count = env.get_array_length(roots).unwrap();
|
||||||
|
(0..count)
|
||||||
|
.map(|i| {
|
||||||
|
env.get_object_array_element(roots, i)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
.and_then(|jobj| decode_sapling_subtree_root(&env, jobj))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
db_data
|
||||||
|
.put_sapling_subtree_roots(start_index, &roots)
|
||||||
|
.map(|()| JNI_TRUE)
|
||||||
|
.map_err(|e| format_err!("Error while storing Sapling subtree roots: {}", e))
|
||||||
|
});
|
||||||
|
|
||||||
|
unwrap_exc_or(&env, res, JNI_FALSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_updateChainTip(
|
||||||
|
env: JNIEnv<'_>,
|
||||||
|
_: JClass<'_>,
|
||||||
|
db_data: JString<'_>,
|
||||||
|
height: jlong,
|
||||||
|
network_id: jint,
|
||||||
|
) -> jboolean {
|
||||||
|
let res = panic::catch_unwind(|| {
|
||||||
|
let network = parse_network(network_id as u32)?;
|
||||||
|
let mut db_data = wallet_db(&env, network, db_data)?;
|
||||||
|
let height = BlockHeight::try_from(height)?;
|
||||||
|
|
||||||
|
db_data
|
||||||
|
.update_chain_tip(height)
|
||||||
|
.map(|()| JNI_TRUE)
|
||||||
|
.map_err(|e| format_err!("Error while updating chain tip to height {}: {}", height, e))
|
||||||
|
});
|
||||||
|
|
||||||
|
unwrap_exc_or(&env, res, JNI_FALSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getFullyScannedHeight(
|
||||||
|
env: JNIEnv<'_>,
|
||||||
|
_: JClass<'_>,
|
||||||
|
db_data: JString<'_>,
|
||||||
|
network_id: jint,
|
||||||
|
) -> jlong {
|
||||||
|
let res = panic::catch_unwind(|| {
|
||||||
|
let network = parse_network(network_id as u32)?;
|
||||||
|
let db_data = wallet_db(&env, network, db_data)?;
|
||||||
|
|
||||||
|
match db_data.block_fully_scanned() {
|
||||||
|
Ok(Some(metadata)) => Ok(i64::from(u32::from(metadata.block_height()))),
|
||||||
|
// Use -1 to return null across the FFI.
|
||||||
|
Ok(None) => Ok(-1),
|
||||||
|
Err(e) => Err(format_err!(
|
||||||
|
"Failed to read block metadata from WalletDb: {:?}",
|
||||||
|
e
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
unwrap_exc_or(&env, res, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getMaxScannedHeight(
|
||||||
|
env: JNIEnv<'_>,
|
||||||
|
_: JClass<'_>,
|
||||||
|
db_data: JString<'_>,
|
||||||
|
network_id: jint,
|
||||||
|
) -> jlong {
|
||||||
|
let res = panic::catch_unwind(|| {
|
||||||
|
let network = parse_network(network_id as u32)?;
|
||||||
|
let db_data = wallet_db(&env, network, db_data)?;
|
||||||
|
|
||||||
|
match db_data.block_max_scanned() {
|
||||||
|
Ok(Some(metadata)) => Ok(i64::from(u32::from(metadata.block_height()))),
|
||||||
|
// Use -1 to return null across the FFI.
|
||||||
|
Ok(None) => Ok(-1),
|
||||||
|
Err(e) => Err(format_err!(
|
||||||
|
"Failed to read block metadata from WalletDb: {:?}",
|
||||||
|
e
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
unwrap_exc_or(&env, res, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a `JniScanProgress` object, provided that numerator is nonnegative, denominator
|
||||||
|
/// is positive, and the represented ratio is in the range 0.0 to 1.0 inclusive.
|
||||||
|
fn encode_scan_progress(env: &JNIEnv<'_>, progress: Ratio<u64>) -> Result<jobject, failure::Error> {
|
||||||
|
let output = env.new_object(
|
||||||
|
"cash/z/ecc/android/sdk/internal/model/JniScanProgress",
|
||||||
|
"(JJ)V",
|
||||||
|
&[
|
||||||
|
JValue::Long(*progress.numerator() as i64),
|
||||||
|
JValue::Long(*progress.denominator() as i64),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
Ok(output.into_raw())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getScanProgress(
|
||||||
|
env: JNIEnv<'_>,
|
||||||
|
_: JClass<'_>,
|
||||||
|
db_data: JString<'_>,
|
||||||
|
network_id: jint,
|
||||||
|
) -> jobject {
|
||||||
|
let res = panic::catch_unwind(|| {
|
||||||
|
let network = parse_network(network_id as u32)?;
|
||||||
|
let db_data = wallet_db(&env, network, db_data)?;
|
||||||
|
|
||||||
|
match db_data
|
||||||
|
.get_wallet_summary(0)
|
||||||
|
.map_err(|e| format_err!("Error while fetching scan progress: {}", e))?
|
||||||
|
.and_then(|summary| summary.scan_progress().filter(|r| r.denominator() > &0))
|
||||||
|
{
|
||||||
|
Some(progress) => encode_scan_progress(&env, progress),
|
||||||
|
None => Ok(ptr::null_mut()),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
unwrap_exc_or(&env, res, ptr::null_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_scan_range<'a>(
|
||||||
|
env: &JNIEnv<'a>,
|
||||||
|
scan_range: ScanRange,
|
||||||
|
) -> jni::errors::Result<JObject<'a>> {
|
||||||
|
let priority = match scan_range.priority() {
|
||||||
|
ScanPriority::Ignored => 0,
|
||||||
|
ScanPriority::Scanned => 10,
|
||||||
|
ScanPriority::Historic => 20,
|
||||||
|
ScanPriority::OpenAdjacent => 30,
|
||||||
|
ScanPriority::FoundNote => 40,
|
||||||
|
ScanPriority::ChainTip => 50,
|
||||||
|
ScanPriority::Verify => 60,
|
||||||
|
};
|
||||||
|
env.new_object(
|
||||||
|
"cash/z/ecc/android/sdk/internal/model/JniScanRange",
|
||||||
|
"(JJJ)V",
|
||||||
|
&[
|
||||||
|
JValue::Long(i64::from(u32::from(scan_range.block_range().start))),
|
||||||
|
JValue::Long(i64::from(u32::from(scan_range.block_range().end))),
|
||||||
|
JValue::Long(priority),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_suggestScanRanges(
|
||||||
|
env: JNIEnv<'_>,
|
||||||
|
_: JClass<'_>,
|
||||||
|
db_data: JString<'_>,
|
||||||
|
network_id: jint,
|
||||||
|
) -> jobjectArray {
|
||||||
|
let res = panic::catch_unwind(|| {
|
||||||
|
let network = parse_network(network_id as u32)?;
|
||||||
|
let db_data = wallet_db(&env, network, db_data)?;
|
||||||
|
|
||||||
|
let ranges = db_data
|
||||||
|
.suggest_scan_ranges()
|
||||||
|
.map_err(|e| format_err!("Error while fetching suggested scan ranges: {}", e))?;
|
||||||
|
|
||||||
|
Ok(utils::rust_vec_to_java(
|
||||||
|
&env,
|
||||||
|
ranges,
|
||||||
|
"cash/z/ecc/android/sdk/internal/model/JniScanRange",
|
||||||
|
|env, scan_range| encode_scan_range(env, scan_range),
|
||||||
|
|env| {
|
||||||
|
encode_scan_range(
|
||||||
|
env,
|
||||||
|
ScanRange::from_parts((0.into())..(0.into()), ScanPriority::Scanned),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
))
|
||||||
|
});
|
||||||
|
unwrap_exc_or(&env, res, ptr::null_mut())
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlocks(
|
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlocks(
|
||||||
env: JNIEnv<'_>,
|
env: JNIEnv<'_>,
|
||||||
_: JClass<'_>,
|
_: JClass<'_>,
|
||||||
db_cache: JString<'_>,
|
db_cache: JString<'_>,
|
||||||
db_data: JString<'_>,
|
db_data: JString<'_>,
|
||||||
|
from_height: jlong,
|
||||||
limit: jlong,
|
limit: jlong,
|
||||||
network_id: jint,
|
network_id: jint,
|
||||||
) -> jboolean {
|
) -> jboolean {
|
||||||
let res = panic::catch_unwind(|| {
|
let res = panic::catch_unwind(|| {
|
||||||
let network = parse_network(network_id as u32)?;
|
let network = parse_network(network_id as u32)?;
|
||||||
let db_cache = block_db(&env, db_cache)?;
|
let db_cache = block_db(&env, db_cache)?;
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let mut db_data = wallet_db(&env, network, db_data)?;
|
||||||
let mut db_data = db_data.get_update_ops()?;
|
let from_height = BlockHeight::try_from(from_height)?;
|
||||||
let limit = u32::try_from(limit).ok();
|
let limit = usize::try_from(limit)?;
|
||||||
|
|
||||||
match scan_cached_blocks(&network, &db_cache, &mut db_data, limit) {
|
match scan_cached_blocks(&network, &db_cache, &mut db_data, from_height, limit) {
|
||||||
Ok(()) => Ok(JNI_TRUE),
|
Ok(()) => Ok(JNI_TRUE),
|
||||||
Err(e) => Err(format_err!(
|
Err(e) => Err(format_err!(
|
||||||
"Rust error while scanning blocks (limit {:?}): {:?}",
|
"Rust error while scanning blocks (limit {:?}): {:?}",
|
||||||
|
@ -1199,8 +1291,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_pu
|
||||||
txid.copy_from_slice(&txid_bytes);
|
txid.copy_from_slice(&txid_bytes);
|
||||||
|
|
||||||
let script_pubkey = Script(env.convert_byte_array(script).unwrap());
|
let script_pubkey = Script(env.convert_byte_array(script).unwrap());
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let mut db_data = wallet_db(&env, network, db_data)?;
|
||||||
let mut db_data = db_data.get_update_ops()?;
|
|
||||||
let addr = utils::java_string_to_rust(&env, address);
|
let addr = utils::java_string_to_rust(&env, address);
|
||||||
let _address = TransparentAddress::decode(&network, &addr).unwrap();
|
let _address = TransparentAddress::decode(&network, &addr).unwrap();
|
||||||
|
|
||||||
|
@ -1233,8 +1324,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_de
|
||||||
) -> jboolean {
|
) -> jboolean {
|
||||||
let res = panic::catch_unwind(|| {
|
let res = panic::catch_unwind(|| {
|
||||||
let network = parse_network(network_id as u32)?;
|
let network = parse_network(network_id as u32)?;
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let mut db_data = wallet_db(&env, network, db_data)?;
|
||||||
let mut db_data = db_data.get_update_ops()?;
|
|
||||||
let tx_bytes = env.convert_byte_array(tx).unwrap();
|
let tx_bytes = env.convert_byte_array(tx).unwrap();
|
||||||
// The consensus branch ID passed in here does not matter:
|
// The consensus branch ID passed in here does not matter:
|
||||||
// - v4 and below cache it internally, but all we do with this transaction while
|
// - v4 and below cache it internally, but all we do with this transaction while
|
||||||
|
@ -1289,11 +1379,10 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_cr
|
||||||
output_params: JString<'_>,
|
output_params: JString<'_>,
|
||||||
network_id: jint,
|
network_id: jint,
|
||||||
use_zip317_fees: jboolean,
|
use_zip317_fees: jboolean,
|
||||||
) -> jlong {
|
) -> jbyteArray {
|
||||||
let res = panic::catch_unwind(|| {
|
let res = panic::catch_unwind(|| {
|
||||||
let network = parse_network(network_id as u32)?;
|
let network = parse_network(network_id as u32)?;
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let mut db_data = wallet_db(&env, network, db_data)?;
|
||||||
let mut db_data = db_data.get_update_ops()?;
|
|
||||||
let usk = decode_usk(&env, usk)?;
|
let usk = decode_usk(&env, usk)?;
|
||||||
let to = utils::java_string_to_rust(&env, to);
|
let to = utils::java_string_to_rust(&env, to);
|
||||||
let value =
|
let value =
|
||||||
|
@ -1334,7 +1423,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_cr
|
||||||
}])
|
}])
|
||||||
.map_err(|e| format_err!("Error creating transaction request: {:?}", e))?;
|
.map_err(|e| format_err!("Error creating transaction request: {:?}", e))?;
|
||||||
|
|
||||||
zip317_helper(
|
let txid = zip317_helper(
|
||||||
(&mut db_data, prover, request),
|
(&mut db_data, prover, request),
|
||||||
use_zip317_fees,
|
use_zip317_fees,
|
||||||
|(wallet_db, prover, request), input_selector| {
|
|(wallet_db, prover, request), input_selector| {
|
||||||
|
@ -1363,9 +1452,11 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_cr
|
||||||
)
|
)
|
||||||
.map_err(|e| format_err!("Error while creating transaction: {}", e))
|
.map_err(|e| format_err!("Error while creating transaction: {}", e))
|
||||||
},
|
},
|
||||||
)
|
)?;
|
||||||
|
|
||||||
|
utils::rust_bytes_to_java(&env, txid.as_ref())
|
||||||
});
|
});
|
||||||
unwrap_exc_or(&env, res, -1)
|
unwrap_exc_or(&env, res, ptr::null_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -1379,17 +1470,16 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_sh
|
||||||
output_params: JString<'_>,
|
output_params: JString<'_>,
|
||||||
network_id: jint,
|
network_id: jint,
|
||||||
use_zip317_fees: jboolean,
|
use_zip317_fees: jboolean,
|
||||||
) -> jlong {
|
) -> jbyteArray {
|
||||||
let res = panic::catch_unwind(|| {
|
let res = panic::catch_unwind(|| {
|
||||||
let network = parse_network(network_id as u32)?;
|
let network = parse_network(network_id as u32)?;
|
||||||
let db_data = wallet_db(&env, network, db_data)?;
|
let mut db_data = wallet_db(&env, network, db_data)?;
|
||||||
let mut db_data = db_data.get_update_ops()?;
|
|
||||||
let usk = decode_usk(&env, usk)?;
|
let usk = decode_usk(&env, usk)?;
|
||||||
let memo_bytes = env.convert_byte_array(memo).unwrap();
|
let memo_bytes = env.convert_byte_array(memo).unwrap();
|
||||||
let spend_params = utils::java_string_to_rust(&env, spend_params);
|
let spend_params = utils::java_string_to_rust(&env, spend_params);
|
||||||
let output_params = utils::java_string_to_rust(&env, output_params);
|
let output_params = utils::java_string_to_rust(&env, output_params);
|
||||||
|
|
||||||
let min_confirmations = 0;
|
let min_confirmations = NonZeroU32::new(1).unwrap();
|
||||||
|
|
||||||
let account = db_data
|
let account = db_data
|
||||||
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())?
|
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())?
|
||||||
|
@ -1400,7 +1490,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_sh
|
||||||
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
|
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
|
||||||
.and_then(|opt_anchor| {
|
.and_then(|opt_anchor| {
|
||||||
opt_anchor
|
opt_anchor
|
||||||
.map(|(_, a)| a)
|
.map(|(target, _)| target) // Include unconfirmed funds.
|
||||||
.ok_or(format_err!("Anchor height not available; scan required."))
|
.ok_or(format_err!("Anchor height not available; scan required."))
|
||||||
})
|
})
|
||||||
.and_then(|anchor| {
|
.and_then(|anchor| {
|
||||||
|
@ -1423,7 +1513,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_sh
|
||||||
|
|
||||||
let shielding_threshold = NonNegativeAmount::from_u64(100000).unwrap();
|
let shielding_threshold = NonNegativeAmount::from_u64(100000).unwrap();
|
||||||
|
|
||||||
zip317_helper(
|
let txid = zip317_helper(
|
||||||
(&mut db_data, prover),
|
(&mut db_data, prover),
|
||||||
use_zip317_fees,
|
use_zip317_fees,
|
||||||
|(wallet_db, prover), input_selector| {
|
|(wallet_db, prover), input_selector| {
|
||||||
|
@ -1454,9 +1544,11 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_sh
|
||||||
)
|
)
|
||||||
.map_err(|e| format_err!("Error while shielding transaction: {}", e))
|
.map_err(|e| format_err!("Error while shielding transaction: {}", e))
|
||||||
},
|
},
|
||||||
)
|
)?;
|
||||||
|
|
||||||
|
utils::rust_bytes_to_java(&env, txid.as_ref())
|
||||||
});
|
});
|
||||||
unwrap_exc_or(&env, res, -1)
|
unwrap_exc_or(&env, res, ptr::null_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
|
use core::slice;
|
||||||
|
|
||||||
use jni::{
|
use jni::{
|
||||||
descriptors::Desc,
|
descriptors::Desc,
|
||||||
errors::Result as JNIResult,
|
errors::Result as JNIResult,
|
||||||
objects::{JClass, JObject, JString},
|
objects::{JClass, JObject, JString},
|
||||||
sys::{jobjectArray, jsize},
|
sys::{jbyteArray, jobjectArray, jsize},
|
||||||
JNIEnv,
|
JNIEnv,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
pub(crate) mod exception;
|
pub(crate) mod exception;
|
||||||
pub(crate) mod target_ndk;
|
pub(crate) mod target_ndk;
|
||||||
pub(crate) mod trace;
|
pub(crate) mod trace;
|
||||||
|
@ -18,6 +18,17 @@ pub(crate) fn java_string_to_rust(env: &JNIEnv<'_>, jstring: JString<'_>) -> Str
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn rust_bytes_to_java(
|
||||||
|
env: &JNIEnv<'_>,
|
||||||
|
data: &[u8],
|
||||||
|
) -> Result<jbyteArray, failure::Error> {
|
||||||
|
// SAFETY: jbyte (i8) has the same size and alignment as u8.
|
||||||
|
let buf = unsafe { slice::from_raw_parts(data.as_ptr().cast(), data.len()) };
|
||||||
|
let jret = env.new_byte_array(data.len() as jsize)?;
|
||||||
|
env.set_byte_array_region(jret, 0, buf)?;
|
||||||
|
Ok(jret)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn rust_vec_to_java<'a, T, U, V, F, G>(
|
pub(crate) fn rust_vec_to_java<'a, T, U, V, F, G>(
|
||||||
env: &JNIEnv<'a>,
|
env: &JNIEnv<'a>,
|
||||||
data: Vec<T>,
|
data: Vec<T>,
|
||||||
|
@ -27,17 +38,17 @@ pub(crate) fn rust_vec_to_java<'a, T, U, V, F, G>(
|
||||||
) -> jobjectArray
|
) -> jobjectArray
|
||||||
where
|
where
|
||||||
U: Desc<'a, JClass<'a>>,
|
U: Desc<'a, JClass<'a>>,
|
||||||
V: Deref<Target = JObject<'a>>,
|
V: Into<JObject<'a>>,
|
||||||
F: Fn(&JNIEnv<'a>, T) -> JNIResult<V>,
|
F: Fn(&JNIEnv<'a>, T) -> JNIResult<V>,
|
||||||
G: Fn(&JNIEnv<'a>) -> JNIResult<V>,
|
G: Fn(&JNIEnv<'a>) -> JNIResult<V>,
|
||||||
{
|
{
|
||||||
let jempty = empty_element(env).expect("Couldn't create Java string!");
|
let jempty = empty_element(env).expect("Couldn't create Java string!");
|
||||||
let jret = env
|
let jret = env
|
||||||
.new_object_array(data.len() as jsize, element_class, *jempty)
|
.new_object_array(data.len() as jsize, element_class, jempty.into())
|
||||||
.expect("Couldn't create Java array!");
|
.expect("Couldn't create Java array!");
|
||||||
for (i, elem) in data.into_iter().enumerate() {
|
for (i, elem) in data.into_iter().enumerate() {
|
||||||
let jelem = element_map(env, elem).expect("Couldn't map element to Java!");
|
let jelem = element_map(env, elem).expect("Couldn't map element to Java!");
|
||||||
env.set_object_array_element(jret, i as jsize, *jelem)
|
env.set_object_array_element(jret, i as jsize, jelem.into())
|
||||||
.expect("Couldn't set Java array element!");
|
.expect("Couldn't set Java array element!");
|
||||||
}
|
}
|
||||||
jret
|
jret
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.ext
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class NumberExtTest {
|
||||||
|
@Test
|
||||||
|
fun is_in_range_test() {
|
||||||
|
assertTrue(1L.isInUIntRange())
|
||||||
|
assertTrue(0L.isInUIntRange())
|
||||||
|
assertTrue(UInt.MAX_VALUE.toLong().isInUIntRange())
|
||||||
|
assertTrue(UInt.MIN_VALUE.toLong().isInUIntRange())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun is_not_in_range_test() {
|
||||||
|
assertFalse(0L.minus(1L).isInUIntRange())
|
||||||
|
assertFalse(Long.MAX_VALUE.isInUIntRange())
|
||||||
|
assertFalse(Long.MIN_VALUE.isInUIntRange())
|
||||||
|
assertFalse(UInt.MAX_VALUE.toLong().plus(1L).isInUIntRange())
|
||||||
|
assertFalse(UInt.MIN_VALUE.toLong().minus(1L).isInUIntRange())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertIs
|
||||||
|
|
||||||
|
class JniBlockMetaTest {
|
||||||
|
@Test
|
||||||
|
fun attributes_within_constraints() {
|
||||||
|
val instance = JniBlockMeta(
|
||||||
|
height = UInt.MAX_VALUE.toLong(),
|
||||||
|
hash = byteArrayOf(),
|
||||||
|
time = 0L,
|
||||||
|
saplingOutputsCount = UInt.MIN_VALUE.toLong(),
|
||||||
|
orchardOutputsCount = UInt.MIN_VALUE.toLong()
|
||||||
|
)
|
||||||
|
assertIs<JniBlockMeta>(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun attributes_not_in_constraints() {
|
||||||
|
assertFailsWith(IllegalArgumentException::class) {
|
||||||
|
JniBlockMeta(
|
||||||
|
height = Long.MAX_VALUE,
|
||||||
|
hash = byteArrayOf(),
|
||||||
|
time = 0L,
|
||||||
|
saplingOutputsCount = Long.MIN_VALUE,
|
||||||
|
orchardOutputsCount = Long.MIN_VALUE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertIs
|
||||||
|
|
||||||
|
class JniScanRangeTest {
|
||||||
|
@Test
|
||||||
|
fun attributes_within_constraints() {
|
||||||
|
val instance = JniScanRange(
|
||||||
|
startHeight = UInt.MIN_VALUE.toLong(),
|
||||||
|
endHeight = UInt.MAX_VALUE.toLong(),
|
||||||
|
priority = 10
|
||||||
|
)
|
||||||
|
assertIs<JniScanRange>(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun attributes_not_in_constraints() {
|
||||||
|
assertFailsWith(IllegalArgumentException::class) {
|
||||||
|
JniScanRange(
|
||||||
|
startHeight = Long.MIN_VALUE,
|
||||||
|
endHeight = Long.MAX_VALUE,
|
||||||
|
priority = 10
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
import kotlin.test.assertIs
|
||||||
|
|
||||||
|
class JniSubtreeRootTest {
|
||||||
|
@Test
|
||||||
|
fun attributes_within_constraints() {
|
||||||
|
val instance = JniSubtreeRoot(
|
||||||
|
rootHash = byteArrayOf(),
|
||||||
|
completingBlockHeight = UInt.MAX_VALUE.toLong()
|
||||||
|
)
|
||||||
|
assertIs<JniSubtreeRoot>(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun attributes_not_in_constraints() {
|
||||||
|
assertFailsWith(IllegalArgumentException::class) {
|
||||||
|
JniSubtreeRoot(
|
||||||
|
rootHash = byteArrayOf(),
|
||||||
|
completingBlockHeight = Long.MAX_VALUE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,11 +10,13 @@ import org.junit.runner.RunWith
|
||||||
/**
|
/**
|
||||||
* Integration test to run in order to catch any regressions in transparent behavior.
|
* Integration test to run in order to catch any regressions in transparent behavior.
|
||||||
*/
|
*/
|
||||||
|
// TODO [#1224]: Refactor and re-enable disabled darkside tests
|
||||||
|
// TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class TransparentIntegrationTest : DarksideTest() {
|
class TransparentIntegrationTest : DarksideTest() {
|
||||||
@Before
|
@Before
|
||||||
fun setup() = runOnce {
|
fun setup() = runOnce {
|
||||||
sithLord.await()
|
// sithLord.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -6,15 +6,19 @@ import cash.z.ecc.android.sdk.darkside.test.ScopedTest
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import org.junit.BeforeClass
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
// TODO [#1224]: Refactor and re-enable disabled darkside tests
|
||||||
|
// TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class InboundTxTests : ScopedTest() {
|
class InboundTxTests : ScopedTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Temporarily disabled")
|
||||||
fun testTargetBlock_synced() {
|
fun testTargetBlock_synced() {
|
||||||
validator.validateMinHeightSynced(firstBlock)
|
// validator.validateMinHeightSynced(firstBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -28,10 +32,11 @@ class InboundTxTests : ScopedTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Temporarily disabled")
|
||||||
fun testTxCountAfter() {
|
fun testTxCountAfter() {
|
||||||
// add 2 transactions to block 663188 and 'mine' that block
|
// add 2 transactions to block 663188 and 'mine' that block
|
||||||
addTransactions(targetTxBlock, tx663174, tx663188)
|
addTransactions(targetTxBlock, tx663174, tx663188)
|
||||||
sithLord.await(timeout = 30_000L, targetHeight = targetTxBlock)
|
// sithLord.await(timeout = 30_000L, targetHeight = targetTxBlock)
|
||||||
validator.validateTxCount(2)
|
validator.validateTxCount(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +96,7 @@ class InboundTxTests : ScopedTest() {
|
||||||
.stageEmptyBlocks(firstBlock + 1, 100)
|
.stageEmptyBlocks(firstBlock + 1, 100)
|
||||||
.applyTipHeight(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))
|
.applyTipHeight(BlockHeight.new(ZcashNetwork.Mainnet, targetTxBlock.value - 1))
|
||||||
|
|
||||||
sithLord.await()
|
// sithLord.await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,34 +3,39 @@ package cash.z.ecc.android.sdk.darkside.reorgs
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
|
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
|
||||||
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
|
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.BeforeClass
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
// TODO [#1224]: Refactor and re-enable disabled darkside tests
|
||||||
|
// TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ReorgSetupTest : ScopedTest() {
|
class ReorgSetupTest : ScopedTest() {
|
||||||
|
|
||||||
|
/*
|
||||||
private val birthdayHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663150)
|
private val birthdayHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663150)
|
||||||
private val targetHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663250)
|
private val targetHeight = BlockHeight.new(ZcashNetwork.Mainnet, 663250)
|
||||||
|
*/
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
sithLord.await()
|
// sithLord.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Temporarily disabled")
|
||||||
fun testBeforeReorg_minHeight() = timeout(30_000L) {
|
fun testBeforeReorg_minHeight() = timeout(30_000L) {
|
||||||
// validate that we are synced, at least to the birthday height
|
// validate that we are synced, at least to the birthday height
|
||||||
validator.validateMinHeightSynced(birthdayHeight)
|
// validator.validateMinHeightSynced(birthdayHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Temporarily disabled")
|
||||||
fun testBeforeReorg_maxHeight() = timeout(30_000L) {
|
fun testBeforeReorg_maxHeight() = timeout(30_000L) {
|
||||||
// validate that we are not synced beyond the target height
|
// validate that we are not synced beyond the target height
|
||||||
validator.validateMaxHeightSynced(targetHeight)
|
// validator.validateMaxHeightSynced(targetHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -3,45 +3,49 @@ package cash.z.ecc.android.sdk.darkside.reorgs
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
|
import cash.z.ecc.android.sdk.darkside.test.DarksideTestCoordinator
|
||||||
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
|
import cash.z.ecc.android.sdk.darkside.test.ScopedTest
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.BeforeClass
|
import org.junit.BeforeClass
|
||||||
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ReorgSmallTest : ScopedTest() {
|
class ReorgSmallTest : ScopedTest() {
|
||||||
|
|
||||||
|
/*
|
||||||
private val targetHeight = BlockHeight.new(
|
private val targetHeight = BlockHeight.new(
|
||||||
ZcashNetwork.Mainnet,
|
ZcashNetwork.Mainnet,
|
||||||
663250
|
663250
|
||||||
)
|
)
|
||||||
private val hashBeforeReorg = "09ec0d5de30d290bc5a2318fbf6a2427a81c7db4790ce0e341a96aeac77108b9"
|
private val hashBeforeReorg = "09ec0d5de30d290bc5a2318fbf6a2427a81c7db4790ce0e341a96aeac77108b9"
|
||||||
private val hashAfterReorg = "tbd"
|
private val hashAfterReorg = "tbd"
|
||||||
|
*/
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
sithLord.await()
|
// sithLord.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Temporarily disabled")
|
||||||
fun testBeforeReorg_latestBlockHash() = timeout(30_000L) {
|
fun testBeforeReorg_latestBlockHash() = timeout(30_000L) {
|
||||||
validator.validateBlockHash(targetHeight, hashBeforeReorg)
|
// validator.validateBlockHash(targetHeight, hashBeforeReorg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Temporarily disabled")
|
||||||
fun testAfterReorg_callbackTriggered() = timeout(30_000L) {
|
fun testAfterReorg_callbackTriggered() = timeout(30_000L) {
|
||||||
hadReorg = false
|
hadReorg = false
|
||||||
// sithLord.triggerSmallReorg()
|
// sithLord.triggerSmallReorg()
|
||||||
sithLord.await()
|
// sithLord.await()
|
||||||
assertTrue(hadReorg)
|
assertTrue(hadReorg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@Ignore("Temporarily disabled")
|
||||||
fun testAfterReorg_latestBlockHash() = timeout(30_000L) {
|
fun testAfterReorg_latestBlockHash() = timeout(30_000L) {
|
||||||
validator.validateBlockHash(targetHeight, hashAfterReorg)
|
// validator.validateBlockHash(targetHeight, hashAfterReorg)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -2,7 +2,6 @@ package cash.z.ecc.android.sdk.darkside.test
|
||||||
|
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.Darkside
|
import cash.z.ecc.android.sdk.model.Darkside
|
||||||
|
@ -13,15 +12,14 @@ import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
import io.grpc.StatusRuntimeException
|
import io.grpc.StatusRuntimeException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
|
||||||
|
// TODO [#1224]: Refactor and re-enable disabled darkside tests
|
||||||
|
// TODO [#1224]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1224
|
||||||
class DarksideTestCoordinator(val wallet: TestWallet) {
|
class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
constructor(
|
constructor(
|
||||||
alias: String = "DarksideTestCoordinator",
|
alias: String = "DarksideTestCoordinator",
|
||||||
|
@ -95,6 +93,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
* Waits for, at most, the given amount of time for the synchronizer to download and scan blocks
|
* Waits for, at most, the given amount of time for the synchronizer to download and scan blocks
|
||||||
* and reach a 'SYNCED' status.
|
* and reach a 'SYNCED' status.
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
fun await(timeout: Long = 60_000L, targetHeight: BlockHeight? = null) = runBlocking {
|
fun await(timeout: Long = 60_000L, targetHeight: BlockHeight? = null) = runBlocking {
|
||||||
ScopedTest.timeoutWith(this, timeout) {
|
ScopedTest.timeoutWith(this, timeout) {
|
||||||
synchronizer.status.map { status ->
|
synchronizer.status.map { status ->
|
||||||
|
@ -110,6 +109,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
}.filter { it == Synchronizer.Status.SYNCED }.first()
|
}.filter { it == Synchronizer.Status.SYNCED }.first()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// /**
|
// /**
|
||||||
// * Send a transaction and wait until it has been fully created and successfully submitted, which
|
// * Send a transaction and wait until it has been fully created and successfully submitted, which
|
||||||
|
@ -135,13 +135,6 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
|
|
||||||
inner class DarksideTestValidator {
|
inner class DarksideTestValidator {
|
||||||
|
|
||||||
fun validateHasBlock(height: BlockHeight) {
|
|
||||||
runBlocking {
|
|
||||||
assertTrue(synchronizer.findBlockHashAsHex(height) != null)
|
|
||||||
assertTrue(synchronizer.findBlockHash(height)?.size ?: 0 > 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun validateLatestHeight(height: BlockHeight) = runBlocking<Unit> {
|
fun validateLatestHeight(height: BlockHeight) = runBlocking<Unit> {
|
||||||
val info = synchronizer.processorInfo.first()
|
val info = synchronizer.processorInfo.first()
|
||||||
val networkBlockHeight = info.networkBlockHeight
|
val networkBlockHeight = info.networkBlockHeight
|
||||||
|
@ -152,6 +145,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
fun validateMinHeightSynced(minHeight: BlockHeight) = runBlocking<Unit> {
|
fun validateMinHeightSynced(minHeight: BlockHeight) = runBlocking<Unit> {
|
||||||
val info = synchronizer.processorInfo.first()
|
val info = synchronizer.processorInfo.first()
|
||||||
val lastSyncedHeight = info.lastSyncedHeight
|
val lastSyncedHeight = info.lastSyncedHeight
|
||||||
|
@ -177,6 +171,7 @@ class DarksideTestCoordinator(val wallet: TestWallet) {
|
||||||
val hash = runBlocking { synchronizer.findBlockHashAsHex(height) }
|
val hash = runBlocking { synchronizer.findBlockHashAsHex(height) }
|
||||||
assertEquals(expectedHash, hash)
|
assertEquals(expectedHash, hash)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
fun onReorg(callback: (errorHeight: BlockHeight, rewindHeight: BlockHeight) -> Unit) {
|
fun onReorg(callback: (errorHeight: BlockHeight, rewindHeight: BlockHeight) -> Unit) {
|
||||||
synchronizer.onChainErrorHandler = callback
|
synchronizer.onChainErrorHandler = callback
|
||||||
|
|
|
@ -5,6 +5,7 @@ import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.bip39.toSeed
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
@ -65,7 +66,9 @@ class TestWallet(
|
||||||
alias,
|
alias,
|
||||||
endpoint,
|
endpoint,
|
||||||
seed,
|
seed,
|
||||||
startHeight
|
startHeight,
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
) as SdkSynchronizer
|
) as SdkSynchronizer
|
||||||
|
|
||||||
val available get() = synchronizer.saplingBalances.value?.available
|
val available get() = synchronizer.saplingBalances.value?.available
|
||||||
|
@ -105,7 +108,7 @@ class TestWallet(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
|
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
|
||||||
synchronizer.rewindToNearestHeight(height, false)
|
synchronizer.rewindToNearestHeight(height)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,7 @@ dependencies {
|
||||||
|
|
||||||
implementation(libs.bundles.grpc)
|
implementation(libs.bundles.grpc)
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
|
implementation(libs.kotlinx.immutable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fladle {
|
fladle {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package cash.z.wallet.sdk.sample.demoapp
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||||
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
||||||
import cash.z.ecc.android.sdk.ext.toHex
|
import cash.z.ecc.android.sdk.ext.toHex
|
||||||
|
@ -202,7 +203,9 @@ class SampleCodeTest {
|
||||||
network,
|
network,
|
||||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(network),
|
||||||
seed = seed,
|
seed = seed,
|
||||||
birthday = null
|
birthday = null,
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ class ComposeActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
SecretState.None -> {
|
SecretState.None -> {
|
||||||
Seed(
|
Seed(
|
||||||
ZcashNetwork.fromResources(applicationContext),
|
zcashNetwork = ZcashNetwork.fromResources(applicationContext),
|
||||||
onExistingWallet = { walletViewModel.persistExistingWallet(it) },
|
onExistingWallet = { walletViewModel.persistExistingWallet(it) },
|
||||||
onNewWallet = { walletViewModel.persistNewWallet() }
|
onNewWallet = { walletViewModel.persistNewWallet() }
|
||||||
)
|
)
|
||||||
|
|
|
@ -63,7 +63,8 @@ internal fun ComposeActivity.Navigation() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goTransactions = { navController.navigateJustOnce(TRANSACTIONS) },
|
goTransactions = { navController.navigateJustOnce(TRANSACTIONS) },
|
||||||
resetSdk = { walletViewModel.resetSdk() }
|
resetSdk = { walletViewModel.resetSdk() },
|
||||||
|
rewind = { walletViewModel.rewind() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.bip39.toSeed
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
import cash.z.ecc.android.sdk.demoapp.util.fromResources
|
||||||
import cash.z.ecc.android.sdk.ext.onFirst
|
import cash.z.ecc.android.sdk.ext.onFirst
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
|
@ -82,6 +83,8 @@ class SharedViewModel(application: Application) : AndroidViewModel(application)
|
||||||
} else {
|
} else {
|
||||||
birthdayHeight.value
|
birthdayHeight.value
|
||||||
},
|
},
|
||||||
|
// We use restore mode as this is always initialization with an older seed
|
||||||
|
walletInitMode = WalletInitMode.RestoreWallet,
|
||||||
alias = OLD_UI_SYNCHRONIZER_ALIAS
|
alias = OLD_UI_SYNCHRONIZER_ALIAS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,10 @@ private val lazy = LazyWithArgument<Context, WalletCoordinator> {
|
||||||
emitAll(EncryptedPreferenceKeys.PERSISTABLE_WALLET.observe(encryptedPreferenceProvider))
|
emitAll(EncryptedPreferenceKeys.PERSISTABLE_WALLET.observe(encryptedPreferenceProvider))
|
||||||
}
|
}
|
||||||
|
|
||||||
WalletCoordinator(it, persistableWalletFlow)
|
WalletCoordinator(
|
||||||
|
context = it,
|
||||||
|
persistableWallet = persistableWalletFlow
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun WalletCoordinator.Companion.getInstance(context: Context) = lazy.getInstance(context)
|
fun WalletCoordinator.Companion.getInstance(context: Context) = lazy.getInstance(context)
|
||||||
|
|
|
@ -9,7 +9,6 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.bip39.toSeed
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
|
||||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding
|
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentGetBalanceBinding
|
||||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||||
|
@ -89,12 +88,6 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
||||||
.flatMapLatest { it.progress }
|
.flatMapLatest { it.progress }
|
||||||
.collect { onProgress(it) }
|
.collect { onProgress(it) }
|
||||||
}
|
}
|
||||||
launch {
|
|
||||||
sharedViewModel.synchronizerFlow
|
|
||||||
.filterNotNull()
|
|
||||||
.flatMapLatest { it.processorInfo }
|
|
||||||
.collect { onProcessorInfoUpdated(it) }
|
|
||||||
}
|
|
||||||
launch {
|
launch {
|
||||||
sharedViewModel.synchronizerFlow
|
sharedViewModel.synchronizerFlow
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
|
@ -179,10 +172,6 @@ class GetBalanceFragment : BaseDemoFragment<FragmentGetBalanceBinding>() {
|
||||||
binding.textStatus.text = "Syncing blocks...${percent.toPercentage()}%"
|
binding.textStatus.text = "Syncing blocks...${percent.toPercentage()}%"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
|
||||||
if (info.isSyncing) binding.textStatus.text = "Syncing blocks...${info.syncProgress}%"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
|
|
|
@ -8,7 +8,6 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
|
||||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding
|
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
|
@ -55,12 +54,6 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
||||||
.flatMapLatest { it.progress }
|
.flatMapLatest { it.progress }
|
||||||
.collect { onProgress(it) }
|
.collect { onProgress(it) }
|
||||||
}
|
}
|
||||||
launch {
|
|
||||||
sharedViewModel.synchronizerFlow
|
|
||||||
.filterNotNull()
|
|
||||||
.flatMapLatest { it.processorInfo }
|
|
||||||
.collect { onProcessorInfoUpdated(it) }
|
|
||||||
}
|
|
||||||
launch {
|
launch {
|
||||||
sharedViewModel.synchronizerFlow
|
sharedViewModel.synchronizerFlow
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
|
@ -75,10 +68,6 @@ class ListTransactionsFragment : BaseDemoFragment<FragmentListTransactionsBindin
|
||||||
// Change listeners
|
// Change listeners
|
||||||
//
|
//
|
||||||
|
|
||||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
|
||||||
if (info.isSyncing) binding.textInfo.text = "Syncing blocks...${info.syncProgress}%"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
private fun onProgress(percent: PercentDecimal) {
|
private fun onProgress(percent: PercentDecimal) {
|
||||||
if (percent.isLessThanHundredPercent()) binding.textInfo.text = "Syncing blocks...${percent.toPercentage()}%"
|
if (percent.isLessThanHundredPercent()) binding.textInfo.text = "Syncing blocks...${percent.toPercentage()}%"
|
||||||
|
|
|
@ -10,7 +10,6 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
|
||||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListUtxosBinding
|
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListUtxosBinding
|
||||||
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext
|
||||||
|
@ -173,12 +172,6 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
||||||
.flatMapLatest { it.progress }
|
.flatMapLatest { it.progress }
|
||||||
.collect { onProgress(it) }
|
.collect { onProgress(it) }
|
||||||
}
|
}
|
||||||
launch {
|
|
||||||
sharedViewModel.synchronizerFlow
|
|
||||||
.filterNotNull()
|
|
||||||
.flatMapLatest { it.processorInfo }
|
|
||||||
.collect { onProcessorInfoUpdated(it) }
|
|
||||||
}
|
|
||||||
launch {
|
launch {
|
||||||
sharedViewModel.synchronizerFlow
|
sharedViewModel.synchronizerFlow
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
|
@ -198,10 +191,6 @@ class ListUtxosFragment : BaseDemoFragment<FragmentListUtxosBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
|
||||||
if (info.isSyncing) binding.textStatus.text = "Syncing blocks...${info.syncProgress}%"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
private fun onProgress(percent: PercentDecimal) {
|
private fun onProgress(percent: PercentDecimal) {
|
||||||
if (percent.isLessThanHundredPercent()) binding.textStatus.text = "Syncing blocks...${percent.toPercentage()}%"
|
if (percent.isLessThanHundredPercent()) binding.textStatus.text = "Syncing blocks...${percent.toPercentage()}%"
|
||||||
|
|
|
@ -9,7 +9,6 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
|
||||||
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment
|
||||||
import cash.z.ecc.android.sdk.demoapp.DemoConstants
|
import cash.z.ecc.android.sdk.demoapp.DemoConstants
|
||||||
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding
|
import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding
|
||||||
|
@ -93,12 +92,6 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
||||||
.flatMapLatest { it.progress }
|
.flatMapLatest { it.progress }
|
||||||
.collect { onProgress(it) }
|
.collect { onProgress(it) }
|
||||||
}
|
}
|
||||||
launch {
|
|
||||||
sharedViewModel.synchronizerFlow
|
|
||||||
.filterNotNull()
|
|
||||||
.flatMapLatest { it.processorInfo }
|
|
||||||
.collect { onProcessorInfoUpdated(it) }
|
|
||||||
}
|
|
||||||
launch {
|
launch {
|
||||||
sharedViewModel.synchronizerFlow
|
sharedViewModel.synchronizerFlow
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
|
@ -133,10 +126,6 @@ class SendFragment : BaseDemoFragment<FragmentSendBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProcessorInfoUpdated(info: CompactBlockProcessor.ProcessorInfo) {
|
|
||||||
if (info.isSyncing) binding.textStatus.text = "Syncing blocks...${info.syncProgress}%"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
private fun onBalance(balance: WalletBalance?) {
|
private fun onBalance(balance: WalletBalance?) {
|
||||||
this.balance = balance
|
this.balance = balance
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package cash.z.ecc.android.sdk.demoapp.fixture
|
package cash.z.ecc.android.sdk.demoapp.fixture
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||||
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.SynchronizerError
|
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.SynchronizerError
|
||||||
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletSnapshot
|
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletSnapshot
|
||||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||||
|
@ -22,7 +22,6 @@ object WalletSnapshotFixture {
|
||||||
fun new(
|
fun new(
|
||||||
status: Synchronizer.Status = STATUS,
|
status: Synchronizer.Status = STATUS,
|
||||||
processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(
|
processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(
|
||||||
null,
|
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
|
|
|
@ -43,7 +43,8 @@ private fun ComposablePreviewHome() {
|
||||||
goAddressDetails = {},
|
goAddressDetails = {},
|
||||||
goTransactions = {},
|
goTransactions = {},
|
||||||
goTestnetFaucet = {},
|
goTestnetFaucet = {},
|
||||||
resetSdk = {}
|
resetSdk = {},
|
||||||
|
rewind = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,9 +60,15 @@ fun Home(
|
||||||
goTransactions: () -> Unit,
|
goTransactions: () -> Unit,
|
||||||
goTestnetFaucet: () -> Unit,
|
goTestnetFaucet: () -> Unit,
|
||||||
resetSdk: () -> Unit,
|
resetSdk: () -> Unit,
|
||||||
|
rewind: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(topBar = {
|
Scaffold(topBar = {
|
||||||
HomeTopAppBar(isTestnet, goTestnetFaucet, resetSdk)
|
HomeTopAppBar(
|
||||||
|
isTestnet,
|
||||||
|
goTestnetFaucet,
|
||||||
|
resetSdk,
|
||||||
|
rewind
|
||||||
|
)
|
||||||
}) { paddingValues ->
|
}) { paddingValues ->
|
||||||
HomeMainContent(
|
HomeMainContent(
|
||||||
paddingValues = paddingValues,
|
paddingValues = paddingValues,
|
||||||
|
@ -79,7 +86,8 @@ fun Home(
|
||||||
private fun HomeTopAppBar(
|
private fun HomeTopAppBar(
|
||||||
isTestnet: Boolean,
|
isTestnet: Boolean,
|
||||||
goTestnetFaucet: () -> Unit,
|
goTestnetFaucet: () -> Unit,
|
||||||
resetSdk: () -> Unit
|
resetSdk: () -> Unit,
|
||||||
|
rewind: () -> Unit
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(text = stringResource(id = R.string.app_name)) },
|
title = { Text(text = stringResource(id = R.string.app_name)) },
|
||||||
|
@ -87,7 +95,8 @@ private fun HomeTopAppBar(
|
||||||
DebugMenu(
|
DebugMenu(
|
||||||
isTestnet,
|
isTestnet,
|
||||||
goTestnetFaucet = goTestnetFaucet,
|
goTestnetFaucet = goTestnetFaucet,
|
||||||
resetSdk = resetSdk
|
resetSdk = resetSdk,
|
||||||
|
rewind = rewind,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -97,7 +106,8 @@ private fun HomeTopAppBar(
|
||||||
private fun DebugMenu(
|
private fun DebugMenu(
|
||||||
isTestnet: Boolean,
|
isTestnet: Boolean,
|
||||||
goTestnetFaucet: () -> Unit,
|
goTestnetFaucet: () -> Unit,
|
||||||
resetSdk: () -> Unit
|
resetSdk: () -> Unit,
|
||||||
|
rewind: () -> Unit
|
||||||
) {
|
) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
IconButton(onClick = { expanded = true }) {
|
IconButton(onClick = { expanded = true }) {
|
||||||
|
@ -117,6 +127,14 @@ private fun DebugMenu(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Quick Rewind") },
|
||||||
|
onClick = {
|
||||||
|
rewind()
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Reset SDK") },
|
text = { Text("Reset SDK") },
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel
|
package cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
|
|
|
@ -7,7 +7,8 @@ import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.bip39.toSeed
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.WalletCoordinator
|
import cash.z.ecc.android.sdk.WalletCoordinator
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||||
import cash.z.ecc.android.sdk.demoapp.getInstance
|
import cash.z.ecc.android.sdk.demoapp.getInstance
|
||||||
import cash.z.ecc.android.sdk.demoapp.preference.EncryptedPreferenceKeys
|
import cash.z.ecc.android.sdk.demoapp.preference.EncryptedPreferenceKeys
|
||||||
import cash.z.ecc.android.sdk.demoapp.preference.EncryptedPreferenceSingleton
|
import cash.z.ecc.android.sdk.demoapp.preference.EncryptedPreferenceSingleton
|
||||||
|
@ -48,7 +49,6 @@ import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
// To make this more multiplatform compatible, we need to remove the dependency on Context
|
// To make this more multiplatform compatible, we need to remove the dependency on Context
|
||||||
// for loading the preferences.
|
// for loading the preferences.
|
||||||
|
@ -101,7 +101,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val walletSnapshot: StateFlow<WalletSnapshot?> = synchronizer
|
val walletSnapshot: StateFlow<WalletSnapshot?> = synchronizer
|
||||||
.flatMapLatest {
|
.flatMapLatest {
|
||||||
if (null == it) {
|
if (null == it) {
|
||||||
|
@ -143,8 +143,12 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
val application = getApplication<Application>()
|
val application = getApplication<Application>()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newWallet = PersistableWallet.new(application, ZcashNetwork.fromResources(application))
|
val newWallet = PersistableWallet.new(
|
||||||
persistExistingWallet(newWallet)
|
application,
|
||||||
|
ZcashNetwork.fromResources(application),
|
||||||
|
WalletInitMode.NewWallet
|
||||||
|
)
|
||||||
|
persistWallet(newWallet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +157,13 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
* to see the side effects. This would be used for a user restoring a wallet from a backup.
|
* to see the side effects. This would be used for a user restoring a wallet from a backup.
|
||||||
*/
|
*/
|
||||||
fun persistExistingWallet(persistableWallet: PersistableWallet) {
|
fun persistExistingWallet(persistableWallet: PersistableWallet) {
|
||||||
|
persistWallet(persistableWallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a wallet asynchronously. Clients observe [secretState] to see the side effects.
|
||||||
|
*/
|
||||||
|
private fun persistWallet(persistableWallet: PersistableWallet) {
|
||||||
val application = getApplication<Application>()
|
val application = getApplication<Application>()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -234,6 +245,18 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
fun resetSdk() {
|
fun resetSdk() {
|
||||||
walletCoordinator.resetSdk()
|
walletCoordinator.resetSdk()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This rewinds to the nearest height, i.e. 14 days back from the current chain tip.
|
||||||
|
*/
|
||||||
|
fun rewind() {
|
||||||
|
val synchronizer = synchronizer.value
|
||||||
|
if (null != synchronizer) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
synchronizer.quickRewind()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.demoapp.R
|
import cash.z.ecc.android.sdk.demoapp.R
|
||||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||||
|
@ -76,7 +77,8 @@ private fun ConfigureSeedMainContent(
|
||||||
val newWallet = PersistableWallet(
|
val newWallet = PersistableWallet(
|
||||||
zcashNetwork,
|
zcashNetwork,
|
||||||
WalletFixture.Alice.getBirthday(zcashNetwork),
|
WalletFixture.Alice.getBirthday(zcashNetwork),
|
||||||
SeedPhrase.new(WalletFixture.Alice.seedPhrase)
|
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||||
|
WalletInitMode.RestoreWallet
|
||||||
)
|
)
|
||||||
onExistingWallet(newWallet)
|
onExistingWallet(newWallet)
|
||||||
}
|
}
|
||||||
|
@ -88,7 +90,8 @@ private fun ConfigureSeedMainContent(
|
||||||
val newWallet = PersistableWallet(
|
val newWallet = PersistableWallet(
|
||||||
zcashNetwork,
|
zcashNetwork,
|
||||||
WalletFixture.Ben.getBirthday(zcashNetwork),
|
WalletFixture.Ben.getBirthday(zcashNetwork),
|
||||||
SeedPhrase.new(WalletFixture.Ben.seedPhrase)
|
SeedPhrase.new(WalletFixture.Ben.seedPhrase),
|
||||||
|
WalletInitMode.RestoreWallet
|
||||||
)
|
)
|
||||||
onExistingWallet(newWallet)
|
onExistingWallet(newWallet)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ import cash.z.ecc.android.sdk.demoapp.R
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||||
import cash.z.ecc.android.sdk.model.WalletAddresses
|
import cash.z.ecc.android.sdk.model.WalletAddresses
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -41,7 +43,7 @@ private fun ComposablePreview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
// TODO [#1090]: Demo: Add Addresses and Transactions Compose Previews
|
// TODO [#1090]: Demo: Add Addresses and Transactions Compose Previews
|
||||||
// TODO [#1090]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1090
|
// TODO [#1090]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1090
|
||||||
// Transactions()
|
// TransactionsView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +74,7 @@ fun Transactions(
|
||||||
paddingValues = paddingValues,
|
paddingValues = paddingValues,
|
||||||
synchronizer,
|
synchronizer,
|
||||||
synchronizer.transactions.collectAsStateWithLifecycle(initialValue = emptyList()).value
|
synchronizer.transactions.collectAsStateWithLifecycle(initialValue = emptyList()).value
|
||||||
|
.toPersistentList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +113,7 @@ private fun TransactionsTopAppBar(
|
||||||
private fun TransactionsMainContent(
|
private fun TransactionsMainContent(
|
||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
synchronizer: Synchronizer,
|
synchronizer: Synchronizer,
|
||||||
transactions: List<TransactionOverview>
|
transactions: ImmutableList<TransactionOverview>
|
||||||
) {
|
) {
|
||||||
val queryScope = rememberCoroutineScope()
|
val queryScope = rememberCoroutineScope()
|
||||||
Column(
|
Column(
|
||||||
|
@ -127,7 +130,7 @@ private fun TransactionsMainContent(
|
||||||
val memos = synchronizer.getMemos(it)
|
val memos = synchronizer.getMemos(it)
|
||||||
queryScope.launch {
|
queryScope.launch {
|
||||||
memos.toList().run {
|
memos.toList().run {
|
||||||
Twig.debug {
|
Twig.info {
|
||||||
"Transaction memos: count: $size, contains: ${joinToString().ifEmpty { "-" }}"
|
"Transaction memos: count: $size, contains: ${joinToString().ifEmpty { "-" }}"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -26,7 +26,7 @@ ZCASH_ASCII_GPG_KEY=
|
||||||
# Configures whether release is an unstable snapshot, therefore published to the snapshot repository.
|
# Configures whether release is an unstable snapshot, therefore published to the snapshot repository.
|
||||||
IS_SNAPSHOT=true
|
IS_SNAPSHOT=true
|
||||||
|
|
||||||
LIBRARY_VERSION=1.21.0-beta01
|
LIBRARY_VERSION=2.0.0-rc.1
|
||||||
|
|
||||||
# Kotlin compiler warnings can be considered errors, failing the build.
|
# Kotlin compiler warnings can be considered errors, failing the build.
|
||||||
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true
|
ZCASH_IS_TREAT_WARNINGS_AS_ERRORS=true
|
||||||
|
@ -136,6 +136,7 @@ JAVAX_ANNOTATION_VERSION=1.3.2
|
||||||
JUNIT_VERSION=5.9.3
|
JUNIT_VERSION=5.9.3
|
||||||
KOTLINX_COROUTINES_VERSION=1.7.3
|
KOTLINX_COROUTINES_VERSION=1.7.3
|
||||||
KOTLINX_DATETIME_VERSION=0.4.0
|
KOTLINX_DATETIME_VERSION=0.4.0
|
||||||
|
KOTLINX_IMMUTABLE_COLLECTIONS_VERSION=0.3.5
|
||||||
KOTLIN_VERSION=1.9.10
|
KOTLIN_VERSION=1.9.10
|
||||||
MOCKITO_KOTLIN_VERSION=2.2.0
|
MOCKITO_KOTLIN_VERSION=2.2.0
|
||||||
MOCKITO_VERSION=5.4.0
|
MOCKITO_VERSION=5.4.0
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package co.electriccoin.lightwallet.client.model
|
||||||
|
|
||||||
|
import cash.z.wallet.sdk.internal.rpc.Service.TreeState
|
||||||
|
|
||||||
|
class TreeStateUnsafe(
|
||||||
|
val encoded: ByteArray
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun new(treeState: TreeState): TreeStateUnsafe {
|
||||||
|
return TreeStateUnsafe(treeState.toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromParts(
|
||||||
|
height: Long,
|
||||||
|
hash: String,
|
||||||
|
time: Int,
|
||||||
|
tree: String
|
||||||
|
): TreeStateUnsafe {
|
||||||
|
val treeState = TreeState.newBuilder()
|
||||||
|
.setHeight(height)
|
||||||
|
.setHash(hash)
|
||||||
|
.setTime(time)
|
||||||
|
.setSaplingTree(tree)
|
||||||
|
.build()
|
||||||
|
return new(treeState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package cash.z.ecc.android.sdk.fixture
|
package cash.z.ecc.android.sdk.fixture
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.PersistableWallet
|
import cash.z.ecc.android.sdk.model.PersistableWallet
|
||||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||||
|
@ -15,9 +16,12 @@ object PersistableWalletFixture {
|
||||||
|
|
||||||
val SEED_PHRASE = SeedPhraseFixture.new()
|
val SEED_PHRASE = SeedPhraseFixture.new()
|
||||||
|
|
||||||
|
val WALLET_INIT_MODE = WalletInitMode.ExistingWallet
|
||||||
|
|
||||||
fun new(
|
fun new(
|
||||||
network: ZcashNetwork = NETWORK,
|
network: ZcashNetwork = NETWORK,
|
||||||
birthday: BlockHeight = BIRTHDAY,
|
birthday: BlockHeight = BIRTHDAY,
|
||||||
seedPhrase: SeedPhrase = SEED_PHRASE
|
seedPhrase: SeedPhrase = SEED_PHRASE,
|
||||||
) = PersistableWallet(network, birthday, seedPhrase)
|
walletInitMode: WalletInitMode = WALLET_INIT_MODE
|
||||||
|
) = PersistableWallet(network, birthday, seedPhrase, walletInitMode)
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,7 @@ class WalletCoordinator(
|
||||||
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(persistableWallet.network),
|
lightWalletEndpoint = LightWalletEndpoint.defaultForNetwork(persistableWallet.network),
|
||||||
birthday = persistableWallet.birthday,
|
birthday = persistableWallet.birthday,
|
||||||
seed = persistableWallet.seedPhrase.toByteArray(),
|
seed = persistableWallet.seedPhrase.toByteArray(),
|
||||||
|
walletInitMode = persistableWallet.walletInitMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
trySend(InternalSynchronizerStatus.Available(closeableSynchronizer))
|
trySend(InternalSynchronizerStatus.Available(closeableSynchronizer))
|
||||||
|
@ -127,7 +128,7 @@ class WalletCoordinator(
|
||||||
synchronizerMutex.withLock {
|
synchronizerMutex.withLock {
|
||||||
synchronizer.value?.let {
|
synchronizer.value?.let {
|
||||||
it.latestBirthdayHeight?.let { height ->
|
it.latestBirthdayHeight?.let { height ->
|
||||||
it.rewindToNearestHeight(height, true)
|
it.rewindToNearestHeight(height)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.model
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.bip39.toEntropy
|
import cash.z.ecc.android.bip39.toEntropy
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
@ -13,8 +14,12 @@ import org.json.JSONObject
|
||||||
data class PersistableWallet(
|
data class PersistableWallet(
|
||||||
val network: ZcashNetwork,
|
val network: ZcashNetwork,
|
||||||
val birthday: BlockHeight?,
|
val birthday: BlockHeight?,
|
||||||
val seedPhrase: SeedPhrase
|
val seedPhrase: SeedPhrase,
|
||||||
|
val walletInitMode: WalletInitMode
|
||||||
) {
|
) {
|
||||||
|
init {
|
||||||
|
_walletInitMode = walletInitMode
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Wallet serialized to JSON format, suitable for long-term encrypted storage.
|
* @return Wallet serialized to JSON format, suitable for long-term encrypted storage.
|
||||||
|
@ -41,6 +46,10 @@ data class PersistableWallet(
|
||||||
internal const val KEY_BIRTHDAY = "birthday"
|
internal const val KEY_BIRTHDAY = "birthday"
|
||||||
internal const val KEY_SEED_PHRASE = "seed_phrase"
|
internal const val KEY_SEED_PHRASE = "seed_phrase"
|
||||||
|
|
||||||
|
// Note: This is not the ideal way to hold such a value. But we also want to avoid persisting the wallet
|
||||||
|
// initialization mode with the persistable wallet.
|
||||||
|
private var _walletInitMode: WalletInitMode = WalletInitMode.ExistingWallet
|
||||||
|
|
||||||
fun from(jsonObject: JSONObject): PersistableWallet {
|
fun from(jsonObject: JSONObject): PersistableWallet {
|
||||||
when (val version = jsonObject.getInt(KEY_VERSION)) {
|
when (val version = jsonObject.getInt(KEY_VERSION)) {
|
||||||
VERSION_1 -> {
|
VERSION_1 -> {
|
||||||
|
@ -56,7 +65,12 @@ data class PersistableWallet(
|
||||||
}
|
}
|
||||||
val seedPhrase = jsonObject.getString(KEY_SEED_PHRASE)
|
val seedPhrase = jsonObject.getString(KEY_SEED_PHRASE)
|
||||||
|
|
||||||
return PersistableWallet(network, birthday, SeedPhrase.new(seedPhrase))
|
return PersistableWallet(
|
||||||
|
network = network,
|
||||||
|
birthday = birthday,
|
||||||
|
seedPhrase = SeedPhrase.new(seedPhrase),
|
||||||
|
walletInitMode = _walletInitMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
throw IllegalArgumentException("Unsupported version $version")
|
throw IllegalArgumentException("Unsupported version $version")
|
||||||
|
@ -67,12 +81,21 @@ data class PersistableWallet(
|
||||||
/**
|
/**
|
||||||
* @return A new PersistableWallet with a random seed phrase.
|
* @return A new PersistableWallet with a random seed phrase.
|
||||||
*/
|
*/
|
||||||
suspend fun new(application: Application, zcashNetwork: ZcashNetwork): PersistableWallet {
|
suspend fun new(
|
||||||
|
application: Application,
|
||||||
|
zcashNetwork: ZcashNetwork,
|
||||||
|
walletInitMode: WalletInitMode
|
||||||
|
): PersistableWallet {
|
||||||
val birthday = BlockHeight.ofLatestCheckpoint(application, zcashNetwork)
|
val birthday = BlockHeight.ofLatestCheckpoint(application, zcashNetwork)
|
||||||
|
|
||||||
val seedPhrase = newSeedPhrase()
|
val seedPhrase = newSeedPhrase()
|
||||||
|
|
||||||
return PersistableWallet(zcashNetwork, birthday, seedPhrase)
|
return PersistableWallet(
|
||||||
|
zcashNetwork,
|
||||||
|
birthday,
|
||||||
|
seedPhrase,
|
||||||
|
walletInitMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor
|
||||||
|
|
||||||
|
// TODO [#1094]: Consider fake SDK sync related components
|
||||||
|
// TODO [#1094]: Testing the CompactBlockProcessor is only available once we can mock the necessary core components like
|
||||||
|
// [CompactBlockDownloader], [DerivedDataRepository], or [TypesafeBackend]
|
||||||
|
// TODO [#1094]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1094
|
||||||
|
@Suppress("EmptyClassBlock")
|
||||||
|
class CompactBlockProcessorTest
|
|
@ -4,6 +4,7 @@ import androidx.test.filters.LargeTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
|
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||||
import cash.z.ecc.android.sdk.ext.onFirst
|
import cash.z.ecc.android.sdk.ext.onFirst
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
|
@ -141,7 +142,9 @@ class TestnetIntegrationTest : ScopedTest() {
|
||||||
lightWalletEndpoint =
|
lightWalletEndpoint =
|
||||||
lightWalletEndpoint,
|
lightWalletEndpoint,
|
||||||
seed = seed,
|
seed = seed,
|
||||||
birthday = BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight)
|
birthday = BlockHeight.new(ZcashNetwork.Testnet, birthdayHeight),
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import androidx.test.filters.SmallTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||||
|
@ -29,7 +30,9 @@ class SdkSynchronizerTest {
|
||||||
alias,
|
alias,
|
||||||
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
||||||
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
||||||
birthday = null
|
birthday = null,
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
).use {
|
).use {
|
||||||
assertFailsWith<IllegalStateException> {
|
assertFailsWith<IllegalStateException> {
|
||||||
Synchronizer.new(
|
Synchronizer.new(
|
||||||
|
@ -38,7 +41,9 @@ class SdkSynchronizerTest {
|
||||||
alias,
|
alias,
|
||||||
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
||||||
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
||||||
birthday = null
|
birthday = null,
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +56,8 @@ class SdkSynchronizerTest {
|
||||||
// Random alias so that repeated invocations of this test will have a clean starting state
|
// Random alias so that repeated invocations of this test will have a clean starting state
|
||||||
val alias = UUID.randomUUID().toString()
|
val alias = UUID.randomUUID().toString()
|
||||||
|
|
||||||
|
// TODO [#1094]: Consider fake SDK sync related components
|
||||||
|
// TODO [#1094]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1094
|
||||||
// In the future, inject fake networking component so that it doesn't require hitting the network
|
// In the future, inject fake networking component so that it doesn't require hitting the network
|
||||||
Synchronizer.new(
|
Synchronizer.new(
|
||||||
InstrumentationRegistry.getInstrumentation().context,
|
InstrumentationRegistry.getInstrumentation().context,
|
||||||
|
@ -58,7 +65,9 @@ class SdkSynchronizerTest {
|
||||||
alias,
|
alias,
|
||||||
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
||||||
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
||||||
birthday = null
|
birthday = null,
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
).use {}
|
).use {}
|
||||||
|
|
||||||
// Second instance should succeed because first one was closed
|
// Second instance should succeed because first one was closed
|
||||||
|
@ -68,7 +77,9 @@ class SdkSynchronizerTest {
|
||||||
alias,
|
alias,
|
||||||
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
LightWalletEndpoint.defaultForNetwork(ZcashNetwork.Mainnet),
|
||||||
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
||||||
birthday = null
|
birthday = null,
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
).use {}
|
).use {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.util
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import cash.z.ecc.android.sdk.CloseableSynchronizer
|
import cash.z.ecc.android.sdk.CloseableSynchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
|
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
|
||||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||||
|
@ -59,9 +60,6 @@ class BalancePrinterUtil {
|
||||||
// val lastDownloaded = downloader.getLastDownloadedHeight()
|
// val lastDownloaded = downloader.getLastDownloadedHeight()
|
||||||
// val blockRange = (Math.max(birthday, lastDownloaded))..latestBlockHeight
|
// val blockRange = (Math.max(birthday, lastDownloaded))..latestBlockHeight
|
||||||
// downloadNewBlocks(blockRange)
|
// downloadNewBlocks(blockRange)
|
||||||
// val error = validateNewBlocks(blockRange)
|
|
||||||
// twig("validation completed with result $error")
|
|
||||||
// assertEquals(-1, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun deleteDb(dbName: String) {
|
private suspend fun deleteDb(dbName: String) {
|
||||||
|
@ -99,7 +97,9 @@ class BalancePrinterUtil {
|
||||||
lightWalletEndpoint = LightWalletEndpoint
|
lightWalletEndpoint = LightWalletEndpoint
|
||||||
.defaultForNetwork(network),
|
.defaultForNetwork(network),
|
||||||
seed = seed,
|
seed = seed,
|
||||||
birthday = birthdayHeight
|
birthday = birthdayHeight,
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
)
|
)
|
||||||
|
|
||||||
// deleteDb(dataDbPath)
|
// deleteDb(dataDbPath)
|
||||||
|
@ -150,13 +150,4 @@ class BalancePrinterUtil {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// private fun validateNewBlocks(range: IntRange?): Int {
|
|
||||||
// // val dummyWallet = initWallet("dummySeed")
|
|
||||||
// Twig.sprout("validating")
|
|
||||||
// twig("validating blocks in range $range")
|
|
||||||
// // val result = rustBackend.validateCombinedChain()
|
|
||||||
// Twig.clip("validating")
|
|
||||||
// return result
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import cash.z.ecc.android.sdk.CloseableSynchronizer
|
import cash.z.ecc.android.sdk.CloseableSynchronizer
|
||||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
import cash.z.ecc.android.sdk.model.defaultForNetwork
|
||||||
|
@ -72,7 +73,9 @@ class DataDbScannerUtil {
|
||||||
birthday = BlockHeight.new(
|
birthday = BlockHeight.new(
|
||||||
ZcashNetwork.Mainnet,
|
ZcashNetwork.Mainnet,
|
||||||
birthdayHeight
|
birthdayHeight
|
||||||
)
|
),
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
)
|
)
|
||||||
|
|
||||||
println("sync!")
|
println("sync!")
|
||||||
|
|
|
@ -5,6 +5,7 @@ import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.android.bip39.toSeed
|
import cash.z.ecc.android.bip39.toSeed
|
||||||
import cash.z.ecc.android.sdk.SdkSynchronizer
|
import cash.z.ecc.android.sdk.SdkSynchronizer
|
||||||
import cash.z.ecc.android.sdk.Synchronizer
|
import cash.z.ecc.android.sdk.Synchronizer
|
||||||
|
import cash.z.ecc.android.sdk.WalletInitMode
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.deriveUnifiedSpendingKey
|
import cash.z.ecc.android.sdk.internal.deriveUnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.internal.jni.RustDerivationTool
|
import cash.z.ecc.android.sdk.internal.jni.RustDerivationTool
|
||||||
|
@ -66,7 +67,9 @@ class TestWallet(
|
||||||
alias,
|
alias,
|
||||||
lightWalletEndpoint = endpoint,
|
lightWalletEndpoint = endpoint,
|
||||||
seed = seed,
|
seed = seed,
|
||||||
startHeight
|
startHeight,
|
||||||
|
// Using existing wallet init mode as simplification for the test
|
||||||
|
walletInitMode = WalletInitMode.ExistingWallet
|
||||||
) as SdkSynchronizer
|
) as SdkSynchronizer
|
||||||
|
|
||||||
val available get() = synchronizer.saplingBalances.value?.available
|
val available get() = synchronizer.saplingBalances.value?.available
|
||||||
|
@ -106,7 +109,7 @@ class TestWallet(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
|
suspend fun rewindToHeight(height: BlockHeight): TestWallet {
|
||||||
synchronizer.rewindToNearestHeight(height, false)
|
synchronizer.rewindToNearestHeight(height)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,9 @@ package cash.z.ecc.fixture
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.internal.Backend
|
import cash.z.ecc.android.sdk.internal.Backend
|
||||||
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniScanProgress
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniScanRange
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot
|
||||||
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
|
import cash.z.ecc.android.sdk.internal.model.JniUnifiedSpendingKey
|
||||||
|
|
||||||
internal class FakeRustBackend(
|
internal class FakeRustBackend(
|
||||||
|
@ -17,11 +20,35 @@ internal class FakeRustBackend(
|
||||||
metadata.removeAll { it.height > height }
|
metadata.removeAll { it.height > height }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLatestHeight(): Long = metadata.maxOf { it.height }
|
override suspend fun putSaplingSubtreeRoots(
|
||||||
override suspend fun validateCombinedChainOrErrorHeight(limit: Long?): Long? {
|
startIndex: Long,
|
||||||
|
roots: List<JniSubtreeRoot>,
|
||||||
|
) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateChainTip(height: Long) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getFullyScannedHeight(): Long? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMaxScannedHeight(): Long? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getScanProgress(): JniScanProgress {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun suggestScanRanges(): List<JniScanRange> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLatestCacheHeight(): Long = metadata.maxOf { it.height }
|
||||||
|
|
||||||
override suspend fun getVerifiedTransparentBalance(address: String): Long {
|
override suspend fun getVerifiedTransparentBalance(address: String): Long {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
@ -58,34 +85,25 @@ internal class FakeRustBackend(
|
||||||
to: String,
|
to: String,
|
||||||
value: Long,
|
value: Long,
|
||||||
memo: ByteArray?
|
memo: ByteArray?
|
||||||
): Long {
|
): ByteArray {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun shieldToAddress(account: Int, unifiedSpendingKey: ByteArray, memo: ByteArray?): Long {
|
override suspend fun shieldToAddress(account: Int, unifiedSpendingKey: ByteArray, memo: ByteArray?): ByteArray {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun decryptAndStoreTransaction(tx: ByteArray) =
|
override suspend fun decryptAndStoreTransaction(tx: ByteArray) =
|
||||||
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
||||||
|
|
||||||
override suspend fun initAccountsTable(vararg keys: String) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun initBlocksTable(
|
|
||||||
checkpointHeight: Long,
|
|
||||||
checkpointHash: String,
|
|
||||||
checkpointTime: Long,
|
|
||||||
checkpointSaplingTree: String
|
|
||||||
) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun initDataDb(seed: ByteArray?): Int =
|
override suspend fun initDataDb(seed: ByteArray?): Int =
|
||||||
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
||||||
|
|
||||||
override suspend fun createAccount(seed: ByteArray): JniUnifiedSpendingKey =
|
override suspend fun createAccount(
|
||||||
|
seed: ByteArray,
|
||||||
|
treeState: ByteArray,
|
||||||
|
recoverUntil: Long?
|
||||||
|
): JniUnifiedSpendingKey =
|
||||||
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
||||||
|
|
||||||
override fun isValidShieldedAddr(addr: String): Boolean =
|
override fun isValidShieldedAddr(addr: String): Boolean =
|
||||||
|
@ -119,10 +137,7 @@ internal class FakeRustBackend(
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getReceivedMemoAsUtf8(idNote: Long): String? =
|
override suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int): String? =
|
||||||
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
|
||||||
|
|
||||||
override suspend fun getSentMemoAsUtf8(idNote: Long): String? =
|
|
||||||
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
||||||
|
|
||||||
override suspend fun getVerifiedBalance(account: Int): Long {
|
override suspend fun getVerifiedBalance(account: Int): Long {
|
||||||
|
@ -133,6 +148,6 @@ internal class FakeRustBackend(
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun scanBlocks(limit: Long?) =
|
override suspend fun scanBlocks(fromHeight: Long, limit: Long) =
|
||||||
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@ import cash.z.ecc.android.sdk.Synchronizer.Status.DISCONNECTED
|
||||||
import cash.z.ecc.android.sdk.Synchronizer.Status.STOPPED
|
import cash.z.ecc.android.sdk.Synchronizer.Status.STOPPED
|
||||||
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
|
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
|
||||||
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCING
|
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCING
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Disconnected
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Disconnected
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Initialized
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Initialized
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Stopped
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Stopped
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Synced
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Synced
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Syncing
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor.State.Syncing
|
||||||
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
|
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
|
||||||
import cash.z.ecc.android.sdk.exception.TransactionSubmitException
|
import cash.z.ecc.android.sdk.exception.TransactionSubmitException
|
||||||
import cash.z.ecc.android.sdk.ext.ConsensusBranchId
|
import cash.z.ecc.android.sdk.ext.ConsensusBranchId
|
||||||
|
@ -24,10 +24,11 @@ import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||||
import cash.z.ecc.android.sdk.internal.db.derived.DbDerivedDataRepository
|
import cash.z.ecc.android.sdk.internal.db.derived.DbDerivedDataRepository
|
||||||
import cash.z.ecc.android.sdk.internal.db.derived.DerivedDataDb
|
import cash.z.ecc.android.sdk.internal.db.derived.DerivedDataDb
|
||||||
import cash.z.ecc.android.sdk.internal.ext.isNullOrEmpty
|
import cash.z.ecc.android.sdk.internal.ext.isNullOrEmpty
|
||||||
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
|
|
||||||
import cash.z.ecc.android.sdk.internal.ext.tryNull
|
import cash.z.ecc.android.sdk.internal.ext.tryNull
|
||||||
import cash.z.ecc.android.sdk.internal.jni.RustBackend
|
import cash.z.ecc.android.sdk.internal.jni.RustBackend
|
||||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.TreeState
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight
|
||||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||||
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
||||||
import cash.z.ecc.android.sdk.internal.storage.block.FileCompactBlockRepository
|
import cash.z.ecc.android.sdk.internal.storage.block.FileCompactBlockRepository
|
||||||
|
@ -40,7 +41,6 @@ import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||||
import cash.z.ecc.android.sdk.model.TransactionOverview
|
import cash.z.ecc.android.sdk.model.TransactionOverview
|
||||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey
|
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
|
@ -52,6 +52,7 @@ import cash.z.ecc.android.sdk.type.AddressType.Unified
|
||||||
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
||||||
import co.electriccoin.lightwallet.client.LightWalletClient
|
import co.electriccoin.lightwallet.client.LightWalletClient
|
||||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
import co.electriccoin.lightwallet.client.new
|
import co.electriccoin.lightwallet.client.new
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -109,6 +110,10 @@ class SdkSynchronizer private constructor(
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Convenience method to create new SdkSynchronizer instance.
|
||||||
|
*
|
||||||
|
* @return Synchronizer instance as CloseableSynchronizer
|
||||||
|
*
|
||||||
* @throws IllegalStateException If multiple instances of synchronizer with the same network+alias are
|
* @throws IllegalStateException If multiple instances of synchronizer with the same network+alias are
|
||||||
* active at the same time. Call `close` to finish one synchronizer before starting another one with the same
|
* active at the same time. Call `close` to finish one synchronizer before starting another one with the same
|
||||||
* network+alias.
|
* network+alias.
|
||||||
|
@ -172,21 +177,17 @@ class SdkSynchronizer private constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pools
|
|
||||||
private val _orchardBalances = MutableStateFlow<WalletBalance?>(null)
|
|
||||||
private val _saplingBalances = MutableStateFlow<WalletBalance?>(null)
|
|
||||||
private val _transparentBalances = MutableStateFlow<WalletBalance?>(null)
|
|
||||||
|
|
||||||
private val _status = MutableStateFlow<Synchronizer.Status>(DISCONNECTED)
|
private val _status = MutableStateFlow<Synchronizer.Status>(DISCONNECTED)
|
||||||
|
|
||||||
var coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
var coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||||
|
|
||||||
override val orchardBalances = _orchardBalances.asStateFlow()
|
override val orchardBalances = processor.orchardBalances.asStateFlow()
|
||||||
override val saplingBalances = _saplingBalances.asStateFlow()
|
override val saplingBalances = processor.saplingBalances.asStateFlow()
|
||||||
override val transparentBalances = _transparentBalances.asStateFlow()
|
override val transparentBalances = processor.transparentBalances.asStateFlow()
|
||||||
|
|
||||||
override val transactions
|
override val transactions
|
||||||
get() = combine(processor.networkHeight, storage.allTransactions) { networkHeight, allTransactions ->
|
get() = combine(processor.networkHeight, storage.allTransactions) { networkHeight, allTransactions ->
|
||||||
val latestBlockHeight = networkHeight ?: storage.lastScannedHeight()
|
val latestBlockHeight = networkHeight ?: backend.getMaxScannedHeight()
|
||||||
allTransactions.map { TransactionOverview.new(it, latestBlockHeight) }
|
allTransactions.map { TransactionOverview.new(it, latestBlockHeight) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,8 +306,8 @@ class SdkSynchronizer private constructor(
|
||||||
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
|
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
|
||||||
processor.getNearestRewindHeight(height)
|
processor.getNearestRewindHeight(height)
|
||||||
|
|
||||||
override suspend fun rewindToNearestHeight(height: BlockHeight, alsoClearBlockCache: Boolean) {
|
override suspend fun rewindToNearestHeight(height: BlockHeight) {
|
||||||
processor.rewindToNearestHeight(height, alsoClearBlockCache)
|
processor.rewindToNearestHeight(height)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun quickRewind() {
|
override suspend fun quickRewind() {
|
||||||
|
@ -314,18 +315,10 @@ class SdkSynchronizer private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMemos(transactionOverview: TransactionOverview): Flow<String> {
|
override fun getMemos(transactionOverview: TransactionOverview): Flow<String> {
|
||||||
return storage.getNoteIds(transactionOverview.id).map {
|
return storage.getSaplingOutputIndices(transactionOverview.id).map {
|
||||||
runCatching {
|
runCatching {
|
||||||
when (transactionOverview.isSentTransaction) {
|
backend.getMemoAsUtf8(transactionOverview.rawId.byteArray, it)
|
||||||
true -> {
|
|
||||||
backend.getSentMemoAsUtf8(it)
|
|
||||||
}
|
|
||||||
false -> {
|
|
||||||
backend.getReceivedMemoAsUtf8(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
// https://github.com/zcash/librustzcash/issues/834
|
|
||||||
Twig.error { "Failed to get memo with: $it" }
|
Twig.error { "Failed to get memo with: $it" }
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
Twig.debug { "Transaction memo queried: $it" }
|
Twig.debug { "Transaction memo queried: $it" }
|
||||||
|
@ -350,14 +343,6 @@ class SdkSynchronizer private constructor(
|
||||||
// to do with the underlying data
|
// to do with the underlying data
|
||||||
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
||||||
|
|
||||||
suspend fun findBlockHash(height: BlockHeight): ByteArray? {
|
|
||||||
return storage.findBlockHash(height)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findBlockHashAsHex(height: BlockHeight): String? {
|
|
||||||
return findBlockHash(height)?.toHexReversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getTransactionCount(): Int {
|
suspend fun getTransactionCount(): Int {
|
||||||
return storage.getTransactionCount().toInt()
|
return storage.getTransactionCount().toInt()
|
||||||
}
|
}
|
||||||
|
@ -366,44 +351,49 @@ class SdkSynchronizer private constructor(
|
||||||
storage.invalidate()
|
storage.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Private API
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the latest balance, based on the blocks that have been scanned and transmit this
|
* Calculate the latest balance based on the blocks that have been scanned and transmit this information into the
|
||||||
* information into the flow of [balances].
|
* [transparentBalances] and [saplingBalances] flow. The [orchardBalances] flow is still not filled with proper data
|
||||||
|
* because of the current limited Orchard support.
|
||||||
*/
|
*/
|
||||||
suspend fun refreshAllBalances() {
|
suspend fun refreshAllBalances() {
|
||||||
refreshSaplingBalance()
|
processor.checkAllBalances()
|
||||||
refreshTransparentBalance()
|
|
||||||
// TODO [#682]: refresh orchard balance
|
// TODO [#682]: refresh orchard balance
|
||||||
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
||||||
Twig.warn { "Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place." }
|
Twig.warn { "Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place." }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the latest Sapling balance based on the blocks that have been scanned and transmit this information
|
||||||
|
* into the [saplingBalances] flow.
|
||||||
|
*/
|
||||||
suspend fun refreshSaplingBalance() {
|
suspend fun refreshSaplingBalance() {
|
||||||
Twig.debug { "refreshing sapling balance" }
|
processor.checkSaplingBalance()
|
||||||
_saplingBalances.value = processor.getBalanceInfo(Account.DEFAULT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the latest Transparent balance based on the blocks that have been scanned and transmit this information
|
||||||
|
* into the [saplingBalances] flow.
|
||||||
|
*/
|
||||||
suspend fun refreshTransparentBalance() {
|
suspend fun refreshTransparentBalance() {
|
||||||
Twig.debug { "refreshing transparent balance" }
|
processor.checkTransparentBalance()
|
||||||
_transparentBalances.value = processor.getUtxoCacheBalance(getTransparentAddress(Account.DEFAULT))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isValidAddress(address: String): Boolean {
|
suspend fun isValidAddress(address: String): Boolean {
|
||||||
return !validateAddress(address).isNotValid
|
return !validateAddress(address).isNotValid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Private API
|
||||||
|
//
|
||||||
|
|
||||||
private fun CoroutineScope.onReady() {
|
private fun CoroutineScope.onReady() {
|
||||||
Twig.debug { "Starting synchronizer…" }
|
Twig.debug { "Starting synchronizer…" }
|
||||||
|
|
||||||
// Triggering UTXOs fetch and transparent balance update at the beginning of the block sync right after the app
|
// Triggering UTXOs and transactions fetching at the beginning of the block synchronization right after the
|
||||||
// start, as it makes the transparent transactions appearance faster
|
// app starts makes the transparent transactions appear faster.
|
||||||
launch(CoroutineExceptionHandler(::onCriticalError)) {
|
launch(CoroutineExceptionHandler(::onCriticalError)) {
|
||||||
refreshUtxos(Account.DEFAULT)
|
refreshUtxos(Account.DEFAULT)
|
||||||
refreshTransparentBalance()
|
|
||||||
refreshTransactions()
|
refreshTransactions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -520,8 +510,21 @@ class SdkSynchronizer private constructor(
|
||||||
//
|
//
|
||||||
|
|
||||||
// Not ready to be a public API; internal for testing only
|
// Not ready to be a public API; internal for testing only
|
||||||
internal suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey =
|
internal suspend fun createAccount(
|
||||||
backend.createAccountAndGetSpendingKey(seed)
|
seed: ByteArray,
|
||||||
|
treeState: TreeState,
|
||||||
|
recoverUntil: BlockHeight?
|
||||||
|
): UnifiedSpendingKey? {
|
||||||
|
return runCatching {
|
||||||
|
backend.createAccountAndGetSpendingKey(
|
||||||
|
seed = seed,
|
||||||
|
treeState = treeState,
|
||||||
|
recoverUntil = recoverUntil
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
Twig.error(it) { "Create account failed." }
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current Unified Address for this account.
|
* Returns the current Unified Address for this account.
|
||||||
|
@ -623,9 +626,30 @@ class SdkSynchronizer private constructor(
|
||||||
|
|
||||||
override suspend fun validateConsensusBranch(): ConsensusMatchType {
|
override suspend fun validateConsensusBranch(): ConsensusMatchType {
|
||||||
val serverBranchId = tryNull { processor.downloader.getServerInfo()?.consensusBranchId }
|
val serverBranchId = tryNull { processor.downloader.getServerInfo()?.consensusBranchId }
|
||||||
val sdkBranchId = tryNull {
|
|
||||||
(txManager as OutboundTransactionManagerImpl).encoder.getConsensusBranchId()
|
val currentChainTip = when (
|
||||||
|
val response =
|
||||||
|
processor.downloader.getLatestBlockHeight()
|
||||||
|
) {
|
||||||
|
is Response.Success -> {
|
||||||
|
Twig.info { "Chain tip for validate consensus branch action fetched: ${response.result.value}" }
|
||||||
|
runCatching { response.result.toBlockHeight(network) }.getOrNull()
|
||||||
|
}
|
||||||
|
is Response.Failure -> {
|
||||||
|
Twig.error {
|
||||||
|
"Chain tip fetch failed for validate consensus branch action with:" +
|
||||||
|
" ${response.toThrowable()}"
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sdkBranchId = currentChainTip?.let {
|
||||||
|
tryNull {
|
||||||
|
(txManager as OutboundTransactionManagerImpl).encoder.getConsensusBranchId(currentChainTip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ConsensusMatchType(
|
return ConsensusMatchType(
|
||||||
sdkBranchId?.let { ConsensusBranchId.fromId(it) },
|
sdkBranchId?.let { ConsensusBranchId.fromId(it) },
|
||||||
serverBranchId?.let { ConsensusBranchId.fromHex(it) }
|
serverBranchId?.let { ConsensusBranchId.fromHex(it) }
|
||||||
|
@ -665,7 +689,8 @@ internal object DefaultSynchronizerFactory {
|
||||||
zcashNetwork: ZcashNetwork,
|
zcashNetwork: ZcashNetwork,
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
seed: ByteArray?,
|
seed: ByteArray?,
|
||||||
viewingKeys: List<UnifiedFullViewingKey>
|
numberOfAccounts: Int,
|
||||||
|
recoverUntil: BlockHeight?
|
||||||
): DerivedDataRepository =
|
): DerivedDataRepository =
|
||||||
DbDerivedDataRepository(
|
DbDerivedDataRepository(
|
||||||
DerivedDataDb.new(
|
DerivedDataDb.new(
|
||||||
|
@ -675,7 +700,8 @@ internal object DefaultSynchronizerFactory {
|
||||||
zcashNetwork,
|
zcashNetwork,
|
||||||
checkpoint,
|
checkpoint,
|
||||||
seed,
|
seed,
|
||||||
viewingKeys
|
numberOfAccounts,
|
||||||
|
recoverUntil
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -718,10 +744,10 @@ internal object DefaultSynchronizerFactory {
|
||||||
repository: DerivedDataRepository,
|
repository: DerivedDataRepository,
|
||||||
birthdayHeight: BlockHeight
|
birthdayHeight: BlockHeight
|
||||||
): CompactBlockProcessor = CompactBlockProcessor(
|
): CompactBlockProcessor = CompactBlockProcessor(
|
||||||
downloader,
|
downloader = downloader,
|
||||||
repository,
|
repository = repository,
|
||||||
backend,
|
backend = backend,
|
||||||
birthdayHeight
|
minimumHeight = birthdayHeight
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package cash.z.ecc.android.sdk
|
package cash.z.ecc.android.sdk
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||||
import cash.z.ecc.android.sdk.internal.Derivation
|
import cash.z.ecc.android.sdk.internal.Derivation
|
||||||
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
import cash.z.ecc.android.sdk.internal.SaplingParamTool
|
||||||
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.PercentDecimal
|
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||||
|
@ -16,10 +18,10 @@ import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
import cash.z.ecc.android.sdk.tool.CheckpointTool
|
||||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
|
||||||
import cash.z.ecc.android.sdk.type.AddressType
|
import cash.z.ecc.android.sdk.type.AddressType
|
||||||
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
||||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
|
||||||
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -163,7 +165,7 @@ interface Synchronizer {
|
||||||
* Sends zatoshi.
|
* Sends zatoshi.
|
||||||
*
|
*
|
||||||
* @param usk the unified spending key associated with the notes that will be spent.
|
* @param usk the unified spending key associated with the notes that will be spent.
|
||||||
* @param zatoshi the amount of zatoshi to send.
|
* @param amount the amount of zatoshi to send.
|
||||||
* @param toAddress the recipient's address.
|
* @param toAddress the recipient's address.
|
||||||
* @param memo the optional memo to include as part of the transaction.
|
* @param memo the optional memo to include as part of the transaction.
|
||||||
*
|
*
|
||||||
|
@ -268,16 +270,27 @@ interface Synchronizer {
|
||||||
*/
|
*/
|
||||||
suspend fun getTransparentBalance(tAddr: String): WalletBalance
|
suspend fun getTransparentBalance(tAddr: String): WalletBalance
|
||||||
|
|
||||||
suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the safest height to which we can rewind, given a desire to rewind to the height
|
* Returns the safest height to which we can rewind, given a desire to rewind to the height
|
||||||
* provided. Due to how witness incrementing works, a wallet cannot simply rewind to any
|
* provided. Due to how witness incrementing works, a wallet cannot simply rewind to any
|
||||||
* arbitrary height. This handles all that complexity yet remains flexible in the future as
|
* arbitrary height. This handles all that complexity yet remains flexible in the future as
|
||||||
* improvements are made.
|
* improvements are made.
|
||||||
*/
|
*/
|
||||||
suspend fun rewindToNearestHeight(height: BlockHeight, alsoClearBlockCache: Boolean = false)
|
suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewinds to the safest height to which we can rewind, given a desire to rewind to the height
|
||||||
|
* provided. Due to how witness incrementing works, a wallet cannot simply rewind to any
|
||||||
|
* arbitrary height. This handles all that complexity yet remains flexible in the future as
|
||||||
|
* improvements are made.
|
||||||
|
*/
|
||||||
|
suspend fun rewindToNearestHeight(height: BlockHeight)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewinds to the safest height approximately 14 days backward from the current chain tip. Due to how witness
|
||||||
|
* incrementing works, a wallet cannot simply rewind to any arbitrary height. This handles all that complexity
|
||||||
|
* yet remains flexible in the future as improvements are made.
|
||||||
|
*/
|
||||||
suspend fun quickRewind()
|
suspend fun quickRewind()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -398,20 +411,32 @@ interface Synchronizer {
|
||||||
* Primary method that SDK clients will use to construct a synchronizer.
|
* Primary method that SDK clients will use to construct a synchronizer.
|
||||||
*
|
*
|
||||||
* @param zcashNetwork the network to use.
|
* @param zcashNetwork the network to use.
|
||||||
|
*
|
||||||
* @param alias A string used to segregate multiple wallets in the filesystem. This implies the string
|
* @param alias A string used to segregate multiple wallets in the filesystem. This implies the string
|
||||||
* should not contain characters unsuitable for the platform's filesystem. The default value is
|
* should not contain characters unsuitable for the platform's filesystem. The default value is
|
||||||
* generally used unless an SDK client needs to support multiple wallets.
|
* generally used unless an SDK client needs to support multiple wallets.
|
||||||
|
*
|
||||||
* @param lightWalletEndpoint Server endpoint. See [cash.z.ecc.android.sdk.model.defaultForNetwork]. If a
|
* @param lightWalletEndpoint Server endpoint. See [cash.z.ecc.android.sdk.model.defaultForNetwork]. If a
|
||||||
* client wishes to change the server endpoint, the active synchronizer will need to be stopped and a new
|
* client wishes to change the server endpoint, the active synchronizer will need to be stopped and a new
|
||||||
* instance created with a new value.
|
* instance created with a new value.
|
||||||
|
*
|
||||||
* @param seed the wallet's seed phrase. This is required the first time a new wallet is set up. For
|
* @param seed the wallet's seed phrase. This is required the first time a new wallet is set up. For
|
||||||
* subsequent calls, seed is only needed if [InitializerException.SeedRequired] is thrown.
|
* subsequent calls, seed is only needed if [InitializerException.SeedRequired] is thrown.
|
||||||
|
*
|
||||||
* @param birthday Block height representing the "birthday" of the wallet. When creating a new wallet, see
|
* @param birthday Block height representing the "birthday" of the wallet. When creating a new wallet, see
|
||||||
* [BlockHeight.ofLatestCheckpoint]. When restoring an existing wallet, use block height that was first used
|
* [BlockHeight.ofLatestCheckpoint]. When restoring an existing wallet, use block height that was first used
|
||||||
* to create the wallet. If that value is unknown, null is acceptable but will result in longer
|
* to create the wallet. If that value is unknown, null is acceptable but will result in longer
|
||||||
* sync times. After sync completes, the birthday can be determined from [Synchronizer.latestBirthdayHeight].
|
* sync times. After sync completes, the birthday can be determined from [Synchronizer.latestBirthdayHeight].
|
||||||
|
*
|
||||||
|
* @param walletInitMode a required parameter with one of [WalletInitMode] values. Use
|
||||||
|
* [WalletInitMode.NewWallet] when starting synchronizer for a newly created wallet. Or use
|
||||||
|
* [WalletInitMode.RestoreWallet] when restoring an existing wallet that was created at some point in the
|
||||||
|
* past. Or use the last [WalletInitMode.ExistingWallet] type for a wallet which is already initialized
|
||||||
|
* and needs follow-up block synchronization.
|
||||||
|
*
|
||||||
* @throws InitializerException.SeedRequired Indicates clients need to call this method again, providing the
|
* @throws InitializerException.SeedRequired Indicates clients need to call this method again, providing the
|
||||||
* seed bytes.
|
* seed bytes.
|
||||||
|
*
|
||||||
* @throws IllegalStateException If multiple instances of synchronizer with the same network+alias are
|
* @throws IllegalStateException If multiple instances of synchronizer with the same network+alias are
|
||||||
* active at the same time. Call `close` to finish one synchronizer before starting another one with the same
|
* active at the same time. Call `close` to finish one synchronizer before starting another one with the same
|
||||||
* network+alias.
|
* network+alias.
|
||||||
|
@ -427,7 +452,8 @@ interface Synchronizer {
|
||||||
alias: String = ZcashSdk.DEFAULT_ALIAS,
|
alias: String = ZcashSdk.DEFAULT_ALIAS,
|
||||||
lightWalletEndpoint: LightWalletEndpoint,
|
lightWalletEndpoint: LightWalletEndpoint,
|
||||||
seed: ByteArray?,
|
seed: ByteArray?,
|
||||||
birthday: BlockHeight?
|
birthday: BlockHeight?,
|
||||||
|
walletInitMode: WalletInitMode
|
||||||
): CloseableSynchronizer {
|
): CloseableSynchronizer {
|
||||||
val applicationContext = context.applicationContext
|
val applicationContext = context.applicationContext
|
||||||
|
|
||||||
|
@ -456,46 +482,57 @@ interface Synchronizer {
|
||||||
DefaultSynchronizerFactory
|
DefaultSynchronizerFactory
|
||||||
.defaultCompactBlockRepository(coordinator.fsBlockDbRoot(zcashNetwork, alias), backend)
|
.defaultCompactBlockRepository(coordinator.fsBlockDbRoot(zcashNetwork, alias), backend)
|
||||||
|
|
||||||
val viewingKeys = seed?.let {
|
val service = DefaultSynchronizerFactory.defaultService(applicationContext, lightWalletEndpoint)
|
||||||
DerivationTool.getInstance().deriveUnifiedFullViewingKeys(
|
val downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore)
|
||||||
seed,
|
|
||||||
zcashNetwork,
|
val chainTip = when (walletInitMode) {
|
||||||
Derivation.DEFAULT_NUMBER_OF_ACCOUNTS
|
is WalletInitMode.RestoreWallet -> {
|
||||||
).toList()
|
when (val response = downloader.getLatestBlockHeight()) {
|
||||||
} ?: emptyList()
|
is Response.Success -> {
|
||||||
|
Twig.info { "Chain tip for recovery until param fetched: ${response.result.value}" }
|
||||||
|
runCatching { response.result.toBlockHeight(zcashNetwork) }.getOrNull()
|
||||||
|
}
|
||||||
|
is Response.Failure -> {
|
||||||
|
Twig.error { "Chain tip fetch for recovery until failed with: ${response.toThrowable()}" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val repository = DefaultSynchronizerFactory.defaultDerivedDataRepository(
|
val repository = DefaultSynchronizerFactory.defaultDerivedDataRepository(
|
||||||
applicationContext,
|
context = applicationContext,
|
||||||
backend,
|
rustBackend = backend,
|
||||||
coordinator.dataDbFile(zcashNetwork, alias),
|
databaseFile = coordinator.dataDbFile(zcashNetwork, alias),
|
||||||
zcashNetwork,
|
zcashNetwork = zcashNetwork,
|
||||||
loadedCheckpoint,
|
checkpoint = loadedCheckpoint,
|
||||||
seed,
|
seed = seed,
|
||||||
viewingKeys
|
numberOfAccounts = Derivation.DEFAULT_NUMBER_OF_ACCOUNTS,
|
||||||
|
recoverUntil = chainTip,
|
||||||
)
|
)
|
||||||
|
|
||||||
val service = DefaultSynchronizerFactory.defaultService(applicationContext, lightWalletEndpoint)
|
|
||||||
val encoder = DefaultSynchronizerFactory.defaultEncoder(backend, saplingParamTool, repository)
|
val encoder = DefaultSynchronizerFactory.defaultEncoder(backend, saplingParamTool, repository)
|
||||||
val downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore)
|
|
||||||
val txManager = DefaultSynchronizerFactory.defaultTxManager(
|
val txManager = DefaultSynchronizerFactory.defaultTxManager(
|
||||||
encoder,
|
encoder,
|
||||||
service
|
service
|
||||||
)
|
)
|
||||||
val processor = DefaultSynchronizerFactory.defaultProcessor(
|
val processor = DefaultSynchronizerFactory.defaultProcessor(
|
||||||
backend,
|
backend = backend,
|
||||||
downloader,
|
downloader = downloader,
|
||||||
repository,
|
repository = repository,
|
||||||
birthday
|
birthdayHeight = birthday ?: zcashNetwork.saplingActivationHeight
|
||||||
?: zcashNetwork.saplingActivationHeight
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return SdkSynchronizer.new(
|
return SdkSynchronizer.new(
|
||||||
zcashNetwork,
|
zcashNetwork = zcashNetwork,
|
||||||
alias,
|
alias = alias,
|
||||||
repository,
|
repository = repository,
|
||||||
txManager,
|
txManager = txManager,
|
||||||
processor,
|
processor = processor,
|
||||||
backend
|
backend = backend
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -513,9 +550,10 @@ interface Synchronizer {
|
||||||
alias: String = ZcashSdk.DEFAULT_ALIAS,
|
alias: String = ZcashSdk.DEFAULT_ALIAS,
|
||||||
lightWalletEndpoint: LightWalletEndpoint,
|
lightWalletEndpoint: LightWalletEndpoint,
|
||||||
seed: ByteArray?,
|
seed: ByteArray?,
|
||||||
birthday: BlockHeight?
|
birthday: BlockHeight?,
|
||||||
|
walletInitMode: WalletInitMode
|
||||||
): CloseableSynchronizer = runBlocking {
|
): CloseableSynchronizer = runBlocking {
|
||||||
new(context, zcashNetwork, alias, lightWalletEndpoint, seed, birthday)
|
new(context, zcashNetwork, alias, lightWalletEndpoint, seed, birthday, walletInitMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -540,6 +578,23 @@ interface Synchronizer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sealed class describing wallet initialization mode.
|
||||||
|
*
|
||||||
|
* Use [NewWallet] type if the seed was just created as part of a
|
||||||
|
* new wallet initialization.
|
||||||
|
*
|
||||||
|
* Use [RestoreWallet] type if an existed wallet is initialized
|
||||||
|
* from a restored seed with older birthday height.
|
||||||
|
*
|
||||||
|
* Use [ExistingWallet] type if the wallet is already initialized.
|
||||||
|
*/
|
||||||
|
sealed class WalletInitMode {
|
||||||
|
data object NewWallet : WalletInitMode()
|
||||||
|
data object RestoreWallet : WalletInitMode()
|
||||||
|
data object ExistingWallet : WalletInitMode()
|
||||||
|
}
|
||||||
|
|
||||||
interface CloseableSynchronizer : Synchronizer, Closeable
|
interface CloseableSynchronizer : Synchronizer, Closeable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,10 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress model class for sharing the whole batch synchronization progress out of the synchronization process.
|
||||||
|
*/
|
||||||
|
internal data class BatchSyncProgress(
|
||||||
|
val order: Long = 0,
|
||||||
|
val resultState: SyncingResult =
|
||||||
|
SyncingResult.AllSuccess
|
||||||
|
)
|
|
@ -0,0 +1,12 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class for sharing get max scanned height action result.
|
||||||
|
*/
|
||||||
|
internal sealed class GetMaxScannedHeightResult {
|
||||||
|
data class Success(val height: BlockHeight) : GetMaxScannedHeightResult()
|
||||||
|
data object None : GetMaxScannedHeightResult()
|
||||||
|
data class Failure(val exception: Throwable) : GetMaxScannedHeightResult()
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ScanProgress
|
||||||
|
import cash.z.ecc.android.sdk.model.PercentDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class for sharing get scan progress action result.
|
||||||
|
*/
|
||||||
|
internal sealed class GetScanProgressResult {
|
||||||
|
data class Success(val scanProgress: ScanProgress) : GetScanProgressResult() {
|
||||||
|
fun toPercentDecimal() = PercentDecimal(scanProgress.getSafeRation())
|
||||||
|
}
|
||||||
|
|
||||||
|
data object None : GetScanProgressResult()
|
||||||
|
data class Failure(val exception: Throwable) : GetScanProgressResult()
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.SubtreeRoot
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class for get subtree roots action result.
|
||||||
|
*/
|
||||||
|
internal sealed class GetSubtreeRootsResult {
|
||||||
|
data class SpendBeforeSync(val subTreeRootList: List<SubtreeRoot>) : GetSubtreeRootsResult()
|
||||||
|
data object Linear : GetSubtreeRootsResult()
|
||||||
|
data object FailureConnection : GetSubtreeRootsResult()
|
||||||
|
data class OtherFailure(val exception: Throwable) : GetSubtreeRootsResult()
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class for sharing put sapling subtree roots action result.
|
||||||
|
*/
|
||||||
|
internal sealed class PutSaplingSubtreeRootsResult {
|
||||||
|
object Success : PutSaplingSubtreeRootsResult()
|
||||||
|
data class Failure(val failedAtHeight: BlockHeight, val exception: Throwable) : PutSaplingSubtreeRootsResult()
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class for sharing pre-synchronization steps result.
|
||||||
|
*/
|
||||||
|
internal sealed class SbSPreparationResult {
|
||||||
|
object ConnectionFailure : SbSPreparationResult()
|
||||||
|
data class ProcessFailure(
|
||||||
|
val failedAtHeight: BlockHeight,
|
||||||
|
val exception: Throwable
|
||||||
|
) : SbSPreparationResult() {
|
||||||
|
fun toBlockProcessingResult(): CompactBlockProcessor.BlockProcessingResult =
|
||||||
|
CompactBlockProcessor.BlockProcessingResult.SyncFailure(
|
||||||
|
this.failedAtHeight,
|
||||||
|
this.exception
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data class Success(
|
||||||
|
val suggestedRangesResult: SuggestScanRangesResult,
|
||||||
|
val verifyRangeResult: VerifySuggestedScanRange
|
||||||
|
) : SbSPreparationResult()
|
||||||
|
object NoMoreBlocksToProcess : SbSPreparationResult()
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ScanRange
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class for sharing suggested scan ranges action result.
|
||||||
|
*/
|
||||||
|
internal sealed class SuggestScanRangesResult {
|
||||||
|
data class Success(val ranges: List<ScanRange>) : SuggestScanRangesResult()
|
||||||
|
data class Failure(val failedAtHeight: BlockHeight, val exception: Throwable) : SuggestScanRangesResult()
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.BlockBatch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common progress model class for sharing a batch synchronization stage result internally in the synchronization loop.
|
||||||
|
*/
|
||||||
|
internal data class SyncStageResult(
|
||||||
|
val batch: BlockBatch,
|
||||||
|
val stageResult: SyncingResult
|
||||||
|
)
|
|
@ -0,0 +1,52 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
|
||||||
|
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class for the overall synchronization process result reporting.
|
||||||
|
*/
|
||||||
|
internal sealed class SyncingResult {
|
||||||
|
override fun toString(): String = this::class.java.simpleName
|
||||||
|
|
||||||
|
object AllSuccess : SyncingResult()
|
||||||
|
object RestartSynchronization : SyncingResult()
|
||||||
|
data class DownloadSuccess(val downloadedBlocks: List<JniBlockMeta>?) : SyncingResult() {
|
||||||
|
override fun toString() = "${this::class.java.simpleName} with ${downloadedBlocks?.size ?: "none"} blocks"
|
||||||
|
}
|
||||||
|
interface Failure {
|
||||||
|
val failedAtHeight: BlockHeight?
|
||||||
|
val exception: CompactBlockProcessorException
|
||||||
|
fun toBlockProcessingResult(): CompactBlockProcessor.BlockProcessingResult =
|
||||||
|
CompactBlockProcessor.BlockProcessingResult.SyncFailure(
|
||||||
|
this.failedAtHeight,
|
||||||
|
this.exception
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data class DownloadFailed(
|
||||||
|
override val failedAtHeight: BlockHeight,
|
||||||
|
override val exception: CompactBlockProcessorException
|
||||||
|
) : Failure, SyncingResult()
|
||||||
|
object ScanSuccess : SyncingResult()
|
||||||
|
data class ScanFailed(
|
||||||
|
override val failedAtHeight: BlockHeight,
|
||||||
|
override val exception: CompactBlockProcessorException
|
||||||
|
) : Failure, SyncingResult()
|
||||||
|
object DeleteSuccess : SyncingResult()
|
||||||
|
data class DeleteFailed(
|
||||||
|
override val failedAtHeight: BlockHeight?,
|
||||||
|
override val exception: CompactBlockProcessorException
|
||||||
|
) : Failure, SyncingResult()
|
||||||
|
object EnhanceSuccess : SyncingResult()
|
||||||
|
data class EnhanceFailed(
|
||||||
|
override val failedAtHeight: BlockHeight,
|
||||||
|
override val exception: CompactBlockProcessorException
|
||||||
|
) : Failure, SyncingResult()
|
||||||
|
object UpdateBirthday : SyncingResult()
|
||||||
|
data class ContinuityError(
|
||||||
|
override val failedAtHeight: BlockHeight,
|
||||||
|
override val exception: CompactBlockProcessorException
|
||||||
|
) : Failure, SyncingResult()
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class for sharing update chain tip action result.
|
||||||
|
*/
|
||||||
|
internal sealed class UpdateChainTipResult {
|
||||||
|
data class Success(val height: BlockHeight) : UpdateChainTipResult()
|
||||||
|
data class Failure(val failedAtHeight: BlockHeight, val exception: Throwable) : UpdateChainTipResult()
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ScanRange
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal class for sharing verify suggested scan range action result.
|
||||||
|
*/
|
||||||
|
internal sealed class VerifySuggestedScanRange {
|
||||||
|
data class ShouldVerify(val scanRange: ScanRange) : VerifySuggestedScanRange()
|
||||||
|
object NoRangeToVerify : VerifySuggestedScanRange()
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.exception
|
||||||
import cash.z.ecc.android.sdk.internal.SaplingParameters
|
import cash.z.ecc.android.sdk.internal.SaplingParameters
|
||||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
|
|
||||||
|
@ -79,22 +80,32 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
|
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
|
||||||
class FailedDownload(cause: Throwable? = null) : CompactBlockProcessorException(
|
class Uninitialized(cause: Throwable? = null) : CompactBlockProcessorException(
|
||||||
"Error while downloading blocks. This most " +
|
|
||||||
"likely means the server is down or slow to respond. See logs for details.",
|
|
||||||
cause
|
|
||||||
)
|
|
||||||
class Disconnected(cause: Throwable? = null) :
|
|
||||||
CompactBlockProcessorException("Disconnected Error. Unable to download blocks due to ${cause?.message}", cause)
|
|
||||||
object Uninitialized : CompactBlockProcessorException(
|
|
||||||
"Cannot process blocks because the wallet has not been" +
|
"Cannot process blocks because the wallet has not been" +
|
||||||
" initialized. Verify that the seed phrase was properly created or imported. If so, then this problem" +
|
" initialized. Verify that the seed phrase was properly created or imported. If so, then this problem" +
|
||||||
" can be fixed by re-importing the wallet."
|
" can be fixed by re-importing the wallet.",
|
||||||
|
cause
|
||||||
)
|
)
|
||||||
object NoAccount : CompactBlockProcessorException(
|
object NoAccount : CompactBlockProcessorException(
|
||||||
"Attempting to scan without an account. This is probably a setup error or a race condition."
|
"Attempting to scan without an account. This is probably a setup error or a race condition."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class FailedDownloadException(cause: Throwable? = null) : CompactBlockProcessorException(
|
||||||
|
"Error while downloading blocks. This most likely means the server is down or slow to respond. " +
|
||||||
|
"See logs for details.",
|
||||||
|
cause
|
||||||
|
)
|
||||||
|
class FailedScanException(cause: Throwable? = null) : CompactBlockProcessorException(
|
||||||
|
"Error while scanning blocks. This most likely means a problem with locally persisted data. " +
|
||||||
|
"See logs for details.",
|
||||||
|
cause
|
||||||
|
)
|
||||||
|
class FailedDeleteException(cause: Throwable? = null) : CompactBlockProcessorException(
|
||||||
|
"Error while deleting block files. This most likely means the data are not persisted correctly." +
|
||||||
|
" See logs for details.",
|
||||||
|
cause
|
||||||
|
)
|
||||||
|
|
||||||
open class EnhanceTransactionError(
|
open class EnhanceTransactionError(
|
||||||
message: String,
|
message: String,
|
||||||
val height: BlockHeight,
|
val height: BlockHeight,
|
||||||
|
@ -240,10 +251,20 @@ sealed class LightWalletException(message: String, cause: Throwable? = null) : S
|
||||||
cause
|
cause
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class GetSubtreeRootsException(code: Int, description: String?, cause: Throwable) : SdkException(
|
||||||
|
"Failed to get subtree roots with code: $code due to: ${description ?: "-"}",
|
||||||
|
cause
|
||||||
|
)
|
||||||
|
|
||||||
class FetchUtxosException(code: Int, description: String?, cause: Throwable) : SdkException(
|
class FetchUtxosException(code: Int, description: String?, cause: Throwable) : SdkException(
|
||||||
"Failed to fetch UTXOs with code: $code due to: ${description ?: "-"}",
|
"Failed to fetch UTXOs with code: $code due to: ${description ?: "-"}",
|
||||||
cause
|
cause
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class GetLatestBlockHeightException(code: Int, description: String?, cause: Throwable) : SdkException(
|
||||||
|
"Failed to fetch latest block height with code: $code due to: ${description ?: "-"}",
|
||||||
|
cause
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -264,7 +285,7 @@ sealed class TransactionEncoderException(
|
||||||
object MissingParamsException : TransactionEncoderException(
|
object MissingParamsException : TransactionEncoderException(
|
||||||
"Cannot send funds due to missing spend or output params and attempting to download them failed."
|
"Cannot send funds due to missing spend or output params and attempting to download them failed."
|
||||||
)
|
)
|
||||||
class TransactionNotFoundException(transactionId: Long) : TransactionEncoderException(
|
class TransactionNotFoundException(transactionId: FirstClassByteArray) : TransactionEncoderException(
|
||||||
"Unable to find transactionId $transactionId in the repository. This means the wallet created a transaction " +
|
"Unable to find transactionId $transactionId in the repository. This means the wallet created a transaction " +
|
||||||
"and then returned a row ID that does not actually exist. This is a scenario where the wallet should " +
|
"and then returned a row ID that does not actually exist. This is a scenario where the wallet should " +
|
||||||
"have thrown an exception but failed to do so."
|
"have thrown an exception but failed to do so."
|
||||||
|
@ -274,7 +295,7 @@ sealed class TransactionEncoderException(
|
||||||
" with id $transactionId, does not have any raw data. This is a scenario where the wallet should have " +
|
" with id $transactionId, does not have any raw data. This is a scenario where the wallet should have " +
|
||||||
"thrown an exception but failed to do so."
|
"thrown an exception but failed to do so."
|
||||||
)
|
)
|
||||||
class IncompleteScanException(lastScannedHeight: BlockHeight) : TransactionEncoderException(
|
class IncompleteScanException(lastScannedHeight: BlockHeight?) : TransactionEncoderException(
|
||||||
"Cannot" +
|
"Cannot" +
|
||||||
" create spending transaction because scanning is incomplete. We must scan up to the" +
|
" create spending transaction because scanning is incomplete. We must scan up to the" +
|
||||||
" latest height to know which consensus rules to apply. However, the last scanned" +
|
" latest height to know which consensus rules to apply. However, the last scanned" +
|
||||||
|
|
|
@ -1,32 +1,28 @@
|
||||||
package cash.z.ecc.android.sdk.internal
|
package cash.z.ecc.android.sdk.internal
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
|
||||||
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ScanProgress
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ScanRange
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.SubtreeRoot
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.TreeState
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import java.lang.RuntimeException
|
|
||||||
import kotlin.jvm.Throws
|
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
internal interface TypesafeBackend {
|
internal interface TypesafeBackend {
|
||||||
|
|
||||||
val network: ZcashNetwork
|
val network: ZcashNetwork
|
||||||
|
|
||||||
suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey)
|
suspend fun createAccountAndGetSpendingKey(
|
||||||
|
|
||||||
suspend fun initAccountsTable(
|
|
||||||
seed: ByteArray,
|
seed: ByteArray,
|
||||||
numberOfAccounts: Int
|
treeState: TreeState,
|
||||||
): List<UnifiedFullViewingKey>
|
recoverUntil: BlockHeight?
|
||||||
|
): UnifiedSpendingKey
|
||||||
suspend fun initBlocksTable(checkpoint: Checkpoint)
|
|
||||||
|
|
||||||
suspend fun createAccountAndGetSpendingKey(seed: ByteArray): UnifiedSpendingKey
|
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
suspend fun createToAddress(
|
suspend fun createToAddress(
|
||||||
|
@ -34,12 +30,12 @@ internal interface TypesafeBackend {
|
||||||
to: String,
|
to: String,
|
||||||
value: Long,
|
value: Long,
|
||||||
memo: ByteArray? = byteArrayOf()
|
memo: ByteArray? = byteArrayOf()
|
||||||
): Long
|
): FirstClassByteArray
|
||||||
|
|
||||||
suspend fun shieldToAddress(
|
suspend fun shieldToAddress(
|
||||||
usk: UnifiedSpendingKey,
|
usk: UnifiedSpendingKey,
|
||||||
memo: ByteArray? = byteArrayOf()
|
memo: ByteArray? = byteArrayOf()
|
||||||
): Long
|
): FirstClassByteArray
|
||||||
|
|
||||||
suspend fun getCurrentAddress(account: Account): String
|
suspend fun getCurrentAddress(account: Account): String
|
||||||
|
|
||||||
|
@ -55,18 +51,12 @@ internal interface TypesafeBackend {
|
||||||
|
|
||||||
suspend fun rewindToHeight(height: BlockHeight)
|
suspend fun rewindToHeight(height: BlockHeight)
|
||||||
|
|
||||||
suspend fun getLatestBlockHeight(): BlockHeight?
|
suspend fun getLatestCacheHeight(): BlockHeight?
|
||||||
|
|
||||||
suspend fun findBlockMetadata(height: BlockHeight): JniBlockMeta?
|
suspend fun findBlockMetadata(height: BlockHeight): JniBlockMeta?
|
||||||
|
|
||||||
suspend fun rewindBlockMetadataToHeight(height: BlockHeight)
|
suspend fun rewindBlockMetadataToHeight(height: BlockHeight)
|
||||||
|
|
||||||
/**
|
|
||||||
* @param limit The limit provides an efficient way how to restrict the portion of blocks, which will be validated.
|
|
||||||
* @return Null if successful. If an error occurs, the height will be the height where the error was detected.
|
|
||||||
*/
|
|
||||||
suspend fun validateCombinedChainOrErrorBlockHeight(limit: Long?): BlockHeight?
|
|
||||||
|
|
||||||
suspend fun getDownloadedUtxoBalance(address: String): WalletBalance
|
suspend fun getDownloadedUtxoBalance(address: String): WalletBalance
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
@ -79,9 +69,7 @@ internal interface TypesafeBackend {
|
||||||
height: BlockHeight
|
height: BlockHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun getSentMemoAsUtf8(idNote: Long): String?
|
suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int): String?
|
||||||
|
|
||||||
suspend fun getReceivedMemoAsUtf8(idNote: Long): String?
|
|
||||||
|
|
||||||
suspend fun initDataDb(seed: ByteArray?): Int
|
suspend fun initDataDb(seed: ByteArray?): Int
|
||||||
|
|
||||||
|
@ -89,7 +77,57 @@ internal interface TypesafeBackend {
|
||||||
* @throws RuntimeException as a common indicator of the operation failure
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
*/
|
*/
|
||||||
@Throws(RuntimeException::class)
|
@Throws(RuntimeException::class)
|
||||||
suspend fun scanBlocks(limit: Long?)
|
suspend fun putSaplingSubtreeRoots(
|
||||||
|
startIndex: Long,
|
||||||
|
roots: List<SubtreeRoot>,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
@Throws(RuntimeException::class)
|
||||||
|
suspend fun updateChainTip(height: BlockHeight)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the height to which the wallet has been fully scanned.
|
||||||
|
*
|
||||||
|
* This is the height for which the wallet has fully trial-decrypted this and all
|
||||||
|
* preceding blocks above the wallet's birthday height.
|
||||||
|
*
|
||||||
|
* @return The height to which the wallet has been fully scanned, or Null if no blocks have been scanned.
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
suspend fun getFullyScannedHeight(): BlockHeight?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum height that the wallet has scanned.
|
||||||
|
*
|
||||||
|
* If the wallet is fully synced, this will be equivalent to `getFullyScannedHeight`;
|
||||||
|
* otherwise the maximal scanned height is likely to be greater than the fully scanned
|
||||||
|
* height due to the fact that out-of-order scanning can leave gaps.
|
||||||
|
*
|
||||||
|
* @return The maximum height that the wallet has scanned, or Null if no blocks have been scanned.
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
suspend fun getMaxScannedHeight(): BlockHeight?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
@Throws(RuntimeException::class)
|
||||||
|
suspend fun scanBlocks(fromHeight: BlockHeight, limit: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
@Throws(RuntimeException::class)
|
||||||
|
suspend fun getScanProgress(): ScanProgress?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException as a common indicator of the operation failure
|
||||||
|
*/
|
||||||
|
@Throws(RuntimeException::class)
|
||||||
|
suspend fun suggestScanRanges(): List<ScanRange>
|
||||||
|
|
||||||
suspend fun decryptAndStoreTransaction(tx: ByteArray)
|
suspend fun decryptAndStoreTransaction(tx: ByteArray)
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
package cash.z.ecc.android.sdk.internal
|
package cash.z.ecc.android.sdk.internal
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
|
||||||
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.JniSubtreeRoot
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ScanProgress
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ScanRange
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.SubtreeRoot
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.TreeState
|
||||||
import cash.z.ecc.android.sdk.model.Account
|
import cash.z.ecc.android.sdk.model.Account
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
|
@ -18,53 +21,45 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
|
||||||
override val network: ZcashNetwork
|
override val network: ZcashNetwork
|
||||||
get() = ZcashNetwork.from(backend.networkId)
|
get() = ZcashNetwork.from(backend.networkId)
|
||||||
|
|
||||||
override suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey) {
|
override suspend fun createAccountAndGetSpendingKey(
|
||||||
val ufvks = Array(keys.size) { keys[it].encoding }
|
|
||||||
@Suppress("SpreadOperator")
|
|
||||||
backend.initAccountsTable(*ufvks)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun initAccountsTable(
|
|
||||||
seed: ByteArray,
|
seed: ByteArray,
|
||||||
numberOfAccounts: Int
|
treeState: TreeState,
|
||||||
): List<UnifiedFullViewingKey> {
|
recoverUntil: BlockHeight?
|
||||||
return DerivationTool.getInstance().deriveUnifiedFullViewingKeys(seed, network, numberOfAccounts)
|
): UnifiedSpendingKey {
|
||||||
}
|
return UnifiedSpendingKey(
|
||||||
|
backend.createAccount(
|
||||||
override suspend fun initBlocksTable(checkpoint: Checkpoint) {
|
seed = seed,
|
||||||
backend.initBlocksTable(
|
treeState = treeState.encoded,
|
||||||
checkpoint.height.value,
|
recoverUntil = recoverUntil?.value
|
||||||
checkpoint.hash,
|
)
|
||||||
checkpoint.epochSeconds,
|
|
||||||
checkpoint.tree
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createAccountAndGetSpendingKey(seed: ByteArray): UnifiedSpendingKey {
|
|
||||||
return UnifiedSpendingKey(backend.createAccount(seed))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
override suspend fun createToAddress(
|
override suspend fun createToAddress(
|
||||||
usk: UnifiedSpendingKey,
|
usk: UnifiedSpendingKey,
|
||||||
to: String,
|
to: String,
|
||||||
value: Long,
|
value: Long,
|
||||||
memo: ByteArray?
|
memo: ByteArray?
|
||||||
): Long = backend.createToAddress(
|
): FirstClassByteArray = FirstClassByteArray(
|
||||||
usk.account.value,
|
backend.createToAddress(
|
||||||
usk.copyBytes(),
|
usk.account.value,
|
||||||
to,
|
usk.copyBytes(),
|
||||||
value,
|
to,
|
||||||
memo
|
value,
|
||||||
|
memo
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun shieldToAddress(
|
override suspend fun shieldToAddress(
|
||||||
usk: UnifiedSpendingKey,
|
usk: UnifiedSpendingKey,
|
||||||
memo: ByteArray?
|
memo: ByteArray?
|
||||||
): Long = backend.shieldToAddress(
|
): FirstClassByteArray = FirstClassByteArray(
|
||||||
usk.account.value,
|
backend.shieldToAddress(
|
||||||
usk.copyBytes(),
|
usk.account.value,
|
||||||
memo
|
usk.copyBytes(),
|
||||||
|
memo
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun getCurrentAddress(account: Account): String {
|
override suspend fun getCurrentAddress(account: Account): String {
|
||||||
|
@ -98,8 +93,8 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
|
||||||
backend.rewindToHeight(height.value)
|
backend.rewindToHeight(height.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLatestBlockHeight(): BlockHeight? {
|
override suspend fun getLatestCacheHeight(): BlockHeight? {
|
||||||
return backend.getLatestHeight()?.let {
|
return backend.getLatestCacheHeight()?.let {
|
||||||
BlockHeight.new(
|
BlockHeight.new(
|
||||||
ZcashNetwork.from(backend.networkId),
|
ZcashNetwork.from(backend.networkId),
|
||||||
it
|
it
|
||||||
|
@ -115,19 +110,6 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
|
||||||
backend.rewindBlockMetadataToHeight(height.value)
|
backend.rewindBlockMetadataToHeight(height.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param limit The limit provides an efficient way how to restrict the portion of blocks, which will be validated.
|
|
||||||
* @return Null if successful. If an error occurs, the height will be the height where the error was detected.
|
|
||||||
*/
|
|
||||||
override suspend fun validateCombinedChainOrErrorBlockHeight(limit: Long?): BlockHeight? {
|
|
||||||
return backend.validateCombinedChainOrErrorHeight(limit)?.let {
|
|
||||||
BlockHeight.new(
|
|
||||||
ZcashNetwork.from(backend.networkId),
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDownloadedUtxoBalance(address: String): WalletBalance {
|
override suspend fun getDownloadedUtxoBalance(address: String): WalletBalance {
|
||||||
// Note this implementation is not ideal because it requires two database queries without a transaction, which
|
// Note this implementation is not ideal because it requires two database queries without a transaction, which
|
||||||
// makes the data potentially inconsistent. However the verified amount is queried first which makes this less
|
// makes the data potentially inconsistent. However the verified amount is queried first which makes this less
|
||||||
|
@ -162,13 +144,54 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSentMemoAsUtf8(idNote: Long) = backend.getSentMemoAsUtf8(idNote)
|
override suspend fun getMemoAsUtf8(txId: ByteArray, outputIndex: Int): String? =
|
||||||
|
backend.getMemoAsUtf8(txId, outputIndex)
|
||||||
override suspend fun getReceivedMemoAsUtf8(idNote: Long): String? = backend.getReceivedMemoAsUtf8(idNote)
|
|
||||||
|
|
||||||
override suspend fun initDataDb(seed: ByteArray?): Int = backend.initDataDb(seed)
|
override suspend fun initDataDb(seed: ByteArray?): Int = backend.initDataDb(seed)
|
||||||
|
|
||||||
override suspend fun scanBlocks(limit: Long?) = backend.scanBlocks(limit)
|
override suspend fun putSaplingSubtreeRoots(startIndex: Long, roots: List<SubtreeRoot>) =
|
||||||
|
backend.putSaplingSubtreeRoots(
|
||||||
|
startIndex = startIndex,
|
||||||
|
roots = roots.map {
|
||||||
|
JniSubtreeRoot.new(
|
||||||
|
rootHash = it.rootHash,
|
||||||
|
completingBlockHeight = it.completingBlockHeight.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun updateChainTip(height: BlockHeight) = backend.updateChainTip(height.value)
|
||||||
|
|
||||||
|
override suspend fun getFullyScannedHeight(): BlockHeight? {
|
||||||
|
return backend.getFullyScannedHeight()?.let {
|
||||||
|
BlockHeight.new(
|
||||||
|
ZcashNetwork.from(backend.networkId),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getMaxScannedHeight(): BlockHeight? {
|
||||||
|
return backend.getMaxScannedHeight()?.let {
|
||||||
|
BlockHeight.new(
|
||||||
|
ZcashNetwork.from(backend.networkId),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun scanBlocks(fromHeight: BlockHeight, limit: Long) = backend.scanBlocks(fromHeight.value, limit)
|
||||||
|
|
||||||
|
override suspend fun getScanProgress(): ScanProgress? = backend.getScanProgress()?.let { jniScanProgress ->
|
||||||
|
ScanProgress.new(jniScanProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun suggestScanRanges(): List<ScanRange> = backend.suggestScanRanges().map { jniScanRange ->
|
||||||
|
ScanRange.new(
|
||||||
|
jniScanRange,
|
||||||
|
network
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun decryptAndStoreTransaction(tx: ByteArray) = backend.decryptAndStoreTransaction(tx)
|
override suspend fun decryptAndStoreTransaction(tx: ByteArray) = backend.decryptAndStoreTransaction(tx)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package cash.z.ecc.android.sdk.internal.block
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.exception.LightWalletException
|
import cash.z.ecc.android.sdk.exception.LightWalletException
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.ext.retryUpTo
|
import cash.z.ecc.android.sdk.internal.ext.retryUpToAndThrow
|
||||||
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
||||||
import cash.z.ecc.android.sdk.internal.model.ext.from
|
import cash.z.ecc.android.sdk.internal.model.ext.from
|
||||||
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
|
||||||
|
@ -12,7 +12,7 @@ import co.electriccoin.lightwallet.client.model.BlockHeightUnsafe
|
||||||
import co.electriccoin.lightwallet.client.model.CompactBlockUnsafe
|
import co.electriccoin.lightwallet.client.model.CompactBlockUnsafe
|
||||||
import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe
|
import co.electriccoin.lightwallet.client.model.LightWalletEndpointInfoUnsafe
|
||||||
import co.electriccoin.lightwallet.client.model.Response
|
import co.electriccoin.lightwallet.client.model.Response
|
||||||
import kotlinx.coroutines.Dispatchers
|
import co.electriccoin.lightwallet.client.model.ShieldedProtocolEnum
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -31,8 +31,7 @@ import kotlinx.coroutines.withContext
|
||||||
*/
|
*/
|
||||||
open class CompactBlockDownloader private constructor(val compactBlockRepository: CompactBlockRepository) {
|
open class CompactBlockDownloader private constructor(val compactBlockRepository: CompactBlockRepository) {
|
||||||
|
|
||||||
lateinit var lightWalletClient: LightWalletClient
|
private lateinit var lightWalletClient: LightWalletClient
|
||||||
private set
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
lightWalletClient: LightWalletClient,
|
lightWalletClient: LightWalletClient,
|
||||||
|
@ -112,7 +111,7 @@ open class CompactBlockDownloader private constructor(val compactBlockRepository
|
||||||
compactBlockRepository.getLatestHeight()
|
compactBlockRepository.getLatestHeight()
|
||||||
|
|
||||||
suspend fun getServerInfo(): LightWalletEndpointInfoUnsafe? = withContext(IO) {
|
suspend fun getServerInfo(): LightWalletEndpointInfoUnsafe? = withContext(IO) {
|
||||||
retryUpTo(GET_SERVER_INFO_RETRIES) {
|
retryUpToAndThrow(GET_SERVER_INFO_RETRIES) {
|
||||||
when (val response = lightWalletClient.getServerInfo()) {
|
when (val response = lightWalletClient.getServerInfo()) {
|
||||||
is Response.Success -> return@withContext response.result
|
is Response.Success -> return@withContext response.result
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -129,11 +128,21 @@ open class CompactBlockDownloader private constructor(val compactBlockRepository
|
||||||
* Stop this downloader and cleanup any resources being used.
|
* Stop this downloader and cleanup any resources being used.
|
||||||
*/
|
*/
|
||||||
suspend fun stop() {
|
suspend fun stop() {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(IO) {
|
||||||
lightWalletClient.shutdown()
|
lightWalletClient.shutdown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconnect to the same or a different server. This is useful when the connection is
|
||||||
|
* unrecoverable. That might be time to switch to a mirror or just reconnect.
|
||||||
|
*/
|
||||||
|
suspend fun reconnect() {
|
||||||
|
withContext(IO) {
|
||||||
|
lightWalletClient.reconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the details of a known transaction.
|
* Fetch the details of a known transaction.
|
||||||
*
|
*
|
||||||
|
@ -141,6 +150,34 @@ open class CompactBlockDownloader private constructor(val compactBlockRepository
|
||||||
*/
|
*/
|
||||||
suspend fun fetchTransaction(txId: ByteArray) = lightWalletClient.fetchTransaction(txId)
|
suspend fun fetchTransaction(txId: ByteArray) = lightWalletClient.fetchTransaction(txId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all UTXOs for the given addresses and from the given height.
|
||||||
|
*
|
||||||
|
* @return Flow of UTXOs for the given [tAddresses] from the [startHeight]
|
||||||
|
*/
|
||||||
|
suspend fun fetchUtxos(
|
||||||
|
tAddresses: List<String>,
|
||||||
|
startHeight: BlockHeightUnsafe
|
||||||
|
) = lightWalletClient.fetchUtxos(
|
||||||
|
tAddresses = tAddresses,
|
||||||
|
startHeight = startHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a stream of information about roots of subtrees of the Sapling and Orchard note commitment trees.
|
||||||
|
*
|
||||||
|
* @return a flow of information about roots of subtrees of the Sapling and Orchard note commitment trees.
|
||||||
|
*/
|
||||||
|
suspend fun getSubtreeRoots(
|
||||||
|
startIndex: Int,
|
||||||
|
shieldedProtocol: ShieldedProtocolEnum,
|
||||||
|
maxEntries: Int
|
||||||
|
) = lightWalletClient.getSubtreeRoots(
|
||||||
|
startIndex = startIndex,
|
||||||
|
shieldedProtocol = shieldedProtocol,
|
||||||
|
maxEntries = maxEntries
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val GET_SERVER_INFO_RETRIES = 6
|
private const val GET_SERVER_INFO_RETRIES = 6
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
package cash.z.ecc.android.sdk.internal.db.derived
|
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import cash.z.ecc.android.sdk.internal.db.queryAndMap
|
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
internal class BlockTable(private val zcashNetwork: ZcashNetwork, private val sqliteDatabase: SupportSQLiteDatabase) {
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val SELECTION_MIN_HEIGHT = arrayOf(
|
|
||||||
String.format(
|
|
||||||
Locale.ROOT,
|
|
||||||
"MIN(%s)", // $NON-NLS
|
|
||||||
BlockTableDefinition.COLUMN_LONG_HEIGHT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private val SELECTION_MAX_HEIGHT = arrayOf(
|
|
||||||
String.format(
|
|
||||||
Locale.ROOT,
|
|
||||||
"MAX(%s)", // $NON-NLS
|
|
||||||
BlockTableDefinition.COLUMN_LONG_HEIGHT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private val SELECTION_BLOCK_HEIGHT = String.format(
|
|
||||||
Locale.ROOT,
|
|
||||||
"%s = ?", // $NON-NLS
|
|
||||||
BlockTableDefinition.COLUMN_LONG_HEIGHT
|
|
||||||
)
|
|
||||||
|
|
||||||
private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS
|
|
||||||
|
|
||||||
private val PROJECTION_HASH = arrayOf(BlockTableDefinition.COLUMN_BLOB_HASH)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun count() = sqliteDatabase.queryAndMap(
|
|
||||||
BlockTableDefinition.TABLE_NAME,
|
|
||||||
columns = PROJECTION_COUNT,
|
|
||||||
cursorParser = { it.getLong(0) }
|
|
||||||
).first()
|
|
||||||
|
|
||||||
suspend fun firstScannedHeight(): BlockHeight {
|
|
||||||
// Note that we assume the Rust layer will add the birthday height as the first block
|
|
||||||
val heightLong =
|
|
||||||
sqliteDatabase.queryAndMap(
|
|
||||||
table = BlockTableDefinition.TABLE_NAME,
|
|
||||||
columns = SELECTION_MIN_HEIGHT,
|
|
||||||
cursorParser = { it.getLong(0) }
|
|
||||||
).first()
|
|
||||||
|
|
||||||
return BlockHeight.new(zcashNetwork, heightLong)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun lastScannedHeight(): BlockHeight {
|
|
||||||
// Note that we assume the Rust layer will add the birthday height as the first block
|
|
||||||
val heightLong =
|
|
||||||
sqliteDatabase.queryAndMap(
|
|
||||||
table = BlockTableDefinition.TABLE_NAME,
|
|
||||||
columns = SELECTION_MAX_HEIGHT,
|
|
||||||
cursorParser = { it.getLong(0) }
|
|
||||||
).first()
|
|
||||||
|
|
||||||
return BlockHeight.new(zcashNetwork, heightLong)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findBlockHash(blockHeight: BlockHeight): ByteArray? {
|
|
||||||
return sqliteDatabase.queryAndMap(
|
|
||||||
table = BlockTableDefinition.TABLE_NAME,
|
|
||||||
columns = PROJECTION_HASH,
|
|
||||||
selection = SELECTION_BLOCK_HEIGHT,
|
|
||||||
selectionArgs = arrayOf(blockHeight.value),
|
|
||||||
cursorParser = { it.getBlob(0) }
|
|
||||||
).firstOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal object BlockTableDefinition {
|
|
||||||
const val TABLE_NAME = "blocks" // $NON-NLS
|
|
||||||
|
|
||||||
const val COLUMN_LONG_HEIGHT = "height" // $NON-NLS
|
|
||||||
|
|
||||||
const val COLUMN_BLOB_HASH = "hash" // $NON-NLS
|
|
||||||
}
|
|
|
@ -4,6 +4,7 @@ import cash.z.ecc.android.sdk.internal.model.DbTransactionOverview
|
||||||
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
||||||
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -17,24 +18,12 @@ internal class DbDerivedDataRepository(
|
||||||
) : DerivedDataRepository {
|
) : DerivedDataRepository {
|
||||||
private val invalidatingFlow = MutableStateFlow(UUID.randomUUID())
|
private val invalidatingFlow = MutableStateFlow(UUID.randomUUID())
|
||||||
|
|
||||||
override suspend fun lastScannedHeight(): BlockHeight {
|
|
||||||
return derivedDataDb.blockTable.lastScannedHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun firstUnenhancedHeight(): BlockHeight? {
|
override suspend fun firstUnenhancedHeight(): BlockHeight? {
|
||||||
return derivedDataDb.allTransactionView.firstUnenhancedHeight()
|
return derivedDataDb.allTransactionView.firstUnenhancedHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun firstScannedHeight(): BlockHeight {
|
override suspend fun findEncodedTransactionByTxId(txId: FirstClassByteArray): EncodedTransaction? {
|
||||||
return derivedDataDb.blockTable.firstScannedHeight()
|
return derivedDataDb.transactionTable.findEncodedTransactionByTxId(txId)
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun isInitialized(): Boolean {
|
|
||||||
return derivedDataDb.blockTable.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findEncodedTransactionById(txId: Long): EncodedTransaction? {
|
|
||||||
return derivedDataDb.transactionTable.findEncodedTransactionById(txId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<DbTransactionOverview> =
|
override suspend fun findNewTransactions(blockHeightRange: ClosedRange<BlockHeight>): List<DbTransactionOverview> =
|
||||||
|
@ -48,8 +37,6 @@ internal class DbDerivedDataRepository(
|
||||||
override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray) = derivedDataDb.transactionTable
|
override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray) = derivedDataDb.transactionTable
|
||||||
.findDatabaseId(rawTransactionId)
|
.findDatabaseId(rawTransactionId)
|
||||||
|
|
||||||
override suspend fun findBlockHash(height: BlockHeight) = derivedDataDb.blockTable.findBlockHash(height)
|
|
||||||
|
|
||||||
override suspend fun getTransactionCount() = derivedDataDb.transactionTable.count()
|
override suspend fun getTransactionCount() = derivedDataDb.transactionTable.count()
|
||||||
|
|
||||||
override fun invalidate() {
|
override fun invalidate() {
|
||||||
|
@ -63,7 +50,8 @@ internal class DbDerivedDataRepository(
|
||||||
override val allTransactions: Flow<List<DbTransactionOverview>>
|
override val allTransactions: Flow<List<DbTransactionOverview>>
|
||||||
get() = invalidatingFlow.map { derivedDataDb.allTransactionView.getAllTransactions().toList() }
|
get() = invalidatingFlow.map { derivedDataDb.allTransactionView.getAllTransactions().toList() }
|
||||||
|
|
||||||
override fun getNoteIds(transactionId: Long) = derivedDataDb.txOutputsView.getNoteIds(transactionId)
|
override fun getSaplingOutputIndices(transactionId: Long) =
|
||||||
|
derivedDataDb.txOutputsView.getSaplingOutputIndices(transactionId)
|
||||||
|
|
||||||
override fun getRecipients(transactionId: Long): Flow<TransactionRecipient> {
|
override fun getRecipients(transactionId: Long): Flow<TransactionRecipient> {
|
||||||
return derivedDataDb.txOutputsView.getRecipients(transactionId)
|
return derivedDataDb.txOutputsView.getRecipients(transactionId)
|
||||||
|
|
|
@ -2,13 +2,13 @@ package cash.z.ecc.android.sdk.internal.db.derived
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException
|
||||||
import cash.z.ecc.android.sdk.internal.NoBackupContextWrapper
|
import cash.z.ecc.android.sdk.internal.NoBackupContextWrapper
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.TypesafeBackend
|
import cash.z.ecc.android.sdk.internal.TypesafeBackend
|
||||||
import cash.z.ecc.android.sdk.internal.db.ReadOnlySupportSqliteOpenHelper
|
import cash.z.ecc.android.sdk.internal.db.ReadOnlySupportSqliteOpenHelper
|
||||||
import cash.z.ecc.android.sdk.internal.ext.tryWarn
|
|
||||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -20,8 +20,6 @@ internal class DerivedDataDb private constructor(
|
||||||
) {
|
) {
|
||||||
val accountTable = AccountTable(sqliteDatabase)
|
val accountTable = AccountTable(sqliteDatabase)
|
||||||
|
|
||||||
val blockTable = BlockTable(zcashNetwork, sqliteDatabase)
|
|
||||||
|
|
||||||
val transactionTable = TransactionTable(zcashNetwork, sqliteDatabase)
|
val transactionTable = TransactionTable(zcashNetwork, sqliteDatabase)
|
||||||
|
|
||||||
val allTransactionView = AllTransactionView(zcashNetwork, sqliteDatabase)
|
val allTransactionView = AllTransactionView(zcashNetwork, sqliteDatabase)
|
||||||
|
@ -39,7 +37,7 @@ internal class DerivedDataDb private constructor(
|
||||||
// SqliteOpenHelper is happy
|
// SqliteOpenHelper is happy
|
||||||
private const val DATABASE_VERSION = 8
|
private const val DATABASE_VERSION = 8
|
||||||
|
|
||||||
@Suppress("LongParameterList", "SpreadOperator")
|
@Suppress("LongParameterList")
|
||||||
suspend fun new(
|
suspend fun new(
|
||||||
context: Context,
|
context: Context,
|
||||||
backend: TypesafeBackend,
|
backend: TypesafeBackend,
|
||||||
|
@ -47,27 +45,16 @@ internal class DerivedDataDb private constructor(
|
||||||
zcashNetwork: ZcashNetwork,
|
zcashNetwork: ZcashNetwork,
|
||||||
checkpoint: Checkpoint,
|
checkpoint: Checkpoint,
|
||||||
seed: ByteArray?,
|
seed: ByteArray?,
|
||||||
viewingKeys: List<UnifiedFullViewingKey>
|
numberOfAccounts: Int,
|
||||||
|
recoverUntil: BlockHeight?
|
||||||
): DerivedDataDb {
|
): DerivedDataDb {
|
||||||
backend.initDataDb(seed)
|
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
// TODO [#681]: consider converting these to typed exceptions in the welding layer
|
val result = backend.initDataDb(seed)
|
||||||
// TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681
|
if (result < 0) {
|
||||||
tryWarn(
|
throw CompactBlockProcessorException.Uninitialized()
|
||||||
message = "Did not initialize the blocks table. It probably was already initialized.",
|
|
||||||
ifContains = "table is not empty"
|
|
||||||
) {
|
|
||||||
backend.initBlocksTable(checkpoint)
|
|
||||||
}
|
|
||||||
tryWarn(
|
|
||||||
message = "Did not initialize the accounts table. It probably was already initialized.",
|
|
||||||
ifContains = "table is not empty"
|
|
||||||
) {
|
|
||||||
backend.initAccountsTable(*viewingKeys.toTypedArray())
|
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Twig.error { "Failed to init derived data database with $it" }
|
throw CompactBlockProcessorException.Uninitialized(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val database = ReadOnlySupportSqliteOpenHelper.openExistingDatabaseAsReadOnly(
|
val database = ReadOnlySupportSqliteOpenHelper.openExistingDatabaseAsReadOnly(
|
||||||
|
@ -79,7 +66,29 @@ internal class DerivedDataDb private constructor(
|
||||||
DATABASE_VERSION
|
DATABASE_VERSION
|
||||||
)
|
)
|
||||||
|
|
||||||
return DerivedDataDb(zcashNetwork, database)
|
val dataDb = DerivedDataDb(zcashNetwork, database)
|
||||||
|
|
||||||
|
// If a seed is provided, fill in the accounts.
|
||||||
|
seed?.let { checkedSeed ->
|
||||||
|
// toInt() should be safe because we expect very few accounts
|
||||||
|
val missingAccounts = numberOfAccounts - dataDb.accountTable.count().toInt()
|
||||||
|
require(missingAccounts >= 0) {
|
||||||
|
"Unexpected number of accounts: $missingAccounts"
|
||||||
|
}
|
||||||
|
repeat(missingAccounts) {
|
||||||
|
runCatching {
|
||||||
|
backend.createAccountAndGetSpendingKey(
|
||||||
|
seed = checkedSeed,
|
||||||
|
treeState = checkpoint.treeState(),
|
||||||
|
recoverUntil = recoverUntil
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
Twig.error(it) { "Create account failed." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataDb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ internal class TransactionTable(
|
||||||
private val SELECTION_TRANSACTION_ID_AND_RAW_NOT_NULL = String.format(
|
private val SELECTION_TRANSACTION_ID_AND_RAW_NOT_NULL = String.format(
|
||||||
Locale.ROOT,
|
Locale.ROOT,
|
||||||
"%s = ? AND %s IS NOT NULL", // $NON-NLS
|
"%s = ? AND %s IS NOT NULL", // $NON-NLS
|
||||||
TransactionTableDefinition.COLUMN_INTEGER_ID,
|
TransactionTableDefinition.COLUMN_BLOB_TRANSACTION_ID,
|
||||||
TransactionTableDefinition.COLUMN_BLOB_RAW
|
TransactionTableDefinition.COLUMN_BLOB_RAW
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -66,18 +66,16 @@ internal class TransactionTable(
|
||||||
cursorParser = { it.getLong(0) }
|
cursorParser = { it.getLong(0) }
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
suspend fun findEncodedTransactionById(id: Long): EncodedTransaction? {
|
suspend fun findEncodedTransactionByTxId(txId: FirstClassByteArray): EncodedTransaction? {
|
||||||
return sqliteDatabase.queryAndMap(
|
return sqliteDatabase.queryAndMap(
|
||||||
table = TransactionTableDefinition.TABLE_NAME,
|
table = TransactionTableDefinition.TABLE_NAME,
|
||||||
columns = PROJECTION_ENCODED_TRANSACTION,
|
columns = PROJECTION_ENCODED_TRANSACTION,
|
||||||
selection = SELECTION_TRANSACTION_ID_AND_RAW_NOT_NULL,
|
selection = SELECTION_TRANSACTION_ID_AND_RAW_NOT_NULL,
|
||||||
selectionArgs = arrayOf(id)
|
selectionArgs = arrayOf(txId.byteArray)
|
||||||
) {
|
) {
|
||||||
val txIdIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_BLOB_TRANSACTION_ID)
|
|
||||||
val rawIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_BLOB_RAW)
|
val rawIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_BLOB_RAW)
|
||||||
val heightIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT)
|
val heightIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT)
|
||||||
|
|
||||||
val txid = it.getBlob(txIdIndex)
|
|
||||||
val raw = it.getBlob(rawIndex)
|
val raw = it.getBlob(rawIndex)
|
||||||
val expiryHeight = if (it.isNull(heightIndex)) {
|
val expiryHeight = if (it.isNull(heightIndex)) {
|
||||||
null
|
null
|
||||||
|
@ -86,7 +84,7 @@ internal class TransactionTable(
|
||||||
}
|
}
|
||||||
|
|
||||||
EncodedTransaction(
|
EncodedTransaction(
|
||||||
FirstClassByteArray(txid),
|
txId,
|
||||||
FirstClassByteArray(raw),
|
FirstClassByteArray(raw),
|
||||||
expiryHeight
|
expiryHeight
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,7 +20,7 @@ internal class TxOutputsView(
|
||||||
TxOutputsViewDefinition.COLUMN_INTEGER_TRANSACTION_ID
|
TxOutputsViewDefinition.COLUMN_INTEGER_TRANSACTION_ID
|
||||||
)
|
)
|
||||||
|
|
||||||
private val PROJECTION_ID = arrayOf(TxOutputsViewDefinition.COLUMN_INTEGER_TRANSACTION_ID)
|
private val PROJECTION_OUTPUT_INDEX = arrayOf(TxOutputsViewDefinition.COLUMN_INTEGER_OUTPUT_INDEX)
|
||||||
|
|
||||||
private val PROJECTION_RECIPIENT = arrayOf(
|
private val PROJECTION_RECIPIENT = arrayOf(
|
||||||
TxOutputsViewDefinition.COLUMN_STRING_TO_ADDRESS,
|
TxOutputsViewDefinition.COLUMN_STRING_TO_ADDRESS,
|
||||||
|
@ -35,17 +35,17 @@ internal class TxOutputsView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNoteIds(transactionId: Long) =
|
fun getSaplingOutputIndices(transactionId: Long) =
|
||||||
sqliteDatabase.queryAndMap(
|
sqliteDatabase.queryAndMap(
|
||||||
table = TxOutputsViewDefinition.VIEW_NAME,
|
table = TxOutputsViewDefinition.VIEW_NAME,
|
||||||
columns = PROJECTION_ID,
|
columns = PROJECTION_OUTPUT_INDEX,
|
||||||
selection = SELECT_BY_TRANSACTION_ID_AND_NOT_CHANGE,
|
selection = SELECT_BY_TRANSACTION_ID_AND_NOT_CHANGE,
|
||||||
selectionArgs = arrayOf(transactionId),
|
selectionArgs = arrayOf(transactionId),
|
||||||
orderBy = ORDER_BY,
|
orderBy = ORDER_BY,
|
||||||
cursorParser = {
|
cursorParser = {
|
||||||
val idColumnIndex = it.getColumnIndex(TxOutputsViewDefinition.COLUMN_INTEGER_TRANSACTION_ID)
|
val idColumnOutputIndex = it.getColumnIndex(TxOutputsViewDefinition.COLUMN_INTEGER_OUTPUT_INDEX)
|
||||||
|
|
||||||
it.getLong(idColumnIndex)
|
it.getInt(idColumnOutputIndex)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -44,3 +44,30 @@ internal inline fun <R> tryWarn(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Do NOT change these texts as they match the ones from ScanError in
|
||||||
|
// librustzcash/zcash_client_backend/src/scanning.rs
|
||||||
|
internal const val PREV_HASH_MISMATCH = "The parent hash of proposed block does not correspond to the block hash at " +
|
||||||
|
"height" // $NON-NLS
|
||||||
|
internal const val BLOCK_HEIGHT_DISCONTINUITY = "Block height discontinuity at height" // $NON-NLS
|
||||||
|
internal const val TREE_SIZE_MISMATCH = "note commitment tree size provided by a compact block did not match the " +
|
||||||
|
"expected size at height" // $NON-NLS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether this error is the result of a failed continuity while scanning new blocks in the Rust layer.
|
||||||
|
*
|
||||||
|
* @return true in case of the check match, false otherwise
|
||||||
|
*/
|
||||||
|
internal fun Throwable.isScanContinuityError(): Boolean {
|
||||||
|
val errorMessages = listOf(
|
||||||
|
PREV_HASH_MISMATCH,
|
||||||
|
BLOCK_HEIGHT_DISCONTINUITY,
|
||||||
|
TREE_SIZE_MISMATCH
|
||||||
|
)
|
||||||
|
errorMessages.forEach { errMessage ->
|
||||||
|
if (this.message?.lowercase()?.contains(errMessage.lowercase()) == true) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
package cash.z.ecc.android.sdk.internal.ext
|
package cash.z.ecc.android.sdk.internal.ext
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
|
import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
|
||||||
import cash.z.ecc.android.sdk.internal.Twig
|
import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import java.io.File
|
import kotlin.math.pow
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,7 +17,7 @@ import kotlin.random.Random
|
||||||
* @param block the code to execute, which will be wrapped in a try/catch and retried whenever an
|
* @param block the code to execute, which will be wrapped in a try/catch and retried whenever an
|
||||||
* exception is thrown up to [retries] attempts.
|
* exception is thrown up to [retries] attempts.
|
||||||
*/
|
*/
|
||||||
suspend inline fun retryUpTo(
|
suspend inline fun retryUpToAndThrow(
|
||||||
retries: Int,
|
retries: Int,
|
||||||
exceptionWrapper: (Throwable) -> Throwable = { it },
|
exceptionWrapper: (Throwable) -> Throwable = { it },
|
||||||
initialDelayMillis: Long = 500L,
|
initialDelayMillis: Long = 500L,
|
||||||
|
@ -35,7 +34,43 @@ suspend inline fun retryUpTo(
|
||||||
if (failedAttempts > retries) {
|
if (failedAttempts > retries) {
|
||||||
throw exceptionWrapper(t)
|
throw exceptionWrapper(t)
|
||||||
}
|
}
|
||||||
val duration = (initialDelayMillis.toDouble() * Math.pow(2.0, failedAttempts.toDouble() - 1)).toLong()
|
val duration = (initialDelayMillis.toDouble() * 2.0.pow(failedAttempts.toDouble() - 1)).toLong()
|
||||||
|
Twig.warn(t) { "Retrying ($failedAttempts/$retries) in ${duration}s..." }
|
||||||
|
delay(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the given block and if it fails, retry up to [retries] more times. If none of the
|
||||||
|
* retries succeed, then leave the block execution unfinished and continue.
|
||||||
|
*
|
||||||
|
* @param retries the number of times to retry the block after the first attempt fails.
|
||||||
|
* @param exceptionWrapper a function that can wrap the final failure to add more useful information
|
||||||
|
* * or context. Default behavior is to just return the final exception.
|
||||||
|
* @param initialDelayMillis the initial amount of time to wait before the first retry.
|
||||||
|
* @param block the code to execute, which will be wrapped in a try/catch and retried whenever an
|
||||||
|
* exception is thrown up to [retries] attempts.
|
||||||
|
*/
|
||||||
|
suspend inline fun retryUpToAndContinue(
|
||||||
|
retries: Int,
|
||||||
|
exceptionWrapper: (Throwable) -> Throwable = { it },
|
||||||
|
initialDelayMillis: Long = 500L,
|
||||||
|
block: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
var failedAttempts = 0
|
||||||
|
while (failedAttempts < retries) {
|
||||||
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
try {
|
||||||
|
block(failedAttempts)
|
||||||
|
return
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
failedAttempts++
|
||||||
|
if (failedAttempts == retries) {
|
||||||
|
exceptionWrapper(t)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val duration = (initialDelayMillis.toDouble() * 2.0.pow(failedAttempts.toDouble() - 1)).toLong()
|
||||||
Twig.warn(t) { "Retrying ($failedAttempts/$retries) in ${duration}s..." }
|
Twig.warn(t) { "Retrying ($failedAttempts/$retries) in ${duration}s..." }
|
||||||
delay(duration)
|
delay(duration)
|
||||||
}
|
}
|
||||||
|
@ -73,10 +108,8 @@ suspend inline fun retryWithBackoff(
|
||||||
|
|
||||||
sequence++
|
sequence++
|
||||||
// initialDelay^(sequence/4) + jitter
|
// initialDelay^(sequence/4) + jitter
|
||||||
var duration = Math.pow(
|
var duration = initialDelayMillis.toDouble().pow((sequence.toDouble() / 4.0)).toLong() +
|
||||||
initialDelayMillis.toDouble(),
|
Random.nextLong(1000L)
|
||||||
(sequence.toDouble() / 4.0)
|
|
||||||
).toLong() + Random.nextLong(1000L)
|
|
||||||
if (duration > maxDelayMillis) {
|
if (duration > maxDelayMillis) {
|
||||||
duration = maxDelayMillis - Random.nextLong(1000L) // include jitter but don't exceed max delay
|
duration = maxDelayMillis - Random.nextLong(1000L) // include jitter but don't exceed max delay
|
||||||
sequence /= 2
|
sequence /= 2
|
||||||
|
@ -86,12 +119,3 @@ suspend inline fun retryWithBackoff(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if the given database already exists.
|
|
||||||
*
|
|
||||||
* @return true when the given database exists in the given context.
|
|
||||||
*/
|
|
||||||
internal fun dbExists(appContext: Context, dbFileName: String): Boolean {
|
|
||||||
return File(appContext.getDatabasePath(dbFileName).absolutePath).exists()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package cash.z.ecc.android.sdk.internal.model
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.isInUIntRange
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,5 +19,12 @@ internal data class Checkpoint(
|
||||||
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
|
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
|
||||||
val tree: String
|
val tree: String
|
||||||
) {
|
) {
|
||||||
|
fun treeState(): TreeState {
|
||||||
|
require(epochSeconds.isInUIntRange()) {
|
||||||
|
"epochSeconds $epochSeconds is outside of allowed UInt range"
|
||||||
|
}
|
||||||
|
return TreeState.fromParts(height.value, hash, epochSeconds.toInt(), tree)
|
||||||
|
}
|
||||||
|
|
||||||
internal companion object
|
internal companion object
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
internal data class ScanProgress(
|
||||||
|
private val numerator: Long,
|
||||||
|
private val denominator: Long
|
||||||
|
) {
|
||||||
|
override fun toString() = "ScanProgress($numerator/$denominator) -> ${getSafeRation()}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns progress ratio in [0, 1] range. Any out-of-range value is treated as 0.
|
||||||
|
*/
|
||||||
|
fun getSafeRation() = numerator.toFloat().div(denominator).let { ration ->
|
||||||
|
if (ration < 0f || ration > 1f) {
|
||||||
|
0f
|
||||||
|
} else {
|
||||||
|
ration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun new(jni: JniScanProgress): ScanProgress {
|
||||||
|
return ScanProgress(
|
||||||
|
numerator = jni.numerator,
|
||||||
|
denominator = jni.denominator
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
|
||||||
|
internal data class ScanRange(
|
||||||
|
val range: ClosedRange<BlockHeight>,
|
||||||
|
val priority: Long
|
||||||
|
) {
|
||||||
|
override fun toString() = "ScanRange(range=$range, priority=${getSuggestScanRangePriority()})"
|
||||||
|
|
||||||
|
internal fun getSuggestScanRangePriority(): SuggestScanRangePriority {
|
||||||
|
return SuggestScanRangePriority.entries
|
||||||
|
.firstOrNull { it.priority == priority } ?: SuggestScanRangePriority.Ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Note that this function subtracts 1 from [JniScanRange.endHeight] as the rest of the logic works with
|
||||||
|
* [ClosedRange] and the endHeight is exclusive.
|
||||||
|
*/
|
||||||
|
fun new(jni: JniScanRange, zcashNetwork: ZcashNetwork): ScanRange {
|
||||||
|
return ScanRange(
|
||||||
|
range =
|
||||||
|
BlockHeight.new(zcashNetwork, jni.startHeight)..(BlockHeight.new(zcashNetwork, jni.endHeight) - 1),
|
||||||
|
priority = jni.priority
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
internal enum class SuggestScanRangePriority(val priority: Long) {
|
||||||
|
Ignored(0),
|
||||||
|
Scanned(10),
|
||||||
|
Historic(20),
|
||||||
|
OpenAdjacent(30),
|
||||||
|
FoundNote(40),
|
||||||
|
ChainTip(50),
|
||||||
|
Verify(60)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import co.electriccoin.lightwallet.client.model.SubtreeRootUnsafe
|
||||||
|
|
||||||
|
internal data class SubtreeRoot(
|
||||||
|
val rootHash: ByteArray,
|
||||||
|
val completingBlockHash: ByteArray,
|
||||||
|
val completingBlockHeight: BlockHeight
|
||||||
|
) {
|
||||||
|
override fun toString() = "SubtreeRoot(completingBlockHeight=${completingBlockHeight.value})"
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as SubtreeRoot
|
||||||
|
|
||||||
|
if (!rootHash.contentEquals(other.rootHash)) return false
|
||||||
|
if (!completingBlockHash.contentEquals(other.completingBlockHash)) return false
|
||||||
|
if (completingBlockHeight != other.completingBlockHeight) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = rootHash.contentHashCode()
|
||||||
|
result = 31 * result + completingBlockHash.contentHashCode()
|
||||||
|
result = 31 * result + completingBlockHeight.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun new(unsafe: SubtreeRootUnsafe, zcashNetwork: ZcashNetwork): SubtreeRoot {
|
||||||
|
return SubtreeRoot(
|
||||||
|
rootHash = unsafe.rootHash,
|
||||||
|
completingBlockHash = unsafe.completingBlockHash,
|
||||||
|
completingBlockHeight = BlockHeight.new(zcashNetwork, unsafe.completingBlockHeight.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import co.electriccoin.lightwallet.client.model.TreeStateUnsafe
|
||||||
|
|
||||||
|
class TreeState(
|
||||||
|
val encoded: ByteArray
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun new(unsafe: TreeStateUnsafe): TreeState {
|
||||||
|
// Potential validation comes here
|
||||||
|
return TreeState(
|
||||||
|
encoded = unsafe.encoded
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromParts(
|
||||||
|
height: Long,
|
||||||
|
hash: String,
|
||||||
|
time: Int,
|
||||||
|
tree: String
|
||||||
|
): TreeState {
|
||||||
|
val unsafeTreeState = TreeStateUnsafe.fromParts(height, hash, time, tree)
|
||||||
|
return TreeState.new(unsafeTreeState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal.repository
|
||||||
import cash.z.ecc.android.sdk.internal.model.DbTransactionOverview
|
import cash.z.ecc.android.sdk.internal.model.DbTransactionOverview
|
||||||
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
||||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@ -12,13 +13,6 @@ import kotlinx.coroutines.flow.Flow
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
internal interface DerivedDataRepository {
|
internal interface DerivedDataRepository {
|
||||||
|
|
||||||
/**
|
|
||||||
* The last height scanned by this repository.
|
|
||||||
*
|
|
||||||
* @return the last height scanned by this repository.
|
|
||||||
*/
|
|
||||||
suspend fun lastScannedHeight(): BlockHeight
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The height of the first transaction that hasn't been enhanced yet.
|
* The height of the first transaction that hasn't been enhanced yet.
|
||||||
*
|
*
|
||||||
|
@ -27,18 +21,6 @@ internal interface DerivedDataRepository {
|
||||||
*/
|
*/
|
||||||
suspend fun firstUnenhancedHeight(): BlockHeight?
|
suspend fun firstUnenhancedHeight(): BlockHeight?
|
||||||
|
|
||||||
/**
|
|
||||||
* The height of the first block in this repository. This is typically the checkpoint that was
|
|
||||||
* used to initialize this wallet. If we overwrite this block, it breaks our ability to spend
|
|
||||||
* funds.
|
|
||||||
*/
|
|
||||||
suspend fun firstScannedHeight(): BlockHeight
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true when this repository has been initialized and seeded with the initial checkpoint.
|
|
||||||
*/
|
|
||||||
suspend fun isInitialized(): Boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the encoded transaction associated with the given id.
|
* Find the encoded transaction associated with the given id.
|
||||||
*
|
*
|
||||||
|
@ -46,7 +28,7 @@ internal interface DerivedDataRepository {
|
||||||
*
|
*
|
||||||
* @return the transaction or null when it cannot be found.
|
* @return the transaction or null when it cannot be found.
|
||||||
*/
|
*/
|
||||||
suspend fun findEncodedTransactionById(txId: Long): EncodedTransaction?
|
suspend fun findEncodedTransactionByTxId(txId: FirstClassByteArray): EncodedTransaction?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all the newly scanned transactions in the given range, including transactions (like
|
* Find all the newly scanned transactions in the given range, including transactions (like
|
||||||
|
@ -75,11 +57,6 @@ internal interface DerivedDataRepository {
|
||||||
|
|
||||||
suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long?
|
suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long?
|
||||||
|
|
||||||
// TODO [#681]: begin converting these into Data Access API. For now, just collect the desired
|
|
||||||
// operations and iterate/refactor, later
|
|
||||||
// TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681
|
|
||||||
suspend fun findBlockHash(height: BlockHeight): ByteArray?
|
|
||||||
|
|
||||||
suspend fun getTransactionCount(): Long
|
suspend fun getTransactionCount(): Long
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,7 +82,7 @@ internal interface DerivedDataRepository {
|
||||||
|
|
||||||
val allTransactions: Flow<List<DbTransactionOverview>>
|
val allTransactions: Flow<List<DbTransactionOverview>>
|
||||||
|
|
||||||
fun getNoteIds(transactionId: Long): Flow<Long>
|
fun getSaplingOutputIndices(transactionId: Long): Flow<Int>
|
||||||
|
|
||||||
fun getRecipients(transactionId: Long): Flow<TransactionRecipient>
|
fun getRecipients(transactionId: Long): Flow<TransactionRecipient>
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ internal class FileCompactBlockRepository(
|
||||||
private val backend: TypesafeBackend
|
private val backend: TypesafeBackend
|
||||||
) : CompactBlockRepository {
|
) : CompactBlockRepository {
|
||||||
|
|
||||||
override suspend fun getLatestHeight() = backend.getLatestBlockHeight()
|
override suspend fun getLatestHeight() = backend.getLatestCacheHeight()
|
||||||
|
|
||||||
override suspend fun findCompactBlock(height: BlockHeight) = backend.findBlockMetadata(height)
|
override suspend fun findCompactBlock(height: BlockHeight) = backend.findBlockMetadata(height)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package cash.z.ecc.android.sdk.internal.transaction
|
package cash.z.ecc.android.sdk.internal.transaction
|
||||||
|
|
||||||
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
|
@ -69,6 +70,10 @@ internal interface TransactionEncoder {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the consensus branch that the encoder is using when making transactions.
|
* Return the consensus branch that the encoder is using when making transactions.
|
||||||
|
*
|
||||||
|
* @param height the height at which we want to get the consensus branch
|
||||||
|
*
|
||||||
|
* @return id of consensus branch
|
||||||
*/
|
*/
|
||||||
suspend fun getConsensusBranchId(): Long
|
suspend fun getConsensusBranchId(height: BlockHeight): Long
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import cash.z.ecc.android.sdk.internal.Twig
|
||||||
import cash.z.ecc.android.sdk.internal.TypesafeBackend
|
import cash.z.ecc.android.sdk.internal.TypesafeBackend
|
||||||
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
|
||||||
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
import cash.z.ecc.android.sdk.model.FirstClassByteArray
|
||||||
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
import cash.z.ecc.android.sdk.model.TransactionRecipient
|
||||||
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
|
@ -47,7 +49,7 @@ internal class TransactionEncoderImpl(
|
||||||
require(recipient is TransactionRecipient.Address)
|
require(recipient is TransactionRecipient.Address)
|
||||||
|
|
||||||
val transactionId = createSpend(usk, amount, recipient.addressValue, memo)
|
val transactionId = createSpend(usk, amount, recipient.addressValue, memo)
|
||||||
return repository.findEncodedTransactionById(transactionId)
|
return repository.findEncodedTransactionByTxId(transactionId)
|
||||||
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +61,7 @@ internal class TransactionEncoderImpl(
|
||||||
require(recipient is TransactionRecipient.Account)
|
require(recipient is TransactionRecipient.Account)
|
||||||
|
|
||||||
val transactionId = createShieldingSpend(usk, memo)
|
val transactionId = createShieldingSpend(usk, memo)
|
||||||
return repository.findEncodedTransactionById(transactionId)
|
return repository.findEncodedTransactionByTxId(transactionId)
|
||||||
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,8 +98,16 @@ internal class TransactionEncoderImpl(
|
||||||
override suspend fun isValidUnifiedAddress(address: String): Boolean =
|
override suspend fun isValidUnifiedAddress(address: String): Boolean =
|
||||||
backend.isValidUnifiedAddr(address)
|
backend.isValidUnifiedAddr(address)
|
||||||
|
|
||||||
override suspend fun getConsensusBranchId(): Long {
|
/**
|
||||||
val height = repository.lastScannedHeight()
|
* Return the consensus branch that the encoder is using when making transactions.
|
||||||
|
*
|
||||||
|
* @param height the height at which we want to get the consensus branch
|
||||||
|
*
|
||||||
|
* @return id of consensus branch
|
||||||
|
*
|
||||||
|
* @throws TransactionEncoderException.IncompleteScanException if the [height] is less than activation height
|
||||||
|
*/
|
||||||
|
override suspend fun getConsensusBranchId(height: BlockHeight): Long {
|
||||||
if (height < backend.network.saplingActivationHeight) {
|
if (height < backend.network.saplingActivationHeight) {
|
||||||
throw TransactionEncoderException.IncompleteScanException(height)
|
throw TransactionEncoderException.IncompleteScanException(height)
|
||||||
}
|
}
|
||||||
|
@ -121,10 +131,10 @@ internal class TransactionEncoderImpl(
|
||||||
amount: Zatoshi,
|
amount: Zatoshi,
|
||||||
toAddress: String,
|
toAddress: String,
|
||||||
memo: ByteArray? = byteArrayOf()
|
memo: ByteArray? = byteArrayOf()
|
||||||
): Long {
|
): FirstClassByteArray {
|
||||||
Twig.debug {
|
Twig.debug {
|
||||||
"creating transaction to spend $amount zatoshi to" +
|
"creating transaction to spend $amount zatoshi to" +
|
||||||
" ${toAddress.masked()} with memo $memo"
|
" ${toAddress.masked()} with memo: ${memo?.decodeToString()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
|
@ -148,7 +158,7 @@ internal class TransactionEncoderImpl(
|
||||||
private suspend fun createShieldingSpend(
|
private suspend fun createShieldingSpend(
|
||||||
usk: UnifiedSpendingKey,
|
usk: UnifiedSpendingKey,
|
||||||
memo: ByteArray? = byteArrayOf()
|
memo: ByteArray? = byteArrayOf()
|
||||||
): Long {
|
): FirstClassByteArray {
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
return try {
|
return try {
|
||||||
saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory)
|
saplingParamTool.ensureParams(saplingParamTool.properties.paramsDirectory)
|
||||||
|
|
|
@ -39,6 +39,24 @@ data class BlockHeight internal constructor(val value: Long) : Comparable<BlockH
|
||||||
return BlockHeight(value + other)
|
return BlockHeight(value + other)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operator fun minus(other: BlockHeight) = BlockHeight(value - other.value)
|
||||||
|
|
||||||
|
operator fun minus(other: Int): BlockHeight {
|
||||||
|
require(other >= 0) {
|
||||||
|
"Cannot subtract negative value $other to BlockHeight"
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlockHeight(value - other.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun minus(other: Long): BlockHeight {
|
||||||
|
require(other >= 0) {
|
||||||
|
"Cannot subtract negative value $other to BlockHeight"
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlockHeight(value - other)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val UINT_RANGE = 0.toLong()..UInt.MAX_VALUE.toLong()
|
private val UINT_RANGE = 0.toLong()..UInt.MAX_VALUE.toLong()
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ data class TransactionOverview internal constructor(
|
||||||
companion object {
|
companion object {
|
||||||
internal fun new(
|
internal fun new(
|
||||||
dbTransactionOverview: DbTransactionOverview,
|
dbTransactionOverview: DbTransactionOverview,
|
||||||
latestBlockHeight: BlockHeight
|
latestBlockHeight: BlockHeight?
|
||||||
): TransactionOverview {
|
): TransactionOverview {
|
||||||
return TransactionOverview(
|
return TransactionOverview(
|
||||||
dbTransactionOverview.id,
|
dbTransactionOverview.id,
|
||||||
|
@ -69,11 +69,13 @@ enum class TransactionState {
|
||||||
|
|
||||||
private const val MIN_CONFIRMATIONS = 10
|
private const val MIN_CONFIRMATIONS = 10
|
||||||
internal fun new(
|
internal fun new(
|
||||||
latestBlockHeight: BlockHeight,
|
latestBlockHeight: BlockHeight?,
|
||||||
minedHeight: BlockHeight?,
|
minedHeight: BlockHeight?,
|
||||||
expiryHeight: BlockHeight?
|
expiryHeight: BlockHeight?
|
||||||
): TransactionState {
|
): TransactionState {
|
||||||
return if (minedHeight != null && (latestBlockHeight.value - minedHeight.value) >= MIN_CONFIRMATIONS) {
|
return if (latestBlockHeight == null) {
|
||||||
|
Pending
|
||||||
|
} else if (minedHeight != null && (latestBlockHeight.value - minedHeight.value) >= MIN_CONFIRMATIONS) {
|
||||||
Confirmed
|
Confirmed
|
||||||
} else if (minedHeight != null && (latestBlockHeight.value - minedHeight.value) < MIN_CONFIRMATIONS) {
|
} else if (minedHeight != null && (latestBlockHeight.value - minedHeight.value) < MIN_CONFIRMATIONS) {
|
||||||
Pending
|
Pending
|
||||||
|
|
|
@ -11,6 +11,9 @@ data class ZcashNetwork(
|
||||||
val saplingActivationHeight: BlockHeight,
|
val saplingActivationHeight: BlockHeight,
|
||||||
val orchardActivationHeight: BlockHeight
|
val orchardActivationHeight: BlockHeight
|
||||||
) {
|
) {
|
||||||
|
fun isMainnet() = id == ID_MAINNET
|
||||||
|
|
||||||
|
fun isTestnet() = id == ID_TESTNET
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package cash.z.ecc.android.sdk.block.processor
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class CompactBlockProcessorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun should_refresh_preparation_test() {
|
||||||
|
assertTrue {
|
||||||
|
CompactBlockProcessor.shouldRefreshPreparation(
|
||||||
|
lastPreparationTime = CompactBlockProcessor.SYNCHRONIZATION_RESTART_TIMEOUT,
|
||||||
|
currentTimeMillis = CompactBlockProcessor.SYNCHRONIZATION_RESTART_TIMEOUT * 2,
|
||||||
|
limitTime = CompactBlockProcessor.SYNCHRONIZATION_RESTART_TIMEOUT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun should_not_refresh_preparation_test() {
|
||||||
|
assertFalse {
|
||||||
|
CompactBlockProcessor.shouldRefreshPreparation(
|
||||||
|
lastPreparationTime = CompactBlockProcessor.SYNCHRONIZATION_RESTART_TIMEOUT,
|
||||||
|
currentTimeMillis = CompactBlockProcessor.SYNCHRONIZATION_RESTART_TIMEOUT,
|
||||||
|
limitTime = CompactBlockProcessor.SYNCHRONIZATION_RESTART_TIMEOUT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package cash.z.ecc.android.sdk.ext
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.BLOCK_HEIGHT_DISCONTINUITY
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.PREV_HASH_MISMATCH
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.TREE_SIZE_MISMATCH
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.isScanContinuityError
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class ExceptionExtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun is_scan_continuity_error() {
|
||||||
|
assertTrue { RuntimeException(PREV_HASH_MISMATCH).isScanContinuityError() }
|
||||||
|
assertTrue { RuntimeException(TREE_SIZE_MISMATCH).isScanContinuityError() }
|
||||||
|
assertTrue { RuntimeException(BLOCK_HEIGHT_DISCONTINUITY).isScanContinuityError() }
|
||||||
|
|
||||||
|
assertTrue { RuntimeException(PREV_HASH_MISMATCH.lowercase()).isScanContinuityError() }
|
||||||
|
|
||||||
|
assertTrue { RuntimeException(PREV_HASH_MISMATCH.plus("Text")).isScanContinuityError() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun is_not_scan_continuity_error() {
|
||||||
|
assertFalse { RuntimeException("Text").isScanContinuityError() }
|
||||||
|
assertFalse { RuntimeException("").isScanContinuityError() }
|
||||||
|
assertFalse { RuntimeException(PREV_HASH_MISMATCH.drop(1)).isScanContinuityError() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package cash.z.ecc.android.sdk.fixture
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ScanProgress
|
||||||
|
|
||||||
|
object ScanProgressFixture {
|
||||||
|
internal const val DEFAULT_NUMERATOR = 50L
|
||||||
|
internal const val DEFAULT_DENOMINATOR = 100L
|
||||||
|
|
||||||
|
internal fun new(
|
||||||
|
numerator: Long = DEFAULT_NUMERATOR,
|
||||||
|
denominator: Long = DEFAULT_DENOMINATOR
|
||||||
|
) = ScanProgress(numerator, denominator)
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package cash.z.ecc.android.sdk.fixture
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.ScanRange
|
||||||
|
import cash.z.ecc.android.sdk.internal.model.SuggestScanRangePriority
|
||||||
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||||
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
|
||||||
|
object ScanRangeFixture {
|
||||||
|
internal val DEFAULT_CLOSED_RANGE =
|
||||||
|
ZcashNetwork.Testnet.saplingActivationHeight..ZcashNetwork.Testnet.saplingActivationHeight + 9
|
||||||
|
internal val DEFAULT_PRIORITY = SuggestScanRangePriority.Verify.priority
|
||||||
|
|
||||||
|
internal fun new(
|
||||||
|
range: ClosedRange<BlockHeight> = DEFAULT_CLOSED_RANGE,
|
||||||
|
priority: Long = DEFAULT_PRIORITY
|
||||||
|
) = ScanRange(range, priority)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.fixture.ScanProgressFixture
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class ScanProgressTest {
|
||||||
|
@Test
|
||||||
|
fun get_valid_ratio_test() {
|
||||||
|
val scanProgress = ScanProgressFixture.new()
|
||||||
|
assertEquals(
|
||||||
|
scanProgress.getSafeRation(),
|
||||||
|
ScanProgressFixture.DEFAULT_NUMERATOR.toFloat().div(ScanProgressFixture.DEFAULT_DENOMINATOR)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun get_fallback_ratio_test() {
|
||||||
|
val scanProgress = ScanProgressFixture.new(
|
||||||
|
denominator = 0
|
||||||
|
)
|
||||||
|
assertEquals(0f, scanProgress.getSafeRation())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package cash.z.ecc.android.sdk.internal.model
|
||||||
|
|
||||||
|
import cash.z.ecc.android.sdk.fixture.ScanRangeFixture
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.isNotEmpty
|
||||||
|
import cash.z.ecc.android.sdk.internal.ext.length
|
||||||
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class ScanRangeTest {
|
||||||
|
@Test
|
||||||
|
fun get_suggest_scan_range_priority_test() {
|
||||||
|
val scanRange = ScanRangeFixture.new(
|
||||||
|
priority = SuggestScanRangePriority.Verify.priority
|
||||||
|
)
|
||||||
|
assertTrue {
|
||||||
|
scanRange.getSuggestScanRangePriority() == SuggestScanRangePriority.Verify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun scan_range_boundaries_test() {
|
||||||
|
val scanRange = ScanRangeFixture.new(
|
||||||
|
range = ZcashNetwork.Testnet.saplingActivationHeight..ZcashNetwork.Testnet.saplingActivationHeight + 9
|
||||||
|
)
|
||||||
|
assertTrue { scanRange.range.isNotEmpty() }
|
||||||
|
assertTrue { scanRange.range.length() == 10L }
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,4 +68,32 @@ class BlockHeightTest {
|
||||||
ZcashNetwork.Mainnet.saplingActivationHeight + -1L
|
ZcashNetwork.Mainnet.saplingActivationHeight + -1L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun subtraction_of_block_height_succeeds() {
|
||||||
|
val one = BlockHeight.new(
|
||||||
|
ZcashNetwork.Mainnet,
|
||||||
|
ZcashNetwork.Mainnet.saplingActivationHeight.value +
|
||||||
|
ZcashNetwork.Mainnet.saplingActivationHeight.value
|
||||||
|
)
|
||||||
|
val two = BlockHeight.new(ZcashNetwork.Mainnet, ZcashNetwork.Mainnet.saplingActivationHeight.value)
|
||||||
|
|
||||||
|
assertEquals(ZcashNetwork.Mainnet.saplingActivationHeight.value, (one - two).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun subtraction_of_long_succeeds() {
|
||||||
|
assertEquals(
|
||||||
|
ZcashNetwork.Mainnet.saplingActivationHeight.value,
|
||||||
|
(BlockHeight(419_323L) - 123L).value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun subtraction_of_int_succeeds() {
|
||||||
|
assertEquals(
|
||||||
|
ZcashNetwork.Mainnet.saplingActivationHeight.value,
|
||||||
|
(BlockHeight(419_323) - 123).value
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue