Feature branch for SDK 2.1.0 (#1411)
* CompactBlockProcessor: Remove `withDownload` parameter * backend-lib: Migrate to latest in-progress revision of Rust crates Includes changes to how accounts are stored and referenced. We now need to remember and provide the seed fingerprint; for now, given that we know we only create derived accounts from a single seed, we search for an account with a matching ZIP 32 account index. * backend-lib: Add `Backend.isSeedRelevantToWallet` * Remove nullability of DownloadSuccess param * Comment update * Fix Detekt warnings * backend-lib: Migrate to latest in-progress revision of Rust crates Includes some renames, and a built-in seed relevancy API that we now use. * Separate tree state fetching - Added continuable retry logic * Integrate Orchard support Closes Electric-Coin-Company/zcash-android-wallet-sdk#528. Closes Electric-Coin-Company/zcash-android-wallet-sdk#761. * Detekt warnings fix * Fix unit tests * Update `TxOutputsView` to use correct column names. (#1425) * Return an error instead of a panic in the case of data corruption. (#1426) This removes an `expect` call that risked crashing the app in the case of database corruption, potentially hiding other bugs. * Include `orchardSubtreeRootList` in final check * Revert `orchardSubtreeRootList` check Explanation comment added * Changelog update * Update to zcash_client_sqlite version 0.10.3 (#1428) --------- Co-authored-by: Honza <rychnovsky.honza@gmail.com> Co-authored-by: Kris Nuttycombe <kris@electriccoin.co>
This commit is contained in:
parent
698b7fe92f
commit
52ee2bc5bc
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -6,6 +6,10 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- The Orchard support has been finished, and the SDK now fully supports sending and receiving funds on the Orchard
|
||||
addresses
|
||||
|
||||
### Fixed
|
||||
- SDK release 1.11.0-beta01 documented that `Synchronizer.new` would throw an
|
||||
exception indicating that an internal migration requires the wallet seed, if
|
||||
|
@ -14,9 +18,12 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
added. The SDK now correctly throws `InitializeException.SeedRequired`.
|
||||
|
||||
### Changed
|
||||
- `Synchronizer.refreshAllBalances` now refreshes the Orchard balances as well
|
||||
- The SDK uses ZIP-317 fee system internally
|
||||
- `ZcashSdk.MINERS_FEE` has been deprecated, and will be removed in 2.1.0
|
||||
- `ZcashSdk.MINERS_FEE` has been deprecated, and will be removed in 2.1.x
|
||||
- `ZecSend` data class now provides `Proposal?` object initiated using `Synchronizer.proposeTransfer`
|
||||
- Wallet initialization using `Synchronizer.new` now could throw a new `SeedNotRelevant` exception when the provided
|
||||
seed is not relevant to any of the derived accounts in the wallet database
|
||||
- Checkpoints update
|
||||
|
||||
## [2.0.7] - 2024-03-08
|
||||
|
@ -34,7 +41,7 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `WalletSnapshot.transparentBalance: WalletBalance` to `WalletSnapshot.transparentBalance: Zatoshi`
|
||||
- `Memo.MAX_MEMO_LENGTH_BYTES` is now available in public API
|
||||
- `Synchronizer.sendToAddress` and `Synchronizer.shieldFunds` have been
|
||||
deprecated, and will be removed in 2.1.0 (which will create multiple
|
||||
deprecated, and will be removed in 2.1.x (which will create multiple
|
||||
transactions at once for some recipients).
|
||||
|
||||
### Added
|
||||
|
@ -96,7 +103,7 @@ and this library adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Removed
|
||||
- `LightWalletEndpointExt` and its functions and variables were removed from the SDK's public APIs entirely. It's
|
||||
preserved only for testing and wallet Demo app purposes. The calling wallet app should provide its own
|
||||
`LightWalletEndpoint` instance within `PersistableWallet` or `SdkSynchornizer` APIs.
|
||||
`LightWalletEndpoint` instance within `PersistableWallet` or `SdkSynchronizer` APIs.
|
||||
|
||||
### Changed
|
||||
- Gradle 8.5
|
||||
|
|
|
@ -730,9 +730,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "incrementalmerkletree"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "361c467824d4d9d4f284be4b2608800839419dccc4d4608f28345237fe354623"
|
||||
checksum = "eb1872810fb725b06b8c153dde9e86f3ec26747b9b60096da7a869883b549cbe"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
@ -1033,9 +1033,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
|||
|
||||
[[package]]
|
||||
name = "orchard"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fb255c3ffdccd3c84fe9ebed72aef64fdc72e6a3e4180dd411002d47abaad42"
|
||||
checksum = "0462569fc8b0d1b158e4d640571867a4e4319225ebee2ab6647e60c70af19ae3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bitvec",
|
||||
|
@ -1444,9 +1444,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "sapling-crypto"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d183012062dfdde85f7e3e758328fcf6e9846d8dd3fce35b04d0efcb6677b0e0"
|
||||
checksum = "02f4270033afcb0c74c5c7d59c73cfd1040367f67f224fe7ed9a919ae618f1b7"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bellman",
|
||||
|
@ -1566,9 +1566,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "shardtree"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbf20c7a2747d9083092e3a3eeb9a7ed75577ae364896bebbc5e0bdcd4e97735"
|
||||
checksum = "d766257c56a1bdd75479c256b97c92e72788a9afb18b5199f58faf7188dc99d9"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"either",
|
||||
|
@ -2159,6 +2159,7 @@ dependencies = [
|
|||
"jni",
|
||||
"libc",
|
||||
"log-panics",
|
||||
"orchard",
|
||||
"paranoid-android",
|
||||
"prost",
|
||||
"rayon",
|
||||
|
@ -2191,9 +2192,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zcash_client_backend"
|
||||
version = "0.11.0"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "185913267d824529b9547c933674963fca2b5bd84ad377a59d0f8ab6159ce798"
|
||||
checksum = "0364e69c446fcf96a1f73f342c6c3fa697ea65ae7eeeae7d76ca847b9c442e40"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bech32",
|
||||
|
@ -2209,6 +2210,7 @@ dependencies = [
|
|||
"memuse",
|
||||
"nom",
|
||||
"nonempty",
|
||||
"orchard",
|
||||
"percent-encoding",
|
||||
"prost",
|
||||
"rand_core",
|
||||
|
@ -2226,14 +2228,15 @@ dependencies = [
|
|||
"zcash_keys",
|
||||
"zcash_note_encryption",
|
||||
"zcash_primitives",
|
||||
"zcash_protocol",
|
||||
"zip32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zcash_client_sqlite"
|
||||
version = "0.9.0"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e883405989b8d7275a0e1180000b7568bb3fa33e36b4806c174eb802678e2dbf"
|
||||
checksum = "d2f94a5b293baca4b99be38695caa65a5e6981e8068f0bb80a69d60f4ee78346"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"byteorder",
|
||||
|
@ -2243,6 +2246,8 @@ dependencies = [
|
|||
"incrementalmerkletree",
|
||||
"jubjub",
|
||||
"maybe-rayon",
|
||||
"nonempty",
|
||||
"orchard",
|
||||
"prost",
|
||||
"rusqlite",
|
||||
"sapling-crypto",
|
||||
|
@ -2250,6 +2255,7 @@ dependencies = [
|
|||
"schemer-rusqlite",
|
||||
"secrecy",
|
||||
"shardtree",
|
||||
"subtle",
|
||||
"time",
|
||||
"tracing",
|
||||
"uuid",
|
||||
|
@ -2258,6 +2264,8 @@ dependencies = [
|
|||
"zcash_encoding",
|
||||
"zcash_keys",
|
||||
"zcash_primitives",
|
||||
"zcash_protocol",
|
||||
"zip32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2272,11 +2280,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zcash_keys"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f22d3407fdd6992b49f037f23862ab376be6013be6f2d0bc85948a635edc1f5"
|
||||
checksum = "663489ffb4e51bc4436ff8796832612a9ff3c6516f1c620b5a840cb5dcd7b866"
|
||||
dependencies = [
|
||||
"bech32",
|
||||
"blake2b_simd",
|
||||
"bls12_381",
|
||||
"bs58",
|
||||
"byteorder",
|
||||
|
@ -2288,11 +2297,13 @@ dependencies = [
|
|||
"orchard",
|
||||
"rand_core",
|
||||
"sapling-crypto",
|
||||
"secrecy",
|
||||
"subtle",
|
||||
"tracing",
|
||||
"zcash_address",
|
||||
"zcash_encoding",
|
||||
"zcash_primitives",
|
||||
"zcash_protocol",
|
||||
"zip32",
|
||||
]
|
||||
|
||||
|
@ -2311,9 +2322,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zcash_primitives"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9070e084570bb78aed4f8d71fd6254492e62c87a5d01e084183980e98117092d"
|
||||
checksum = "b5a8d812efec385ecbcefc862c0005bb1336474ea7dd9b671d5bbddaadd04be2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bip0039",
|
||||
|
@ -2343,15 +2354,16 @@ dependencies = [
|
|||
"zcash_address",
|
||||
"zcash_encoding",
|
||||
"zcash_note_encryption",
|
||||
"zcash_protocol",
|
||||
"zcash_spec",
|
||||
"zip32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zcash_proofs"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8a02eb1f151d9b9a6e16408d2c55ff440bd2fb232b7377277146d0fa2df9bc8"
|
||||
checksum = "5163a1110f4265cc5f2fdf87ac4497fd1e014b6ce0760ca8d16d8e3853a5c0f7"
|
||||
dependencies = [
|
||||
"bellman",
|
||||
"blake2b_simd",
|
||||
|
@ -2372,9 +2384,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zcash_protocol"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c651f9f95d319cd1e5211178108dcd3aa73063806d09af54b799e1a329a575bf"
|
||||
checksum = "8f8189d4a304e8aa3aef3b75e89f3874bb0dc84b1cd623316a84e79e06cddabc"
|
||||
dependencies = [
|
||||
"document-features",
|
||||
"memuse",
|
||||
|
@ -2431,9 +2443,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zip32"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d724a63be4dfb50b7f3617e542984e22e4b4a5b8ca5de91f55613152885e6b22"
|
||||
checksum = "4226d0aee9c9407c27064dfeec9d7b281c917de3374e1e5a2e2cfad9e09de19e"
|
||||
dependencies = [
|
||||
"blake2b_simd",
|
||||
"memuse",
|
||||
|
|
|
@ -16,6 +16,7 @@ hdwallet = "0.4"
|
|||
hdwallet-bitcoin = "0.4"
|
||||
hex = "0.4"
|
||||
jni = { version = "0.21", default-features = false }
|
||||
orchard = "0.8"
|
||||
prost = "0.12"
|
||||
rusqlite = "0.29"
|
||||
sapling = { package = "sapling-crypto", version = "0.1", default-features = false }
|
||||
|
@ -23,10 +24,10 @@ schemer = "0.2"
|
|||
secp256k1 = "0.26"
|
||||
secrecy = "0.8"
|
||||
zcash_address = "0.3"
|
||||
zcash_client_backend = { version = "0.11", features = ["transparent-inputs", "unstable"] }
|
||||
zcash_client_sqlite = { version = "0.9", features = ["transparent-inputs", "unstable"] }
|
||||
zcash_primitives = "0.14"
|
||||
zcash_proofs = "0.14"
|
||||
zcash_client_backend = { version = "0.12.1", features = ["orchard", "transparent-inputs", "unstable"] }
|
||||
zcash_client_sqlite = { version = "0.10.3", features = ["orchard", "transparent-inputs", "unstable"] }
|
||||
zcash_primitives = "0.15"
|
||||
zcash_proofs = "0.15"
|
||||
|
||||
# Initialization
|
||||
rayon = "1.7"
|
||||
|
|
|
@ -48,7 +48,8 @@ interface Backend {
|
|||
* 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.
|
||||
* requested migrations, 2 if the provided seed is not relevant to any of the
|
||||
* derived accounts in the wallet.
|
||||
*
|
||||
* @throws RuntimeException as a common indicator of the operation failure
|
||||
*/
|
||||
|
@ -65,6 +66,12 @@ interface Backend {
|
|||
recoverUntil: Long?
|
||||
): JniUnifiedSpendingKey
|
||||
|
||||
/**
|
||||
* @throws RuntimeException as a common indicator of the operation failure
|
||||
*/
|
||||
@Throws(RuntimeException::class)
|
||||
suspend fun isSeedRelevantToAnyDerivedAccounts(seed: ByteArray): Boolean
|
||||
|
||||
fun isValidSaplingAddr(addr: String): Boolean
|
||||
|
||||
fun isValidTransparentAddr(addr: String): Boolean
|
||||
|
@ -102,9 +109,11 @@ interface Backend {
|
|||
* @throws RuntimeException as a common indicator of the operation failure
|
||||
*/
|
||||
@Throws(RuntimeException::class)
|
||||
suspend fun putSaplingSubtreeRoots(
|
||||
startIndex: Long,
|
||||
roots: List<JniSubtreeRoot>,
|
||||
suspend fun putSubtreeRoots(
|
||||
saplingStartIndex: Long,
|
||||
saplingRoots: List<JniSubtreeRoot>,
|
||||
orchardStartIndex: Long,
|
||||
orchardRoots: List<JniSubtreeRoot>,
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -154,6 +163,7 @@ interface Backend {
|
|||
@Throws(RuntimeException::class)
|
||||
suspend fun scanBlocks(
|
||||
fromHeight: Long,
|
||||
fromState: ByteArray,
|
||||
limit: Long
|
||||
): JniScanSummary
|
||||
|
||||
|
|
|
@ -91,6 +91,15 @@ class RustBackend private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun isSeedRelevantToAnyDerivedAccounts(seed: ByteArray): Boolean =
|
||||
withContext(SdkDispatchers.DATABASE_IO) {
|
||||
isSeedRelevantToAnyDerivedAccounts(
|
||||
dataDbFile.absolutePath,
|
||||
seed,
|
||||
networkId = networkId
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getCurrentAddress(account: Int) =
|
||||
withContext(SdkDispatchers.DATABASE_IO) {
|
||||
getCurrentAddress(
|
||||
|
@ -193,14 +202,18 @@ class RustBackend private constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun putSaplingSubtreeRoots(
|
||||
startIndex: Long,
|
||||
roots: List<JniSubtreeRoot>,
|
||||
override suspend fun putSubtreeRoots(
|
||||
saplingStartIndex: Long,
|
||||
saplingRoots: List<JniSubtreeRoot>,
|
||||
orchardStartIndex: Long,
|
||||
orchardRoots: List<JniSubtreeRoot>,
|
||||
) = withContext(SdkDispatchers.DATABASE_IO) {
|
||||
putSaplingSubtreeRoots(
|
||||
putSubtreeRoots(
|
||||
dataDbFile.absolutePath,
|
||||
startIndex,
|
||||
roots.toTypedArray(),
|
||||
saplingStartIndex,
|
||||
saplingRoots.toTypedArray(),
|
||||
orchardStartIndex,
|
||||
orchardRoots.toTypedArray(),
|
||||
networkId = networkId
|
||||
)
|
||||
}
|
||||
|
@ -263,6 +276,7 @@ class RustBackend private constructor(
|
|||
|
||||
override suspend fun scanBlocks(
|
||||
fromHeight: Long,
|
||||
fromState: ByteArray,
|
||||
limit: Long
|
||||
): JniScanSummary {
|
||||
return withContext(SdkDispatchers.DATABASE_IO) {
|
||||
|
@ -270,6 +284,7 @@ class RustBackend private constructor(
|
|||
fsBlockDbRoot.absolutePath,
|
||||
dataDbFile.absolutePath,
|
||||
fromHeight,
|
||||
fromState,
|
||||
limit,
|
||||
networkId = networkId
|
||||
)
|
||||
|
@ -432,6 +447,13 @@ class RustBackend private constructor(
|
|||
networkId: Int
|
||||
): JniUnifiedSpendingKey
|
||||
|
||||
@JvmStatic
|
||||
private external fun isSeedRelevantToAnyDerivedAccounts(
|
||||
dbDataPath: String,
|
||||
seed: ByteArray,
|
||||
networkId: Int
|
||||
): Boolean
|
||||
|
||||
@JvmStatic
|
||||
private external fun getCurrentAddress(
|
||||
dbDataPath: String,
|
||||
|
@ -519,10 +541,13 @@ class RustBackend private constructor(
|
|||
)
|
||||
|
||||
@JvmStatic
|
||||
private external fun putSaplingSubtreeRoots(
|
||||
@Suppress("LongParameterList")
|
||||
private external fun putSubtreeRoots(
|
||||
dbDataPath: String,
|
||||
startIndex: Long,
|
||||
roots: Array<JniSubtreeRoot>,
|
||||
saplingStartIndex: Long,
|
||||
saplingRoots: Array<JniSubtreeRoot>,
|
||||
orchardStartIndex: Long,
|
||||
orchardRoots: Array<JniSubtreeRoot>,
|
||||
networkId: Int
|
||||
)
|
||||
|
||||
|
@ -558,10 +583,12 @@ class RustBackend private constructor(
|
|||
): Array<JniScanRange>
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("LongParameterList")
|
||||
private external fun scanBlocks(
|
||||
dbCachePath: String,
|
||||
dbDataPath: String,
|
||||
fromHeight: Long,
|
||||
fromState: ByteArray,
|
||||
limit: Long,
|
||||
networkId: Int
|
||||
): JniScanSummary
|
||||
|
|
|
@ -14,11 +14,14 @@ import cash.z.ecc.android.sdk.internal.ext.isInUIntRange
|
|||
* @param progressDenominator the denominator of the progress ratio
|
||||
* @param nextSaplingSubtreeIndex the Sapling subtree index that should start
|
||||
* the next range of subtree roots passed to `Backend.putSaplingSubtreeRoots`.
|
||||
* @param nextOrchardSubtreeIndex the Orchard subtree index that should start
|
||||
* the next range of subtree roots passed to `Backend.putOrchardSubtreeRoots`.
|
||||
* @throws IllegalArgumentException unless (progressNumerator is nonnegative,
|
||||
* progressDenominator is positive, and the represented ratio is in the
|
||||
* range 0.0 to 1.0 inclusive).
|
||||
*/
|
||||
@Keep
|
||||
@Suppress("LongParameterList")
|
||||
class JniWalletSummary(
|
||||
val accountBalances: Array<JniAccountBalance>,
|
||||
val chainTipHeight: Long,
|
||||
|
@ -26,6 +29,7 @@ class JniWalletSummary(
|
|||
val progressNumerator: Long,
|
||||
val progressDenominator: Long,
|
||||
val nextSaplingSubtreeIndex: Long,
|
||||
val nextOrchardSubtreeIndex: Long,
|
||||
) {
|
||||
init {
|
||||
require(chainTipHeight.isInUIntRange()) {
|
||||
|
@ -49,5 +53,8 @@ class JniWalletSummary(
|
|||
require(nextSaplingSubtreeIndex.isInUIntRange()) {
|
||||
"Height $nextSaplingSubtreeIndex is outside of allowed UInt range"
|
||||
}
|
||||
require(nextOrchardSubtreeIndex.isInUIntRange()) {
|
||||
"Height $nextOrchardSubtreeIndex is outside of allowed UInt range"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::convert::{Infallible, TryFrom, TryInto};
|
||||
use std::error::Error;
|
||||
use std::num::NonZeroU32;
|
||||
use std::panic;
|
||||
use std::path::Path;
|
||||
|
@ -12,7 +13,6 @@ use jni::{
|
|||
JNIEnv,
|
||||
};
|
||||
use prost::Message;
|
||||
use schemer::MigratorError;
|
||||
use secrecy::{ExposeSecret, SecretVec};
|
||||
use tracing::{debug, error};
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
@ -27,8 +27,8 @@ use zcash_client_backend::{
|
|||
create_proposed_transactions, decrypt_and_store_transaction,
|
||||
input_selection::GreedyInputSelector, propose_shielding, propose_transfer,
|
||||
},
|
||||
AccountBalance, AccountBirthday, InputSource, WalletCommitmentTrees, WalletRead,
|
||||
WalletSummary, WalletWrite,
|
||||
Account, AccountBalance, AccountBirthday, AccountSource, InputSource, SeedRelevance,
|
||||
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite,
|
||||
},
|
||||
encoding::AddressCodec,
|
||||
fees::{standard::SingleOutputChangeStrategy, DustOutputPolicy},
|
||||
|
@ -41,7 +41,7 @@ use zcash_client_backend::{
|
|||
use zcash_client_sqlite::{
|
||||
chain::{init::init_blockmeta_db, BlockMeta},
|
||||
wallet::init::{init_wallet_db, WalletMigrationError},
|
||||
FsBlockDb, WalletDb,
|
||||
AccountId, FsBlockDb, WalletDb,
|
||||
};
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
|
@ -58,7 +58,7 @@ use zcash_primitives::{
|
|||
fees::StandardFeeRule,
|
||||
Transaction, TxId,
|
||||
},
|
||||
zip32::{AccountId, DiversifierIndex},
|
||||
zip32::{self, DiversifierIndex},
|
||||
};
|
||||
use zcash_proofs::prover::LocalTxProver;
|
||||
|
||||
|
@ -71,7 +71,7 @@ const ANCHOR_OFFSET: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(ANCHOR_OFFS
|
|||
|
||||
// Do not generate Orchard receivers until we support receiving Orchard funds.
|
||||
const DEFAULT_ADDRESS_REQUEST: UnifiedAddressRequest =
|
||||
UnifiedAddressRequest::unsafe_new(false, true, true);
|
||||
UnifiedAddressRequest::unsafe_new(true, true, true);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn print_debug_state() {
|
||||
|
@ -97,13 +97,54 @@ fn block_db(env: &mut JNIEnv, fsblockdb_root: JString) -> Result<FsBlockDb, fail
|
|||
.map_err(|e| format_err!("Error opening block source database connection: {:?}", e))
|
||||
}
|
||||
|
||||
fn account_id_from_jint(account: jint) -> Result<AccountId, failure::Error> {
|
||||
fn account_id_from_jint(account: jint) -> Result<zip32::AccountId, failure::Error> {
|
||||
u32::try_from(account)
|
||||
.map_err(|_| ())
|
||||
.and_then(|id| AccountId::try_from(id).map_err(|_| ()))
|
||||
.and_then(|id| zip32::AccountId::try_from(id).map_err(|_| ()))
|
||||
.map_err(|_| format_err!("Invalid account ID"))
|
||||
}
|
||||
|
||||
fn account_id_from_jni<'local, P: Parameters>(
|
||||
db_data: &WalletDb<rusqlite::Connection, P>,
|
||||
account_index: jint,
|
||||
) -> Result<AccountId, failure::Error> {
|
||||
let requested_account_index = account_id_from_jint(account_index)?;
|
||||
|
||||
// Find the single account matching the given ZIP 32 account index.
|
||||
let mut accounts = db_data
|
||||
.get_account_ids()?
|
||||
.into_iter()
|
||||
.filter_map(|account_id| {
|
||||
db_data
|
||||
.get_account(account_id)
|
||||
.map_err(|e| {
|
||||
format_err!(
|
||||
"Database error encountered retrieving account {:?}: {}",
|
||||
account_id,
|
||||
e
|
||||
)
|
||||
})
|
||||
.and_then(|acct_opt|
|
||||
acct_opt.ok_or_else(||
|
||||
format_err!(
|
||||
"Wallet data corrupted: unable to retrieve account data for account {:?}",
|
||||
account_id
|
||||
)
|
||||
).map(|account| match account.source() {
|
||||
AccountSource::Derived { account_index, .. } if account_index == requested_account_index => Some(account),
|
||||
_ => None
|
||||
})
|
||||
)
|
||||
.transpose()
|
||||
});
|
||||
|
||||
match (accounts.next(), accounts.next()) {
|
||||
(Some(account), None) => Ok(account?.id()),
|
||||
(None, None) => Err(format_err!("Account does not exist")),
|
||||
(_, Some(_)) => Err(format_err!("Account index matches more than one account")),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initOnLoad<'local>(
|
||||
_env: JNIEnv<'local>,
|
||||
|
@ -173,8 +214,10 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initBlock
|
|||
///
|
||||
/// If `seed` is `null`, database migrations will be attempted without it.
|
||||
///
|
||||
/// Returns 0 if successful, 1 if the seed must be provided in order to execute the requested
|
||||
/// migrations, or -1 otherwise.
|
||||
/// Returns:
|
||||
/// - 0 if successful.
|
||||
/// - 1 if the seed must be provided in order to execute the requested migrations.
|
||||
/// - 2 if the provided seed is not relevant to any of the derived accounts in the wallet.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initDataDb<'local>(
|
||||
mut env: JNIEnv<'local>,
|
||||
|
@ -192,11 +235,22 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initDataD
|
|||
|
||||
match init_wallet_db(&mut db_data, seed) {
|
||||
Ok(()) => Ok(0),
|
||||
Err(MigratorError::Migration { error, .. })
|
||||
if matches!(error, WalletMigrationError::SeedRequired) =>
|
||||
Err(e)
|
||||
if matches!(
|
||||
e.source().and_then(|e| e.downcast_ref()),
|
||||
Some(&WalletMigrationError::SeedRequired)
|
||||
) =>
|
||||
{
|
||||
Ok(1)
|
||||
}
|
||||
Err(e)
|
||||
if matches!(
|
||||
e.source().and_then(|e| e.downcast_ref()),
|
||||
Some(&WalletMigrationError::SeedNotRelevant)
|
||||
) =>
|
||||
{
|
||||
Ok(2)
|
||||
}
|
||||
Err(e) => Err(format_err!("Error while initializing data DB: {}", e)),
|
||||
}
|
||||
});
|
||||
|
@ -205,7 +259,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_initDataD
|
|||
|
||||
fn encode_usk<'a>(
|
||||
env: &mut JNIEnv<'a>,
|
||||
account: AccountId,
|
||||
account: zip32::AccountId,
|
||||
usk: UnifiedSpendingKey,
|
||||
) -> jni::errors::Result<JObject<'a>> {
|
||||
let encoded = SecretVec::new(usk.to_bytes(Era::Orchard));
|
||||
|
@ -280,15 +334,46 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_createAcc
|
|||
}
|
||||
})?;
|
||||
|
||||
let (account, usk) = db_data
|
||||
.create_account(&seed, birthday)
|
||||
let (account_id, usk) = db_data
|
||||
.create_account(&seed, &birthday)
|
||||
.map_err(|e| format_err!("Error while initializing accounts: {}", e))?;
|
||||
|
||||
Ok(encode_usk(env, account, usk)?.into_raw())
|
||||
let account = db_data.get_account(account_id)?.expect("just created");
|
||||
let account_index = match account.source() {
|
||||
AccountSource::Derived { account_index, .. } => account_index,
|
||||
AccountSource::Imported => unreachable!("just created"),
|
||||
};
|
||||
|
||||
Ok(encode_usk(env, account_index, usk)?.into_raw())
|
||||
});
|
||||
unwrap_exc_or(&mut env, res, ptr::null_mut())
|
||||
}
|
||||
|
||||
/// Checks whether the given seed is relevant to any of the derived accounts in the wallet.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_isSeedRelevantToAnyDerivedAccounts<
|
||||
'local,
|
||||
>(
|
||||
mut env: JNIEnv<'local>,
|
||||
_: JClass<'local>,
|
||||
db_data: JString<'local>,
|
||||
seed: JByteArray<'local>,
|
||||
network_id: jint,
|
||||
) -> jboolean {
|
||||
let res = catch_unwind(&mut env, |env| {
|
||||
let network = parse_network(network_id as u32)?;
|
||||
let db_data = wallet_db(env, network, db_data)?;
|
||||
let seed = SecretVec::new(env.convert_byte_array(seed).unwrap());
|
||||
|
||||
// Replicate the logic from `initWalletDb`.
|
||||
Ok(match db_data.seed_relevance_to_derived_accounts(&seed)? {
|
||||
SeedRelevance::Relevant { .. } | SeedRelevance::NoAccounts => JNI_TRUE,
|
||||
SeedRelevance::NotRelevant | SeedRelevance::NoDerivedAccounts => JNI_FALSE,
|
||||
})
|
||||
});
|
||||
unwrap_exc_or(&mut env, res, JNI_FALSE)
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
@ -341,8 +426,8 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_de
|
|||
|
||||
let ufvks: Vec<_> = (0..accounts)
|
||||
.map(|account| {
|
||||
let account_id =
|
||||
AccountId::try_from(account).map_err(|_| format_err!("Invalid account ID"))?;
|
||||
let account_id = zip32::AccountId::try_from(account)
|
||||
.map_err(|_| format_err!("Invalid account ID"))?;
|
||||
UnifiedSpendingKey::from_seed(&network, &seed, account_id)
|
||||
.map_err(|e| {
|
||||
format_err!("error generating unified spending key from seed: {:?}", e)
|
||||
|
@ -422,7 +507,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustDerivationTool_de
|
|||
|
||||
// Derive the default Unified Address (containing the default Sapling payment
|
||||
// address that older SDKs used).
|
||||
let (ua, _) = ufvk.default_address(DEFAULT_ADDRESS_REQUEST);
|
||||
let (ua, _) = ufvk.default_address(DEFAULT_ADDRESS_REQUEST)?;
|
||||
let address_str = ua.encode(&network);
|
||||
let output = env
|
||||
.new_string(address_str)
|
||||
|
@ -469,9 +554,9 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getCurren
|
|||
let _span = tracing::info_span!("RustBackend.getCurrentAddress").entered();
|
||||
let network = parse_network(network_id as u32)?;
|
||||
let db_data = wallet_db(env, network, db_data)?;
|
||||
let account = account_id_from_jint(account)?;
|
||||
let account = account_id_from_jni(&db_data, account)?;
|
||||
|
||||
match (&db_data).get_current_address(account) {
|
||||
match db_data.get_current_address(account) {
|
||||
Ok(Some(addr)) => {
|
||||
let addr_str = addr.encode(&network);
|
||||
let output = env
|
||||
|
@ -961,10 +1046,11 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_rewindToH
|
|||
unwrap_exc_or(&mut env, res, JNI_FALSE)
|
||||
}
|
||||
|
||||
fn decode_sapling_subtree_root(
|
||||
fn decode_subtree_root<H>(
|
||||
env: &mut JNIEnv,
|
||||
obj: JObject,
|
||||
) -> Result<CommitmentTreeRoot<sapling::Node>, failure::Error> {
|
||||
node_parser: impl FnOnce(&[u8]) -> std::io::Result<H>,
|
||||
) -> Result<CommitmentTreeRoot<H>, failure::Error> {
|
||||
fn long_as_u32(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<u32, failure::Error> {
|
||||
Ok(u32::try_from(env.get_field(obj, name, "J")?.j()?)?)
|
||||
}
|
||||
|
@ -976,48 +1062,68 @@ fn decode_sapling_subtree_root(
|
|||
|
||||
Ok(CommitmentTreeRoot::from_parts(
|
||||
BlockHeight::from_u32(long_as_u32(env, &obj, "completingBlockHeight")?),
|
||||
sapling::Node::read(&byte_array(env, &obj, "rootHash")?[..])?,
|
||||
node_parser(&byte_array(env, &obj, "rootHash")?[..])?,
|
||||
))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_putSaplingSubtreeRoots<
|
||||
'local,
|
||||
>(
|
||||
pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_putSubtreeRoots<'local>(
|
||||
mut env: JNIEnv<'local>,
|
||||
_: JClass<'local>,
|
||||
db_data: JString<'local>,
|
||||
start_index: jlong,
|
||||
roots: JObjectArray<'local>,
|
||||
sapling_start_index: jlong,
|
||||
sapling_roots: JObjectArray<'local>,
|
||||
orchard_start_index: jlong,
|
||||
orchard_roots: JObjectArray<'local>,
|
||||
network_id: jint,
|
||||
) -> jboolean {
|
||||
let res = catch_unwind(&mut env, |env| {
|
||||
let _span = tracing::info_span!("RustBackend.putSaplingSubtreeRoots").entered();
|
||||
let _span = tracing::info_span!("RustBackend.putSubtreeRoots").entered();
|
||||
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 = {
|
||||
fn parse_roots<H>(
|
||||
env: &mut JNIEnv,
|
||||
roots: JObjectArray,
|
||||
node_parser: impl Fn(&[u8]) -> std::io::Result<H>,
|
||||
) -> Result<Vec<CommitmentTreeRoot<H>>, failure::Error> {
|
||||
let count = env.get_array_length(&roots).unwrap();
|
||||
(0..count)
|
||||
.scan(env, |env, i| {
|
||||
Some(
|
||||
env.get_object_array_element(&roots, i)
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|jobj| decode_sapling_subtree_root(env, jobj)),
|
||||
.and_then(|jobj| decode_subtree_root(env, jobj, &node_parser)),
|
||||
)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
let sapling_start_index = if sapling_start_index >= 0 {
|
||||
sapling_start_index as u64
|
||||
} else {
|
||||
return Err(format_err!("Sapling start index must be nonnegative."));
|
||||
};
|
||||
let sapling_roots = parse_roots(env, sapling_roots, |n| sapling::Node::read(n))?;
|
||||
|
||||
let orchard_start_index = if orchard_start_index >= 0 {
|
||||
orchard_start_index as u64
|
||||
} else {
|
||||
return Err(format_err!("Orchard start index must be nonnegative."));
|
||||
};
|
||||
let orchard_roots = parse_roots(env, orchard_roots, |n| {
|
||||
orchard::tree::MerkleHashOrchard::read(n)
|
||||
})?;
|
||||
|
||||
db_data
|
||||
.put_sapling_subtree_roots(start_index, &roots)
|
||||
.map(|()| JNI_TRUE)
|
||||
.map_err(|e| format_err!("Error while storing Sapling subtree roots: {}", e))
|
||||
.put_sapling_subtree_roots(sapling_start_index, &sapling_roots)
|
||||
.map_err(|e| format_err!("Error while storing Sapling subtree roots: {}", e))?;
|
||||
|
||||
db_data
|
||||
.put_orchard_subtree_roots(orchard_start_index, &orchard_roots)
|
||||
.map_err(|e| format_err!("Error while storing Orchard subtree roots: {}", e))?;
|
||||
|
||||
Ok(JNI_TRUE)
|
||||
});
|
||||
|
||||
unwrap_exc_or(&mut env, res, JNI_FALSE)
|
||||
|
@ -1104,7 +1210,7 @@ const JNI_ACCOUNT_BALANCE: &str = "cash/z/ecc/android/sdk/internal/model/JniAcco
|
|||
|
||||
fn encode_account_balance<'a>(
|
||||
env: &mut JNIEnv<'a>,
|
||||
account: &AccountId,
|
||||
account: &zip32::AccountId,
|
||||
balance: &AccountBalance,
|
||||
) -> jni::errors::Result<JObject<'a>> {
|
||||
let sapling_verified_balance = Amount::from(balance.sapling_balance().spendable_value());
|
||||
|
@ -1143,16 +1249,31 @@ fn encode_account_balance<'a>(
|
|||
///
|
||||
/// If these conditions are not met, this fails and leaves an `IllegalArgumentException`
|
||||
/// pending.
|
||||
fn encode_wallet_summary<'a>(
|
||||
fn encode_wallet_summary<'a, P: Parameters>(
|
||||
env: &mut JNIEnv<'a>,
|
||||
db_data: &WalletDb<rusqlite::Connection, P>,
|
||||
summary: WalletSummary<AccountId>,
|
||||
) -> jni::errors::Result<JObject<'a>> {
|
||||
) -> Result<JObject<'a>, failure::Error> {
|
||||
let account_balances = utils::rust_vec_to_java(
|
||||
env,
|
||||
summary.account_balances().into_iter().collect(),
|
||||
summary
|
||||
.account_balances()
|
||||
.into_iter()
|
||||
.map(|(account_id, balance)| {
|
||||
let account_index = match db_data
|
||||
.get_account(*account_id)?
|
||||
.expect("the account exists in the wallet")
|
||||
.source()
|
||||
{
|
||||
AccountSource::Derived { account_index, .. } => account_index,
|
||||
AccountSource::Imported => unreachable!("Imported accounts are unimplemented"),
|
||||
};
|
||||
Ok::<_, failure::Error>((account_index, balance))
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
JNI_ACCOUNT_BALANCE,
|
||||
|env, (account, balance)| encode_account_balance(env, account, balance),
|
||||
|env| encode_account_balance(env, &AccountId::ZERO, &AccountBalance::ZERO),
|
||||
|env, (account_index, balance)| encode_account_balance(env, &account_index, balance),
|
||||
|env| encode_account_balance(env, &zip32::AccountId::ZERO, &AccountBalance::ZERO),
|
||||
)?;
|
||||
|
||||
let (progress_numerator, progress_denominator) = summary
|
||||
|
@ -1160,9 +1281,9 @@ fn encode_wallet_summary<'a>(
|
|||
.map(|progress| (*progress.numerator(), *progress.denominator()))
|
||||
.unwrap_or((0, 1));
|
||||
|
||||
env.new_object(
|
||||
Ok(env.new_object(
|
||||
"cash/z/ecc/android/sdk/internal/model/JniWalletSummary",
|
||||
&format!("([L{};JJJJJ)V", JNI_ACCOUNT_BALANCE),
|
||||
&format!("([L{};JJJJJJ)V", JNI_ACCOUNT_BALANCE),
|
||||
&[
|
||||
(&account_balances).into(),
|
||||
JValue::Long(i64::from(u32::from(summary.chain_tip_height()))),
|
||||
|
@ -1170,8 +1291,9 @@ fn encode_wallet_summary<'a>(
|
|||
JValue::Long(progress_numerator as i64),
|
||||
JValue::Long(progress_denominator as i64),
|
||||
JValue::Long(summary.next_sapling_subtree_index() as i64),
|
||||
JValue::Long(summary.next_orchard_subtree_index() as i64),
|
||||
],
|
||||
)
|
||||
)?)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
@ -1195,7 +1317,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_getWallet
|
|||
.map(|r| r.denominator() > &0)
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
Some(summary) => Ok(encode_wallet_summary(env, summary)?.into_raw()),
|
||||
Some(summary) => Ok(encode_wallet_summary(env, &db_data, summary)?.into_raw()),
|
||||
None => Ok(ptr::null_mut()),
|
||||
}
|
||||
});
|
||||
|
@ -1283,6 +1405,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlock
|
|||
db_cache: JString<'local>,
|
||||
db_data: JString<'local>,
|
||||
from_height: jlong,
|
||||
from_state: JByteArray<'local>,
|
||||
limit: jlong,
|
||||
network_id: jint,
|
||||
) -> jobject {
|
||||
|
@ -1292,9 +1415,19 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlock
|
|||
let db_cache = block_db(env, db_cache)?;
|
||||
let mut db_data = wallet_db(env, network, db_data)?;
|
||||
let from_height = BlockHeight::try_from(from_height)?;
|
||||
let from_state = TreeState::decode(&env.convert_byte_array(from_state).unwrap()[..])
|
||||
.map_err(|e| format_err!("Invalid TreeState: {}", e))?
|
||||
.to_chain_state()?;
|
||||
let limit = usize::try_from(limit)?;
|
||||
|
||||
match scan_cached_blocks(&network, &db_cache, &mut db_data, from_height, limit) {
|
||||
match scan_cached_blocks(
|
||||
&network,
|
||||
&db_cache,
|
||||
&mut db_data,
|
||||
from_height,
|
||||
&from_state,
|
||||
limit,
|
||||
) {
|
||||
Ok(scan_summary) => Ok(encode_scan_summary(env, scan_summary)?.into_raw()),
|
||||
Err(e) => Err(format_err!(
|
||||
"Rust error while scanning blocks (limit {:?}): {}",
|
||||
|
@ -1397,7 +1530,7 @@ fn zip317_helper<DbT>(
|
|||
StandardFeeRule::PreZip313
|
||||
};
|
||||
GreedyInputSelector::new(
|
||||
SingleOutputChangeStrategy::new(fee_rule, change_memo, ShieldedProtocol::Sapling),
|
||||
SingleOutputChangeStrategy::new(fee_rule, change_memo, ShieldedProtocol::Orchard),
|
||||
DustOutputPolicy::default(),
|
||||
)
|
||||
}
|
||||
|
@ -1418,10 +1551,10 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeTr
|
|||
let _span = tracing::info_span!("RustBackend.proposeTransfer").entered();
|
||||
let network = parse_network(network_id as u32)?;
|
||||
let mut db_data = wallet_db(env, network, db_data)?;
|
||||
let account = account_id_from_jint(account)?;
|
||||
let account = account_id_from_jni(&db_data, account)?;
|
||||
let to = utils::java_string_to_rust(env, &to);
|
||||
let value = NonNegativeAmount::from_nonnegative_i64(value)
|
||||
.map_err(|()| format_err!("Invalid amount, out of range"))?;
|
||||
.map_err(|_| format_err!("Invalid amount, out of range"))?;
|
||||
let memo_bytes = env.convert_byte_array(memo).unwrap();
|
||||
|
||||
let to = match Address::decode(&network, &to) {
|
||||
|
@ -1490,9 +1623,9 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh
|
|||
let _span = tracing::info_span!("RustBackend.proposeShielding").entered();
|
||||
let network = parse_network(network_id as u32)?;
|
||||
let mut db_data = wallet_db(env, network, db_data)?;
|
||||
let account = account_id_from_jint(account)?;
|
||||
let account = account_id_from_jni(&db_data, account)?;
|
||||
let shielding_threshold = NonNegativeAmount::from_nonnegative_i64(shielding_threshold)
|
||||
.map_err(|()| format_err!("Invalid shielding threshold, out of range"))?;
|
||||
.map_err(|_| format_err!("Invalid shielding threshold, out of range"))?;
|
||||
let memo_bytes = env.convert_byte_array(memo).unwrap();
|
||||
|
||||
let transparent_receiver =
|
||||
|
@ -1512,8 +1645,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_proposeSh
|
|||
Ok(Some(addr))
|
||||
} else {
|
||||
Err(format_err!(
|
||||
"Transparent receiver does not belong to account {}",
|
||||
u32::from(account),
|
||||
"Transparent receiver does not belong to account",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -1694,11 +1826,9 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_listTrans
|
|||
let res = catch_unwind(&mut env, |env| {
|
||||
let _span = tracing::info_span!("RustBackend.listTransparentReceivers").entered();
|
||||
let network = parse_network(network_id as u32)?;
|
||||
let zcash_network = network
|
||||
.address_network()
|
||||
.expect("network_id parsing should not have resulted in an unrecognized network type.");
|
||||
let zcash_network = network.network_type();
|
||||
let db_data = wallet_db(env, network, db_data)?;
|
||||
let account = account_id_from_jint(account_id)?;
|
||||
let account = account_id_from_jni(&db_data, account_id)?;
|
||||
|
||||
match db_data.get_transparent_receivers(account) {
|
||||
Ok(receivers) => {
|
||||
|
|
|
@ -15,7 +15,8 @@ class JniWalletSummaryTest {
|
|||
fullyScannedHeight = 0,
|
||||
progressNumerator = 1L,
|
||||
progressDenominator = 100L,
|
||||
nextSaplingSubtreeIndex = 0
|
||||
nextSaplingSubtreeIndex = 0L,
|
||||
nextOrchardSubtreeIndex = 0L
|
||||
)
|
||||
assertIs<JniWalletSummary>(instance)
|
||||
}
|
||||
|
@ -29,7 +30,8 @@ class JniWalletSummaryTest {
|
|||
fullyScannedHeight = 0,
|
||||
progressNumerator = 1L,
|
||||
progressDenominator = 100L,
|
||||
nextSaplingSubtreeIndex = 0
|
||||
nextSaplingSubtreeIndex = 0L,
|
||||
nextOrchardSubtreeIndex = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +45,8 @@ class JniWalletSummaryTest {
|
|||
fullyScannedHeight = 0,
|
||||
progressNumerator = -1L,
|
||||
progressDenominator = 100L,
|
||||
nextSaplingSubtreeIndex = 0
|
||||
nextSaplingSubtreeIndex = 0L,
|
||||
nextOrchardSubtreeIndex = 0L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +60,8 @@ class JniWalletSummaryTest {
|
|||
fullyScannedHeight = 0,
|
||||
progressNumerator = 1L,
|
||||
progressDenominator = 0L,
|
||||
nextSaplingSubtreeIndex = 0
|
||||
nextSaplingSubtreeIndex = 0L,
|
||||
nextOrchardSubtreeIndex = 0L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +75,8 @@ class JniWalletSummaryTest {
|
|||
fullyScannedHeight = 0,
|
||||
progressNumerator = 100L,
|
||||
progressDenominator = 1L,
|
||||
nextSaplingSubtreeIndex = 0
|
||||
nextSaplingSubtreeIndex = 0L,
|
||||
nextOrchardSubtreeIndex = 0L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +90,8 @@ class JniWalletSummaryTest {
|
|||
fullyScannedHeight = 0,
|
||||
progressNumerator = 1L,
|
||||
progressDenominator = 100L,
|
||||
nextSaplingSubtreeIndex = -1
|
||||
nextSaplingSubtreeIndex = -1L,
|
||||
nextOrchardSubtreeIndex = -1L
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import co.electriccoin.lightwallet.client.model.Response
|
|||
import co.electriccoin.lightwallet.client.model.SendResponseUnsafe
|
||||
import co.electriccoin.lightwallet.client.model.ShieldedProtocolEnum
|
||||
import co.electriccoin.lightwallet.client.model.SubtreeRootUnsafe
|
||||
import co.electriccoin.lightwallet.client.model.TreeStateUnsafe
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
@ -20,6 +21,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
/**
|
||||
* Client for interacting with lightwalletd.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface LightWalletClient {
|
||||
/**
|
||||
* @return the full transaction info.
|
||||
|
@ -90,6 +92,11 @@ interface LightWalletClient {
|
|||
maxEntries: UInt
|
||||
): Flow<Response<SubtreeRootUnsafe>>
|
||||
|
||||
/**
|
||||
* @return the latest block height known to the service.
|
||||
*/
|
||||
suspend fun getTreeState(height: BlockHeightUnsafe): Response<TreeStateUnsafe>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
@ -15,6 +15,7 @@ import co.electriccoin.lightwallet.client.model.Response
|
|||
import co.electriccoin.lightwallet.client.model.SendResponseUnsafe
|
||||
import co.electriccoin.lightwallet.client.model.ShieldedProtocolEnum
|
||||
import co.electriccoin.lightwallet.client.model.SubtreeRootUnsafe
|
||||
import co.electriccoin.lightwallet.client.model.TreeStateUnsafe
|
||||
import com.google.protobuf.ByteString
|
||||
import io.grpc.CallOptions
|
||||
import io.grpc.Channel
|
||||
|
@ -87,7 +88,6 @@ internal class LightWalletClientImpl private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("SwallowedException")
|
||||
override suspend fun getServerInfo(): Response<LightWalletEndpointInfoUnsafe> {
|
||||
return try {
|
||||
val lightdInfo =
|
||||
|
@ -233,6 +233,18 @@ internal class LightWalletClientImpl private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getTreeState(height: BlockHeightUnsafe): Response<TreeStateUnsafe> {
|
||||
return try {
|
||||
val response =
|
||||
requireChannel().createStub(singleRequestTimeout)
|
||||
.getTreeState(height.toBlockHeight())
|
||||
|
||||
Response.Success(TreeStateUnsafe.new(response))
|
||||
} catch (e: StatusException) {
|
||||
GrpcStatusResolver.resolveFailureFromStatus(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
channel.shutdown()
|
||||
}
|
||||
|
|
|
@ -14,14 +14,16 @@ class TreeStateUnsafe(
|
|||
height: Long,
|
||||
hash: String,
|
||||
time: Int,
|
||||
tree: String
|
||||
saplingTree: String,
|
||||
orchardTree: String
|
||||
): TreeStateUnsafe {
|
||||
val treeState =
|
||||
TreeState.newBuilder()
|
||||
.setHeight(height)
|
||||
.setHash(hash)
|
||||
.setTime(time)
|
||||
.setSaplingTree(tree)
|
||||
.setSaplingTree(saplingTree)
|
||||
.setOrchardTree(orchardTree)
|
||||
.build()
|
||||
return new(treeState)
|
||||
}
|
||||
|
|
|
@ -8,10 +8,17 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
|
|||
|
||||
object WalletFixture {
|
||||
val NETWORK = ZcashNetwork.Mainnet
|
||||
|
||||
// This is the "Ben" wallet phrase from sdk-incubator-lib.
|
||||
const val SEED_PHRASE =
|
||||
"kitchen renew wide common vague fold vacuum tilt amazing pear square gossip jewel month" +
|
||||
" tree shock scan alpha just spot fluid toilet view dinner"
|
||||
|
||||
// This is the "Alice" wallet phrase from sdk-incubator-lib.
|
||||
const val ALICE_SEED_PHRASE =
|
||||
"wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness" +
|
||||
" cannon grab despair throw review deal slush frame"
|
||||
|
||||
suspend fun getUnifiedSpendingKey(
|
||||
seed: String = SEED_PHRASE,
|
||||
network: ZcashNetwork = NETWORK,
|
||||
|
|
|
@ -5,7 +5,8 @@ import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
|||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_EPOCH_SECONDS
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_HASH
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_HEIGHT
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_TREE
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_ORCHARD_TREE
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_SAPLING_TREE
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_VERSION
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.VERSION_1
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.from
|
||||
|
@ -45,7 +46,8 @@ class CheckpointTest {
|
|||
put(Checkpoint.KEY_HEIGHT, UInt.MAX_VALUE.toLong())
|
||||
put(Checkpoint.KEY_HASH, CheckpointFixture.HASH)
|
||||
put(Checkpoint.KEY_EPOCH_SECONDS, CheckpointFixture.EPOCH_SECONDS)
|
||||
put(Checkpoint.KEY_TREE, CheckpointFixture.TREE)
|
||||
put(Checkpoint.KEY_SAPLING_TREE, CheckpointFixture.SAPLING_TREE)
|
||||
put(Checkpoint.KEY_ORCHARD_TREE, CheckpointFixture.ORCHARD_TREE)
|
||||
}.toString()
|
||||
|
||||
Checkpoint.from(CheckpointFixture.NETWORK, jsonString).also {
|
||||
|
|
|
@ -5,12 +5,15 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.android.sdk.WalletInitMode
|
||||
import cash.z.ecc.android.sdk.exception.InitializeException
|
||||
import cash.z.ecc.android.sdk.fixture.LightWalletEndpointFixture
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import java.util.UUID
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class SdkSynchronizerTest {
|
||||
|
@ -82,4 +85,45 @@ class SdkSynchronizerTest {
|
|||
walletInitMode = WalletInitMode.ExistingWallet
|
||||
).use {}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
fun detects_irrelevant_seeds() =
|
||||
runTest {
|
||||
// Random alias so that repeated invocations of this test will have a clean starting state
|
||||
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
|
||||
Synchronizer.new(
|
||||
InstrumentationRegistry.getInstrumentation().context,
|
||||
ZcashNetwork.Mainnet,
|
||||
alias,
|
||||
LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet),
|
||||
Mnemonics.MnemonicCode(WalletFixture.SEED_PHRASE).toEntropy(),
|
||||
birthday = null,
|
||||
// Using existing wallet init mode as simplification for the test
|
||||
walletInitMode = WalletInitMode.ExistingWallet
|
||||
).use {
|
||||
it.getSaplingAddress(Account.DEFAULT)
|
||||
}
|
||||
|
||||
// Second instance should fail because the seed is not relevant to the wallet.
|
||||
val error =
|
||||
assertFailsWith<InitializeException> {
|
||||
Synchronizer.new(
|
||||
InstrumentationRegistry.getInstrumentation().context,
|
||||
ZcashNetwork.Mainnet,
|
||||
alias,
|
||||
LightWalletEndpointFixture.newEndpointForNetwork(ZcashNetwork.Mainnet),
|
||||
Mnemonics.MnemonicCode(WalletFixture.ALICE_SEED_PHRASE).toEntropy(),
|
||||
birthday = null,
|
||||
// Using existing wallet init mode as simplification for the test
|
||||
walletInitMode = WalletInitMode.ExistingWallet
|
||||
).use {}
|
||||
}
|
||||
assertEquals(InitializeException.SeedNotRelevant, error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
|
|||
object ExpectedMainnet : Expected {
|
||||
override val tAddr = "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4"
|
||||
override val zAddr = "zs1yc4sgtfwwzz6xfsy2xsradzr6m4aypgxhfw2vcn3hatrh5ryqsr08sgpemlg39vdh9kfupx20py"
|
||||
override val uAddr = "u1607xqhx72u8x94xcg6kyt9sd83aw8zvys2vwlr5n956e5jfytcaaeuzrk938c03jv4t0kdk73yxz9yd8rdksutw68ycpy6yt9vzhu28z58rh89gtt653cspr0c50ev4av0ddzj5vrrh"
|
||||
override val uAddr = "u1t23erzgkn7c6c2jn66rspl4m45lg8rn3f7mn7le4yxk7693wr7sgx472jn95s00x8kx3hct5ej4tf76k59dfhsd809t7mzt9ldzw8f5083fw4xqvxfshl9u7ed2wyv6ypmzny0px0nvszslr5kr7fgk2zgfnlycddzqak4adsqjdzp76y7fl0k4ygamjr43t6rpxsf6xql8g20rdk0h"
|
||||
override val tAccountPrivKey = "xprv9z1aorRbyM5A6ok9QmdCUztMRRgthiNpus4u8Rgn9YeZEz1EVkLthFpJS1Y1FaXAvgNDPKTwxvshUMj7KJiGeNVhKL8RzDv14yHbUu3szy5"
|
||||
override val tskCompressed = "L4BvDC33yLjMRxipZvdiUmdYeRfZmR8viziwsVwe72zJdGbiJPv2"
|
||||
override val tpk = "03b1d7fb28d17c125b504d06b1530097e0a3c76ada184237e3bc0925041230a5af"
|
||||
|
@ -63,7 +63,7 @@ class TransparentTest(val expected: Expected, val network: ZcashNetwork) {
|
|||
object ExpectedTestnet : Expected {
|
||||
override val tAddr = "tm9v3KTsjXK8XWSqiwFjic6Vda6eHY9Mjjq"
|
||||
override val zAddr = "ztestsapling1wn3tw9w5rs55x5yl586gtk72e8hcfdq8zsnjzcu8p7ghm8lrx54axc74mvm335q7lmy3g0sqje6"
|
||||
override val uAddr = "utest1cy80kzr6fj5vrrazldtcgmycs6rgu2x73pvwrjjmlwrwx343m06lxua5u36jdwyeckn4a6a0fkxm4y7t3lvhzscqrwg3gxpj4rgrgmf93m0cpm9ddkzn5qyzgadktuwza5d5kucewv3"
|
||||
override val uAddr = "utest10prpna6ydq6042q7t9qqr66qg9xhj8gn7aesrxnxelp7sqh5lc56w9qzl4pydhjvt9v34cxp5krdracecwg3dpkvgv8fttvz4hcql2m35se6u2n8h9p86xc7c6wm8fj7p4r3kq0mvvl4g650s6xdkhkg9yhtnnne4vy9k3hw27m0y6ctlmgkeadvn38v6wp9fpdwwhwrgn52z8fp6pc"
|
||||
override val tAccountPrivKey = "xprv9yUDoMsKVAQ8W8tf3VuPGyBKHuDPa4SkBXT7KHp4dfW7iBWKUEgAYG1g6ZpdotTWc4iMrj6vgaT8otHCWRj5SYtXkDcxkheFCp6QZEW9dPi"
|
||||
override val tskCompressed = "KzVugoXxR7AtTMdR5sdJtHxCNvMzQ4H196k7ATv4nnjoummsRC9G"
|
||||
override val tpk = "03b1d7fb28d17c125b504d06b1530097e0a3c76ada184237e3bc0925041230a5af"
|
||||
|
|
|
@ -4,7 +4,8 @@ import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
|||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_EPOCH_SECONDS
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_HASH
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_HEIGHT
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_TREE
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_ORCHARD_TREE
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_SAPLING_TREE
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.KEY_VERSION
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.VERSION_1
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
|
@ -14,20 +15,30 @@ import org.json.JSONObject
|
|||
object CheckpointFixture {
|
||||
val NETWORK = ZcashNetwork.Mainnet
|
||||
|
||||
// These came from the mainnet 1500000.json file
|
||||
val HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 1500000L)
|
||||
const val HASH = "00000000019e5b25a95c7607e7789eb326fddd69736970ebbe1c7d00247ef902"
|
||||
const val EPOCH_SECONDS = 1639913234L
|
||||
// These came from the mainnet 1700000.json file
|
||||
val HEIGHT = BlockHeight.new(ZcashNetwork.Mainnet, 1700000L)
|
||||
const val HASH = "0000000000f430793e0c6381b40b47ed77b0ed76d21c2c667acdfe7747a8ed5b"
|
||||
const val EPOCH_SECONDS = 1654991544L
|
||||
|
||||
@Suppress("MaxLineLength", "ktlint:standard:max-line-length")
|
||||
const val TREE = "01ce183032b16ed87fcc5052a42d908376526126346567773f55bc58a63e4480160013000001bae5112769a07772345dd402039f2949c457478fe9327363ff631ea9d78fb80d0177c0b6c21aa9664dc255336ed450914088108c38a9171c85875b4e53d31b3e140171add6f9129e124651ca894aa842a3c71b1738f3ee2b7ba829106524ef51e62101f9cebe2141ee9d0a3f3a3e28bce07fa6b6e1c7b42c01cc4fe611269e9d52da540001d0adff06de48569129bd2a211e3253716362da97270d3504d9c1b694689ebe3c0122aaaea90a7fa2773b8166937310f79a4278b25d759128adf3138d052da3725b0137fb2cbc176075a45db2a3c32d3f78e669ff2258fd974e99ec9fb314d7fd90180165aaee3332ea432d13a9398c4863b38b8a7a491877a5c46b0802dcd88f7e324301a9a262f8b92efc2e0e3e4bd1207486a79d62e87b4ab9cc41814d62a23c4e28040001e3c4ee998682df5c5e230d6968e947f83d0c03682f0cfc85f1e6ec8e8552c95a000155989fed7a8cc7a0d479498d6881ca3bafbe05c7095110f85c64442d6a06c25c0185cd8c141e620eda0ca0516f42240aedfabdf9189c8c6ac834b7bdebc171331d01ecceb776c043662617d62646ee60985521b61c0b860f3a9731e66ef74ed8fb320118f64df255c9c43db708255e7bf6bffd481e5c2f38fe9ed8f3d189f7f9cf2644"
|
||||
const val SAPLING_TREE = "01c3674a676a784f00d3383330124e7c5c5470a2603a41a57114c6aed647d9356f001401c33e33c5a1711c589fef4a8553b5f965e4e06b5126944576d90ff770c6718e1600013d4a7407f15c6d9640675ab0db03155d17b467a678b8bc2578be04a7568bef5100019d14969270212b3fe9df2307a404f646b16c9152364fc3127b01f68a70ccdd0f000000000001b8b49a61716ecee15ea34cb00bfcfdd498eefef7d99a1e99dea510873dc10c4f018dff53df886aa66e320c9cd060ab7b821aed21e4cac2a871f48c385ff20ecb1f01b66c7874f1b7721ff890be6e832804877b534ffcb2b768ce6514e7c1a523e8380134136b9f1f00c2e9d16dc7358ef920862511c8fc42fb7074cbc9016d8d4e8b4c015eddc191a81221b7900bbdcd8610e5df595e3cdc7fd5b3432e3825206ae35b05017eda713cd733ccc555123788692a1876f9ca292b0aa2ddf3f45ed2b47f027340000000015ec9e9b1295908beed437df4126032ca57ada8e3ebb67067cd22a73c79a84009"
|
||||
|
||||
@Suppress("MaxLineLength", "ktlint:standard:max-line-length")
|
||||
const val ORCHARD_TREE = "01ffe841309dd0d9fd5073282a966b5daaf3a36834b62ac25e350dd581cfce6e2f01f6be2fb34b7ead63fcf256751c7839f138121c5237b745f6bd1bf17b4b16da1e1f0102c2cc2ee89c7561d05e34d642efa5eb991141579cca7b0ff2c7faf7a253501d013490d36beed18879794594a1b9bf0def458e30cd99ddc5ae716c2eb121ccce37000001941b26a7f09a7a3887aec0879dfc1275225b83efbcef54674930c3c2dfe3322200000123f80f8c4446da4a147c92340b492788dca810ce0a997860f151a86927e86f390000000000000000000000000000000000000000000000"
|
||||
|
||||
internal fun new(
|
||||
height: BlockHeight = HEIGHT,
|
||||
hash: String = HASH,
|
||||
time: Long = EPOCH_SECONDS,
|
||||
tree: String = TREE
|
||||
) = Checkpoint(height = height, hash = hash, epochSeconds = time, tree = tree)
|
||||
saplingTree: String = SAPLING_TREE,
|
||||
orchardTree: String = ORCHARD_TREE
|
||||
) = Checkpoint(
|
||||
height = height,
|
||||
hash = hash,
|
||||
epochSeconds = time,
|
||||
saplingTree = saplingTree,
|
||||
orchardTree = orchardTree
|
||||
)
|
||||
}
|
||||
|
||||
internal fun Checkpoint.toJson() =
|
||||
|
@ -36,5 +47,6 @@ internal fun Checkpoint.toJson() =
|
|||
put(Checkpoint.KEY_HEIGHT, height.value)
|
||||
put(Checkpoint.KEY_HASH, hash)
|
||||
put(Checkpoint.KEY_EPOCH_SECONDS, epochSeconds)
|
||||
put(Checkpoint.KEY_TREE, tree)
|
||||
put(Checkpoint.KEY_SAPLING_TREE, saplingTree)
|
||||
put(Checkpoint.KEY_ORCHARD_TREE, orchardTree)
|
||||
}.toString()
|
||||
|
|
|
@ -20,9 +20,11 @@ internal class FakeRustBackend(
|
|||
metadata.removeAll { it.height > height }
|
||||
}
|
||||
|
||||
override suspend fun putSaplingSubtreeRoots(
|
||||
startIndex: Long,
|
||||
roots: List<JniSubtreeRoot>,
|
||||
override suspend fun putSubtreeRoots(
|
||||
saplingStartIndex: Long,
|
||||
saplingRoots: List<JniSubtreeRoot>,
|
||||
orchardStartIndex: Long,
|
||||
orchardRoots: List<JniSubtreeRoot>,
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
@ -114,6 +116,9 @@ internal class FakeRustBackend(
|
|||
recoverUntil: Long?
|
||||
): JniUnifiedSpendingKey = error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
||||
|
||||
override suspend fun isSeedRelevantToAnyDerivedAccounts(seed: ByteArray): Boolean =
|
||||
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
||||
|
||||
override fun isValidSaplingAddr(addr: String): Boolean =
|
||||
error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
||||
|
||||
|
@ -154,6 +159,7 @@ internal class FakeRustBackend(
|
|||
|
||||
override suspend fun scanBlocks(
|
||||
fromHeight: Long,
|
||||
fromState: ByteArray,
|
||||
limit: Long
|
||||
) = error("Intentionally not implemented in mocked FakeRustBackend implementation.")
|
||||
}
|
||||
|
|
|
@ -365,14 +365,10 @@ class SdkSynchronizer private constructor(
|
|||
|
||||
/**
|
||||
* Calculate the latest balance based on the blocks that have been scanned and transmit this information into the
|
||||
* [transparentBalance] and [saplingBalances] flow. The [orchardBalances] flow is still not filled with proper data
|
||||
* because of the current limited Orchard support.
|
||||
* [transparentBalance], [saplingBalances], and [orchardBalances] flows.
|
||||
*/
|
||||
suspend fun refreshAllBalances() {
|
||||
processor.refreshWalletSummary()
|
||||
// TODO [#682]: refresh orchard balance
|
||||
// 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." }
|
||||
}
|
||||
|
||||
suspend fun isValidAddress(address: String): Boolean {
|
||||
|
|
|
@ -42,6 +42,7 @@ import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
|
|||
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.SuggestScanRangePriority
|
||||
import cash.z.ecc.android.sdk.internal.model.TreeState
|
||||
import cash.z.ecc.android.sdk.internal.model.WalletSummary
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.from
|
||||
import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight
|
||||
|
@ -207,7 +208,7 @@ class CompactBlockProcessor internal constructor(
|
|||
suspend fun start() {
|
||||
val traceScope = TraceScope("CompactBlockProcessor.start")
|
||||
|
||||
val startIndex = refreshWalletSummary()
|
||||
val (saplingStartIndex, orchardStartIndex) = refreshWalletSummary()
|
||||
|
||||
verifySetup()
|
||||
|
||||
|
@ -226,7 +227,7 @@ class CompactBlockProcessor internal constructor(
|
|||
|
||||
// Download note commitment tree data from lightwalletd to decide if we communicate with linear
|
||||
// or spend-before-sync node.
|
||||
var subTreeRootResult = getSubtreeRoots(downloader, network, startIndex)
|
||||
var subTreeRootResult = getSubtreeRoots(downloader, network, saplingStartIndex, orchardStartIndex)
|
||||
Twig.info { "Fetched SubTreeRoot result: $subTreeRootResult" }
|
||||
|
||||
Twig.debug { "Setup verified. Processor starting..." }
|
||||
|
@ -251,10 +252,14 @@ class CompactBlockProcessor internal constructor(
|
|||
val result =
|
||||
putSaplingSubtreeRoots(
|
||||
backend = backend,
|
||||
startIndex = startIndex,
|
||||
subTreeRootList =
|
||||
saplingStartIndex = saplingStartIndex,
|
||||
saplingSubtreeRootList =
|
||||
(subTreeRootResult as GetSubtreeRootsResult.SpendBeforeSync)
|
||||
.subTreeRootList,
|
||||
.saplingSubtreeRootList,
|
||||
orchardStartIndex = orchardStartIndex,
|
||||
orchardSubtreeRootList =
|
||||
(subTreeRootResult as GetSubtreeRootsResult.SpendBeforeSync)
|
||||
.orchardSubtreeRootList,
|
||||
lastValidHeight = lowerBoundHeight
|
||||
)
|
||||
) {
|
||||
|
@ -290,7 +295,8 @@ class CompactBlockProcessor internal constructor(
|
|||
}
|
||||
GetSubtreeRootsResult.FailureConnection -> {
|
||||
// SubtreeRoot fetching retry
|
||||
subTreeRootResult = getSubtreeRoots(downloader, network, startIndex)
|
||||
subTreeRootResult =
|
||||
getSubtreeRoots(downloader, network, saplingStartIndex, orchardStartIndex)
|
||||
BlockProcessingResult.Reconnecting
|
||||
}
|
||||
}
|
||||
|
@ -464,7 +470,6 @@ class CompactBlockProcessor internal constructor(
|
|||
repository = repository,
|
||||
network = network,
|
||||
syncRange = verifyRangeResult.scanRange.range,
|
||||
withDownload = true,
|
||||
enhanceStartHeight = firstUnenhancedHeight
|
||||
).collect { batchSyncProgress ->
|
||||
// Update sync progress and wallet balance
|
||||
|
@ -561,7 +566,6 @@ class CompactBlockProcessor internal constructor(
|
|||
repository = repository,
|
||||
network = network,
|
||||
syncRange = scanRange.range,
|
||||
withDownload = true,
|
||||
enhanceStartHeight = firstUnenhancedHeight
|
||||
).map { batchSyncProgress ->
|
||||
// Update sync progress and wallet balance
|
||||
|
@ -728,15 +732,13 @@ class CompactBlockProcessor internal constructor(
|
|||
|
||||
/**
|
||||
* Update the latest balances using the given wallet summary, and transmit this information
|
||||
* into the related internal flows. Note that the Orchard balance is not supported.
|
||||
* into the related internal flows.
|
||||
*/
|
||||
internal suspend fun updateAllBalances(summary: WalletSummary) {
|
||||
summary.accountBalances[Account.DEFAULT]?.let {
|
||||
Twig.debug { "Updating Sapling balance" }
|
||||
Twig.debug { "Updating balances" }
|
||||
saplingBalances.value = it.sapling
|
||||
// TODO [#682]: Uncomment this once we have Orchard support.
|
||||
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
||||
// orchardBalances.value = it.orchard
|
||||
orchardBalances.value = it.orchard
|
||||
// We only allow stored transparent balance to be shielded, and we do so with
|
||||
// a zero-conf transaction, so treat all unshielded balance as available.
|
||||
transparentBalance.value = it.unshielded
|
||||
|
@ -745,23 +747,26 @@ class CompactBlockProcessor internal constructor(
|
|||
|
||||
/**
|
||||
* Refreshes the SDK's wallet summary from the Rust backend, and transmits this information
|
||||
* into the related internal flows. Note that the Orchard balance is not yet supported.
|
||||
* into the related internal flows.
|
||||
*
|
||||
* @return the next subtree index to fetch.
|
||||
*/
|
||||
internal suspend fun refreshWalletSummary(): UInt {
|
||||
internal suspend fun refreshWalletSummary(): Pair<UInt, UInt> {
|
||||
when (val result = getWalletSummary(backend)) {
|
||||
is GetWalletSummaryResult.Success -> {
|
||||
val resultProgress = result.scanProgressPercentDecimal()
|
||||
Twig.info { "Progress from rust: ${resultProgress.decimal}" }
|
||||
setProgress(resultProgress)
|
||||
updateAllBalances(result.walletSummary)
|
||||
return result.walletSummary.nextSaplingSubtreeIndex
|
||||
return Pair(
|
||||
result.walletSummary.nextSaplingSubtreeIndex,
|
||||
result.walletSummary.nextOrchardSubtreeIndex
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Do not report the progress and balances in case of any error, and
|
||||
// tell the caller to fetch all subtree roots.
|
||||
return UInt.MIN_VALUE
|
||||
return Pair(UInt.MIN_VALUE, UInt.MIN_VALUE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1122,26 +1127,31 @@ class CompactBlockProcessor internal constructor(
|
|||
* @return GetSubtreeRootsResult as a wrapper for the lightwalletd response result
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@Suppress("LongMethod")
|
||||
internal suspend fun getSubtreeRoots(
|
||||
downloader: CompactBlockDownloader,
|
||||
network: ZcashNetwork,
|
||||
startIndex: UInt
|
||||
saplingStartIndex: UInt,
|
||||
orchardStartIndex: UInt
|
||||
): GetSubtreeRootsResult {
|
||||
Twig.debug { "Fetching SubtreeRoots..." }
|
||||
val traceScope = TraceScope("CompactBlockProcessor.getSubtreeRoots")
|
||||
|
||||
var result: GetSubtreeRootsResult = GetSubtreeRootsResult.Linear
|
||||
|
||||
var saplingSubtreeRootList: List<SubtreeRoot> = emptyList()
|
||||
var orchardSubtreeRootList: List<SubtreeRoot> = emptyList()
|
||||
|
||||
retryUpToAndContinue(GET_SUBTREE_ROOTS_RETRIES) {
|
||||
downloader.getSubtreeRoots(
|
||||
startIndex,
|
||||
saplingStartIndex,
|
||||
maxEntries = UInt.MIN_VALUE,
|
||||
shieldedProtocol = ShieldedProtocolEnum.SAPLING
|
||||
).onEach { response ->
|
||||
when (response) {
|
||||
is Response.Success -> {
|
||||
Twig.verbose {
|
||||
"SubtreeRoot fetched successfully: its completingHeight is: ${response.result
|
||||
"Sapling SubtreeRoot fetched successfully: its completingHeight is: ${response.result
|
||||
.completingBlockHeight}"
|
||||
}
|
||||
}
|
||||
|
@ -1154,12 +1164,14 @@ class CompactBlockProcessor internal constructor(
|
|||
)
|
||||
if (response is Response.Failure.Server.Unavailable) {
|
||||
Twig.error {
|
||||
"Fetching SubtreeRoot failed due to server communication problem with " +
|
||||
"Fetching Sapling SubtreeRoot failed due to server communication problem with " +
|
||||
"failure: ${response.toThrowable()}"
|
||||
}
|
||||
result = GetSubtreeRootsResult.FailureConnection
|
||||
} else {
|
||||
Twig.error { "Fetching SubtreeRoot failed with failure: ${response.toThrowable()}" }
|
||||
Twig.error {
|
||||
"Fetching Sapling SubtreeRoot failed with failure: ${response.toThrowable()}"
|
||||
}
|
||||
result = GetSubtreeRootsResult.OtherFailure(error)
|
||||
}
|
||||
traceScope.end()
|
||||
|
@ -1175,15 +1187,74 @@ class CompactBlockProcessor internal constructor(
|
|||
.map {
|
||||
SubtreeRoot.new(it, network)
|
||||
}.let {
|
||||
result =
|
||||
if (it.isEmpty()) {
|
||||
GetSubtreeRootsResult.Linear
|
||||
} else {
|
||||
GetSubtreeRootsResult.SpendBeforeSync(startIndex, it)
|
||||
}
|
||||
saplingSubtreeRootList = it
|
||||
}
|
||||
}
|
||||
|
||||
retryUpToAndContinue(GET_SUBTREE_ROOTS_RETRIES) {
|
||||
downloader.getSubtreeRoots(
|
||||
orchardStartIndex,
|
||||
maxEntries = UInt.MIN_VALUE,
|
||||
shieldedProtocol = ShieldedProtocolEnum.ORCHARD
|
||||
).onEach { response ->
|
||||
when (response) {
|
||||
is Response.Success -> {
|
||||
Twig.verbose {
|
||||
"Orchard SubtreeRoot fetched successfully: its completingHeight is: ${response.result
|
||||
.completingBlockHeight}"
|
||||
}
|
||||
}
|
||||
is Response.Failure -> {
|
||||
val error =
|
||||
LightWalletException.GetSubtreeRootsException(
|
||||
response.code,
|
||||
response.description,
|
||||
response.toThrowable()
|
||||
)
|
||||
if (response is Response.Failure.Server.Unavailable) {
|
||||
Twig.error {
|
||||
"Fetching Orchard SubtreeRoot failed due to server communication problem with " +
|
||||
"failure: ${response.toThrowable()}"
|
||||
}
|
||||
result = GetSubtreeRootsResult.FailureConnection
|
||||
} else {
|
||||
Twig.error {
|
||||
"Fetching Orchard SubtreeRoot failed with failure: ${response.toThrowable()}"
|
||||
}
|
||||
result = GetSubtreeRootsResult.OtherFailure(error)
|
||||
}
|
||||
traceScope.end()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
.filterIsInstance<Response.Success<SubtreeRootUnsafe>>()
|
||||
.map { response ->
|
||||
response.result
|
||||
}
|
||||
.toList()
|
||||
.map {
|
||||
SubtreeRoot.new(it, network)
|
||||
}.let {
|
||||
orchardSubtreeRootList = it
|
||||
}
|
||||
}
|
||||
|
||||
// Intentionally omitting [orchardSubtreeRootList], e.g., for Mainnet usage, we could check it, but on
|
||||
// custom networks without NU5 activation, it wouldn't work. If the Orchard subtree roots are empty, it's
|
||||
// technically still ok (as Orchard activates after Sapling, so on a network that doesn't have NU5
|
||||
// activated, this would behave correctly). In contrast, if the Sapling subtree roots are empty, we
|
||||
// cannot do SbS at all.
|
||||
if (saplingSubtreeRootList.isNotEmpty()) {
|
||||
result =
|
||||
GetSubtreeRootsResult.SpendBeforeSync(
|
||||
saplingStartIndex,
|
||||
saplingSubtreeRootList,
|
||||
orchardStartIndex,
|
||||
orchardSubtreeRootList
|
||||
)
|
||||
}
|
||||
|
||||
traceScope.end()
|
||||
return result
|
||||
}
|
||||
|
@ -1197,22 +1268,27 @@ class CompactBlockProcessor internal constructor(
|
|||
* @return PutSaplingSubtreeRootsResult
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@Suppress("LongParameterList")
|
||||
internal suspend fun putSaplingSubtreeRoots(
|
||||
backend: TypesafeBackend,
|
||||
startIndex: UInt,
|
||||
subTreeRootList: List<SubtreeRoot>,
|
||||
saplingStartIndex: UInt,
|
||||
saplingSubtreeRootList: List<SubtreeRoot>,
|
||||
orchardStartIndex: UInt,
|
||||
orchardSubtreeRootList: List<SubtreeRoot>,
|
||||
lastValidHeight: BlockHeight
|
||||
): PutSaplingSubtreeRootsResult {
|
||||
return runCatching {
|
||||
backend.putSaplingSubtreeRoots(
|
||||
startIndex = startIndex,
|
||||
roots = subTreeRootList
|
||||
backend.putSubtreeRoots(
|
||||
saplingStartIndex = saplingStartIndex,
|
||||
saplingRoots = saplingSubtreeRootList,
|
||||
orchardStartIndex = orchardStartIndex,
|
||||
orchardRoots = orchardSubtreeRootList,
|
||||
)
|
||||
}
|
||||
.onSuccess {
|
||||
Twig.info {
|
||||
"Sapling subtree roots put successfully with startIndex: $startIndex and roots: " +
|
||||
"${subTreeRootList.size}"
|
||||
"Subtree roots put successfully with saplingStartIndex: $saplingStartIndex and " +
|
||||
"orchardStartIndex: $orchardStartIndex"
|
||||
}
|
||||
}
|
||||
.onFailure {
|
||||
|
@ -1342,8 +1418,6 @@ class CompactBlockProcessor internal constructor(
|
|||
* @param repository the derived data repository component
|
||||
* @param network the network in which the sync mechanism operates
|
||||
* @param syncRange the range of blocks to download
|
||||
* @param withDownload the flag indicating whether the blocks should also be downloaded and processed, or
|
||||
* processed existing blocks
|
||||
* @param enhanceStartHeight the height in which the enhancing should start, or null in case of no previous
|
||||
* transaction enhancing done yet
|
||||
*
|
||||
|
@ -1357,7 +1431,6 @@ class CompactBlockProcessor internal constructor(
|
|||
repository: DerivedDataRepository,
|
||||
network: ZcashNetwork,
|
||||
syncRange: ClosedRange<BlockHeight>,
|
||||
withDownload: Boolean,
|
||||
enhanceStartHeight: BlockHeight?
|
||||
): Flow<BatchSyncProgress> =
|
||||
flow {
|
||||
|
@ -1389,14 +1462,10 @@ class CompactBlockProcessor internal constructor(
|
|||
SyncStageResult(
|
||||
batch = it,
|
||||
stageResult =
|
||||
if (withDownload) {
|
||||
downloadBatchOfBlocks(
|
||||
downloader = downloader,
|
||||
batch = it
|
||||
)
|
||||
} else {
|
||||
SyncingResult.DownloadSuccess(null)
|
||||
}
|
||||
downloadBatchOfBlocks(
|
||||
downloader = downloader,
|
||||
batch = it
|
||||
)
|
||||
)
|
||||
}.buffer(1).map { downloadStageResult ->
|
||||
Twig.debug { "Download stage done with result: $downloadStageResult" }
|
||||
|
@ -1413,7 +1482,8 @@ class CompactBlockProcessor internal constructor(
|
|||
downloadStageResult.batch,
|
||||
scanBatchOfBlocks(
|
||||
backend = backend,
|
||||
batch = downloadStageResult.batch
|
||||
batch = downloadStageResult.batch,
|
||||
fromState = downloadStageResult.stageResult.fromState
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -1605,6 +1675,16 @@ class CompactBlockProcessor internal constructor(
|
|||
batch: BlockBatch
|
||||
): SyncingResult {
|
||||
val traceScope = TraceScope("CompactBlockProcessor.downloadBatchOfBlocks")
|
||||
|
||||
val fromState =
|
||||
fetchTreeStateForHeight(
|
||||
height = batch.range.start - 1,
|
||||
downloader = downloader
|
||||
) ?: return SyncingResult.DownloadFailed(
|
||||
batch.range.start,
|
||||
CompactBlockProcessorException.FailedDownloadException()
|
||||
)
|
||||
|
||||
var downloadedBlocks = listOf<JniBlockMeta>()
|
||||
var downloadException: CompactBlockProcessorException.FailedDownloadException? = null
|
||||
|
||||
|
@ -1626,7 +1706,7 @@ class CompactBlockProcessor internal constructor(
|
|||
Twig.verbose { "Successfully downloaded batch: $batch of $downloadedBlocks blocks" }
|
||||
|
||||
return if (downloadedBlocks.isNotEmpty()) {
|
||||
SyncingResult.DownloadSuccess(downloadedBlocks)
|
||||
SyncingResult.DownloadSuccess(fromState, downloadedBlocks)
|
||||
} else {
|
||||
SyncingResult.DownloadFailed(
|
||||
batch.range.start,
|
||||
|
@ -1635,15 +1715,43 @@ class CompactBlockProcessor internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal suspend fun fetchTreeStateForHeight(
|
||||
height: BlockHeight,
|
||||
downloader: CompactBlockDownloader,
|
||||
): TreeState? {
|
||||
retryUpToAndContinue(retries = RETRIES) { failedAttempts ->
|
||||
if (failedAttempts == 0) {
|
||||
Twig.debug { "Starting to fetch tree state for height ${height.value}" }
|
||||
} else {
|
||||
Twig.warn {
|
||||
"Retrying to fetch tree state for height ${height.value} after $failedAttempts failure(s)..."
|
||||
}
|
||||
}
|
||||
when (val response = downloader.getTreeState(BlockHeightUnsafe(height.value))) {
|
||||
is Response.Success -> {
|
||||
return TreeState.new(response.result)
|
||||
}
|
||||
is Response.Failure -> {
|
||||
Twig.error(response.toThrowable()) { "Tree state fetch failed" }
|
||||
throw response.toThrowable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal suspend fun scanBatchOfBlocks(
|
||||
batch: BlockBatch,
|
||||
fromState: TreeState,
|
||||
backend: TypesafeBackend
|
||||
): SyncingResult {
|
||||
val traceScope = TraceScope("CompactBlockProcessor.scanBatchOfBlocks")
|
||||
val result =
|
||||
runCatching {
|
||||
backend.scanBlocks(batch.range.start, batch.range.length())
|
||||
backend.scanBlocks(batch.range.start, fromState, batch.range.length())
|
||||
}.onSuccess {
|
||||
Twig.verbose { "Successfully scanned batch $batch" }
|
||||
}.onFailure {
|
||||
|
|
|
@ -7,8 +7,10 @@ import cash.z.ecc.android.sdk.internal.model.SubtreeRoot
|
|||
*/
|
||||
internal sealed class GetSubtreeRootsResult {
|
||||
data class SpendBeforeSync(
|
||||
val startIndex: UInt,
|
||||
val subTreeRootList: List<SubtreeRoot>
|
||||
val saplingStartIndex: UInt,
|
||||
val saplingSubtreeRootList: List<SubtreeRoot>,
|
||||
val orchardStartIndex: UInt,
|
||||
val orchardSubtreeRootList: List<SubtreeRoot>
|
||||
) : GetSubtreeRootsResult()
|
||||
|
||||
data object Linear : GetSubtreeRootsResult()
|
||||
|
|
|
@ -4,6 +4,7 @@ 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.internal.model.ScanSummary
|
||||
import cash.z.ecc.android.sdk.internal.model.TreeState
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
|
||||
/**
|
||||
|
@ -16,8 +17,11 @@ internal sealed class SyncingResult {
|
|||
|
||||
object RestartSynchronization : SyncingResult()
|
||||
|
||||
data class DownloadSuccess(val downloadedBlocks: List<JniBlockMeta>?) : SyncingResult() {
|
||||
override fun toString() = "${this::class.java.simpleName} with ${downloadedBlocks?.size ?: "none"} blocks"
|
||||
data class DownloadSuccess(
|
||||
val fromState: TreeState,
|
||||
val downloadedBlocks: List<JniBlockMeta>
|
||||
) : SyncingResult() {
|
||||
override fun toString() = "${this::class.java.simpleName} with ${downloadedBlocks.size} blocks"
|
||||
}
|
||||
|
||||
interface Failure {
|
||||
|
|
|
@ -174,6 +174,12 @@ sealed class InitializeException(message: String, cause: Throwable? = null) : Sd
|
|||
private fun readResolve(): Any = SeedRequired
|
||||
}
|
||||
|
||||
data object SeedNotRelevant : InitializeException(
|
||||
"The provided seed is not relevant to any of the derived accounts in the wallet database."
|
||||
) {
|
||||
private fun readResolve(): Any = SeedNotRelevant
|
||||
}
|
||||
|
||||
class FalseStart(cause: Throwable?) : InitializeException("Failed to initialize accounts due to: $cause", cause)
|
||||
|
||||
class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializeException(
|
||||
|
|
|
@ -85,9 +85,11 @@ internal interface TypesafeBackend {
|
|||
* @throws RuntimeException as a common indicator of the operation failure
|
||||
*/
|
||||
@Throws(RuntimeException::class)
|
||||
suspend fun putSaplingSubtreeRoots(
|
||||
startIndex: UInt,
|
||||
roots: List<SubtreeRoot>,
|
||||
suspend fun putSubtreeRoots(
|
||||
saplingStartIndex: UInt,
|
||||
saplingRoots: List<SubtreeRoot>,
|
||||
orchardStartIndex: UInt,
|
||||
orchardRoots: List<SubtreeRoot>,
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -125,6 +127,7 @@ internal interface TypesafeBackend {
|
|||
@Throws(RuntimeException::class)
|
||||
suspend fun scanBlocks(
|
||||
fromHeight: BlockHeight,
|
||||
fromState: TreeState,
|
||||
limit: Long
|
||||
): ScanSummary
|
||||
|
||||
|
|
|
@ -153,6 +153,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
|
|||
override suspend fun initDataDb(seed: ByteArray?) {
|
||||
val ret = backend.initDataDb(seed)
|
||||
when (ret) {
|
||||
2 -> throw InitializeException.SeedNotRelevant
|
||||
1 -> throw InitializeException.SeedRequired
|
||||
0 -> { /* Successful case - no action needed */ }
|
||||
-1 -> error("Rust backend only uses -1 as an error sentinel")
|
||||
|
@ -160,18 +161,28 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun putSaplingSubtreeRoots(
|
||||
startIndex: UInt,
|
||||
roots: List<SubtreeRoot>
|
||||
) = backend.putSaplingSubtreeRoots(
|
||||
startIndex = startIndex.toLong(),
|
||||
roots =
|
||||
roots.map {
|
||||
override suspend fun putSubtreeRoots(
|
||||
saplingStartIndex: UInt,
|
||||
saplingRoots: List<SubtreeRoot>,
|
||||
orchardStartIndex: UInt,
|
||||
orchardRoots: List<SubtreeRoot>
|
||||
) = backend.putSubtreeRoots(
|
||||
saplingStartIndex = saplingStartIndex.toLong(),
|
||||
saplingRoots =
|
||||
saplingRoots.map {
|
||||
JniSubtreeRoot.new(
|
||||
rootHash = it.rootHash,
|
||||
completingBlockHeight = it.completingBlockHeight.value
|
||||
)
|
||||
}
|
||||
},
|
||||
orchardStartIndex = orchardStartIndex.toLong(),
|
||||
orchardRoots =
|
||||
orchardRoots.map {
|
||||
JniSubtreeRoot.new(
|
||||
rootHash = it.rootHash,
|
||||
completingBlockHeight = it.completingBlockHeight.value
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
override suspend fun updateChainTip(height: BlockHeight) = backend.updateChainTip(height.value)
|
||||
|
@ -196,8 +207,9 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
|
|||
|
||||
override suspend fun scanBlocks(
|
||||
fromHeight: BlockHeight,
|
||||
fromState: TreeState,
|
||||
limit: Long
|
||||
): ScanSummary = ScanSummary.new(backend.scanBlocks(fromHeight.value, limit), network)
|
||||
): ScanSummary = ScanSummary.new(backend.scanBlocks(fromHeight.value, fromState.encoded, limit), network)
|
||||
|
||||
override suspend fun getWalletSummary(): WalletSummary? =
|
||||
backend.getWalletSummary()?.let { jniWalletSummary ->
|
||||
|
|
|
@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext
|
|||
* @property lightWalletClient the client used for requesting compact blocks
|
||||
* @property compactBlockStore responsible for persisting the compact blocks that are received
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
open class CompactBlockDownloader private constructor(val compactBlockRepository: CompactBlockRepository) {
|
||||
private lateinit var lightWalletClient: LightWalletClient
|
||||
|
||||
|
@ -178,6 +179,13 @@ open class CompactBlockDownloader private constructor(val compactBlockRepository
|
|||
maxEntries = maxEntries
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns information about roots of subtrees of the Sapling and Orchard note commitment trees.
|
||||
*
|
||||
* @return information about roots of subtrees of the Sapling and Orchard note commitment trees.
|
||||
*/
|
||||
suspend fun getTreeState(height: BlockHeightUnsafe) = lightWalletClient.getTreeState(height = height)
|
||||
|
||||
companion object {
|
||||
private const val GET_SERVER_INFO_RETRIES = 6
|
||||
}
|
||||
|
|
|
@ -83,11 +83,11 @@ internal object TxOutputsViewDefinition {
|
|||
|
||||
const val COLUMN_INTEGER_OUTPUT_INDEX = "output_index" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_FROM_ACCOUNT = "from_account" // $NON-NLS
|
||||
const val COLUMN_INTEGER_FROM_ACCOUNT = "from_account_id" // $NON-NLS
|
||||
|
||||
const val COLUMN_STRING_TO_ADDRESS = "to_address" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_TO_ACCOUNT = "to_account" // $NON-NLS
|
||||
const val COLUMN_INTEGER_TO_ACCOUNT = "to_account_id" // $NON-NLS
|
||||
|
||||
const val COLUMN_INTEGER_VALUE = "value" // $NON-NLS
|
||||
|
||||
|
|
|
@ -16,14 +16,14 @@ internal data class Checkpoint(
|
|||
val hash: String,
|
||||
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
|
||||
val epochSeconds: Long,
|
||||
// Note: this field does NOT match the name of the JSON, so will break with field-based JSON parsing
|
||||
val tree: String
|
||||
val saplingTree: String,
|
||||
val orchardTree: String
|
||||
) {
|
||||
fun treeState(): TreeState {
|
||||
require(epochSeconds.isInUIntRange()) {
|
||||
"epochSeconds $epochSeconds is outside of allowed UInt range"
|
||||
}
|
||||
return TreeState.fromParts(height.value, hash, epochSeconds.toInt(), tree)
|
||||
return TreeState.fromParts(height.value, hash, epochSeconds.toInt(), saplingTree, orchardTree)
|
||||
}
|
||||
|
||||
internal companion object
|
||||
|
|
|
@ -17,9 +17,10 @@ class TreeState(
|
|||
height: Long,
|
||||
hash: String,
|
||||
time: Int,
|
||||
tree: String
|
||||
saplingTree: String,
|
||||
orchardTree: String
|
||||
): TreeState {
|
||||
val unsafeTreeState = TreeStateUnsafe.fromParts(height, hash, time, tree)
|
||||
val unsafeTreeState = TreeStateUnsafe.fromParts(height, hash, time, saplingTree, orchardTree)
|
||||
return TreeState.new(unsafeTreeState)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ internal data class WalletSummary(
|
|||
val chainTipHeight: BlockHeight,
|
||||
val fullyScannedHeight: BlockHeight,
|
||||
val scanProgress: ScanProgress,
|
||||
val nextSaplingSubtreeIndex: UInt
|
||||
val nextSaplingSubtreeIndex: UInt,
|
||||
val nextOrchardSubtreeIndex: UInt
|
||||
) {
|
||||
companion object {
|
||||
fun new(jni: JniWalletSummary): WalletSummary {
|
||||
|
@ -20,7 +21,8 @@ internal data class WalletSummary(
|
|||
chainTipHeight = BlockHeight(jni.chainTipHeight),
|
||||
fullyScannedHeight = BlockHeight(jni.fullyScannedHeight),
|
||||
scanProgress = ScanProgress.new(jni),
|
||||
nextSaplingSubtreeIndex = jni.nextSaplingSubtreeIndex.toUInt()
|
||||
nextSaplingSubtreeIndex = jni.nextSaplingSubtreeIndex.toUInt(),
|
||||
nextOrchardSubtreeIndex = jni.nextOrchardSubtreeIndex.toUInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package cash.z.ecc.android.sdk.internal.model.ext
|
||||
|
||||
import cash.z.ecc.android.sdk.internal.Twig
|
||||
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
||||
import cash.z.ecc.android.sdk.model.BlockHeight
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
// Version is not returned from the server, so version 1 is implied. A version is declared here
|
||||
|
@ -17,8 +19,10 @@ internal val Checkpoint.Companion.KEY_HASH
|
|||
get() = "hash"
|
||||
internal val Checkpoint.Companion.KEY_EPOCH_SECONDS
|
||||
get() = "time"
|
||||
internal val Checkpoint.Companion.KEY_TREE
|
||||
internal val Checkpoint.Companion.KEY_SAPLING_TREE
|
||||
get() = "saplingTree"
|
||||
internal val Checkpoint.Companion.KEY_ORCHARD_TREE
|
||||
get() = "orchardTree"
|
||||
|
||||
internal fun Checkpoint.Companion.from(
|
||||
zcashNetwork: ZcashNetwork,
|
||||
|
@ -38,9 +42,21 @@ private fun Checkpoint.Companion.from(
|
|||
}
|
||||
val hash = jsonObject.getString(Checkpoint.KEY_HASH)
|
||||
val epochSeconds = jsonObject.getLong(Checkpoint.KEY_EPOCH_SECONDS)
|
||||
val tree = jsonObject.getString(Checkpoint.KEY_TREE)
|
||||
val saplingTree = jsonObject.getString(Checkpoint.KEY_SAPLING_TREE)
|
||||
val orchardTree =
|
||||
try {
|
||||
jsonObject.getString(Checkpoint.KEY_ORCHARD_TREE)
|
||||
} catch (e: JSONException) {
|
||||
Twig.warn(e) { "This checkpoint does not contain an Orchard tree state" }
|
||||
// For checkpoints that don't contain an Orchard tree state, we can use
|
||||
// the empty Orchard tree state as long as the height is before NU5.
|
||||
require(height < zcashNetwork.orchardActivationHeight) {
|
||||
"Post-NU5 checkpoint at height $height missing orchardTree field"
|
||||
}
|
||||
"000000" // NON-NLS
|
||||
}
|
||||
|
||||
return Checkpoint(height, hash, epochSeconds, tree)
|
||||
return Checkpoint(height, hash, epochSeconds, saplingTree, orchardTree)
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException("Unsupported version $version")
|
||||
|
|
Loading…
Reference in New Issue