Merge branch 'fast-spendability'

This commit is contained in:
Kris Nuttycombe 2023-09-12 16:20:44 -06:00
commit 4d99ad10ac
102 changed files with 3602 additions and 2088 deletions

View File

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

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

1040
backend-lib/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}
}
}
} }
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,30 +21,18 @@ 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")
@ -50,22 +41,26 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke
to: String, to: String,
value: Long, value: Long,
memo: ByteArray? memo: ByteArray?
): Long = backend.createToAddress( ): FirstClassByteArray = FirstClassByteArray(
backend.createToAddress(
usk.account.value, usk.account.value,
usk.copyBytes(), usk.copyBytes(),
to, to,
value, value,
memo memo
) )
)
override suspend fun shieldToAddress( override suspend fun shieldToAddress(
usk: UnifiedSpendingKey, usk: UnifiedSpendingKey,
memo: ByteArray? memo: ByteArray?
): Long = backend.shieldToAddress( ): FirstClassByteArray = FirstClassByteArray(
backend.shieldToAddress(
usk.account.value, usk.account.value,
usk.copyBytes(), usk.copyBytes(),
memo memo
) )
)
override suspend fun getCurrentAddress(account: Account): String { override suspend fun getCurrentAddress(account: Account): String {
return backend.getCurrentAddress(account.value) return backend.getCurrentAddress(account.value)
@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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