zcash-android-wallet-sdk/backend-lib/src/main/java/cash/z/ecc/android/sdk/internal/jni/RustBackend.kt

638 lines
18 KiB
Kotlin

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<String> {
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<JniBlockMeta>) =
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<JniSubtreeRoot>,
) = withContext(SdkDispatchers.DATABASE_IO) {
putSaplingSubtreeRoots(
dataDbFile.absolutePath,
startIndex,
roots.toTypedArray(),
networkId = networkId
)
}
override suspend fun updateChainTip(height: Long) =
withContext(SdkDispatchers.DATABASE_IO) {
updateChainTip(
dataDbFile.absolutePath,
height,
networkId = networkId
)
}
override suspend fun getFullyScannedHeight() =
withContext(SdkDispatchers.DATABASE_IO) {
val height =
getFullyScannedHeight(
dataDbFile.absolutePath,
networkId = networkId
)
if (-1L == height) {
null
} else {
height
}
}
override suspend fun getMaxScannedHeight() =
withContext(SdkDispatchers.DATABASE_IO) {
val height =
getMaxScannedHeight(
dataDbFile.absolutePath,
networkId = networkId
)
if (-1L == height) {
null
} else {
height
}
}
override suspend fun getWalletSummary(): JniWalletSummary? =
withContext(SdkDispatchers.DATABASE_IO) {
getWalletSummary(
dataDbFile.absolutePath,
networkId = networkId
)
}
override suspend fun suggestScanRanges(): List<JniScanRange> {
return withContext(SdkDispatchers.DATABASE_IO) {
suggestScanRanges(
dataDbFile.absolutePath,
networkId = networkId
).asList()
}
}
override suspend fun scanBlocks(
fromHeight: Long,
limit: Long
): 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<String>
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<JniBlockMeta>
)
@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<JniSubtreeRoot>,
networkId: Int
)
@JvmStatic
private external fun updateChainTip(
dbDataPath: String,
height: Long,
networkId: Int
)
@JvmStatic
private external fun getFullyScannedHeight(
dbDataPath: String,
networkId: Int
): Long
@JvmStatic
private external fun getMaxScannedHeight(
dbDataPath: String,
networkId: Int
): Long
@JvmStatic
private external fun getWalletSummary(
dbDataPath: String,
networkId: Int
): JniWalletSummary?
@JvmStatic
private external fun suggestScanRanges(
dbDataPath: String,
networkId: Int
): Array<JniScanRange>
@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
}
}