[#1213] Remove `BlockTable` and its APIs
* [#1213] Remove `count()` from BlockTable API * [#1213] Remove `firstScannedHeight()` from BlockTable API * [#1213] Remove `findBlockHash()` from BlockTable * [#1213] Remove `lastScannedHeight()` from BlockTable * [#1213] Remove `BlockTable` entirely
This commit is contained in:
parent
14d854f96e
commit
fc14082a1c
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -14,7 +14,21 @@
|
||||||
- `CompactBlockProcessor.ProcessorInfo.isSyncing`. Use `Synchronizer.status` instead.
|
- `CompactBlockProcessor.ProcessorInfo.isSyncing`. Use `Synchronizer.status` instead.
|
||||||
- `CompactBlockProcessor.ProcessorInfo.syncProgress`. Use `Synchronizer.progress` instead.
|
- `CompactBlockProcessor.ProcessorInfo.syncProgress`. Use `Synchronizer.progress` instead.
|
||||||
- `alsoClearBlockCache` parameter from rewind functions of `Synchronizer` and `CompactBlockProcessor` as it take no
|
- `alsoClearBlockCache` parameter from rewind functions of `Synchronizer` and `CompactBlockProcessor` as it take no
|
||||||
affect on the current rewind functionality result.
|
effect on the current rewind functionality result.
|
||||||
|
- Internally, we removed access to the shared block table from the Kotlin layer, which resulted in eliminating these
|
||||||
|
APIs:
|
||||||
|
- `SdkSynchornizer.findBlockHash()`
|
||||||
|
- `SdkSynchornizer.findBlockHashAsHex()`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `CompactBlockProcessor.quickRewind()` and `CompactBlockProcessor.rewindToNearestHeight()` now might fail due to
|
||||||
|
internal changes in getting scanned height. Thus, these functions return `Boolean` results.
|
||||||
|
|
||||||
|
### 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 briefly 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,
|
||||||
|
@ -32,12 +46,6 @@ which speeds up discovering the wallet's spendable balance.
|
||||||
- etc.
|
- etc.
|
||||||
- Checkpoints
|
- Checkpoints
|
||||||
|
|
||||||
### 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 briefly after every new transaction is discovered via `Synchronizer.transactions` API.
|
|
||||||
Issue **#1170**.
|
|
||||||
|
|
||||||
## 1.20.0-beta01
|
## 1.20.0-beta01
|
||||||
- The SDK internally migrated from `BackendExt` rust backend extension functions to more type-safe `TypesafeBackend`.
|
- The SDK internally migrated from `BackendExt` rust backend extension functions to more type-safe `TypesafeBackend`.
|
||||||
- `Synchronizer.getMemos()` now internally handles expected `RuntimeException` from the rust layer and transforms it
|
- `Synchronizer.getMemos()` now internally handles expected `RuntimeException` from the rust layer and transforms it
|
||||||
|
|
|
@ -36,6 +36,17 @@ interface Backend {
|
||||||
|
|
||||||
suspend fun decryptAndStoreTransaction(tx: ByteArray)
|
suspend fun decryptAndStoreTransaction(tx: ByteArray)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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::class)
|
||||||
suspend fun initDataDb(seed: ByteArray?): Int
|
suspend fun initDataDb(seed: ByteArray?): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,7 +24,6 @@ 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
|
||||||
|
@ -185,7 +184,7 @@ class SdkSynchronizer private constructor(
|
||||||
|
|
||||||
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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,14 +340,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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import androidx.annotation.VisibleForTesting
|
||||||
import cash.z.ecc.android.sdk.BuildConfig
|
import cash.z.ecc.android.sdk.BuildConfig
|
||||||
import cash.z.ecc.android.sdk.annotation.OpenForTesting
|
import cash.z.ecc.android.sdk.annotation.OpenForTesting
|
||||||
import cash.z.ecc.android.sdk.block.processor.model.BatchSyncProgress
|
import cash.z.ecc.android.sdk.block.processor.model.BatchSyncProgress
|
||||||
|
import cash.z.ecc.android.sdk.block.processor.model.GetMaxScannedHeightResult
|
||||||
import cash.z.ecc.android.sdk.block.processor.model.GetSubtreeRootsResult
|
import cash.z.ecc.android.sdk.block.processor.model.GetSubtreeRootsResult
|
||||||
import cash.z.ecc.android.sdk.block.processor.model.PutSaplingSubtreeRootsResult
|
import cash.z.ecc.android.sdk.block.processor.model.PutSaplingSubtreeRootsResult
|
||||||
import cash.z.ecc.android.sdk.block.processor.model.SbSPreparationResult
|
import cash.z.ecc.android.sdk.block.processor.model.SbSPreparationResult
|
||||||
|
@ -218,7 +219,10 @@ class CompactBlockProcessor internal constructor(
|
||||||
// Clear any undeleted left over block files from previous sync attempts
|
// Clear any undeleted left over block files from previous sync attempts
|
||||||
deleteAllBlockFiles(
|
deleteAllBlockFiles(
|
||||||
downloader = downloader,
|
downloader = downloader,
|
||||||
lastKnownHeight = getLastScannedHeight(repository)
|
lastKnownHeight = when (val result = getMaxScannedHeight(backend)) {
|
||||||
|
is GetMaxScannedHeightResult.Success -> result.height
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Download note commitment tree data from lightwalletd to decide if we communicate with linear
|
// Download note commitment tree data from lightwalletd to decide if we communicate with linear
|
||||||
|
@ -347,7 +351,7 @@ class CompactBlockProcessor internal constructor(
|
||||||
stop()
|
stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkErrorResult(failedHeight: BlockHeight) {
|
suspend fun checkErrorResult(failedHeight: BlockHeight?) {
|
||||||
if (consecutiveChainErrors.get() >= RETRIES) {
|
if (consecutiveChainErrors.get() >= RETRIES) {
|
||||||
val errorMessage = "ERROR: unable to resolve reorg at height $failedHeight after " +
|
val errorMessage = "ERROR: unable to resolve reorg at height $failedHeight after " +
|
||||||
"${consecutiveChainErrors.get()} correction attempts!"
|
"${consecutiveChainErrors.get()} correction attempts!"
|
||||||
|
@ -476,8 +480,13 @@ class CompactBlockProcessor internal constructor(
|
||||||
// Continue with processing the rest of the ranges
|
// Continue with processing the rest of the ranges
|
||||||
} else -> {
|
} else -> {
|
||||||
// An error came - remove persisted but not scanned blocks
|
// An error came - remove persisted but not scanned blocks
|
||||||
val lastScannedHeight = getLastScannedHeight(repository)
|
val lastScannedHeight = when (val result = getMaxScannedHeight(backend)) {
|
||||||
|
is GetMaxScannedHeightResult.Success -> result.height
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
lastScannedHeight?.let {
|
||||||
downloader.rewindToHeight(lastScannedHeight)
|
downloader.rewindToHeight(lastScannedHeight)
|
||||||
|
}
|
||||||
deleteAllBlockFiles(
|
deleteAllBlockFiles(
|
||||||
downloader = downloader,
|
downloader = downloader,
|
||||||
lastKnownHeight = lastScannedHeight
|
lastKnownHeight = lastScannedHeight
|
||||||
|
@ -579,8 +588,13 @@ class CompactBlockProcessor internal constructor(
|
||||||
return BlockProcessingResult.RestartSynchronization
|
return BlockProcessingResult.RestartSynchronization
|
||||||
} else -> {
|
} else -> {
|
||||||
// An error came - remove persisted but not scanned blocks
|
// An error came - remove persisted but not scanned blocks
|
||||||
val lastScannedHeight = getLastScannedHeight(repository)
|
val lastScannedHeight = when (val result = getMaxScannedHeight(backend)) {
|
||||||
|
is GetMaxScannedHeightResult.Success -> result.height
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
lastScannedHeight?.let {
|
||||||
downloader.rewindToHeight(lastScannedHeight)
|
downloader.rewindToHeight(lastScannedHeight)
|
||||||
|
}
|
||||||
deleteAllBlockFiles(
|
deleteAllBlockFiles(
|
||||||
downloader = downloader,
|
downloader = downloader,
|
||||||
lastKnownHeight = lastScannedHeight
|
lastKnownHeight = lastScannedHeight
|
||||||
|
@ -713,7 +727,7 @@ class CompactBlockProcessor internal constructor(
|
||||||
object Success : BlockProcessingResult()
|
object Success : BlockProcessingResult()
|
||||||
object Reconnecting : BlockProcessingResult()
|
object Reconnecting : BlockProcessingResult()
|
||||||
object RestartSynchronization : BlockProcessingResult()
|
object RestartSynchronization : BlockProcessingResult()
|
||||||
data class SyncFailure(val failedAtHeight: BlockHeight, val error: Throwable) : BlockProcessingResult()
|
data class SyncFailure(val failedAtHeight: BlockHeight?, val error: Throwable) : BlockProcessingResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -767,10 +781,7 @@ class CompactBlockProcessor internal constructor(
|
||||||
// TODO [#1127]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1127
|
// TODO [#1127]: https://github.com/zcash/zcash-android-wallet-sdk/issues/1127
|
||||||
@Suppress("NestedBlockDepth")
|
@Suppress("NestedBlockDepth")
|
||||||
private suspend fun verifySetup() {
|
private suspend fun verifySetup() {
|
||||||
// verify that the data is initialized
|
val error = if (repository.getAccountCount() == 0) {
|
||||||
val error = if (!repository.isInitialized()) {
|
|
||||||
CompactBlockProcessorException.Uninitialized
|
|
||||||
} else if (repository.getAccountCount() == 0) {
|
|
||||||
CompactBlockProcessorException.NoAccount
|
CompactBlockProcessorException.NoAccount
|
||||||
} else {
|
} else {
|
||||||
// verify that the server is correct
|
// verify that the server is correct
|
||||||
|
@ -1531,7 +1542,7 @@ class CompactBlockProcessor internal constructor(
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal suspend fun deleteAllBlockFiles(
|
internal suspend fun deleteAllBlockFiles(
|
||||||
downloader: CompactBlockDownloader,
|
downloader: CompactBlockDownloader,
|
||||||
lastKnownHeight: BlockHeight
|
lastKnownHeight: BlockHeight?
|
||||||
): SyncingResult {
|
): SyncingResult {
|
||||||
Twig.verbose { "Starting to delete all temporary block files" }
|
Twig.verbose { "Starting to delete all temporary block files" }
|
||||||
return if (downloader.compactBlockRepository.deleteAllCompactBlockFiles()) {
|
return if (downloader.compactBlockRepository.deleteAllCompactBlockFiles()) {
|
||||||
|
@ -1691,8 +1702,26 @@ class CompactBlockProcessor internal constructor(
|
||||||
* @return the last scanned height reported by the repository.
|
* @return the last scanned height reported by the repository.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal suspend fun getLastScannedHeight(repository: DerivedDataRepository) =
|
internal suspend fun getMaxScannedHeight(backend: TypesafeBackend): GetMaxScannedHeightResult {
|
||||||
repository.lastScannedHeight()
|
return runCatching {
|
||||||
|
backend.getMaxScannedHeight()
|
||||||
|
}.onSuccess {
|
||||||
|
Twig.verbose { "Successfully called getMaxScannedHeight with result: $it" }
|
||||||
|
}.onFailure {
|
||||||
|
Twig.error { "Failed to call getMaxScannedHeight with result: $it" }
|
||||||
|
}.fold(
|
||||||
|
onSuccess = {
|
||||||
|
if (it == null) {
|
||||||
|
GetMaxScannedHeightResult.None
|
||||||
|
} else {
|
||||||
|
GetMaxScannedHeightResult.Success(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
GetMaxScannedHeightResult.Failure(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the height of the first un-enhanced transaction detail from the repository.
|
* Get the height of the first un-enhanced transaction detail from the repository.
|
||||||
|
@ -1784,16 +1813,16 @@ class CompactBlockProcessor internal constructor(
|
||||||
_state.value = newState
|
_state.value = newState
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleChainError(errorHeight: BlockHeight) {
|
private suspend fun handleChainError(errorHeight: BlockHeight?) {
|
||||||
// TODO [#683]: Consider an error object containing hash information
|
|
||||||
// TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683
|
|
||||||
printValidationErrorInfo(errorHeight)
|
printValidationErrorInfo(errorHeight)
|
||||||
|
errorHeight?.let {
|
||||||
determineLowerBound(errorHeight).let { lowerBound ->
|
determineLowerBound(errorHeight).let { lowerBound ->
|
||||||
Twig.debug { "Handling chain error at $errorHeight by rewinding to block $lowerBound" }
|
Twig.debug { "Handling chain error at $errorHeight by rewinding to block $lowerBound" }
|
||||||
onChainErrorListener?.invoke(errorHeight, lowerBound)
|
onChainErrorListener?.invoke(errorHeight, lowerBound)
|
||||||
rewindToNearestHeight(lowerBound)
|
rewindToNearestHeight(lowerBound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight {
|
suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight {
|
||||||
// TODO [#683]: add a concept of original checkpoint height to the processor. For now, derive it
|
// TODO [#683]: add a concept of original checkpoint height to the processor. For now, derive it
|
||||||
|
@ -1813,20 +1842,26 @@ class CompactBlockProcessor internal constructor(
|
||||||
/**
|
/**
|
||||||
* Rewind back at least two weeks worth of blocks.
|
* Rewind back at least two weeks worth of blocks.
|
||||||
*/
|
*/
|
||||||
suspend fun quickRewind() {
|
suspend fun quickRewind(): Boolean {
|
||||||
val height = repository.lastScannedHeight()
|
val height = when (val result = getMaxScannedHeight(backend)) {
|
||||||
|
is GetMaxScannedHeightResult.Success -> result.height
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
val blocksPer14Days = 14.days.inWholeMilliseconds / ZcashSdk.BLOCK_INTERVAL_MILLIS.toInt()
|
val blocksPer14Days = 14.days.inWholeMilliseconds / ZcashSdk.BLOCK_INTERVAL_MILLIS.toInt()
|
||||||
val twoWeeksBack = BlockHeight.new(
|
val twoWeeksBack = BlockHeight.new(
|
||||||
network,
|
network,
|
||||||
(height.value - blocksPer14Days).coerceAtLeast(lowerBoundHeight.value)
|
(height.value - blocksPer14Days).coerceAtLeast(lowerBoundHeight.value)
|
||||||
)
|
)
|
||||||
rewindToNearestHeight(twoWeeksBack)
|
return rewindToNearestHeight(twoWeeksBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
suspend fun rewindToNearestHeight(height: BlockHeight) {
|
suspend fun rewindToNearestHeight(height: BlockHeight): Boolean {
|
||||||
processingMutex.withLockLogged("rewindToHeight") {
|
processingMutex.withLockLogged("rewindToHeight") {
|
||||||
val lastLocalBlock = repository.lastScannedHeight()
|
val lastLocalBlock = when (val result = getMaxScannedHeight(backend)) {
|
||||||
|
is GetMaxScannedHeightResult.Success -> result.height
|
||||||
|
else -> return false
|
||||||
|
}
|
||||||
val targetHeight = getNearestRewindHeight(height)
|
val targetHeight = getNearestRewindHeight(height)
|
||||||
|
|
||||||
Twig.debug {
|
Twig.debug {
|
||||||
|
@ -1854,40 +1889,37 @@ class CompactBlockProcessor internal constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** insightful function for debugging these critical errors */
|
/** insightful function for debugging these critical errors */
|
||||||
private suspend fun printValidationErrorInfo(errorHeight: BlockHeight, count: Int = 11) {
|
private suspend fun printValidationErrorInfo(errorHeight: BlockHeight?, count: Int = 11) {
|
||||||
// Note: blocks are public information so it's okay to print them but, still, let's not unless we're
|
// Note: blocks are public information so it's okay to print them but, still, let's not unless we're
|
||||||
// debugging something
|
// debugging something
|
||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorInfo = fetchValidationErrorInfo(errorHeight)
|
if (errorHeight == null) {
|
||||||
Twig.debug { "validation failed at block ${errorInfo.errorHeight} with hash: ${errorInfo.hash}" }
|
Twig.debug { "Validation failed at unspecified block height" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
errorInfo = fetchValidationErrorInfo(errorHeight + 1)
|
var errorInfo = ValidationErrorInfo(errorHeight)
|
||||||
Twig.debug { "the next block is ${errorInfo.errorHeight} with hash: ${errorInfo.hash}" }
|
Twig.debug { "Validation failed at block ${errorInfo.errorHeight}" }
|
||||||
|
|
||||||
|
errorInfo = ValidationErrorInfo(errorHeight + 1)
|
||||||
|
Twig.debug { "The next block is ${errorInfo.errorHeight}" }
|
||||||
|
|
||||||
Twig.debug { "=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: START ========" }
|
Twig.debug { "=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: START ========" }
|
||||||
repeat(count) { i ->
|
repeat(count) { i ->
|
||||||
val height = errorHeight + i
|
val height = errorHeight + i
|
||||||
val block = downloader.compactBlockRepository.findCompactBlock(height)
|
val block = downloader.compactBlockRepository.findCompactBlock(height)
|
||||||
// sometimes the initial block was inserted via checkpoint and will not appear in the cache. We can get
|
Twig.debug { "block: $height\thash=${block?.hash?.toHexReversed()}" }
|
||||||
// the hash another way.
|
|
||||||
val checkedHash = block?.hash ?: repository.findBlockHash(height)
|
|
||||||
Twig.debug { "block: $height\thash=${checkedHash?.toHexReversed()}" }
|
|
||||||
}
|
}
|
||||||
Twig.debug { "=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: END ========" }
|
Twig.debug { "=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: END ========" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchValidationErrorInfo(errorHeight: BlockHeight): ValidationErrorInfo {
|
|
||||||
val hash = repository.findBlockHash(errorHeight + 1)?.toHexReversed()
|
|
||||||
|
|
||||||
return ValidationErrorInfo(errorHeight, hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called for every noteworthy error.
|
* Called for every noteworthy error.
|
||||||
*
|
*
|
||||||
|
@ -2045,8 +2077,7 @@ class CompactBlockProcessor internal constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ValidationErrorInfo(
|
data class ValidationErrorInfo(
|
||||||
val errorHeight: BlockHeight,
|
val errorHeight: BlockHeight
|
||||||
val hash: String?
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ internal sealed class SyncingResult {
|
||||||
override fun toString() = "${this::class.java.simpleName} with ${downloadedBlocks?.size ?: "none"} blocks"
|
override fun toString() = "${this::class.java.simpleName} with ${downloadedBlocks?.size ?: "none"} blocks"
|
||||||
}
|
}
|
||||||
interface Failure {
|
interface Failure {
|
||||||
val failedAtHeight: BlockHeight
|
val failedAtHeight: BlockHeight?
|
||||||
val exception: CompactBlockProcessorException
|
val exception: CompactBlockProcessorException
|
||||||
fun toBlockProcessingResult(): CompactBlockProcessor.BlockProcessingResult =
|
fun toBlockProcessingResult(): CompactBlockProcessor.BlockProcessingResult =
|
||||||
CompactBlockProcessor.BlockProcessingResult.SyncFailure(
|
CompactBlockProcessor.BlockProcessingResult.SyncFailure(
|
||||||
|
@ -36,7 +36,7 @@ internal sealed class SyncingResult {
|
||||||
) : Failure, SyncingResult()
|
) : Failure, SyncingResult()
|
||||||
object DeleteSuccess : SyncingResult()
|
object DeleteSuccess : SyncingResult()
|
||||||
data class DeleteFailed(
|
data class DeleteFailed(
|
||||||
override val failedAtHeight: BlockHeight,
|
override val failedAtHeight: BlockHeight?,
|
||||||
override val exception: CompactBlockProcessorException
|
override val exception: CompactBlockProcessorException
|
||||||
) : Failure, SyncingResult()
|
) : Failure, SyncingResult()
|
||||||
object EnhanceSuccess : SyncingResult()
|
object EnhanceSuccess : SyncingResult()
|
||||||
|
|
|
@ -80,10 +80,11 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
|
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
|
||||||
object Uninitialized : CompactBlockProcessorException(
|
class Uninitialized(cause: Throwable? = null) : 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."
|
||||||
|
@ -294,7 +295,7 @@ sealed class TransactionEncoderException(
|
||||||
" with id $transactionId, does not have any raw data. This is a scenario where the wallet should have " +
|
" with id $transactionId, does not have any raw data. This is a scenario where the wallet should have " +
|
||||||
"thrown an exception but failed to do so."
|
"thrown an exception but failed to do so."
|
||||||
)
|
)
|
||||||
class IncompleteScanException(lastScannedHeight: BlockHeight) : TransactionEncoderException(
|
class IncompleteScanException(lastScannedHeight: BlockHeight?) : TransactionEncoderException(
|
||||||
"Cannot" +
|
"Cannot" +
|
||||||
" create spending transaction because scanning is incomplete. We must scan up to the" +
|
" create spending transaction because scanning is incomplete. We must scan up to the" +
|
||||||
" latest height to know which consensus rules to apply. However, the last scanned" +
|
" latest height to know which consensus rules to apply. However, the last scanned" +
|
||||||
|
|
|
@ -1,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
|
|
||||||
}
|
|
|
@ -18,22 +18,10 @@ 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 {
|
|
||||||
return derivedDataDb.blockTable.firstScannedHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun isInitialized(): Boolean {
|
|
||||||
return derivedDataDb.blockTable.count() > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun findEncodedTransactionByTxId(txId: FirstClassByteArray): EncodedTransaction? {
|
override suspend fun findEncodedTransactionByTxId(txId: FirstClassByteArray): EncodedTransaction? {
|
||||||
return derivedDataDb.transactionTable.findEncodedTransactionByTxId(txId)
|
return derivedDataDb.transactionTable.findEncodedTransactionByTxId(txId)
|
||||||
}
|
}
|
||||||
|
@ -49,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() {
|
||||||
|
|
|
@ -2,6 +2,7 @@ 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
|
||||||
|
@ -19,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)
|
||||||
|
@ -49,7 +48,14 @@ internal class DerivedDataDb private constructor(
|
||||||
numberOfAccounts: Int,
|
numberOfAccounts: Int,
|
||||||
recoverUntil: BlockHeight?
|
recoverUntil: BlockHeight?
|
||||||
): DerivedDataDb {
|
): DerivedDataDb {
|
||||||
backend.initDataDb(seed)
|
runCatching {
|
||||||
|
val result = backend.initDataDb(seed)
|
||||||
|
if (result < 0) {
|
||||||
|
throw CompactBlockProcessorException.Uninitialized()
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
throw CompactBlockProcessorException.Uninitialized(it)
|
||||||
|
}
|
||||||
|
|
||||||
val database = ReadOnlySupportSqliteOpenHelper.openExistingDatabaseAsReadOnly(
|
val database = ReadOnlySupportSqliteOpenHelper.openExistingDatabaseAsReadOnly(
|
||||||
NoBackupContextWrapper(
|
NoBackupContextWrapper(
|
||||||
|
|
|
@ -4,7 +4,7 @@ internal data class ScanProgress(
|
||||||
val numerator: Long,
|
val numerator: Long,
|
||||||
val denominator: Long
|
val denominator: Long
|
||||||
) {
|
) {
|
||||||
override fun toString() = "ScanProgress($numerator / $denominator)"
|
override fun toString() = "ScanProgress($numerator/$denominator) -> ${numerator / (denominator.toFloat())}"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun new(jni: JniScanProgress): ScanProgress {
|
fun new(jni: JniScanProgress): ScanProgress {
|
||||||
|
|
|
@ -13,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.
|
||||||
*
|
*
|
||||||
|
@ -28,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.
|
||||||
*
|
*
|
||||||
|
@ -76,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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -98,8 +98,8 @@ internal class TransactionEncoderImpl(
|
||||||
backend.isValidUnifiedAddr(address)
|
backend.isValidUnifiedAddr(address)
|
||||||
|
|
||||||
override suspend fun getConsensusBranchId(): Long {
|
override suspend fun getConsensusBranchId(): Long {
|
||||||
val height = repository.lastScannedHeight()
|
val height = backend.getMaxScannedHeight()
|
||||||
if (height < backend.network.saplingActivationHeight) {
|
if (height == null || height < backend.network.saplingActivationHeight) {
|
||||||
throw TransactionEncoderException.IncompleteScanException(height)
|
throw TransactionEncoderException.IncompleteScanException(height)
|
||||||
}
|
}
|
||||||
return backend.getBranchIdForHeight(height)
|
return backend.getBranchIdForHeight(height)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue