[#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:
Honza Rychnovský 2023-09-07 18:00:34 +02:00 committed by Honza
parent 14d854f96e
commit fc14082a1c
14 changed files with 136 additions and 200 deletions

View File

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

View File

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

View File

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

View File

@ -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)) {
downloader.rewindToHeight(lastScannedHeight) is GetMaxScannedHeightResult.Success -> result.height
else -> null
}
lastScannedHeight?.let {
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)) {
downloader.rewindToHeight(lastScannedHeight) is GetMaxScannedHeightResult.Success -> result.height
else -> null
}
lastScannedHeight?.let {
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,14 +1813,14 @@ 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)
determineLowerBound(errorHeight).let { lowerBound -> errorHeight?.let {
Twig.debug { "Handling chain error at $errorHeight by rewinding to block $lowerBound" } determineLowerBound(errorHeight).let { lowerBound ->
onChainErrorListener?.invoke(errorHeight, lowerBound) Twig.debug { "Handling chain error at $errorHeight by rewinding to block $lowerBound" }
rewindToNearestHeight(lowerBound) onChainErrorListener?.invoke(errorHeight, lowerBound)
rewindToNearestHeight(lowerBound)
}
} }
} }
@ -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?
) )
// //

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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