package cash.z.ecc.android.sdk.internal.jni import cash.z.ecc.android.sdk.internal.Backend 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.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 import cash.z.ecc.android.sdk.internal.model.ProposalUnsafe import kotlinx.coroutines.withContext import java.io.File /** * Serves as the JNI boundary between the Kotlin and Rust layers. Functions in this class should * not be called directly by code outside of the SDK. Instead, one of the higher-level components * should be used such as WalletCoordinator.kt or CompactBlockProcessor.kt. */ @Suppress("TooManyFunctions") class RustBackend private constructor( override val networkId: Int, private val dataDbFile: File, private val fsBlockDbRoot: File, private val saplingSpendFile: File, private val saplingOutputFile: File, ) : Backend { /** * This function deletes the data database file and the cache directory (compact blocks files) if set by input * parameters. * * @param clearCache to request the cache directory and its content deletion * @param clearDataDb to request the data database file deletion * * @return false in case of any required and failed deletion, true otherwise. */ suspend fun clear( clearCache: Boolean = true, clearDataDb: Boolean = true ): Boolean { var cacheClearResult = true var dataClearResult = true if (clearCache) { fsBlockDbRoot.deleteRecursivelySuspend().also { result -> cacheClearResult = result } } if (clearDataDb) { dataDbFile.deleteSuspend().also { result -> dataClearResult = result } } return cacheClearResult && dataClearResult } // // Wrapper Functions // override suspend fun initBlockMetaDb() = withContext(SdkDispatchers.DATABASE_IO) { initBlockMetaDb( fsBlockDbRoot.absolutePath, ) } override suspend fun initDataDb(seed: ByteArray?) = withContext(SdkDispatchers.DATABASE_IO) { initDataDb( dataDbFile.absolutePath, seed, networkId = networkId ) } override suspend fun createAccount( seed: ByteArray, treeState: ByteArray, recoverUntil: Long? ): JniUnifiedSpendingKey { return withContext(SdkDispatchers.DATABASE_IO) { createAccount( dataDbFile.absolutePath, seed, treeState, recoverUntil ?: -1, networkId = networkId ) } } override suspend fun getCurrentAddress(account: Int) = withContext(SdkDispatchers.DATABASE_IO) { getCurrentAddress( dataDbFile.absolutePath, account, networkId = networkId ) } override fun getTransparentReceiver(ua: String) = getTransparentReceiverForUnifiedAddress(ua) override fun getSaplingReceiver(ua: String) = getSaplingReceiverForUnifiedAddress(ua) override suspend fun listTransparentReceivers(account: Int): List { return withContext(SdkDispatchers.DATABASE_IO) { listTransparentReceivers( dbDataPath = dataDbFile.absolutePath, account = account, networkId = networkId ).asList() } } override suspend fun getMemoAsUtf8( txId: ByteArray, outputIndex: Int ) = withContext(SdkDispatchers.DATABASE_IO) { getMemoAsUtf8( dataDbFile.absolutePath, txId, outputIndex, networkId = networkId ) } override suspend fun writeBlockMetadata(blockMetadata: List) = withContext(SdkDispatchers.DATABASE_IO) { writeBlockMetadata( fsBlockDbRoot.absolutePath, blockMetadata.toTypedArray() ) } override suspend fun getLatestCacheHeight() = withContext(SdkDispatchers.DATABASE_IO) { val height = getLatestCacheHeight(fsBlockDbRoot.absolutePath) if (-1L == height) { null } else { height } } override suspend fun findBlockMetadata(height: Long) = withContext(SdkDispatchers.DATABASE_IO) { findBlockMetadata( fsBlockDbRoot.absolutePath, height ) } override suspend fun rewindBlockMetadataToHeight(height: Long) = withContext(SdkDispatchers.DATABASE_IO) { rewindBlockMetadataToHeight( fsBlockDbRoot.absolutePath, height ) } override suspend fun getTotalTransparentBalance(address: String): Long = withContext(SdkDispatchers.DATABASE_IO) { getTotalTransparentBalance( dataDbFile.absolutePath, address, networkId = networkId ) } override suspend fun getNearestRewindHeight(height: Long): Long = withContext(SdkDispatchers.DATABASE_IO) { getNearestRewindHeight( dataDbFile.absolutePath, height, networkId = networkId ) } /** * Deletes data for all blocks above the given height. Boils down to: * * DELETE FROM blocks WHERE height > ? */ override suspend fun rewindToHeight(height: Long) = withContext(SdkDispatchers.DATABASE_IO) { rewindToHeight( dataDbFile.absolutePath, height, networkId = networkId ) } override suspend fun putSaplingSubtreeRoots( startIndex: Long, roots: List, ) = 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 getWalletSummary(): JniWalletSummary? = withContext(SdkDispatchers.DATABASE_IO) { getWalletSummary( dataDbFile.absolutePath, networkId = networkId ) } override suspend fun suggestScanRanges(): List { return withContext(SdkDispatchers.DATABASE_IO) { suggestScanRanges( dataDbFile.absolutePath, networkId = networkId ).asList() } } override suspend fun scanBlocks( fromHeight: Long, limit: Long ): JniScanSummary { return withContext(SdkDispatchers.DATABASE_IO) { scanBlocks( fsBlockDbRoot.absolutePath, dataDbFile.absolutePath, fromHeight, limit, networkId = networkId ) } } override suspend fun decryptAndStoreTransaction(tx: ByteArray) = withContext(SdkDispatchers.DATABASE_IO) { decryptAndStoreTransaction( dataDbFile.absolutePath, tx, networkId = networkId ) } override suspend fun proposeTransfer( account: Int, to: String, value: Long, memo: ByteArray? ): ProposalUnsafe = withContext(SdkDispatchers.DATABASE_IO) { ProposalUnsafe.parse( proposeTransfer( dataDbFile.absolutePath, account, to, value, memo ?: ByteArray(0), networkId = networkId, useZip317Fees = IS_USE_ZIP_317_FEES ) ) } override suspend fun proposeShielding( account: Int, shieldingThreshold: Long, memo: ByteArray?, transparentReceiver: String? ): ProposalUnsafe? { return withContext(SdkDispatchers.DATABASE_IO) { proposeShielding( dataDbFile.absolutePath, account, shieldingThreshold, memo ?: ByteArray(0), transparentReceiver, networkId = networkId, useZip317Fees = IS_USE_ZIP_317_FEES )?.let { ProposalUnsafe.parse( it ) } } } override suspend fun createProposedTransaction( proposal: ProposalUnsafe, unifiedSpendingKey: ByteArray ): ByteArray = withContext(SdkDispatchers.DATABASE_IO) { createProposedTransaction( dataDbFile.absolutePath, proposal.toByteArray(), unifiedSpendingKey, spendParamsPath = saplingSpendFile.absolutePath, outputParamsPath = saplingOutputFile.absolutePath, networkId = networkId ) } override suspend fun putUtxo( tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: Long ) = withContext(SdkDispatchers.DATABASE_IO) { putUtxo( dataDbFile.absolutePath, tAddress, txId, index, script, value, height, networkId = networkId ) } override fun isValidSaplingAddr(addr: String) = isValidSaplingAddress(addr, networkId = networkId) override fun isValidTransparentAddr(addr: String) = isValidTransparentAddress(addr, networkId = networkId) override fun isValidUnifiedAddr(addr: String) = isValidUnifiedAddress(addr, networkId = networkId) override fun getBranchIdForHeight(height: Long): Long = branchIdForHeight(height, networkId = networkId) /** * Exposes all of the librustzcash functions along with helpers for loading the static library. */ companion object { internal val rustLibraryLoader = NativeLibraryLoader("zcashwalletsdk") private const val IS_USE_ZIP_317_FEES = false suspend fun loadLibrary() { rustLibraryLoader.load { initOnLoad() } } /** * Loads the library and initializes path variables. Although it is best to only call this * function once, it is idempotent. */ suspend fun new( fsBlockDbRoot: File, dataDbFile: File, saplingSpendFile: File, saplingOutputFile: File, zcashNetworkId: Int, ): RustBackend { loadLibrary() return RustBackend( zcashNetworkId, dataDbFile = dataDbFile, fsBlockDbRoot = fsBlockDbRoot, saplingSpendFile = saplingSpendFile, saplingOutputFile = saplingOutputFile ) } // // External Functions // @JvmStatic private external fun initOnLoad() @JvmStatic private external fun initBlockMetaDb(fsBlockDbRoot: String): Int @JvmStatic private external fun initDataDb( dbDataPath: String, seed: ByteArray?, networkId: Int ): Int @JvmStatic private external fun createAccount( dbDataPath: String, seed: ByteArray, treeState: ByteArray, recoverUntil: Long, networkId: Int ): JniUnifiedSpendingKey @JvmStatic private external fun getCurrentAddress( dbDataPath: String, account: Int, networkId: Int ): String @JvmStatic private external fun getTransparentReceiverForUnifiedAddress(ua: String): String? @JvmStatic private external fun getSaplingReceiverForUnifiedAddress(ua: String): String? @JvmStatic private external fun listTransparentReceivers( dbDataPath: String, account: Int, networkId: Int ): Array fun validateUnifiedSpendingKey(bytes: ByteArray) = isValidSpendingKey(bytes) @JvmStatic private external fun isValidSpendingKey(bytes: ByteArray): Boolean @JvmStatic private external fun isValidSaplingAddress( addr: String, networkId: Int ): Boolean @JvmStatic private external fun isValidTransparentAddress( addr: String, networkId: Int ): Boolean @JvmStatic private external fun isValidUnifiedAddress( addr: String, networkId: Int ): Boolean @JvmStatic private external fun getMemoAsUtf8( dbDataPath: String, txId: ByteArray, outputIndex: Int, networkId: Int ): String? @JvmStatic private external fun writeBlockMetadata( dbCachePath: String, blockMeta: Array ) @JvmStatic private external fun getLatestCacheHeight(dbCachePath: String): Long @JvmStatic private external fun findBlockMetadata( dbCachePath: String, height: Long ): JniBlockMeta? @JvmStatic private external fun rewindBlockMetadataToHeight( dbCachePath: String, height: Long ) @JvmStatic private external fun getNearestRewindHeight( dbDataPath: String, height: Long, networkId: Int ): Long @JvmStatic private external fun rewindToHeight( dbDataPath: String, height: Long, networkId: Int ) @JvmStatic private external fun putSaplingSubtreeRoots( dbDataPath: String, startIndex: Long, roots: Array, 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 getWalletSummary( dbDataPath: String, networkId: Int ): JniWalletSummary? @JvmStatic private external fun suggestScanRanges( dbDataPath: String, networkId: Int ): Array @JvmStatic private external fun scanBlocks( dbCachePath: String, dbDataPath: String, fromHeight: Long, limit: Long, networkId: Int ): JniScanSummary @JvmStatic private external fun decryptAndStoreTransaction( dbDataPath: String, tx: ByteArray, networkId: Int ) @JvmStatic @Suppress("LongParameterList") private external fun proposeTransfer( dbDataPath: String, account: Int, to: String, value: Long, memo: ByteArray, networkId: Int, useZip317Fees: Boolean ): ByteArray @JvmStatic @Suppress("LongParameterList") private external fun proposeShielding( dbDataPath: String, account: Int, shieldingThreshold: Long, memo: ByteArray, transparentReceiver: String?, networkId: Int, useZip317Fees: Boolean ): ByteArray? @JvmStatic @Suppress("LongParameterList") private external fun createProposedTransaction( dbDataPath: String, proposal: ByteArray, usk: ByteArray, spendParamsPath: String, outputParamsPath: String, networkId: Int ): ByteArray @JvmStatic private external fun branchIdForHeight( height: Long, networkId: Int ): Long @JvmStatic @Suppress("LongParameterList") private external fun putUtxo( dbDataPath: String, tAddress: String, txId: ByteArray, index: Int, script: ByteArray, value: Long, height: Long, networkId: Int ) @JvmStatic private external fun getTotalTransparentBalance( pathDataDb: String, taddr: String, networkId: Int ): Long } }