diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt index 292f9f62..a121d6fc 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/Backend.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.internal.model.JniBlockMeta import cash.z.ecc.android.sdk.internal.model.JniScanRange +import cash.z.ecc.android.sdk.internal.model.JniScanSummary 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.JniWalletSummary @@ -152,7 +153,7 @@ interface Backend { suspend fun scanBlocks( fromHeight: Long, limit: Long - ) + ): JniScanSummary /** * @throws RuntimeException as a common indicator of the operation failure diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt index c84b8e41..02028ce0 100644 --- a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt @@ -6,6 +6,7 @@ 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.model.JniBlockMeta import cash.z.ecc.android.sdk.internal.model.JniScanRange +import cash.z.ecc.android.sdk.internal.model.JniScanSummary 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.JniWalletSummary @@ -272,7 +273,7 @@ class RustBackend private constructor( override suspend fun scanBlocks( fromHeight: Long, limit: Long - ) { + ): JniScanSummary { return withContext(SdkDispatchers.DATABASE_IO) { scanBlocks( fsBlockDbRoot.absolutePath, @@ -566,7 +567,7 @@ class RustBackend private constructor( fromHeight: Long, limit: Long, networkId: Int - ) + ): JniScanSummary @JvmStatic private external fun decryptAndStoreTransaction( diff --git a/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniScanSummary.kt b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniScanSummary.kt new file mode 100644 index 00000000..655bbe9c --- /dev/null +++ b/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/JniScanSummary.kt @@ -0,0 +1,40 @@ +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 scanned range (inclusive). + * Although it's type Long, it needs to be a UInt. + * @param endHeight the maximum height in the scanned range (exclusive). + * Although it's type Long, it needs to be a UInt. + * @param spentSaplingNoteCount the number of Sapling notes detected as spent in + * the scanned range. + * @param receivedSaplingNoteCount the number of Sapling notes detected as + * received in the scanned range. + * @throws IllegalArgumentException unless (startHeight and endHeight are UInts, + * and startHeight is not less than endHeight). + */ +@Keep +class JniScanSummary( + val startHeight: Long, + val endHeight: Long, + val spentSaplingNoteCount: Long, + val receivedSaplingNoteCount: Long +) { + init { + // We require some of the parameters below to be in the range of + // unsigned integer, because they are block heights. + 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." + } + } +} diff --git a/backend-lib/src/main/rust/lib.rs b/backend-lib/src/main/rust/lib.rs index 6549bd03..a725e2db 100644 --- a/backend-lib/src/main/rust/lib.rs +++ b/backend-lib/src/main/rust/lib.rs @@ -21,7 +21,7 @@ use zcash_address::{ToAddress, ZcashAddress}; use zcash_client_backend::{ address::{Address, UnifiedAddress}, data_api::{ - chain::{scan_cached_blocks, CommitmentTreeRoot}, + chain::{scan_cached_blocks, CommitmentTreeRoot, ScanSummary}, scanning::{ScanPriority, ScanRange}, wallet::{ create_proposed_transaction, decrypt_and_store_transaction, @@ -1245,6 +1245,23 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_suggestSc unwrap_exc_or(&mut env, res, ptr::null_mut()) } +fn encode_scan_summary<'a>( + env: &mut JNIEnv<'a>, + scan_summary: ScanSummary, +) -> Result, failure::Error> { + let scanned_range = scan_summary.scanned_range(); + Ok(env.new_object( + "cash/z/ecc/android/sdk/internal/model/JniScanSummary", + "(JJJJ)V", + &[ + i64::from(u32::from(scanned_range.start)).into(), + i64::from(u32::from(scanned_range.end)).into(), + i64::try_from(scan_summary.spent_sapling_note_count())?.into(), + i64::try_from(scan_summary.received_sapling_note_count())?.into(), + ], + )?) +} + #[no_mangle] pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlocks<'local>( mut env: JNIEnv<'local>, @@ -1254,7 +1271,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlock from_height: jlong, limit: jlong, network_id: jint, -) -> jboolean { +) -> jobject { let res = catch_unwind(&mut env, |env| { let network = parse_network(network_id as u32)?; let db_cache = block_db(env, db_cache)?; @@ -1263,8 +1280,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlock let limit = usize::try_from(limit)?; match scan_cached_blocks(&network, &db_cache, &mut db_data, from_height, limit) { - // TODO: Return ScanSummary. - Ok(_) => Ok(JNI_TRUE), + Ok(scan_summary) => Ok(encode_scan_summary(env, scan_summary)?.into_raw()), Err(e) => Err(format_err!( "Rust error while scanning blocks (limit {:?}): {}", limit, @@ -1272,7 +1288,7 @@ pub extern "C" fn Java_cash_z_ecc_android_sdk_internal_jni_RustBackend_scanBlock )), } }); - unwrap_exc_or(&mut env, res, JNI_FALSE) + unwrap_exc_or(&mut env, res, ptr::null_mut()) } #[no_mangle] diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt index e9b507db..ad116872 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/CompactBlockProcessor.kt @@ -1346,7 +1346,7 @@ class CompactBlockProcessor internal constructor( * @return Flow of [BatchSyncProgress] sync and enhancement results */ @VisibleForTesting - @Suppress("LongParameterList", "LongMethod") + @Suppress("CyclomaticComplexMethod", "LongParameterList", "LongMethod") internal suspend fun runSyncingAndEnhancingOnRange( backend: TypesafeBackend, downloader: CompactBlockDownloader, @@ -1416,17 +1416,25 @@ class CompactBlockProcessor internal constructor( }.map { scanResult -> Twig.debug { "Scan stage done with result: $scanResult" } - if (scanResult.stageResult != SyncingResult.ScanSuccess) { - scanResult - } else { - // Run deletion stage - SyncStageResult( - scanResult.batch, - deleteFilesOfBatchOfBlocks( - downloader = downloader, - batch = scanResult.batch + when (scanResult.stageResult) { + is SyncingResult.ScanSuccess -> { + // TODO [#1369]: Use the scan summary to trigger balance updates. + // TODO [#1369]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/1369 + val scannedRange = scanResult.stageResult.summary.scannedRange + assert(scanResult.batch.range.start <= scannedRange.start) + assert(scannedRange.endInclusive <= scanResult.batch.range.endInclusive) + + // Run deletion stage + SyncStageResult( + scanResult.batch, + deleteFilesOfBatchOfBlocks( + downloader = downloader, + batch = scanResult.batch + ) ) - ) + } else -> { + scanResult + } } }.onEach { continuousResult -> Twig.debug { "Deletion stage done with result: $continuousResult" } @@ -1614,7 +1622,7 @@ class CompactBlockProcessor internal constructor( }.onFailure { Twig.error { "Failed while scanning batch $batch with $it" } }.fold( - onSuccess = { SyncingResult.ScanSuccess }, + onSuccess = { SyncingResult.ScanSuccess(it) }, onFailure = { // Check if the error is continuity type if (it.isScanContinuityError()) { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/model/SyncingResult.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/model/SyncingResult.kt index 44b87654..bc5d582e 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/model/SyncingResult.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/processor/model/SyncingResult.kt @@ -3,6 +3,7 @@ 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.internal.model.ScanSummary import cash.z.ecc.android.sdk.model.BlockHeight /** @@ -35,7 +36,9 @@ internal sealed class SyncingResult { override val exception: CompactBlockProcessorException ) : Failure, SyncingResult() - object ScanSuccess : SyncingResult() + data class ScanSuccess( + val summary: ScanSummary + ) : SyncingResult() data class ScanFailed( override val failedAtHeight: BlockHeight, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt index f1426935..ed5a6365 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackend.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal import cash.z.ecc.android.sdk.internal.model.JniBlockMeta import cash.z.ecc.android.sdk.internal.model.ScanRange +import cash.z.ecc.android.sdk.internal.model.ScanSummary import cash.z.ecc.android.sdk.internal.model.SubtreeRoot import cash.z.ecc.android.sdk.internal.model.TreeState import cash.z.ecc.android.sdk.internal.model.WalletSummary @@ -121,7 +122,7 @@ internal interface TypesafeBackend { suspend fun scanBlocks( fromHeight: BlockHeight, limit: Long - ) + ): ScanSummary /** * @throws RuntimeException as a common indicator of the operation failure diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt index f89b874b..28e6d344 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/TypesafeBackendImpl.kt @@ -3,6 +3,7 @@ package cash.z.ecc.android.sdk.internal 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.ScanRange +import cash.z.ecc.android.sdk.internal.model.ScanSummary import cash.z.ecc.android.sdk.internal.model.SubtreeRoot import cash.z.ecc.android.sdk.internal.model.TreeState import cash.z.ecc.android.sdk.internal.model.WalletSummary @@ -193,7 +194,7 @@ internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBacke override suspend fun scanBlocks( fromHeight: BlockHeight, limit: Long - ) = backend.scanBlocks(fromHeight.value, limit) + ): ScanSummary = ScanSummary.new(backend.scanBlocks(fromHeight.value, limit), network) override suspend fun getWalletSummary(): WalletSummary? = backend.getWalletSummary()?.let { jniWalletSummary -> diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ScanSummary.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ScanSummary.kt new file mode 100644 index 00000000..e593961e --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/ScanSummary.kt @@ -0,0 +1,37 @@ +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 ScanSummary( + val scannedRange: ClosedRange, + val spentSaplingNoteCount: Long, + val receivedSaplingNoteCount: Long +) { + companion object { + /** + * Note that this function subtracts 1 from [JniScanSummary.endHeight] + * as the rest of the logic works with [ClosedRange] and the endHeight + * is exclusive. + */ + fun new( + jni: JniScanSummary, + zcashNetwork: ZcashNetwork + ): ScanSummary { + return ScanSummary( + scannedRange = + BlockHeight.new( + zcashNetwork, + jni.startHeight + )..( + BlockHeight.new( + zcashNetwork, + jni.endHeight + ) - 1 + ), + spentSaplingNoteCount = jni.spentSaplingNoteCount, + receivedSaplingNoteCount = jni.receivedSaplingNoteCount + ) + } + } +}