[#1014] Decorate Backend with Typesafe APIs

* [#1014] Decorate Backend with Typesafe APIs
This commit is contained in:
Honza Rychnovský 2023-07-17 12:50:53 +02:00 committed by GitHub
parent 0edaaafad1
commit f1b5e3aade
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 327 additions and 301 deletions

View File

@ -1,5 +1,8 @@
# Change Log
## Unreleased
- The SDK internally migrated from `BackendExt` rust backend extension functions to more type-safe `TypesafeBackend`.
## 1.19.0-beta01
### Changed
- Adopted the latest Bip39 version 1.0.5

View File

@ -12,7 +12,7 @@ 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 Wallet.kt or CompactBlockProcessor.kt.
* should be used such as WalletCoordinator.kt or CompactBlockProcessor.kt.
*/
@Suppress("TooManyFunctions")
class RustBackend private constructor(
@ -37,13 +37,11 @@ class RustBackend private constructor(
var dataClearResult = true
if (clearCache) {
fsBlockDbRoot.deleteRecursivelySuspend().also { result ->
// Twig.debug { "Deletion of the cache files ${if (result) "succeeded" else "failed"}!" }
cacheClearResult = result
}
}
if (clearDataDb) {
dataDbFile.deleteSuspend().also { result ->
// Twig.debug { "Deletion of the data database ${if (result) "succeeded" else "failed"}!" }
dataClearResult = result
}
}
@ -356,28 +354,6 @@ class RustBackend private constructor(
override fun getBranchIdForHeight(height: Long): Long =
branchIdForHeight(height, networkId = networkId)
// /**
// * This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI
// * boundary as a grpc server. It is slightly inefficient in terms of both space and time but
// * given that it is all done locally, on the heap, it seems to be a worthwhile tradeoff because
// * it reduces the complexity and expands the capacity for the two layers to communicate.
// *
// * We're able to keep the "unsafe" byteArray functions private and wrap them in typeSafe
// * equivalents and, eventually, surface any parse errors (for now, errors are only logged).
// */
// override fun parseTransactionDataList(
// tdl: LocalRpcTypes.TransactionDataList
// ): LocalRpcTypes.TransparentTransactionList {
// return try {
// // serialize the list, send it over to rust and get back a serialized set of results that we parse out
// // and return
// return LocalRpcTypes.TransparentTransactionList.parseFrom(parseTransactionDataList(tdl.toByteArray()))
// } catch (t: Throwable) {
// twig("ERROR: failed to parse transaction data list due to: $t caused by: ${t.cause}")
// LocalRpcTypes.TransparentTransactionList.newBuilder().build()
// }
// }
/**
* Exposes all of the librustzcash functions along with helpers for loading the static library.
*/

View File

@ -1,6 +1,7 @@
package cash.z.ecc.android.sdk.internal.storage.block
import cash.z.ecc.android.sdk.internal.Backend
import cash.z.ecc.android.sdk.internal.TypesafeBackend
import cash.z.ecc.android.sdk.internal.TypesafeBackendImpl
import cash.z.ecc.android.sdk.internal.ext.deleteRecursivelySuspend
import cash.z.ecc.android.sdk.internal.ext.existsSuspend
import cash.z.ecc.android.sdk.internal.ext.listSuspend
@ -10,7 +11,6 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.fixture.FakeRustBackendFixture
import cash.z.ecc.fixture.FilePathFixture
import co.electriccoin.lightwallet.client.fixture.ListOfCompactBlocksFixture
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.count
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
@ -29,7 +29,6 @@ import kotlin.test.assertTrue
class FileCompactBlockRepositoryTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setup() = runTest {
val blocksDirectory = FilePathFixture.newBlocksDir()
@ -40,27 +39,28 @@ class FileCompactBlockRepositoryTest {
blocksDirectory.mkdirsSuspend()
}
@OptIn(ExperimentalCoroutinesApi::class)
@After
fun tearDown() = runTest {
FilePathFixture.newBlocksDir().deleteRecursivelySuspend()
}
private fun getMockedFileCompactBlockRepository(
rustBackend: Backend,
backend: TypesafeBackend,
rootBlocksDirectory: File
): FileCompactBlockRepository = runBlocking {
FileCompactBlockRepository(
rootBlocksDirectory,
rustBackend
backend
)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun getLatestHeightTest() = runTest {
val rustBackend = FakeRustBackendFixture().new()
val blockRepository = getMockedFileCompactBlockRepository(rustBackend, FilePathFixture.newBlocksDir())
val blockRepository = getMockedFileCompactBlockRepository(
TypesafeBackendImpl(rustBackend),
FilePathFixture.newBlocksDir()
)
val blocks = ListOfCompactBlocksFixture.newFlow()
@ -69,12 +69,14 @@ class FileCompactBlockRepositoryTest {
assertEquals(blocks.last().height, blockRepository.getLatestHeight()?.value)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun findCompactBlockTest() = runTest {
val network = ZcashNetwork.Testnet
val rustBackend = FakeRustBackendFixture().new()
val blockRepository = getMockedFileCompactBlockRepository(rustBackend, FilePathFixture.newBlocksDir())
val blockRepository = getMockedFileCompactBlockRepository(
TypesafeBackendImpl(rustBackend),
FilePathFixture.newBlocksDir()
)
val blocks = ListOfCompactBlocksFixture.newFlow()
@ -99,11 +101,13 @@ class FileCompactBlockRepositoryTest {
assertNull(blockRepository.findCompactBlock(notPersistedBlockHeight))
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun writeBlocksTest() = runTest {
val rustBackend = FakeRustBackendFixture().new()
val blockRepository = getMockedFileCompactBlockRepository(rustBackend, FilePathFixture.newBlocksDir())
val blockRepository = getMockedFileCompactBlockRepository(
TypesafeBackendImpl(rustBackend),
FilePathFixture.newBlocksDir()
)
assertTrue { rustBackend.metadata.isEmpty() }
@ -114,11 +118,13 @@ class FileCompactBlockRepositoryTest {
assertEquals(blocks.count(), rustBackend.metadata.size)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun writeFewBlocksTest() = runTest {
val rustBackend = FakeRustBackendFixture().new()
val blockRepository = getMockedFileCompactBlockRepository(rustBackend, FilePathFixture.newBlocksDir())
val blockRepository = getMockedFileCompactBlockRepository(
TypesafeBackendImpl(rustBackend),
FilePathFixture.newBlocksDir()
)
assertTrue { rustBackend.metadata.isEmpty() }
@ -134,12 +140,14 @@ class FileCompactBlockRepositoryTest {
assertEquals(reducedBlocksList.count(), rustBackend.metadata.size)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun writeBlocksAndCheckStorageTest() = runTest {
val rustBackend = FakeRustBackendFixture().new()
val rootBlocksDirectory = FilePathFixture.newBlocksDir()
val blockRepository = getMockedFileCompactBlockRepository(rustBackend, rootBlocksDirectory)
val blockRepository = getMockedFileCompactBlockRepository(
TypesafeBackendImpl(rustBackend),
FilePathFixture.newBlocksDir()
)
assertTrue { rootBlocksDirectory.exists() }
assertTrue { rootBlocksDirectory.list()!!.isEmpty() }
@ -153,14 +161,16 @@ class FileCompactBlockRepositoryTest {
assertEquals(blocks.count(), rootBlocksDirectory.list()!!.size)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun deleteCompactBlockFilesTest() = runTest {
val rustBackend = FakeRustBackendFixture().new()
val blocksDirectory = FilePathFixture.newBlocksDir()
val parentDirectory = blocksDirectory.parentFile!!
val blockRepository = getMockedFileCompactBlockRepository(rustBackend, blocksDirectory)
val blockRepository = getMockedFileCompactBlockRepository(
TypesafeBackendImpl(rustBackend),
FilePathFixture.newBlocksDir()
)
val testedBlocksRange = ListOfCompactBlocksFixture.DEFAULT_FILE_BLOCK_RANGE
val blocks = ListOfCompactBlocksFixture.newFlow(testedBlocksRange)
@ -190,11 +200,13 @@ class FileCompactBlockRepositoryTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun rewindToTest() = runTest {
val rustBackend = FakeRustBackendFixture().new()
val blockRepository = getMockedFileCompactBlockRepository(rustBackend, FilePathFixture.newBlocksDir())
val blockRepository = getMockedFileCompactBlockRepository(
TypesafeBackendImpl(rustBackend),
FilePathFixture.newBlocksDir()
)
val testedBlocksRange = ListOfCompactBlocksFixture.DEFAULT_FILE_BLOCK_RANGE
@ -229,7 +241,6 @@ class FileCompactBlockRepositoryTest {
assertEquals(expectedKeptMetadataCount, keptMetadataBelowRewindHeight.size)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun createTemporaryFileTest() = runTest {
val blocksDir = FilePathFixture.newBlocksDir()
@ -241,7 +252,6 @@ class FileCompactBlockRepositoryTest {
assertTrue { file.existsSuspend() }
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun finalizeFileTest() = runTest {
val blocksDir = FilePathFixture.newBlocksDir()

View File

@ -2,10 +2,9 @@ package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.annotation.MaintainedTest
import cash.z.ecc.android.sdk.annotation.TestPurpose
import cash.z.ecc.android.sdk.internal.Backend
import cash.z.ecc.android.sdk.internal.getBranchIdForHeight
import cash.z.ecc.android.sdk.internal.TypesafeBackend
import cash.z.ecc.android.sdk.internal.TypesafeBackendImpl
import cash.z.ecc.android.sdk.internal.jni.RustBackend
import cash.z.ecc.android.sdk.internal.network
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.ZcashNetwork
import kotlinx.coroutines.runBlocking
@ -25,15 +24,15 @@ class BranchIdTest internal constructor(
private val height: BlockHeight,
private val branchId: Long,
private val branchHex: String,
private val rustBackend: Backend
private val backend: TypesafeBackend
) {
@Test
fun testBranchId_Hex() {
val branchId = rustBackend.getBranchIdForHeight(height)
val branchId = backend.getBranchIdForHeight(height)
val clientBranch = "%x".format(branchId)
assertEquals(
"Invalid branch Id Hex value for $networkName at height $height on ${rustBackend.network.networkName}",
"Invalid branch Id Hex value for $networkName at height $height on ${backend.network.networkName}",
branchHex,
clientBranch
)
@ -41,9 +40,9 @@ class BranchIdTest internal constructor(
@Test
fun testBranchId_Numeric() {
val actual = rustBackend.getBranchIdForHeight(height)
val actual = backend.getBranchIdForHeight(height)
assertEquals(
"Invalid branch ID for $networkName at height $height on ${rustBackend.network.networkName}",
"Invalid branch ID for $networkName at height $height on ${backend.network.networkName}",
branchId,
actual
)
@ -60,21 +59,25 @@ class BranchIdTest internal constructor(
// However, due to quirks on certain devices, we created this test at the Android level,
// as a sanity check
val testnetBackend = runBlocking {
RustBackend.new(
File(""),
File(""),
File(""),
File(""),
ZcashNetwork.Testnet.id,
TypesafeBackendImpl(
RustBackend.new(
File(""),
File(""),
File(""),
File(""),
ZcashNetwork.Testnet.id,
)
)
}
val mainnetBackend = runBlocking {
RustBackend.new(
File(""),
File(""),
File(""),
File(""),
ZcashNetwork.Mainnet.id,
TypesafeBackendImpl(
RustBackend.new(
File(""),
File(""),
File(""),
File(""),
ZcashNetwork.Mainnet.id,
)
)
}
return listOf(

View File

@ -15,9 +15,10 @@ import cash.z.ecc.android.sdk.exception.TransactionEncoderException
import cash.z.ecc.android.sdk.exception.TransactionSubmitException
import cash.z.ecc.android.sdk.ext.ConsensusBranchId
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.internal.Backend
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.TypesafeBackend
import cash.z.ecc.android.sdk.internal.TypesafeBackendImpl
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator
import cash.z.ecc.android.sdk.internal.db.derived.DbDerivedDataRepository
@ -94,7 +95,7 @@ class SdkSynchronizer private constructor(
private val storage: DerivedDataRepository,
private val txManager: OutboundTransactionManager,
val processor: CompactBlockProcessor,
private val backend: Backend
private val backend: TypesafeBackend
) : CloseableSynchronizer {
companion object {
@ -120,7 +121,7 @@ class SdkSynchronizer private constructor(
repository: DerivedDataRepository,
txManager: OutboundTransactionManager,
processor: CompactBlockProcessor,
backend: Backend
backend: TypesafeBackend
): CloseableSynchronizer {
val synchronizerKey = SynchronizerKey(zcashNetwork, alias)
@ -510,7 +511,7 @@ class SdkSynchronizer private constructor(
// Not ready to be a public API; internal for testing only
internal suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey =
CompactBlockProcessor.createAccount(backend, seed)
backend.createAccountAndGetSpendingKey(seed)
/**
* Returns the current Unified Address for this account.
@ -634,20 +635,22 @@ internal object DefaultSynchronizerFactory {
alias: String,
saplingParamTool: SaplingParamTool,
coordinator: DatabaseCoordinator
): Backend {
return RustBackend.new(
coordinator.fsBlockDbRoot(network, alias),
coordinator.dataDbFile(network, alias),
saplingOutputFile = saplingParamTool.outputParamsFile,
saplingSpendFile = saplingParamTool.spendParamsFile,
zcashNetworkId = network.id
): TypesafeBackend {
return TypesafeBackendImpl(
RustBackend.new(
coordinator.fsBlockDbRoot(network, alias),
coordinator.dataDbFile(network, alias),
saplingOutputFile = saplingParamTool.outputParamsFile,
saplingSpendFile = saplingParamTool.spendParamsFile,
zcashNetworkId = network.id
)
)
}
@Suppress("LongParameterList")
internal suspend fun defaultDerivedDataRepository(
context: Context,
rustBackend: Backend,
rustBackend: TypesafeBackend,
databaseFile: File,
zcashNetwork: ZcashNetwork,
checkpoint: Checkpoint,
@ -666,7 +669,10 @@ internal object DefaultSynchronizerFactory {
)
)
internal suspend fun defaultCompactBlockRepository(blockCacheRoot: File, backend: Backend): CompactBlockRepository =
internal suspend fun defaultCompactBlockRepository(
blockCacheRoot: File,
backend: TypesafeBackend
): CompactBlockRepository =
FileCompactBlockRepository.new(
blockCacheRoot,
backend
@ -676,7 +682,7 @@ internal object DefaultSynchronizerFactory {
LightWalletClient.new(context, lightWalletEndpoint)
internal fun defaultEncoder(
backend: Backend,
backend: TypesafeBackend,
saplingParamTool: SaplingParamTool,
repository: DerivedDataRepository
): TransactionEncoder = TransactionEncoderImpl(backend, saplingParamTool, repository)
@ -697,7 +703,7 @@ internal object DefaultSynchronizerFactory {
}
internal fun defaultProcessor(
backend: Backend,
backend: TypesafeBackend,
downloader: CompactBlockDownloader,
repository: DerivedDataRepository,
birthdayHeight: BlockHeight

View File

@ -13,35 +13,23 @@ import cash.z.ecc.android.sdk.exception.RustLayerException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.ZcashSdk.MAX_BACKOFF_INTERVAL
import cash.z.ecc.android.sdk.ext.ZcashSdk.POLL_INTERVAL
import cash.z.ecc.android.sdk.internal.Backend
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.TypesafeBackend
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.internal.createAccountAndGetSpendingKey
import cash.z.ecc.android.sdk.internal.ext.isNullOrEmpty
import cash.z.ecc.android.sdk.internal.ext.length
import cash.z.ecc.android.sdk.internal.ext.retryUpTo
import cash.z.ecc.android.sdk.internal.ext.retryWithBackoff
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
import cash.z.ecc.android.sdk.internal.getBalance
import cash.z.ecc.android.sdk.internal.getBranchIdForHeight
import cash.z.ecc.android.sdk.internal.getCurrentAddress
import cash.z.ecc.android.sdk.internal.getDownloadedUtxoBalance
import cash.z.ecc.android.sdk.internal.getNearestRewindHeight
import cash.z.ecc.android.sdk.internal.getVerifiedBalance
import cash.z.ecc.android.sdk.internal.listTransparentReceivers
import cash.z.ecc.android.sdk.internal.model.BlockBatch
import cash.z.ecc.android.sdk.internal.model.DbTransactionOverview
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.model.ext.from
import cash.z.ecc.android.sdk.internal.model.ext.toBlockHeight
import cash.z.ecc.android.sdk.internal.network
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
import cash.z.ecc.android.sdk.internal.rewindToHeight
import cash.z.ecc.android.sdk.internal.validateCombinedChainOrErrorBlockHeight
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.ZcashNetwork
import co.electriccoin.lightwallet.client.ext.BenchmarkingExt
@ -94,7 +82,7 @@ import kotlin.time.toDuration
class CompactBlockProcessor internal constructor(
val downloader: CompactBlockDownloader,
private val repository: DerivedDataRepository,
private val backend: Backend,
private val backend: TypesafeBackend,
minimumHeight: BlockHeight
) {
/**
@ -627,7 +615,7 @@ class CompactBlockProcessor internal constructor(
utxo.index,
utxo.script,
utxo.valueZat,
utxo.height
BlockHeight.new(backend.network, utxo.height)
)
true
} catch (t: Throwable) {
@ -685,7 +673,7 @@ class CompactBlockProcessor internal constructor(
/**
* Requests, processes and persists all blocks from the given range.
*
* @param rustBackend the Rust backend component
* @param backend the Rust backend component
* @param downloader the compact block downloader component
* @param repository the derived data repository component
* @param network the network in which the sync mechanism operates
@ -700,7 +688,7 @@ class CompactBlockProcessor internal constructor(
@VisibleForTesting
@Suppress("LongParameterList", "LongMethod")
internal suspend fun runSyncingAndEnhancing(
backend: Backend,
backend: TypesafeBackend,
downloader: CompactBlockDownloader,
repository: DerivedDataRepository,
network: ZcashNetwork,
@ -819,7 +807,7 @@ class CompactBlockProcessor internal constructor(
enhanceTransactionDetails(
range = currentEnhancingRange,
repository = repository,
rustBackend = backend,
backend = backend,
downloader = downloader
).collect { enhancingResult ->
Twig.debug { "Enhancing result: $enhancingResult" }
@ -917,7 +905,7 @@ class CompactBlockProcessor internal constructor(
}
@VisibleForTesting
internal suspend fun validateBatchOfBlocks(batch: BlockBatch, backend: Backend): BlockProcessingResult {
internal suspend fun validateBatchOfBlocks(batch: BlockBatch, backend: TypesafeBackend): BlockProcessingResult {
Twig.verbose { "Starting to validate batch $batch" }
val result = backend.validateCombinedChainOrErrorBlockHeight(batch.range.length())
@ -931,7 +919,7 @@ class CompactBlockProcessor internal constructor(
}
@VisibleForTesting
internal suspend fun scanBatchOfBlocks(batch: BlockBatch, backend: Backend): BlockProcessingResult {
internal suspend fun scanBatchOfBlocks(batch: BlockBatch, backend: TypesafeBackend): BlockProcessingResult {
return runCatching {
backend.scanBlocks(batch.range.length())
}.onSuccess {
@ -980,7 +968,7 @@ class CompactBlockProcessor internal constructor(
internal suspend fun enhanceTransactionDetails(
range: ClosedRange<BlockHeight>,
repository: DerivedDataRepository,
rustBackend: Backend,
backend: TypesafeBackend,
downloader: CompactBlockDownloader
): Flow<BlockProcessingResult> = flow {
Twig.debug { "Enhancing transaction details for blocks $range" }
@ -998,7 +986,7 @@ class CompactBlockProcessor internal constructor(
}
newTxs.filter { it.minedHeight != null }.onEach { newTransaction ->
val trEnhanceResult = enhanceTransaction(newTransaction, rustBackend, downloader)
val trEnhanceResult = enhanceTransaction(newTransaction, backend, downloader)
if (trEnhanceResult is BlockProcessingResult.FailedEnhance) {
Twig.error { "Encountered transaction enhancing error: ${trEnhanceResult.error}" }
emit(trEnhanceResult)
@ -1013,7 +1001,7 @@ class CompactBlockProcessor internal constructor(
private suspend fun enhanceTransaction(
transaction: DbTransactionOverview,
backend: Backend,
backend: TypesafeBackend,
downloader: CompactBlockDownloader
): BlockProcessingResult {
Twig.debug { "Starting enhancing transaction (id:${transaction.id} block:${transaction.minedHeight})" }
@ -1083,7 +1071,7 @@ class CompactBlockProcessor internal constructor(
private suspend fun decryptTransaction(
transactionData: ByteArray,
minedHeight: BlockHeight,
backend: Backend,
backend: TypesafeBackend,
) {
runCatching {
backend.decryptAndStoreTransaction(transactionData)
@ -1119,27 +1107,22 @@ class CompactBlockProcessor internal constructor(
internal suspend fun getLastDownloadedHeight(downloader: CompactBlockDownloader) =
downloader.getLastDownloadedHeight()
// CompactBlockProcessor is the wrong place for this, but it's where all the other APIs that need
// access to the RustBackend live. This should be refactored.
internal suspend fun createAccount(rustBackend: Backend, seed: ByteArray): UnifiedSpendingKey =
rustBackend.createAccountAndGetSpendingKey(seed)
/**
* Get the current unified address for the given wallet account.
*
* @return the current unified address of this account.
*/
internal suspend fun getCurrentAddress(rustBackend: Backend, account: Account) =
rustBackend.getCurrentAddress(account)
internal suspend fun getCurrentAddress(backend: TypesafeBackend, account: Account) =
backend.getCurrentAddress(account)
/**
* Get the legacy Sapling address corresponding to the current unified address for the given wallet account.
*
* @return a Sapling address.
*/
internal suspend fun getLegacySaplingAddress(rustBackend: Backend, account: Account) =
rustBackend.getSaplingReceiver(
rustBackend.getCurrentAddress(account)
internal suspend fun getLegacySaplingAddress(backend: TypesafeBackend, account: Account) =
backend.getSaplingReceiver(
backend.getCurrentAddress(account)
)
?: throw InitializeException.MissingAddressException("legacy Sapling")
@ -1148,9 +1131,9 @@ class CompactBlockProcessor internal constructor(
*
* @return a transparent address.
*/
internal suspend fun getTransparentAddress(rustBackend: Backend, account: Account) =
rustBackend.getTransparentReceiver(
rustBackend.getCurrentAddress(account)
internal suspend fun getTransparentAddress(backend: TypesafeBackend, account: Account) =
backend.getTransparentReceiver(
backend.getCurrentAddress(account)
)
?: throw InitializeException.MissingAddressException("legacy transparent")
}

View File

@ -1,143 +0,0 @@
@file:Suppress("TooManyFunctions")
package cash.z.ecc.android.sdk.internal
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import kotlinx.coroutines.withContext
internal val Backend.network: ZcashNetwork
get() = ZcashNetwork.from(networkId)
internal suspend fun Backend.initAccountsTable(vararg keys: UnifiedFullViewingKey) {
val ufvks = Array(keys.size) { keys[it].encoding }
@Suppress("SpreadOperator")
return initAccountsTable(*ufvks)
}
internal suspend fun Backend.initAccountsTableTypesafe(
seed: ByteArray,
numberOfAccounts: Int
): List<UnifiedFullViewingKey> {
return DerivationTool.getInstance().deriveUnifiedFullViewingKeys(seed, network, numberOfAccounts)
}
internal suspend fun Backend.initBlocksTable(checkpoint: Checkpoint) = initBlocksTable(
checkpoint.height.value,
checkpoint.hash,
checkpoint.epochSeconds,
checkpoint.tree
)
internal suspend fun Backend.createAccountAndGetSpendingKey(seed: ByteArray): UnifiedSpendingKey = UnifiedSpendingKey(
createAccount(seed)
)
@Suppress("LongParameterList")
internal suspend fun Backend.createToAddress(
usk: UnifiedSpendingKey,
to: String,
value: Long,
memo: ByteArray? = byteArrayOf()
): Long = createToAddress(
usk.account.value,
usk.copyBytes(),
to,
value,
memo
)
internal suspend fun Backend.shieldToAddress(
usk: UnifiedSpendingKey,
memo: ByteArray? = byteArrayOf()
): Long = shieldToAddress(
usk.account.value,
usk.copyBytes(),
memo
)
internal suspend fun Backend.getCurrentAddress(account: Account): String = getCurrentAddress(account.value)
internal suspend fun Backend.listTransparentReceivers(account: Account): List<String> =
listTransparentReceivers(account.value)
internal suspend fun Backend.getBalance(account: Account): Zatoshi = Zatoshi(getBalance(account.value))
internal fun Backend.getBranchIdForHeight(height: BlockHeight): Long = getBranchIdForHeight(height.value)
internal suspend fun Backend.getVerifiedBalance(account: Account): Zatoshi = Zatoshi(
getVerifiedBalance
(account.value)
)
internal suspend fun Backend.getNearestRewindHeight(height: BlockHeight): BlockHeight = BlockHeight.new(
ZcashNetwork.from(networkId),
getNearestRewindHeight(height.value)
)
internal suspend fun Backend.rewindToHeight(height: BlockHeight) = rewindToHeight(height.value)
internal suspend fun Backend.getLatestBlockHeight(): BlockHeight? = getLatestHeight()?.let {
BlockHeight.new(
ZcashNetwork.from(networkId),
it
)
}
internal suspend fun Backend.findBlockMetadata(height: BlockHeight): JniBlockMeta? =
findBlockMetadata(height.value)
internal suspend fun Backend.rewindBlockMetadataToHeight(height: BlockHeight) =
rewindBlockMetadataToHeight(height.value)
/**
* @param limit The limit provides an efficient way how to restrict the portion of blocks, which will be validated.
* @return Null if successful. If an error occurs, the height will be the height where the error was detected.
*/
internal suspend fun Backend.validateCombinedChainOrErrorBlockHeight(limit: Long?): BlockHeight? =
validateCombinedChainOrErrorHeight(limit)?.let {
BlockHeight.new(
ZcashNetwork.from(networkId),
it
)
}
internal suspend fun Backend.getDownloadedUtxoBalance(address: String): WalletBalance {
// Note this implementation is not ideal because it requires two database queries without a transaction, which makes
// the data potentially inconsistent. However the verified amount is queried first which makes this less bad.
val verified = withContext(SdkDispatchers.DATABASE_IO) {
getVerifiedTransparentBalance(address)
}
val total = withContext(SdkDispatchers.DATABASE_IO) {
getTotalTransparentBalance(
address
)
}
return WalletBalance(Zatoshi(total), Zatoshi(verified))
}
@Suppress("LongParameterList")
internal suspend fun Backend.putUtxo(
tAddress: String,
txId: ByteArray,
index: Int,
script: ByteArray,
value: Long,
height: BlockHeight
) = putUtxo(
tAddress,
txId,
index,
script,
value,
height.value
)

View File

@ -5,12 +5,18 @@ import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import java.lang.RuntimeException
import kotlin.jvm.Throws
@Suppress("TooManyFunctions")
internal interface TypesafeBackend {
val network: ZcashNetwork
suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey)
suspend fun initAccountsTable(
@ -20,6 +26,21 @@ internal interface TypesafeBackend {
suspend fun initBlocksTable(checkpoint: Checkpoint)
suspend fun createAccountAndGetSpendingKey(seed: ByteArray): UnifiedSpendingKey
@Suppress("LongParameterList")
suspend fun createToAddress(
usk: UnifiedSpendingKey,
to: String,
value: Long,
memo: ByteArray? = byteArrayOf()
): Long
suspend fun shieldToAddress(
usk: UnifiedSpendingKey,
memo: ByteArray? = byteArrayOf()
): Long
suspend fun getCurrentAddress(account: Account): String
suspend fun listTransparentReceivers(account: Account): List<String>
@ -47,4 +68,46 @@ internal interface TypesafeBackend {
suspend fun validateCombinedChainOrErrorBlockHeight(limit: Long?): BlockHeight?
suspend fun getDownloadedUtxoBalance(address: String): WalletBalance
@Suppress("LongParameterList")
suspend fun putUtxo(
tAddress: String,
txId: ByteArray,
index: Int,
script: ByteArray,
value: Long,
height: BlockHeight
)
suspend fun getSentMemoAsUtf8(idNote: Long): String?
suspend fun getReceivedMemoAsUtf8(idNote: Long): String?
suspend fun initDataDb(seed: ByteArray?): Int
/**
* @throws RuntimeException as a common indicator of the operation failure
*/
@Throws(RuntimeException::class)
suspend fun scanBlocks(limit: Long?)
suspend fun decryptAndStoreTransaction(tx: ByteArray)
fun getSaplingReceiver(ua: String): String?
fun getTransparentReceiver(ua: String): String?
suspend fun initBlockMetaDb(): Int
/**
* @throws RuntimeException as a common indicator of the operation failure
*/
@Throws(RuntimeException::class)
suspend fun writeBlockMetadata(blockMetadata: List<JniBlockMeta>)
fun isValidShieldedAddr(addr: String): Boolean
fun isValidTransparentAddr(addr: String): Boolean
fun isValidUnifiedAddr(addr: String): Boolean
}

View File

@ -5,51 +5,185 @@ import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import kotlinx.coroutines.withContext
// This class is currently unused, although the goal is to swap out usages of BackendExt for this throughout the SDK.
@Suppress("TooManyFunctions")
internal class TypesafeBackendImpl(private val backend: Backend) : TypesafeBackend {
override suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey) =
backend.initAccountsTable(*keys)
override val network: ZcashNetwork
get() = ZcashNetwork.from(backend.networkId)
override suspend fun initAccountsTable(vararg keys: UnifiedFullViewingKey) {
val ufvks = Array(keys.size) { keys[it].encoding }
@Suppress("SpreadOperator")
backend.initAccountsTable(*ufvks)
}
override suspend fun initAccountsTable(
seed: ByteArray,
numberOfAccounts: Int
): List<UnifiedFullViewingKey> = backend.initAccountsTableTypesafe(seed, numberOfAccounts)
): List<UnifiedFullViewingKey> {
return DerivationTool.getInstance().deriveUnifiedFullViewingKeys(seed, network, numberOfAccounts)
}
override suspend fun initBlocksTable(checkpoint: Checkpoint) = backend.initBlocksTable(checkpoint)
override suspend fun initBlocksTable(checkpoint: Checkpoint) {
backend.initBlocksTable(
checkpoint.height.value,
checkpoint.hash,
checkpoint.epochSeconds,
checkpoint.tree
)
}
override suspend fun getCurrentAddress(account: Account): String = getCurrentAddress(account)
override suspend fun createAccountAndGetSpendingKey(seed: ByteArray): UnifiedSpendingKey {
return UnifiedSpendingKey(backend.createAccount(seed))
}
override suspend fun listTransparentReceivers(account: Account): List<String> =
backend.listTransparentReceivers(account)
@Suppress("LongParameterList")
override suspend fun createToAddress(
usk: UnifiedSpendingKey,
to: String,
value: Long,
memo: ByteArray?
): Long = backend.createToAddress(
usk.account.value,
usk.copyBytes(),
to,
value,
memo
)
override suspend fun getBalance(account: Account): Zatoshi = backend.getBalance(account)
override suspend fun shieldToAddress(
usk: UnifiedSpendingKey,
memo: ByteArray?
): Long = backend.shieldToAddress(
usk.account.value,
usk.copyBytes(),
memo
)
override fun getBranchIdForHeight(height: BlockHeight): Long = backend.getBranchIdForHeight(height.value)
override suspend fun getCurrentAddress(account: Account): String {
return backend.getCurrentAddress(account.value)
}
override suspend fun getVerifiedBalance(account: Account): Zatoshi = backend.getVerifiedBalance(account)
override suspend fun listTransparentReceivers(account: Account): List<String> {
return backend.listTransparentReceivers(account.value)
}
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight =
backend.getNearestRewindHeight(height)
override suspend fun getBalance(account: Account): Zatoshi {
return Zatoshi(backend.getBalance(account.value))
}
override suspend fun rewindToHeight(height: BlockHeight) = backend.rewindToHeight(height)
override fun getBranchIdForHeight(height: BlockHeight): Long {
return backend.getBranchIdForHeight(height.value)
}
override suspend fun getLatestBlockHeight(): BlockHeight? = backend.getLatestBlockHeight()
override suspend fun getVerifiedBalance(account: Account): Zatoshi {
return Zatoshi(backend.getVerifiedBalance(account.value))
}
override suspend fun findBlockMetadata(height: BlockHeight): JniBlockMeta? = backend.findBlockMetadata(height)
override suspend fun getNearestRewindHeight(height: BlockHeight): BlockHeight {
return BlockHeight.new(
ZcashNetwork.from(backend.networkId),
backend.getNearestRewindHeight(height.value)
)
}
override suspend fun rewindBlockMetadataToHeight(height: BlockHeight) = backend.rewindBlockMetadataToHeight(height)
override suspend fun rewindToHeight(height: BlockHeight) {
backend.rewindToHeight(height.value)
}
override suspend fun getLatestBlockHeight(): BlockHeight? {
return backend.getLatestHeight()?.let {
BlockHeight.new(
ZcashNetwork.from(backend.networkId),
it
)
}
}
override suspend fun findBlockMetadata(height: BlockHeight): JniBlockMeta? {
return backend.findBlockMetadata(height.value)
}
override suspend fun rewindBlockMetadataToHeight(height: BlockHeight) {
backend.rewindBlockMetadataToHeight(height.value)
}
/**
* @param limit The limit provides an efficient way how to restrict the portion of blocks, which will be validated.
* @return Null if successful. If an error occurs, the height will be the height where the error was detected.
*/
override suspend fun validateCombinedChainOrErrorBlockHeight(limit: Long?): BlockHeight? =
backend.validateCombinedChainOrErrorBlockHeight(limit)
override suspend fun validateCombinedChainOrErrorBlockHeight(limit: Long?): BlockHeight? {
return backend.validateCombinedChainOrErrorHeight(limit)?.let {
BlockHeight.new(
ZcashNetwork.from(backend.networkId),
it
)
}
}
override suspend fun getDownloadedUtxoBalance(address: String): WalletBalance =
backend.getDownloadedUtxoBalance(address)
override suspend fun getDownloadedUtxoBalance(address: String): WalletBalance {
// Note this implementation is not ideal because it requires two database queries without a transaction, which
// makes the data potentially inconsistent. However the verified amount is queried first which makes this less
// bad.
val verified = withContext(SdkDispatchers.DATABASE_IO) {
backend.getVerifiedTransparentBalance(address)
}
val total = withContext(SdkDispatchers.DATABASE_IO) {
backend.getTotalTransparentBalance(
address
)
}
return WalletBalance(Zatoshi(total), Zatoshi(verified))
}
@Suppress("LongParameterList")
override suspend fun putUtxo(
tAddress: String,
txId: ByteArray,
index: Int,
script: ByteArray,
value: Long,
height: BlockHeight
) {
return backend.putUtxo(
tAddress,
txId,
index,
script,
value,
height.value
)
}
override suspend fun getSentMemoAsUtf8(idNote: Long) = backend.getSentMemoAsUtf8(idNote)
override suspend fun getReceivedMemoAsUtf8(idNote: Long): String? = backend.getReceivedMemoAsUtf8(idNote)
override suspend fun initDataDb(seed: ByteArray?): Int = backend.initDataDb(seed)
override suspend fun scanBlocks(limit: Long?) = backend.scanBlocks(limit)
override suspend fun decryptAndStoreTransaction(tx: ByteArray) = backend.decryptAndStoreTransaction(tx)
override fun getSaplingReceiver(ua: String): String? = backend.getSaplingReceiver(ua)
override fun getTransparentReceiver(ua: String): String? = backend.getTransparentReceiver(ua)
override suspend fun initBlockMetaDb(): Int = backend.initBlockMetaDb()
override suspend fun writeBlockMetadata(blockMetadata: List<JniBlockMeta>) =
backend.writeBlockMetadata(blockMetadata)
override fun isValidShieldedAddr(addr: String): Boolean = backend.isValidShieldedAddr(addr)
override fun isValidTransparentAddr(addr: String): Boolean = backend.isValidTransparentAddr(addr)
override fun isValidUnifiedAddr(addr: String): Boolean = backend.isValidUnifiedAddr(addr)
}

View File

@ -2,13 +2,11 @@ package cash.z.ecc.android.sdk.internal.db.derived
import android.content.Context
import androidx.sqlite.db.SupportSQLiteDatabase
import cash.z.ecc.android.sdk.internal.Backend
import cash.z.ecc.android.sdk.internal.NoBackupContextWrapper
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.TypesafeBackend
import cash.z.ecc.android.sdk.internal.db.ReadOnlySupportSqliteOpenHelper
import cash.z.ecc.android.sdk.internal.ext.tryWarn
import cash.z.ecc.android.sdk.internal.initAccountsTable
import cash.z.ecc.android.sdk.internal.initBlocksTable
import cash.z.ecc.android.sdk.internal.model.Checkpoint
import cash.z.ecc.android.sdk.model.UnifiedFullViewingKey
import cash.z.ecc.android.sdk.model.ZcashNetwork
@ -44,7 +42,7 @@ internal class DerivedDataDb private constructor(
@Suppress("LongParameterList", "SpreadOperator")
suspend fun new(
context: Context,
backend: Backend,
backend: TypesafeBackend,
databaseFile: File,
zcashNetwork: ZcashNetwork,
checkpoint: Checkpoint,

View File

@ -1,8 +1,8 @@
package cash.z.ecc.android.sdk.internal.storage.block
import androidx.annotation.VisibleForTesting
import cash.z.ecc.android.sdk.internal.Backend
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.TypesafeBackend
import cash.z.ecc.android.sdk.internal.ext.createNewFileSuspend
import cash.z.ecc.android.sdk.internal.ext.deleteRecursivelySuspend
import cash.z.ecc.android.sdk.internal.ext.deleteSuspend
@ -13,11 +13,8 @@ import cash.z.ecc.android.sdk.internal.ext.mkdirsSuspend
import cash.z.ecc.android.sdk.internal.ext.renameToSuspend
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
import cash.z.ecc.android.sdk.internal.ext.writeBytesSuspend
import cash.z.ecc.android.sdk.internal.findBlockMetadata
import cash.z.ecc.android.sdk.internal.getLatestBlockHeight
import cash.z.ecc.android.sdk.internal.model.JniBlockMeta
import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository
import cash.z.ecc.android.sdk.internal.rewindBlockMetadataToHeight
import cash.z.ecc.android.sdk.model.BlockHeight
import co.electriccoin.lightwallet.client.model.CompactBlockUnsafe
import kotlinx.coroutines.flow.Flow
@ -25,7 +22,7 @@ import java.io.File
internal class FileCompactBlockRepository(
private val blocksDirectory: File,
private val backend: Backend
private val backend: TypesafeBackend
) : CompactBlockRepository {
override suspend fun getLatestHeight() = backend.getLatestBlockHeight()
@ -139,7 +136,7 @@ internal class FileCompactBlockRepository(
*/
suspend fun new(
blockCacheRoot: File,
rustBackend: Backend
backend: TypesafeBackend
): FileCompactBlockRepository {
// create and check cache directories
val blocksDirectory = File(blockCacheRoot, BLOCKS_DOWNLOAD_DIRECTORY).also {
@ -149,9 +146,9 @@ internal class FileCompactBlockRepository(
error("${blocksDirectory.path} directory does not exist and could not be created.")
}
rustBackend.initBlockMetaDb()
backend.initBlockMetaDb()
return FileCompactBlockRepository(blocksDirectory, rustBackend)
return FileCompactBlockRepository(blocksDirectory, backend)
}
}
}

View File

@ -2,15 +2,11 @@ package cash.z.ecc.android.sdk.internal.transaction
import cash.z.ecc.android.sdk.exception.TransactionEncoderException
import cash.z.ecc.android.sdk.ext.masked
import cash.z.ecc.android.sdk.internal.Backend
import cash.z.ecc.android.sdk.internal.SaplingParamTool
import cash.z.ecc.android.sdk.internal.Twig
import cash.z.ecc.android.sdk.internal.createToAddress
import cash.z.ecc.android.sdk.internal.getBranchIdForHeight
import cash.z.ecc.android.sdk.internal.TypesafeBackend
import cash.z.ecc.android.sdk.internal.model.EncodedTransaction
import cash.z.ecc.android.sdk.internal.network
import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository
import cash.z.ecc.android.sdk.internal.shieldToAddress
import cash.z.ecc.android.sdk.model.TransactionRecipient
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.Zatoshi
@ -25,7 +21,7 @@ import cash.z.ecc.android.sdk.model.Zatoshi
* such as the raw bytes and raw txId.
*/
internal class TransactionEncoderImpl(
private val backend: Backend,
private val backend: TypesafeBackend,
private val saplingParamTool: SaplingParamTool,
private val repository: DerivedDataRepository
) : TransactionEncoder {