From 4c2807aefd98995b579fea65c5cf866b2879506c Mon Sep 17 00:00:00 2001 From: Carter Jernigan Date: Wed, 19 Oct 2022 16:52:54 -0400 Subject: [PATCH] [#705] Roomoval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Android and Rust code have previously managed joint custody of the derived data database. With more complex migrations now required, we need to make the Android side read-only. To achieve that, the Android side will remove Room and instead rely on more primitive SQLite APIs for read-only access. As part of implementing this change, database management throughout the SDK is being refactored. There will be multiple representations of the data: - Database representation (and some Entity representations in the places that Room hasn’t been removed yet). These representations are not as type safe and don’t match Kotlin best practices in all cases. - Once #615 is implemented there will also be network representations - Type safe models, which often appear in the public API The Database and Network representations will be converted to and from the type safe model representation. --- .../runConfigurations/assembleAndroidTest.xml | 0 CHANGELOG.md | 8 +- .../android/sdk/darkside/test/TestWallet.kt | 2 +- .../sdk/sample/demoapp/SampleCodeTest.kt | 2 +- .../ListTransactionsFragment.kt | 12 +- .../listtransactions/TransactionAdapter.kt | 14 +- .../listtransactions/TransactionViewHolder.kt | 24 +- .../demos/listutxos/ListUtxosFragment.kt | 13 +- .../demoapp/demos/listutxos/UtxoAdapter.kt | 14 +- .../demoapp/demos/listutxos/UtxoViewHolder.kt | 26 +- .../sdk/demoapp/demos/send/SendFragment.kt | 30 +- docs/Architecture.md | 3 +- gradle.properties | 1 + sdk-lib/build.gradle.kts | 6 + .../2.json | 96 ---- .../3.json | 345 ----------- .../4.json | 345 ----------- .../5.json | 407 ------------- .../6.json | 419 -------------- .../7.json | 419 -------------- .../1.json | 0 .../1.json | 0 .../cash/z/ecc/android/sdk/InitializerTest.kt | 98 ---- .../sdk/db/CommonDatabaseBuilderTest.kt | 4 +- .../android/sdk/db/DatabaseCoordinatorTest.kt | 10 +- .../ecc/android/sdk/integration/SanityTest.kt | 22 +- .../ecc/android/sdk/integration/SmokeTest.kt | 6 +- .../sdk/integration/TestnetIntegrationTest.kt | 2 +- .../integration/service/ChangeServiceTest.kt | 4 +- .../PersistentTransactionManagerTest.kt | 45 +- .../cash/z/ecc/android/sdk/util/TestWallet.kt | 2 +- .../cash/z/ecc/fixture/DatabaseNameFixture.kt | 2 +- .../cash/z/ecc/android/sdk/SdkSynchronizer.kt | 219 ++----- .../cash/z/ecc/android/sdk/Synchronizer.kt | 48 +- .../sdk/block/CompactBlockProcessor.kt | 77 ++- .../z/ecc/android/sdk/db/entity/Account.kt | 16 - .../cash/z/ecc/android/sdk/db/entity/Block.kt | 34 -- .../z/ecc/android/sdk/db/entity/Received.kt | 65 --- .../cash/z/ecc/android/sdk/db/entity/Sent.kt | 78 --- .../ecc/android/sdk/db/entity/Transactions.kt | 417 -------------- .../cash/z/ecc/android/sdk/db/entity/Utxo.kt | 73 --- .../z/ecc/android/sdk/exception/Exceptions.kt | 20 +- .../sdk/internal/NoBackupContextWrapper.kt | 3 - .../internal/block/CompactBlockDownloader.kt | 19 +- .../ecc/android/sdk/internal/db/CursorExt.kt | 12 + .../android/sdk/internal/db/CursorParser.kt | 16 + .../{ => internal}/db/DatabaseCoordinator.kt | 8 +- .../android/sdk/internal/db/DerivedDataDb.kt | 541 ------------------ .../internal/db/ReadOnlySqliteOpenHelper.kt | 46 ++ .../db/ReadOnlySupportSqliteOpenHelper.kt | 61 ++ .../sdk/internal/db/SQLiteDatabaseExt.kt | 109 ++++ .../internal/db/{ => block}/CompactBlockDb.kt | 3 +- .../db/block}/CompactBlockEntity.kt | 16 +- .../block/DbCompactBlockRepository.kt} | 15 +- .../sdk/internal/db/derived/AccountTable.kt | 22 + .../internal/db/derived/AllTransactionView.kt | 142 +++++ .../sdk/internal/db/derived/BlockTable.kt | 88 +++ .../db/derived/DbDerivedDataRepository.kt | 69 +++ .../sdk/internal/db/derived/DerivedDataDb.kt | 81 +++ .../db/derived/ReceivedTransactionView.kt | 105 ++++ .../db/derived/SentTransactionView.kt | 99 ++++ .../internal/db/derived/TransactionTable.kt | 137 +++++ .../db/{ => pending}/PendingTransactionDb.kt | 3 +- .../db/pending/PendingTransactionEntity.kt | 142 +++++ .../z/ecc/android/sdk/internal/model/Block.kt | 11 + .../sdk/internal/model/CompactBlock.kt | 9 + .../sdk/internal/model/EncodedTransaction.kt | 10 + .../CompactBlockRepository.kt} | 4 +- .../DerivedDataRepository.kt} | 53 +- .../transaction/PagedTransactionRepository.kt | 249 -------- .../PersistentTransactionManager.kt | 100 ++-- .../transaction/TransactionEncoder.kt | 4 +- .../transaction/TransactionManager.kt | 2 +- .../transaction/WalletTransactionEncoder.kt | 5 +- .../android/sdk/model/PendingTransaction.kt | 139 +++++ .../z/ecc/android/sdk/model/Transaction.kt | 35 ++ .../android/sdk/model/TransactionOverview.kt | 26 + settings.gradle.kts | 3 + 78 files changed, 1774 insertions(+), 4041 deletions(-) rename .run/assembleAndroidTest.run.xml => .idea/runConfigurations/assembleAndroidTest.xml (100%) delete mode 100644 sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.CompactBlockDb/2.json delete mode 100644 sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/3.json delete mode 100644 sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/4.json delete mode 100644 sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/5.json delete mode 100644 sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/6.json delete mode 100644 sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/7.json rename sdk-lib/schemas/{cash.z.ecc.android.sdk.internal.db.CompactBlockDb => cash.z.ecc.android.sdk.internal.db.block.CompactBlockDb}/1.json (100%) rename sdk-lib/schemas/{cash.z.ecc.android.sdk.internal.db.PendingTransactionDb => cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb}/1.json (100%) delete mode 100644 sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/InitializerTest.kt delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Account.kt delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Block.kt delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Received.kt delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Sent.kt delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Utxo.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CursorExt.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CursorParser.kt rename sdk-lib/src/main/java/cash/z/ecc/android/sdk/{ => internal}/db/DatabaseCoordinator.kt (98%) delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/ReadOnlySqliteOpenHelper.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/ReadOnlySupportSqliteOpenHelper.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/SQLiteDatabaseExt.kt rename sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/{ => block}/CompactBlockDb.kt (94%) rename sdk-lib/src/main/java/cash/z/ecc/android/sdk/{db/entity => internal/db/block}/CompactBlockEntity.kt (53%) rename sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/{block/CompactBlockDbStore.kt => db/block/DbCompactBlockRepository.kt} (86%) create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AccountTable.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AllTransactionView.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/BlockTable.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedTransactionView.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/SentTransactionView.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TransactionTable.kt rename sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/{ => pending}/PendingTransactionDb.kt (96%) create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/pending/PendingTransactionEntity.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/Block.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/CompactBlock.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/EncodedTransaction.kt rename sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/{block/CompactBlockStore.kt => repository/CompactBlockRepository.kt} (92%) rename sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/{transaction/TransactionRepository.kt => repository/DerivedDataRepository.kt} (66%) delete mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/PendingTransaction.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Transaction.kt create mode 100644 sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionOverview.kt diff --git a/.run/assembleAndroidTest.run.xml b/.idea/runConfigurations/assembleAndroidTest.xml similarity index 100% rename from .run/assembleAndroidTest.run.xml rename to .idea/runConfigurations/assembleAndroidTest.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 10aea142..a6117a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ Change Log - `cash.z.ecc.android.sdk.model`: - `Account` - `FirstClassByteArray` + - `PendingTransaction` + - `Transaction` - `UnifiedSpendingKey` - `cash.z.ecc.android.sdk.tool`: - `DerivationTool.deriveUnifiedSpendingKey` @@ -36,9 +38,7 @@ Change Log - `Initializer.Config.newWallet` - `Initializer.Config.setViewingKeys` - `cash.z.ecc.android.sdk`: - - `Synchronizer.Companion.new` now takes a `seed` argument. A non-null value should be - provided if `Synchronizer.Companion.new` throws an error that a database migration - requires the wallet seed. + - `Synchronizer.Companion.new` now takes many of the arguments previously passed to `Initializer`. In addition, an optional `seed` argument is required for first-time initialization or if `Synchronizer.new` throws an exception indicating that an internal migration requires the wallet seed. (This second case will be true the first time existing clients upgrade to this new version of the SDK). - `Synchronizer.sendToAddress` now takes a `UnifiedSpendingKey` instead of an encoded Sapling extended spending key, and the `fromAccountIndex` argument is now implicit in the `UnifiedSpendingKey`. @@ -48,9 +48,11 @@ Change Log ### Removed - `cash.z.ecc.android.sdk`: + - `Initializer` (use `Synchronizer.new` instead) - `Synchronizer.getAddress` (use `Synchronizer.getCurrentAddress` instead). - `Synchronizer.getShieldedAddress` (use `Synchronizer.getLegacySaplingAddress` instead). - `Synchronizer.getTransparentAddress` (use `Synchronizer.getLegacyTransparentAddress` instead). + - `Synchronizer.cancel` - `cash.z.ecc.android.sdk.type.UnifiedViewingKey` - This type had a bug where the `extpub` field actually was storing a plain transparent public key, and not the extended public key as intended. This made it incompatible diff --git a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt index 9f1e9c0c..827db939 100644 --- a/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt +++ b/darkside-test-lib/src/androidTest/java/cash/z/ecc/android/sdk/darkside/test/TestWallet.kt @@ -5,7 +5,6 @@ import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.sdk.SdkSynchronizer import cash.z.ecc.android.sdk.Synchronizer -import cash.z.ecc.android.sdk.db.entity.isPending import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.twig @@ -16,6 +15,7 @@ import cash.z.ecc.android.sdk.model.LightWalletEndpoint 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.model.isPending import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt b/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt index 066f77b9..7c9a5ff2 100644 --- a/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt +++ b/demo-app/src/androidTest/java/cash/z/wallet/sdk/sample/demoapp/SampleCodeTest.kt @@ -2,7 +2,6 @@ package cash.z.wallet.sdk.sample.demoapp import androidx.test.platform.app.InstrumentationRegistry import cash.z.ecc.android.sdk.Synchronizer -import cash.z.ecc.android.sdk.db.entity.isFailure import cash.z.ecc.android.sdk.demoapp.util.fromResources import cash.z.ecc.android.sdk.ext.convertZecToZatoshi import cash.z.ecc.android.sdk.ext.toHex @@ -16,6 +15,7 @@ import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.Mainnet import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork +import cash.z.ecc.android.sdk.model.isFailure import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.flow.collect import kotlinx.coroutines.runBlocking diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt index 4392c2f6..89f12cb5 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt @@ -10,7 +10,6 @@ import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListTransactionsBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext @@ -19,9 +18,12 @@ import cash.z.ecc.android.sdk.ext.collectWith import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.LightWalletEndpoint +import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork import cash.z.ecc.android.sdk.tool.DerivationTool +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking /** @@ -79,7 +81,11 @@ class ListTransactionsFragment : BaseDemoFragment) { + private fun onTransactionsUpdated(transactions: List) { twig("got a new paged list of transactions") adapter.submitList(transactions) diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionAdapter.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionAdapter.kt index 0aefe737..f976842f 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionAdapter.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionAdapter.kt @@ -4,22 +4,22 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.demoapp.R +import cash.z.ecc.android.sdk.model.TransactionOverview /** * Simple adapter implementation that knows how to bind a recyclerview to ClearedTransactions. */ -class TransactionAdapter : ListAdapter( - object : DiffUtil.ItemCallback() { +class TransactionAdapter : ListAdapter( + object : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: ConfirmedTransaction, - newItem: ConfirmedTransaction + oldItem: TransactionOverview, + newItem: TransactionOverview ) = oldItem.minedHeight == newItem.minedHeight override fun areContentsTheSame( - oldItem: ConfirmedTransaction, - newItem: ConfirmedTransaction + oldItem: TransactionOverview, + newItem: TransactionOverview ) = oldItem == newItem } ) { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt index 3223c487..15076b8a 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt @@ -5,10 +5,10 @@ import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction -import cash.z.ecc.android.sdk.db.entity.valueInZatoshi import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString +import cash.z.ecc.android.sdk.model.TransactionOverview +import cash.z.ecc.android.sdk.model.Zatoshi import java.text.SimpleDateFormat import java.util.Locale @@ -17,22 +17,24 @@ import java.util.Locale */ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val amountText = itemView.findViewById(R.id.text_transaction_amount) - private val infoText = itemView.findViewById(R.id.text_transaction_info) private val timeText = itemView.findViewById(R.id.text_transaction_timestamp) private val icon = itemView.findViewById(R.id.image_transaction_type) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) @Suppress("MagicNumber") - fun bindTo(transaction: ConfirmedTransaction?) { - val isInbound = transaction?.toAddress.isNullOrEmpty() - amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString() + fun bindTo(transaction: TransactionOverview) { + bindTo(!transaction.isSentTransaction, transaction.blockTimeEpochSeconds, transaction.netValue) + } + + @Suppress("MagicNumber") + fun bindTo(isInbound: Boolean, time: Long, value: Zatoshi) { + amountText.text = value.convertZatoshiToZecString() timeText.text = - if (transaction == null || transaction.blockTimeInSeconds == 0L) { + if (time == 0L) { "Pending" } else { - formatter.format(transaction.blockTimeInSeconds * 1000L) + formatter.format(time * 1000L) } - infoText.text = getMemoString(transaction) icon.rotation = if (isInbound) 0f else 180f icon.rotation = if (isInbound) 0f else 180f @@ -40,8 +42,4 @@ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) ContextCompat.getColor(itemView.context, if (isInbound) R.color.tx_inbound else R.color.tx_outbound) ) } - - private fun getMemoString(transaction: ConfirmedTransaction?): String { - return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo" - } } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt index f134becf..19fb4a4e 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/ListUtxosFragment.kt @@ -11,7 +11,6 @@ import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.sdk.SdkSynchronizer import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.databinding.FragmentListUtxosBinding import cash.z.ecc.android.sdk.demoapp.ext.requireApplicationContext @@ -22,11 +21,13 @@ import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.LightWalletEndpoint +import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.defaultForNetwork import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.math.max @@ -142,7 +143,7 @@ class ListUtxosFragment : BaseDemoFragment() { lifecycleScope.launch { withContext(Dispatchers.IO) { - finalCount = (synchronizer as SdkSynchronizer).getTransactionCount() + finalCount = (synchronizer as SdkSynchronizer).getTransactionCount().toInt() withContext(Dispatchers.Main) { @Suppress("MagicNumber") delay(100) @@ -193,10 +194,12 @@ class ListUtxosFragment : BaseDemoFragment() { lifecycleScope.launch { withContext(Dispatchers.IO) { synchronizer.prepare() - initialCount = (synchronizer as SdkSynchronizer).getTransactionCount() + initialCount = (synchronizer as SdkSynchronizer).getTransactionCount().toInt() } + + onTransactionsUpdated(synchronizer.clearedTransactions.first()) } - synchronizer.clearedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated) + // synchronizer.receivedTransactions.collectWith(lifecycleScope, ::onTransactionsUpdated) } catch (t: Throwable) { twig("failed to start the synchronizer!!! due to : $t") @@ -257,7 +260,7 @@ class ListUtxosFragment : BaseDemoFragment() { binding.textStatus.visibility = View.INVISIBLE } - private fun onTransactionsUpdated(transactions: List) { + private fun onTransactionsUpdated(transactions: List) { twig("got a new paged list of transactions of size ${transactions.size}") adapter.submitList(transactions) } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoAdapter.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoAdapter.kt index 02f5e03c..ba7fbd68 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoAdapter.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoAdapter.kt @@ -4,22 +4,22 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.demoapp.R +import cash.z.ecc.android.sdk.model.TransactionOverview /** * Simple adapter implementation that knows how to bind a recyclerview to ClearedTransactions. */ -class UtxoAdapter : ListAdapter( - object : DiffUtil.ItemCallback() { +class UtxoAdapter : ListAdapter( + object : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: ConfirmedTransaction, - newItem: ConfirmedTransaction + oldItem: TransactionOverview, + newItem: TransactionOverview ) = oldItem.minedHeight == newItem.minedHeight override fun areContentsTheSame( - oldItem: ConfirmedTransaction, - newItem: ConfirmedTransaction + oldItem: TransactionOverview, + newItem: TransactionOverview ) = oldItem == newItem } ) { diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoViewHolder.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoViewHolder.kt index 04b0672d..aeae87d2 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoViewHolder.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/listutxos/UtxoViewHolder.kt @@ -3,10 +3,10 @@ package cash.z.ecc.android.sdk.demoapp.demos.listutxos import android.view.View import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction -import cash.z.ecc.android.sdk.db.entity.valueInZatoshi import cash.z.ecc.android.sdk.demoapp.R import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString +import cash.z.ecc.android.sdk.model.TransactionOverview +import cash.z.ecc.android.sdk.model.Zatoshi import java.text.SimpleDateFormat import java.util.Locale @@ -15,23 +15,21 @@ import java.util.Locale */ class UtxoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val amountText = itemView.findViewById(R.id.text_transaction_amount) - private val infoText = itemView.findViewById(R.id.text_transaction_info) private val timeText = itemView.findViewById(R.id.text_transaction_timestamp) private val formatter = SimpleDateFormat("M/d h:mma", Locale.getDefault()) @Suppress("MagicNumber") - fun bindTo(transaction: ConfirmedTransaction?) { - amountText.text = transaction?.valueInZatoshi.convertZatoshiToZecString() - timeText.text = - if (transaction == null || transaction.blockTimeInSeconds == 0L) { - "Pending" - } else { - formatter.format(transaction.blockTimeInSeconds * 1000L) - } - infoText.text = getMemoString(transaction) + fun bindTo(transaction: TransactionOverview) { + bindToHelper(transaction.netValue, transaction.blockTimeEpochSeconds) } - private fun getMemoString(transaction: ConfirmedTransaction?): String { - return transaction?.memo?.takeUnless { it[0] < 0 }?.let { String(it) } ?: "no memo" + @Suppress("MagicNumber") + private fun bindToHelper(amount: Zatoshi, time: Long) { + amountText.text = amount.convertZatoshiToZecString() + timeText.text = if (time == 0L) { + "Pending" + } else { + formatter.format(time * 1000L) + } } } diff --git a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt index e37e937d..56d0e461 100644 --- a/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt +++ b/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/demos/send/SendFragment.kt @@ -10,13 +10,6 @@ import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.block.CompactBlockProcessor -import cash.z.ecc.android.sdk.db.entity.PendingTransaction -import cash.z.ecc.android.sdk.db.entity.isCreated -import cash.z.ecc.android.sdk.db.entity.isCreating -import cash.z.ecc.android.sdk.db.entity.isFailedEncoding -import cash.z.ecc.android.sdk.db.entity.isFailedSubmit -import cash.z.ecc.android.sdk.db.entity.isMined -import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess import cash.z.ecc.android.sdk.demoapp.BaseDemoFragment import cash.z.ecc.android.sdk.demoapp.DemoConstants import cash.z.ecc.android.sdk.demoapp.databinding.FragmentSendBinding @@ -31,11 +24,19 @@ import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.LightWalletEndpoint +import cash.z.ecc.android.sdk.model.PendingTransaction 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 cash.z.ecc.android.sdk.model.defaultForNetwork +import cash.z.ecc.android.sdk.model.isCreated +import cash.z.ecc.android.sdk.model.isCreating +import cash.z.ecc.android.sdk.model.isFailedEncoding +import cash.z.ecc.android.sdk.model.isFailedSubmit +import cash.z.ecc.android.sdk.model.isMined +import cash.z.ecc.android.sdk.model.isSubmitSuccess import cash.z.ecc.android.sdk.tool.DerivationTool +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking /** @@ -172,12 +173,15 @@ class SendFragment : BaseDemoFragment() { isSending = true val amount = amountInput.text.toString().toDouble().convertZecToZatoshi() val toAddress = addressInput.text.toString().trim() - synchronizer.sendToAddress( - spendingKey, - amount, - toAddress, - "Funds from Demo App" - ).collectWith(lifecycleScope, ::onPendingTxUpdated) + lifecycleScope.launch { + synchronizer.sendToAddress( + spendingKey, + amount, + toAddress, + "Funds from Demo App" + ).collectWith(lifecycleScope, ::onPendingTxUpdated) + } + mainActivity()?.hideKeyboard() } diff --git a/docs/Architecture.md b/docs/Architecture.md index 879b5d47..8934a9a6 100644 --- a/docs/Architecture.md +++ b/docs/Architecture.md @@ -15,8 +15,7 @@ Thankfully, the only thing an app developer has to be concerned with is the foll | **CompactBlockStore** | Stores compact blocks that have been downloaded from the `LightWalletService` | | **CompactBlockProcessor** | Validates and scans the compact blocks in the `CompactBlockStore` for transaction details | | **OutboundTransactionManager** | Creates, Submits and manages transactions for spending funds | -| **Initializer** | Responsible for all setup that must happen before synchronization can begin. Loads the rust library and helps initialize databases. | -| **DerivationTool**, **BirthdayTool** | Utilities for deriving keys, addresses and loading wallet checkpoints, called "birthdays." | +| **DerivationTool** | Utilities for deriving keys and addresses | | **RustBackend** | Wraps and simplifies the rust library and exposes its functionality to the Kotlin SDK | # Checkpoints diff --git a/gradle.properties b/gradle.properties index eb42e4b8..248d6b44 100644 --- a/gradle.properties +++ b/gradle.properties @@ -84,6 +84,7 @@ ANDROIDX_ANNOTATION_VERSION=1.3.0 ANDROIDX_APPCOMPAT_VERSION=1.4.2 ANDROIDX_CONSTRAINT_LAYOUT_VERSION=2.1.4 ANDROIDX_CORE_VERSION=1.8.0 +ANDROIDX_DATABASE_VERSION=2.2.0 ANDROIDX_ESPRESSO_VERSION=3.4.0 ANDROIDX_LIFECYCLE_VERSION=2.4.1 ANDROIDX_MULTIDEX_VERSION=2.0.1 diff --git a/sdk-lib/build.gradle.kts b/sdk-lib/build.gradle.kts index 7dc060ff..7bd780e9 100644 --- a/sdk-lib/build.gradle.kts +++ b/sdk-lib/build.gradle.kts @@ -262,6 +262,12 @@ dependencies { implementation(libs.androidx.paging) ksp(libs.androidx.room.compiler) + // For direct database access + // TODO [#703]: Eliminate this dependency + // https://github.com/zcash/zcash-android-wallet-sdk/issues/703 + implementation(libs.androidx.sqlite) + implementation(libs.androidx.sqlite.framework) + // Kotlin implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.core) diff --git a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.CompactBlockDb/2.json b/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.CompactBlockDb/2.json deleted file mode 100644 index 5e7dccb9..00000000 --- a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.CompactBlockDb/2.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "11cfa01fe0b00e5d1e61a46e78f68ee2", - "entities": [ - { - "tableName": "compactblocks", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER NOT NULL, `data` BLOB NOT NULL, PRIMARY KEY(`height`))", - "fields": [ - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "data", - "columnName": "data", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "height" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "utxos", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `address` TEXT NOT NULL, `txid` BLOB, `tx_index` INTEGER, `script` BLOB, `value` INTEGER NOT NULL, `height` INTEGER)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "txid", - "columnName": "txid", - "affinity": "BLOB", - "notNull": false - }, - { - "fieldPath": "transactionIndex", - "columnName": "tx_index", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "script", - "columnName": "script", - "affinity": "BLOB", - "notNull": false - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11cfa01fe0b00e5d1e61a46e78f68ee2')" - ] - } -} \ No newline at end of file diff --git a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/3.json b/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/3.json deleted file mode 100644 index 6a8c15db..00000000 --- a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/3.json +++ /dev/null @@ -1,345 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 3, - "identityHash": "d6e9b05e0607d399f821058adb43dc15", - "entities": [ - { - "tableName": "transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_tx", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "txid", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "transactionIndex", - "columnName": "tx_index", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "created", - "columnName": "created", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "expiryHeight", - "columnName": "expiry_height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "minedHeight", - "columnName": "block", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "raw", - "columnName": "raw", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_tx" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "blocks", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "block" - ], - "referencedColumns": [ - "height" - ] - } - ] - }, - { - "tableName": "blocks", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))", - "fields": [ - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "time", - "columnName": "time", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "saplingTree", - "columnName": "sapling_tree", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "height" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "received_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spent", - "columnName": "spent", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "diversifier", - "columnName": "diversifier", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "rcm", - "columnName": "rcm", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "nf", - "columnName": "nf", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "isChange", - "columnName": "is_change", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "account" - ], - "referencedColumns": [ - "account" - ] - }, - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "spent" - ], - "referencedColumns": [ - "id_tx" - ] - } - ] - }, - { - "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))", - "fields": [ - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "extendedFullViewingKey", - "columnName": "extfvk", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "account" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "sent_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "from_account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "from_account" - ], - "referencedColumns": [ - "account" - ] - } - ] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6e9b05e0607d399f821058adb43dc15')" - ] - } -} diff --git a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/4.json b/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/4.json deleted file mode 100644 index 083d50ee..00000000 --- a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/4.json +++ /dev/null @@ -1,345 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 4, - "identityHash": "d6e9b05e0607d399f821058adb43dc15", - "entities": [ - { - "tableName": "transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_tx", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "txid", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "transactionIndex", - "columnName": "tx_index", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "created", - "columnName": "created", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "expiryHeight", - "columnName": "expiry_height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "minedHeight", - "columnName": "block", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "raw", - "columnName": "raw", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_tx" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "blocks", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "block" - ], - "referencedColumns": [ - "height" - ] - } - ] - }, - { - "tableName": "blocks", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))", - "fields": [ - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "time", - "columnName": "time", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "saplingTree", - "columnName": "sapling_tree", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "height" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "received_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spent", - "columnName": "spent", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "diversifier", - "columnName": "diversifier", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "rcm", - "columnName": "rcm", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "nf", - "columnName": "nf", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "isChange", - "columnName": "is_change", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "account" - ], - "referencedColumns": [ - "account" - ] - }, - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "spent" - ], - "referencedColumns": [ - "id_tx" - ] - } - ] - }, - { - "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))", - "fields": [ - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "extendedFullViewingKey", - "columnName": "extfvk", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "account" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "sent_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "from_account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "from_account" - ], - "referencedColumns": [ - "account" - ] - } - ] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6e9b05e0607d399f821058adb43dc15')" - ] - } -} diff --git a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/5.json b/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/5.json deleted file mode 100644 index dcc619bc..00000000 --- a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/5.json +++ /dev/null @@ -1,407 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 5, - "identityHash": "9431cf7a9bc49395e07834e4c81c5ed1", - "entities": [ - { - "tableName": "transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_tx", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "txid", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "transactionIndex", - "columnName": "tx_index", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "created", - "columnName": "created", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "expiryHeight", - "columnName": "expiry_height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "minedHeight", - "columnName": "block", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "raw", - "columnName": "raw", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_tx" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "blocks", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "block" - ], - "referencedColumns": [ - "height" - ] - } - ] - }, - { - "tableName": "blocks", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))", - "fields": [ - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "time", - "columnName": "time", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "saplingTree", - "columnName": "sapling_tree", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "height" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "received_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spent", - "columnName": "spent", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "diversifier", - "columnName": "diversifier", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "rcm", - "columnName": "rcm", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "nf", - "columnName": "nf", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "isChange", - "columnName": "is_change", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "account" - ], - "referencedColumns": [ - "account" - ] - }, - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "spent" - ], - "referencedColumns": [ - "id_tx" - ] - } - ] - }, - { - "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))", - "fields": [ - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "extendedFullViewingKey", - "columnName": "extfvk", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "account" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "sent_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "from_account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "from_account" - ], - "referencedColumns": [ - "account" - ] - } - ] - }, - { - "tableName": "utxos", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `address` TEXT NOT NULL, `prevout_txid` BLOB, `prevout_idx` INTEGER, `script` BLOB, `value_zat` INTEGER NOT NULL, `height` INTEGER, `spent_in_tx` INTEGER)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "txid", - "columnName": "prevout_txid", - "affinity": "BLOB", - "notNull": false - }, - { - "fieldPath": "transactionIndex", - "columnName": "prevout_idx", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "script", - "columnName": "script", - "affinity": "BLOB", - "notNull": false - }, - { - "fieldPath": "value", - "columnName": "value_zat", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "spent", - "columnName": "spent_in_tx", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9431cf7a9bc49395e07834e4c81c5ed1')" - ] - } -} \ No newline at end of file diff --git a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/6.json b/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/6.json deleted file mode 100644 index b49bce45..00000000 --- a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/6.json +++ /dev/null @@ -1,419 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 6, - "identityHash": "fa97f2995039ee4a382a54d224f4d8b9", - "entities": [ - { - "tableName": "transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_tx", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "txid", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "transactionIndex", - "columnName": "tx_index", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "created", - "columnName": "created", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "expiryHeight", - "columnName": "expiry_height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "minedHeight", - "columnName": "block", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "raw", - "columnName": "raw", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_tx" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "blocks", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "block" - ], - "referencedColumns": [ - "height" - ] - } - ] - }, - { - "tableName": "blocks", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))", - "fields": [ - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "time", - "columnName": "time", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "saplingTree", - "columnName": "sapling_tree", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "height" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "received_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spent", - "columnName": "spent", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "diversifier", - "columnName": "diversifier", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "rcm", - "columnName": "rcm", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "nf", - "columnName": "nf", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "isChange", - "columnName": "is_change", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "account" - ], - "referencedColumns": [ - "account" - ] - }, - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "spent" - ], - "referencedColumns": [ - "id_tx" - ] - } - ] - }, - { - "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))", - "fields": [ - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "extendedFullViewingKey", - "columnName": "extfvk", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "account" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "sent_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "from_account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "from_account" - ], - "referencedColumns": [ - "account" - ] - } - ] - }, - { - "tableName": "utxos", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_utxo` INTEGER, `address` TEXT NOT NULL, `prevout_txid` BLOB NOT NULL, `prevout_idx` INTEGER NOT NULL, `script` BLOB NOT NULL, `value_zat` INTEGER NOT NULL, `height` INTEGER NOT NULL, `spent_in_tx` INTEGER, PRIMARY KEY(`id_utxo`), FOREIGN KEY(`spent_in_tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_utxo", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "txid", - "columnName": "prevout_txid", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "transactionIndex", - "columnName": "prevout_idx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "script", - "columnName": "script", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value_zat", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spent", - "columnName": "spent_in_tx", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_utxo" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "spent_in_tx" - ], - "referencedColumns": [ - "id_tx" - ] - } - ] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fa97f2995039ee4a382a54d224f4d8b9')" - ] - } -} \ No newline at end of file diff --git a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/7.json b/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/7.json deleted file mode 100644 index dac12190..00000000 --- a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.DerivedDataDb/7.json +++ /dev/null @@ -1,419 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 7, - "identityHash": "70ef5c6e545e393e2a67bc81143e82e6", - "entities": [ - { - "tableName": "transactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_tx", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "txid", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "transactionIndex", - "columnName": "tx_index", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "created", - "columnName": "created", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "expiryHeight", - "columnName": "expiry_height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "minedHeight", - "columnName": "block", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "raw", - "columnName": "raw", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_tx" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "blocks", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "block" - ], - "referencedColumns": [ - "height" - ] - } - ] - }, - { - "tableName": "blocks", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))", - "fields": [ - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "time", - "columnName": "time", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "saplingTree", - "columnName": "sapling_tree", - "affinity": "BLOB", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "height" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "received_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spent", - "columnName": "spent", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "diversifier", - "columnName": "diversifier", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "rcm", - "columnName": "rcm", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "nf", - "columnName": "nf", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "isChange", - "columnName": "is_change", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "account" - ], - "referencedColumns": [ - "account" - ] - }, - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "spent" - ], - "referencedColumns": [ - "id_tx" - ] - } - ] - }, - { - "tableName": "accounts", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `ufvk` TEXT, PRIMARY KEY(`account`))", - "fields": [ - { - "fieldPath": "account", - "columnName": "account", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "unifiedFullViewingKey", - "columnName": "ufvk", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "account" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "sent_notes", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_pool` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_note", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "transactionId", - "columnName": "tx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputPool", - "columnName": "output_pool", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "outputIndex", - "columnName": "output_index", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "account", - "columnName": "from_account", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "memo", - "columnName": "memo", - "affinity": "BLOB", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_note" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "tx" - ], - "referencedColumns": [ - "id_tx" - ] - }, - { - "table": "accounts", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "from_account" - ], - "referencedColumns": [ - "account" - ] - } - ] - }, - { - "tableName": "utxos", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_utxo` INTEGER, `address` TEXT NOT NULL, `prevout_txid` BLOB NOT NULL, `prevout_idx` INTEGER NOT NULL, `script` BLOB NOT NULL, `value_zat` INTEGER NOT NULL, `height` INTEGER NOT NULL, `spent_in_tx` INTEGER, PRIMARY KEY(`id_utxo`), FOREIGN KEY(`spent_in_tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id_utxo", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "txid", - "columnName": "prevout_txid", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "transactionIndex", - "columnName": "prevout_idx", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "script", - "columnName": "script", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value_zat", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "height", - "columnName": "height", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "spent", - "columnName": "spent_in_tx", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "id_utxo" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "transactions", - "onDelete": "NO ACTION", - "onUpdate": "NO ACTION", - "columns": [ - "spent_in_tx" - ], - "referencedColumns": [ - "id_tx" - ] - } - ] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '70ef5c6e545e393e2a67bc81143e82e6')" - ] - } -} \ No newline at end of file diff --git a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.CompactBlockDb/1.json b/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.block.CompactBlockDb/1.json similarity index 100% rename from sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.CompactBlockDb/1.json rename to sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.block.CompactBlockDb/1.json diff --git a/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.PendingTransactionDb/1.json b/sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb/1.json similarity index 100% rename from sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.PendingTransactionDb/1.json rename to sdk-lib/schemas/cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb/1.json diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/InitializerTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/InitializerTest.kt deleted file mode 100644 index fd46674b..00000000 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/InitializerTest.kt +++ /dev/null @@ -1,98 +0,0 @@ -package cash.z.ecc.android.sdk - -class InitializerTest { - -// lateinit var initializer: Initializer -// -// @After -// fun cleanUp() { -// // don't leave databases sitting around after this test is run -// if (::initializer.isInitialized) initializer.erase() -// } -// -// @Test -// fun testInit() { -// val height = 980000 -// -// initializer = Initializer(context) { config -> -// config.importedWalletBirthday(height) -// config.setViewingKeys( -// "zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v", -// "zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7" -// ) -// config.alias = "VkInitTest1" -// } -// assertEquals(height, initializer.birthday.height) -// initializer.erase() -// } -// -// @Test -// fun testErase() { -// val alias = "VkInitTest2" -// initializer = Initializer(context) { config -> -// config.importedWalletBirthday(1_419_900) -// config.setViewingKeys( -// "zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v", -// "zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7" -// ) -// config.alias = alias -// } -// -// assertTrue("Failed to erase initializer", Initializer.erase(context, alias)) -// assertFalse("Expected false when erasing nothing.", Initializer.erase(context)) -// } -// -// @Test(expected = InitializerException.MissingDefaultBirthdayException::class) -// fun testMissingBirthday() { -// val config = Initializer.Config { config -> -// config.setViewingKeys("vk1") -// } -// config.validate() -// } -// -// @Test(expected = InitializerException.InvalidBirthdayHeightException::class) -// fun testOutOfBoundsBirthday() { -// val config = Initializer.Config { config -> -// config.setViewingKeys("vk1") -// config.setBirthdayHeight(ZcashSdk.SAPLING_ACTIVATION_HEIGHT - 1) -// } -// config.validate() -// } -// -// @Test -// fun testImportedWalletUsesSaplingActivation() { -// initializer = Initializer(context) { config -> -// config.setViewingKeys("vk1") -// config.importWallet(ByteArray(32)) -// } -// assertEquals("Incorrect height used for import.", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height) -// } -// -// @Test -// fun testDefaultToOldestHeight_true() { -// initializer = Initializer(context) { config -> -// config.setViewingKeys("vk1") -// config.setBirthdayHeight(null, true) -// } -// assertEquals("Height should equal sapling activation height when defaultToOldestHeight is true", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, initializer.birthday.height) -// } -// -// @Test -// fun testDefaultToOldestHeight_false() { -// val initialHeight = 750_000 -// initializer = Initializer(context) { config -> -// config.setViewingKeys("vk1") -// config.setBirthdayHeight(initialHeight, false) -// } -// val h = initializer.birthday.height -// assertNotEquals("Height should not equal sapling activation height when defaultToOldestHeight is false", ZcashSdk.SAPLING_ACTIVATION_HEIGHT, h) -// assertTrue("expected $h to be higher", h >= initialHeight) -// } -// -// companion object { -// private val context = InstrumentationRegistry.getInstrumentation().context -// init { -// Twig.plant(TroubleshootingTwig()) -// } -// } -} diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/CommonDatabaseBuilderTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/CommonDatabaseBuilderTest.kt index 11aeb11d..a2c76972 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/CommonDatabaseBuilderTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/CommonDatabaseBuilderTest.kt @@ -2,7 +2,9 @@ package cash.z.ecc.android.sdk.db import androidx.test.filters.SmallTest import cash.z.ecc.android.sdk.internal.AndroidApiVersion -import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb +import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator +import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder +import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb import cash.z.ecc.android.sdk.test.getAppContext import cash.z.ecc.fixture.DatabaseNameFixture import cash.z.ecc.fixture.DatabasePathFixture diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/DatabaseCoordinatorTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/DatabaseCoordinatorTest.kt index 2319be03..fef9e079 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/DatabaseCoordinatorTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/db/DatabaseCoordinatorTest.kt @@ -3,7 +3,7 @@ package cash.z.ecc.android.sdk.db import androidx.test.filters.FlakyTest import androidx.test.filters.MediumTest import androidx.test.filters.SmallTest -import cash.z.ecc.android.sdk.internal.ext.createNewFileSuspend +import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator import cash.z.ecc.android.sdk.internal.ext.existsSuspend import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.test.getAppContext @@ -192,14 +192,14 @@ class DatabaseCoordinatorTest { } } - private suspend fun getEmptyFile(parent: File, fileName: String): File { + private fun getEmptyFile(parent: File, fileName: String): File { return File(parent, fileName).apply { assertTrue(parentFile != null) parentFile!!.mkdirs() - assertTrue(parentFile!!.existsSuspend()) + assertTrue(parentFile!!.exists()) - createNewFileSuspend() - assertTrue(existsSuspend()) + createNewFile() + assertTrue(exists()) } } diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt index 3969a1c5..8161a857 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SanityTest.kt @@ -4,8 +4,9 @@ import androidx.test.core.app.ApplicationProvider import cash.z.ecc.android.sdk.DefaultSynchronizerFactory import cash.z.ecc.android.sdk.annotation.MaintainedTest import cash.z.ecc.android.sdk.annotation.TestPurpose -import cash.z.ecc.android.sdk.db.DatabaseCoordinator import cash.z.ecc.android.sdk.ext.BlockExplorer +import cash.z.ecc.android.sdk.internal.SaplingParamTool +import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.ZcashNetwork @@ -40,27 +41,29 @@ class SanityTest( val rustBackend = runBlocking { DefaultSynchronizerFactory.defaultRustBackend( ApplicationProvider.getApplicationContext(), - ZcashNetwork.Testnet, + wallet.network, "TestWallet", - TestWallet.Backups.SAMPLE_WALLET.testnetBirthday + birthday, + SaplingParamTool.new(ApplicationProvider.getApplicationContext()) ) } assertTrue( - "$name has invalid DataDB file", + "$name has invalid DataDB file actual=${rustBackend.dataDbFile.absolutePath}" + + "expected suffix=no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_DATA_NAME}", rustBackend.dataDbFile.absolutePath.endsWith( "no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_DATA_NAME}" ) ) assertTrue( - "$name has invalid CacheDB file", + "$name has invalid CacheDB file $rustBackend.cacheDbFile.absolutePath", rustBackend.cacheDbFile.absolutePath.endsWith( "no_backup/co.electricoin.zcash/TestWallet_${networkName}_${DatabaseCoordinator.DB_CACHE_NAME}" ) ) assertTrue( - "$name has invalid params dir", - rustBackend.saplingParamDir.path.endsWith( + "$name has invalid params dir ${rustBackend.saplingParamDir.absolutePath}", + rustBackend.saplingParamDir.absolutePath.endsWith( "no_backup/co.electricoin.zcash" ) ) @@ -108,7 +111,10 @@ class SanityTest( runCatching { wallet.service.getLatestBlockHeight() }.getOrNull() ?: return@runBlocking - assertTrue("$networkName failed to return a proper block. Height was ${block.height} but we expected $height", block.height == height.value) + assertTrue( + "$networkName failed to return a proper block. Height was ${block.height} but we expected $height", + block.height == height.value + ) } companion object { diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt index edf9e1d5..c32b99ba 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/SmokeTest.kt @@ -6,7 +6,8 @@ import androidx.test.filters.MediumTest import cash.z.ecc.android.sdk.DefaultSynchronizerFactory import cash.z.ecc.android.sdk.annotation.MaintainedTest import cash.z.ecc.android.sdk.annotation.TestPurpose -import cash.z.ecc.android.sdk.db.DatabaseCoordinator +import cash.z.ecc.android.sdk.internal.SaplingParamTool +import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.util.TestWallet import kotlinx.coroutines.runBlocking @@ -29,7 +30,8 @@ class SmokeTest { ApplicationProvider.getApplicationContext(), ZcashNetwork.Testnet, "TestWallet", - TestWallet.Backups.SAMPLE_WALLET.testnetBirthday + TestWallet.Backups.SAMPLE_WALLET.testnetBirthday, + SaplingParamTool.new(ApplicationProvider.getApplicationContext()) ) } assertTrue( diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt index a6cd44df..097cf0ba 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/TestnetIntegrationTest.kt @@ -4,7 +4,6 @@ import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED -import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.ext.onFirst import cash.z.ecc.android.sdk.internal.TroubleshootingTwig @@ -16,6 +15,7 @@ import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.LightWalletEndpoint import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.ZcashNetwork +import cash.z.ecc.android.sdk.model.isSubmitSuccess import cash.z.ecc.android.sdk.test.ScopedTest import cash.z.ecc.android.sdk.tool.CheckpointTool import cash.z.ecc.android.sdk.tool.DerivationTool diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/service/ChangeServiceTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/service/ChangeServiceTest.kt index cc9d13e9..54e983a2 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/service/ChangeServiceTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/integration/service/ChangeServiceTest.kt @@ -9,7 +9,7 @@ import cash.z.ecc.android.sdk.annotation.TestPurpose import cash.z.ecc.android.sdk.exception.LightWalletException.ChangeServerException.ChainInfoNotMatching import cash.z.ecc.android.sdk.exception.LightWalletException.ChangeServerException.StatusException import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader -import cash.z.ecc.android.sdk.internal.block.CompactBlockStore +import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.twig @@ -45,7 +45,7 @@ class ChangeServiceTest : ScopedTest() { private val eccEndpoint = LightWalletEndpoint("lightwalletd.electriccoin.co", 9087, true) @Mock - lateinit var mockBlockStore: CompactBlockStore + lateinit var mockBlockStore: CompactBlockRepository var mockCloseable: AutoCloseable? = null @Spy diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManagerTest.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManagerTest.kt index f97eb2de..10b4fb2b 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManagerTest.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManagerTest.kt @@ -4,15 +4,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import cash.z.ecc.android.sdk.annotation.MaintainedTest import cash.z.ecc.android.sdk.annotation.TestPurpose -import cash.z.ecc.android.sdk.db.entity.EncodedTransaction -import cash.z.ecc.android.sdk.db.entity.PendingTransaction -import cash.z.ecc.android.sdk.db.entity.isCancelled import cash.z.ecc.android.sdk.internal.TroubleshootingTwig import cash.z.ecc.android.sdk.internal.Twig +import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder +import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb +import cash.z.ecc.android.sdk.internal.model.EncodedTransaction import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.model.Account +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray +import cash.z.ecc.android.sdk.model.PendingTransaction import cash.z.ecc.android.sdk.model.Zatoshi +import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.test.ScopedTest +import cash.z.ecc.android.sdk.test.getAppContext import cash.z.ecc.fixture.DatabaseNameFixture import cash.z.ecc.fixture.DatabasePathFixture import com.nhaarman.mockitokotlin2.any @@ -26,7 +31,6 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations import java.io.File -import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -36,9 +40,11 @@ import kotlin.test.assertTrue @SmallTest class PersistentTransactionManagerTest : ScopedTest() { - @Mock lateinit var mockEncoder: TransactionEncoder + @Mock + internal lateinit var mockEncoder: TransactionEncoder - @Mock lateinit var mockService: LightWalletService + @Mock + lateinit var mockService: LightWalletService private val pendingDbFile = File( DatabasePathFixture.new(), @@ -56,7 +62,12 @@ class PersistentTransactionManagerTest : ScopedTest() { fun setup() { initMocks() deleteDb() - manager = PersistentTransactionManager(context, mockEncoder, mockService, pendingDbFile) + val db = commonDatabaseBuilder( + getAppContext(), + PendingTransactionDb::class.java, + pendingDbFile + ).build() + manager = PersistentTransactionManager(db, ZcashNetwork.Mainnet, mockEncoder, mockService) } private fun deleteDb() { @@ -71,21 +82,21 @@ class PersistentTransactionManagerTest : ScopedTest() { }.thenAnswer { runBlocking { delay(200) - EncodedTransaction(byteArrayOf(1, 2, 3), byteArrayOf(8, 9), 5_000_000) + EncodedTransaction( + FirstClassByteArray(byteArrayOf(1, 2, 3)), + FirstClassByteArray( + byteArrayOf( + 8, + 9 + ) + ), + BlockHeight.new(ZcashNetwork.Mainnet, 5_000_000) + ) } } } } - @Test - fun testCancel() = runBlocking { - var tx = manager.initSpend(Zatoshi(1234), "a", "b", Account.DEFAULT) - assertFalse(tx.isCancelled()) - manager.cancel(tx.id) - tx = manager.findById(tx.id)!! - assertTrue(tx.isCancelled(), "Transaction was not cancelled") - } - @Test fun testAbort() = runBlocking { var tx: PendingTransaction? = manager.initSpend(Zatoshi(1234), "a", "b", Account.DEFAULT) diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt index b660fc87..81026e27 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/android/sdk/util/TestWallet.kt @@ -5,7 +5,6 @@ import cash.z.ecc.android.bip39.Mnemonics import cash.z.ecc.android.bip39.toSeed import cash.z.ecc.android.sdk.SdkSynchronizer import cash.z.ecc.android.sdk.Synchronizer -import cash.z.ecc.android.sdk.db.entity.isPending import cash.z.ecc.android.sdk.internal.Twig import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.twig @@ -16,6 +15,7 @@ import cash.z.ecc.android.sdk.model.Testnet 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.model.isPending import cash.z.ecc.android.sdk.tool.DerivationTool import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/DatabaseNameFixture.kt b/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/DatabaseNameFixture.kt index 2c2b1c83..bc9344e5 100644 --- a/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/DatabaseNameFixture.kt +++ b/sdk-lib/src/androidTest/java/cash/z/ecc/fixture/DatabaseNameFixture.kt @@ -1,6 +1,6 @@ package cash.z.ecc.fixture -import cash.z.ecc.android.sdk.db.DatabaseCoordinator +import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator import cash.z.ecc.android.sdk.model.ZcashNetwork object DatabaseNameFixture { diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt index e6363aa8..7d5d33ab 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/SdkSynchronizer.kt @@ -17,37 +17,26 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanned import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanning import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Stopped import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Validating -import cash.z.ecc.android.sdk.db.DatabaseCoordinator -import cash.z.ecc.android.sdk.db.entity.PendingTransaction -import cash.z.ecc.android.sdk.db.entity.hasRawTransactionId -import cash.z.ecc.android.sdk.db.entity.isCancelled -import cash.z.ecc.android.sdk.db.entity.isExpired -import cash.z.ecc.android.sdk.db.entity.isFailedSubmit -import cash.z.ecc.android.sdk.db.entity.isLongExpired -import cash.z.ecc.android.sdk.db.entity.isMarkedForDeletion -import cash.z.ecc.android.sdk.db.entity.isMined -import cash.z.ecc.android.sdk.db.entity.isSafeToDiscard -import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess -import cash.z.ecc.android.sdk.db.entity.isSubmitted import cash.z.ecc.android.sdk.exception.SynchronizerException import cash.z.ecc.android.sdk.ext.ConsensusBranchId import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.internal.SaplingParamTool -import cash.z.ecc.android.sdk.internal.block.CompactBlockDbStore import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader -import cash.z.ecc.android.sdk.internal.block.CompactBlockStore -import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend +import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator +import cash.z.ecc.android.sdk.internal.db.block.DbCompactBlockRepository +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.ext.toHexReversed import cash.z.ecc.android.sdk.internal.ext.tryNull import cash.z.ecc.android.sdk.internal.isEmpty import cash.z.ecc.android.sdk.internal.model.Checkpoint +import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository +import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManager -import cash.z.ecc.android.sdk.internal.transaction.PagedTransactionRepository import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManager import cash.z.ecc.android.sdk.internal.transaction.TransactionEncoder -import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository import cash.z.ecc.android.sdk.internal.transaction.WalletTransactionEncoder import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twigTask @@ -55,10 +44,17 @@ import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.LightWalletEndpoint +import cash.z.ecc.android.sdk.model.PendingTransaction 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.model.isExpired +import cash.z.ecc.android.sdk.model.isLongExpired +import cash.z.ecc.android.sdk.model.isMarkedForDeletion +import cash.z.ecc.android.sdk.model.isMined +import cash.z.ecc.android.sdk.model.isSafeToDiscard +import cash.z.ecc.android.sdk.model.isSubmitSuccess import cash.z.ecc.android.sdk.type.AddressType import cash.z.ecc.android.sdk.type.AddressType.Shielded import cash.z.ecc.android.sdk.type.AddressType.Transparent @@ -78,14 +74,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import java.io.File import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -105,7 +97,7 @@ import kotlin.coroutines.EmptyCoroutineContext @FlowPreview @Suppress("TooManyFunctions") class SdkSynchronizer internal constructor( - private val storage: TransactionRepository, + private val storage: DerivedDataRepository, private val txManager: OutboundTransactionManager, val processor: CompactBlockProcessor ) : Synchronizer { @@ -335,7 +327,7 @@ class SdkSynchronizer internal constructor( // TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682 suspend fun findBlockHash(height: BlockHeight): ByteArray? { - return (storage as? PagedTransactionRepository)?.findBlockHash(height) + return storage.findBlockHash(height) } suspend fun findBlockHashAsHex(height: BlockHeight): String? { @@ -343,7 +335,7 @@ class SdkSynchronizer internal constructor( } suspend fun getTransactionCount(): Int { - return (storage as? PagedTransactionRepository)?.getTransactionCount() ?: 0 + return storage.getTransactionCount().toInt() } fun refreshTransactions() { @@ -535,7 +527,7 @@ class SdkSynchronizer internal constructor( .forEach { pendingTx -> twig("checking for updates on pendingTx id: ${pendingTx.id}") pendingTx.rawTransactionId?.let { rawId -> - storage.findMinedHeight(rawId)?.let { minedHeight -> + storage.findMinedHeight(rawId.byteArray)?.let { minedHeight -> twig( "found matching transaction for pending transaction with id" + " ${pendingTx.id} mined at height $minedHeight!" @@ -545,31 +537,6 @@ class SdkSynchronizer internal constructor( } } - twig("[cleanup] beginning to cleanup cancelled transactions", -1) - var hasCleaned = false - // Experimental: cleanup cancelled transactions - allPendingTxs.filter { it.isCancelled() && it.hasRawTransactionId() }.let { cancellable -> - cancellable.forEachIndexed { index, pendingTx -> - twig( - "[cleanup] FOUND (${index + 1} of ${cancellable.size})" + - " CANCELLED pendingTxId: ${pendingTx.id}" - ) - hasCleaned = hasCleaned || cleanupCancelledTx(pendingTx) - } - } - - // Experimental: cleanup failed transactions - allPendingTxs.filter { it.isSubmitted() && it.isFailedSubmit() && !it.isMarkedForDeletion() } - .let { failed -> - failed.forEachIndexed { index, pendingTx -> - twig( - "[cleanup] FOUND (${index + 1} of ${failed.size})" + - " FAILED pendingTxId: ${pendingTx.id}" - ) - cleanupCancelledTx(pendingTx) - } - } - twig("[cleanup] beginning to cleanup expired transactions", -1) // Experimental: cleanup expired transactions // note: don't delete the pendingTx until the related data has been scrubbed, or else you @@ -589,39 +556,18 @@ class SdkSynchronizer internal constructor( lastScannedHeight, network.saplingActivationHeight ) || it.isSafeToDiscard() + }.forEach { + val result = txManager.abort(it) + twig( + "[cleanup] FOUND EXPIRED pendingTX (lastScanHeight: $lastScannedHeight " + + " expiryHeight: ${it.expiryHeight}): and ${it.id} " + + "${if (result > 0) "successfully removed" else "failed to remove"} it" + ) } - .forEach { - val result = txManager.abort(it) - twig( - "[cleanup] FOUND EXPIRED pendingTX (lastScanHeight: $lastScannedHeight " + - " expiryHeight: ${it.expiryHeight}): and ${it.id} " + - "${if (result > 0) "successfully removed" else "failed to remove"} it" - ) - } - twig("[cleanup] deleting expired transactions from storage", -1) - val expiredCount = storage.deleteExpired(lastScannedHeight) - if (expiredCount > 0) { - twig("[cleanup] deleted $expiredCount expired transaction(s)!") - } - hasCleaned = hasCleaned || (expiredCount > 0) - - if (hasCleaned) { - refreshAllBalances() - } twig("[cleanup] done refreshing and cleaning up pending transactions", -1) } - private suspend fun cleanupCancelledTx(pendingTx: PendingTransaction): Boolean { - return if (storage.cleanupCancelledTx(pendingTx.rawTransactionId!!)) { - txManager.markForDeletion(pendingTx.id) - true - } else { - twig("[cleanup] no matching tx was cleaned so the pendingTx will not be marked for deletion") - false - } - } - // // Account management // @@ -654,65 +600,38 @@ class SdkSynchronizer internal constructor( override suspend fun getLegacyTransparentAddress(account: Account): String = processor.getTransparentAddress(account) - override fun sendToAddress( + override suspend fun sendToAddress( usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: String - ): Flow = flow { - twig("Initializing pending transaction") + ): Flow { // Emit the placeholder transaction, then switch to monitoring the database - txManager.initSpend(amount, toAddress, memo, usk.account).let { placeHolderTx -> - emit(placeHolderTx) - txManager.encode(usk, placeHolderTx).let { encodedTx -> - // only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX. - if (encodedTx.isCancelled()) { - twig("[cleanup] this tx has been cancelled so we will cleanup instead of submitting") - if (cleanupCancelledTx(encodedTx)) { - refreshAllBalances() - } - } else { - txManager.submit(encodedTx) - } - } - } - }.flatMapLatest { - // switch this flow over to monitoring the database for transactions - // so we emit the placeholder TX above, then watch the database for all further updates - twig("Monitoring pending transaction (id: ${it.id}) for updates...") - txManager.monitorById(it.id) - }.distinctUntilChanged() + val placeHolderTx = txManager.initSpend(amount, toAddress, memo, usk.account) - override fun shieldFunds( + txManager.encode(usk, placeHolderTx).let { encodedTx -> + txManager.submit(encodedTx) + } + + return txManager.monitorById(placeHolderTx.id) + } + + override suspend fun shieldFunds( usk: UnifiedSpendingKey, memo: String - ): Flow = flow { + ): Flow { twig("Initializing shielding transaction") - // TODO(str4d): This only shields funds from the current UA's transparent receiver. Fix this once we start - // rolling UAs. val tAddr = processor.getTransparentAddress(usk.account) val tBalance = processor.getUtxoCacheBalance(tAddr) val zAddr = getCurrentAddress(usk.account) // Emit the placeholder transaction, then switch to monitoring the database - txManager.initSpend(tBalance.available, zAddr, memo, usk.account).let { placeHolderTx -> - emit(placeHolderTx) - txManager.encode("", usk, placeHolderTx).let { encodedTx -> - // only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX. - if (encodedTx.isCancelled()) { - twig("[cleanup] this shielding tx has been cancelled so we will cleanup instead of submitting") - if (cleanupCancelledTx(encodedTx)) { - refreshAllBalances() - } - } else { - txManager.submit(encodedTx) - } - } - } - }.flatMapLatest { - twig("Monitoring shielding transaction (id: ${it.id}) for updates...") - txManager.monitorById(it.id) - }.distinctUntilChanged() + val placeHolderTx = txManager.initSpend(tBalance.available, zAddr, memo, usk.account) + val encodedTx = txManager.encode("", usk, placeHolderTx) + txManager.submit(encodedTx) + + return txManager.monitorById(placeHolderTx.id) + } override suspend fun refreshUtxos(tAddr: String, since: BlockHeight): Int? { return processor.refreshUtxos(tAddr, since) @@ -784,16 +703,13 @@ class SdkSynchronizer internal constructor( * * See the helper methods for generating default values. */ -object DefaultSynchronizerFactory { +internal object DefaultSynchronizerFactory { fun new( - repository: TransactionRepository, + repository: DerivedDataRepository, txManager: OutboundTransactionManager, processor: CompactBlockProcessor ): Synchronizer { - // call the actual constructor now that all dependencies have been injected - // alternatively, this entire object graph can be supplied by Dagger - // This builder just makes that easier. return SdkSynchronizer( repository, txManager, @@ -805,48 +721,34 @@ object DefaultSynchronizerFactory { context: Context, network: ZcashNetwork, alias: String, - blockHeight: BlockHeight + blockHeight: BlockHeight, + saplingParamTool: SaplingParamTool ): RustBackend { val coordinator = DatabaseCoordinator.getInstance(context) return RustBackend.init( coordinator.cacheDbFile(network, alias), coordinator.dataDbFile(network, alias), - File(context.getCacheDirSuspend(), "params"), + saplingParamTool.properties.paramsDirectory, network, blockHeight ) } - // TODO [#242]: Don't hard code page size. It is a workaround for Uncaught Exception: - // android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy - // can touch its views. and is probably related to FlowPagedList - // TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242 - private const val DEFAULT_PAGE_SIZE = 1000 - @Suppress("LongParameterList") - internal suspend fun defaultTransactionRepository( + internal suspend fun defaultDerivedDataRepository( context: Context, rustBackend: RustBackend, zcashNetwork: ZcashNetwork, checkpoint: Checkpoint, - viewingKeys: List, - seed: ByteArray? - ): TransactionRepository = - PagedTransactionRepository.new( - context, - zcashNetwork, - DEFAULT_PAGE_SIZE, - rustBackend, - seed, - checkpoint, - viewingKeys, - false - ) + seed: ByteArray?, + viewingKeys: List + ): DerivedDataRepository = + DbDerivedDataRepository(DerivedDataDb.new(context, rustBackend, zcashNetwork, checkpoint, seed, viewingKeys)) - internal fun defaultBlockStore(context: Context, rustBackend: RustBackend, zcashNetwork: ZcashNetwork): - CompactBlockStore = - CompactBlockDbStore.new( + internal fun defaultCompactBlockRepository(context: Context, rustBackend: RustBackend, zcashNetwork: ZcashNetwork): + CompactBlockRepository = + DbCompactBlockRepository.new( context, zcashNetwork, rustBackend.cacheDbFile @@ -858,15 +760,15 @@ object DefaultSynchronizerFactory { internal fun defaultEncoder( rustBackend: RustBackend, saplingParamTool: SaplingParamTool, - repository: TransactionRepository + repository: DerivedDataRepository ): TransactionEncoder = WalletTransactionEncoder(rustBackend, saplingParamTool, repository) fun defaultDownloader( service: LightWalletService, - blockStore: CompactBlockStore + blockStore: CompactBlockRepository ): CompactBlockDownloader = CompactBlockDownloader(service, blockStore) - suspend fun defaultTxManager( + internal suspend fun defaultTxManager( context: Context, zcashNetwork: ZcashNetwork, alias: String, @@ -878,8 +780,9 @@ object DefaultSynchronizerFactory { alias ) - return PersistentTransactionManager( + return PersistentTransactionManager.new( context, + zcashNetwork, encoder, service, databaseFile @@ -889,7 +792,7 @@ object DefaultSynchronizerFactory { internal fun defaultProcessor( rustBackend: RustBackend, downloader: CompactBlockDownloader, - repository: TransactionRepository + repository: DerivedDataRepository ): CompactBlockProcessor = CompactBlockProcessor( downloader, repository, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt index 89d748a4..e53b9885 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/Synchronizer.kt @@ -2,14 +2,15 @@ package cash.z.ecc.android.sdk import android.content.Context import cash.z.ecc.android.sdk.block.CompactBlockProcessor -import cash.z.ecc.android.sdk.db.DatabaseCoordinator -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction -import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.internal.SaplingParamTool +import cash.z.ecc.android.sdk.internal.db.DatabaseCoordinator import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.LightWalletEndpoint +import cash.z.ecc.android.sdk.model.PendingTransaction +import cash.z.ecc.android.sdk.model.Transaction +import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.ecc.android.sdk.model.Zatoshi @@ -136,17 +137,17 @@ interface Synchronizer { /** * A flow of all the transactions that are on the blockchain. */ - val clearedTransactions: Flow> + val clearedTransactions: Flow> /** * A flow of all transactions related to sending funds. */ - val sentTransactions: Flow> + val sentTransactions: Flow> /** * A flow of all transactions related to receiving funds. */ - val receivedTransactions: Flow> + val receivedTransactions: Flow> // // Latest Properties @@ -237,14 +238,14 @@ interface Synchronizer { * useful for updating the UI without needing to poll. Of course, polling is always an option * for any wallet that wants to ignore this return value. */ - fun sendToAddress( + suspend fun sendToAddress( usk: UnifiedSpendingKey, amount: Zatoshi, toAddress: String, memo: String = "" ): Flow - fun shieldFunds( + suspend fun shieldFunds( usk: UnifiedSpendingKey, memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX ): Flow @@ -493,6 +494,8 @@ interface Synchronizer { validateAlias(alias) + val saplingParamTool = SaplingParamTool.new(applicationContext) + val loadedCheckpoint = CheckpointTool.loadNearest( applicationContext, zcashNetwork, @@ -503,7 +506,8 @@ interface Synchronizer { applicationContext, zcashNetwork, alias, - loadedCheckpoint.height + loadedCheckpoint.height, + saplingParamTool ) val viewingKeys = seed?.let { @@ -514,25 +518,31 @@ interface Synchronizer { ).toList() } ?: emptyList() - val repository = DefaultSynchronizerFactory.defaultTransactionRepository( + val repository = DefaultSynchronizerFactory.defaultDerivedDataRepository( applicationContext, rustBackend, zcashNetwork, loadedCheckpoint, - viewingKeys, - seed + seed, + viewingKeys ) - val saplingParamTool = SaplingParamTool.new(applicationContext) - - val blockStore = DefaultSynchronizerFactory.defaultBlockStore(applicationContext, rustBackend, zcashNetwork) + val blockStore = DefaultSynchronizerFactory.defaultCompactBlockRepository( + applicationContext, + rustBackend, + zcashNetwork + ) val service = DefaultSynchronizerFactory.defaultService(applicationContext, lightWalletEndpoint) val encoder = DefaultSynchronizerFactory.defaultEncoder(rustBackend, saplingParamTool, repository) val downloader = DefaultSynchronizerFactory.defaultDownloader(service, blockStore) - val txManager = - DefaultSynchronizerFactory.defaultTxManager(applicationContext, zcashNetwork, alias, encoder, service) - val processor = - DefaultSynchronizerFactory.defaultProcessor(rustBackend, downloader, repository) + val txManager = DefaultSynchronizerFactory.defaultTxManager( + applicationContext, + zcashNetwork, + alias, + encoder, + service + ) + val processor = DefaultSynchronizerFactory.defaultProcessor(rustBackend, downloader, repository) return SdkSynchronizer( repository, diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt index c0d87252..344a3249 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/block/CompactBlockProcessor.kt @@ -11,13 +11,12 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanned import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanning import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Stopped import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Validating -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDecryptError import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.EnhanceTransactionError.EnhanceTxDownloadError import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedBranch import cash.z.ecc.android.sdk.exception.CompactBlockProcessorException.MismatchedNetwork -import cash.z.ecc.android.sdk.exception.InitializerException +import cash.z.ecc.android.sdk.exception.InitializeException import cash.z.ecc.android.sdk.exception.RustLayerException import cash.z.ecc.android.sdk.ext.BatchMetrics import cash.z.ecc.android.sdk.ext.ZcashSdk @@ -34,14 +33,14 @@ 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.isEmpty -import cash.z.ecc.android.sdk.internal.transaction.PagedTransactionRepository -import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository +import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twigTask import cash.z.ecc.android.sdk.jni.RustBackend import cash.z.ecc.android.sdk.jni.RustBackendWelding import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.TransactionOverview import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.WalletBalance import cash.z.wallet.sdk.rpc.Service @@ -51,7 +50,6 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first import kotlinx.coroutines.isActive import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -81,7 +79,7 @@ import kotlin.time.Duration.Companion.days @Suppress("TooManyFunctions", "LargeClass") class CompactBlockProcessor internal constructor( val downloader: CompactBlockDownloader, - private val repository: TransactionRepository, + private val repository: DerivedDataRepository, private val rustBackend: RustBackendWelding, minimumHeight: BlockHeight = rustBackend.network.saplingActivationHeight ) { @@ -192,6 +190,7 @@ class CompactBlockProcessor internal constructor( /** * Download compact blocks, verify and scan them until [stop] is called. */ + @Suppress("LongMethod") suspend fun start() = withContext(IO) { verifySetup() updateBirthdayHeight() @@ -225,13 +224,15 @@ class CompactBlockProcessor internal constructor( consecutiveChainErrors.set(0) val napTime = calculatePollInterval() twig( - "$summary${if (result == BlockProcessingResult.FailedEnhance) { + "$summary${ + if (result == BlockProcessingResult.FailedEnhance) { " (but there were" + " enhancement errors! We ignore those, for now. Memos in this block range are" + " probably missing! This will be improved in a future release.)" } else { "" - }}! Sleeping" + + } + }! Sleeping" + " for ${napTime}ms (latest height: ${currentInfo.networkBlockHeight})." ) delay(napTime) @@ -405,7 +406,7 @@ class CompactBlockProcessor internal constructor( } else { twig("enhancing ${newTxs.size} transaction(s)!") // if the first transaction has been added - if (newTxs.size == repository.count()) { + if (newTxs.size.toLong() == repository.getTransactionCount()) { twig("Encountered the first transaction. This changes the birthday height!") updateBirthdayHeight() } @@ -423,30 +424,31 @@ class CompactBlockProcessor internal constructor( Twig.clip("enhancing") } } - // TODO [#683]: we still need a way to identify those transactions that failed to be enhanced // TODO [#683]: https://github.com/zcash/zcash-android-wallet-sdk/issues/683 - private suspend fun enhance(transaction: ConfirmedTransaction) = withContext(Dispatchers.IO) { - var downloaded = false - @Suppress("TooGenericExceptionCaught") - try { - twig("START: enhancing transaction (id:${transaction.id} block:${transaction.minedHeight})") - downloader.fetchTransaction(transaction.rawTransactionId)?.let { tx -> - downloaded = true - twig("decrypting and storing transaction (id:${transaction.id} block:${transaction.minedHeight})") - rustBackend.decryptAndStoreTransaction(tx.data.toByteArray()) - } ?: twig("no transaction found. Nothing to enhance. This probably shouldn't happen.") - twig("DONE: enhancing transaction (id:${transaction.id} block:${transaction.minedHeight})") - } catch (t: Throwable) { - twig("Warning: failure on transaction: error: $t\ttransaction: $transaction") - onProcessorError( - if (downloaded) { - EnhanceTxDecryptError(transaction.minedBlockHeight, t) - } else { - EnhanceTxDownloadError(transaction.minedBlockHeight, t) + private suspend fun enhance(transaction: TransactionOverview) = withContext(Dispatchers.IO) { + enhanceHelper(transaction.id, transaction.rawId.byteArray, transaction.minedHeight) + } + + private suspend fun enhanceHelper(id: Long, rawTransactionId: ByteArray, minedHeight: BlockHeight) { + twig("START: enhancing transaction (id:$id block:$minedHeight)") + + runCatching { + downloader.fetchTransaction(rawTransactionId) + }.onSuccess { tx -> + tx?.let { + runCatching { + twig("decrypting and storing transaction (id:$id block:$minedHeight)") + rustBackend.decryptAndStoreTransaction(it.data.toByteArray()) + }.onSuccess { + twig("DONE: enhancing transaction (id:$id block:$minedHeight)") + }.onFailure { error -> + onProcessorError(EnhanceTxDecryptError(minedHeight, error)) } - ) + } ?: twig("no transaction found. Nothing to enhance. This probably shouldn't happen.") + }.onFailure { error -> + onProcessorError(EnhanceTxDownloadError(minedHeight, error)) } } @@ -908,11 +910,11 @@ class CompactBlockProcessor internal constructor( twig("=================== BLOCKS [$errorHeight..${errorHeight.value + count - 1}]: START ========") repeat(count) { i -> val height = errorHeight + i - val block = downloader.compactBlockStore.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 // the hash another way but prevHash is correctly null. val hash = block?.hash?.toByteArray() - ?: (repository as PagedTransactionRepository).findBlockHash(height) + ?: repository.findBlockHash(height) twig( "block: $height\thash=${hash?.toHexReversed()} \tprevHash=${ block?.prevHash?.toByteArray()?.toHexReversed() @@ -923,11 +925,11 @@ class CompactBlockProcessor internal constructor( } private suspend fun fetchValidationErrorInfo(errorHeight: BlockHeight): ValidationErrorInfo { - val hash = (repository as PagedTransactionRepository).findBlockHash(errorHeight + 1) + val hash = repository.findBlockHash(errorHeight + 1) ?.toHexReversed() val prevHash = repository.findBlockHash(errorHeight)?.toHexReversed() - val compactBlock = downloader.compactBlockStore.findCompactBlock(errorHeight + 1) + val compactBlock = downloader.compactBlockRepository.findCompactBlock(errorHeight + 1) val expectedPrevHash = compactBlock?.prevHash?.toByteArray()?.toHexReversed() return ValidationErrorInfo(errorHeight, hash, expectedPrevHash, prevHash) } @@ -975,10 +977,7 @@ class CompactBlockProcessor internal constructor( var oldestTransactionHeight: BlockHeight? = null @Suppress("TooGenericExceptionCaught") try { - val tempOldestTransactionHeight = repository.receivedTransactions - .first() - .lastOrNull() - ?.minedBlockHeight + val tempOldestTransactionHeight = repository.getOldestTransaction()?.minedHeight ?: lowerBoundHeight // to be safe adjust for reorgs (and generally a little cushion is good for privacy) // so we round down to the nearest 100 and then subtract 100 to ensure that the result is always at least @@ -1036,7 +1035,7 @@ class CompactBlockProcessor internal constructor( rustBackend.getSaplingReceiver( rustBackend.getCurrentAddress(account.value) ) - ?: throw InitializerException.MissingAddressException("legacy Sapling") + ?: throw InitializeException.MissingAddressException("legacy Sapling") /** * Get the legacy transparent address corresponding to the current unified address for the given wallet account. @@ -1047,7 +1046,7 @@ class CompactBlockProcessor internal constructor( rustBackend.getTransparentReceiver( rustBackend.getCurrentAddress(account.value) ) - ?: throw InitializerException.MissingAddressException("legacy transparent") + ?: throw InitializeException.MissingAddressException("legacy transparent") /** * Calculates the latest balance info. Defaults to the first account. diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Account.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Account.kt deleted file mode 100644 index 2500ec8d..00000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Account.kt +++ /dev/null @@ -1,16 +0,0 @@ -package cash.z.ecc.android.sdk.db.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity - -@Entity( - tableName = "accounts", - primaryKeys = ["account"] -) -data class Account( - - val account: Int? = 0, - - @ColumnInfo(name = "ufvk") - val unifiedFullViewingKey: String? = "" -) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Block.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Block.kt deleted file mode 100644 index e2fcd61a..00000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Block.kt +++ /dev/null @@ -1,34 +0,0 @@ -package cash.z.ecc.android.sdk.db.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity - -@Entity(primaryKeys = ["height"], tableName = "blocks") -data class Block( - val height: Int?, - @ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "hash") - val hash: ByteArray, - val time: Int, - @ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "sapling_tree") - val saplingTree: ByteArray -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Block) return false - - if (height != other.height) return false - if (!hash.contentEquals(other.hash)) return false - if (time != other.time) return false - if (!saplingTree.contentEquals(other.saplingTree)) return false - - return true - } - - override fun hashCode(): Int { - var result = height ?: 0 - result = 31 * result + hash.contentHashCode() - result = 31 * result + time - result = 31 * result + saplingTree.contentHashCode() - return result - } -} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Received.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Received.kt deleted file mode 100644 index 9428aebc..00000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Received.kt +++ /dev/null @@ -1,65 +0,0 @@ -package cash.z.ecc.android.sdk.db.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.RoomWarnings - -@Entity( - tableName = "received_notes", - primaryKeys = ["id_note"], - foreignKeys = [ - ForeignKey( - entity = TransactionEntity::class, - parentColumns = ["id_tx"], - childColumns = ["tx"] - ), - ForeignKey( - entity = Account::class, - parentColumns = ["account"], - childColumns = ["account"] - ), - ForeignKey( - entity = TransactionEntity::class, - parentColumns = ["id_tx"], - childColumns = ["spent"] - ) - ] -) -@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD) -data class Received( - @ColumnInfo(name = "id_note") - val id: Int? = 0, - - /** - * A reference to the transaction this note was received in - */ - @ColumnInfo(name = "tx") - val transactionId: Int = 0, - - @ColumnInfo(name = "output_index") - val outputIndex: Int = 0, - - val account: Int = 0, - val value: Long = 0, - - /** - * A reference to the transaction this note was later spent in - */ - val spent: Int? = 0, - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - val diversifier: ByteArray = byteArrayOf(), - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - val rcm: ByteArray = byteArrayOf(), - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - val nf: ByteArray = byteArrayOf(), - - @ColumnInfo(name = "is_change") - val isChange: Boolean = false, - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - val memo: ByteArray? = byteArrayOf() -) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Sent.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Sent.kt deleted file mode 100644 index 2b00af11..00000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Sent.kt +++ /dev/null @@ -1,78 +0,0 @@ -package cash.z.ecc.android.sdk.db.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.RoomWarnings - -@Entity( - tableName = "sent_notes", - primaryKeys = ["id_note"], - foreignKeys = [ - ForeignKey( - entity = TransactionEntity::class, - parentColumns = ["id_tx"], - childColumns = ["tx"] - ), ForeignKey( - entity = Account::class, - parentColumns = ["account"], - childColumns = ["from_account"] - ) - ] -) -@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD) -data class Sent( - @ColumnInfo(name = "id_note") - val id: Int? = 0, - - @ColumnInfo(name = "tx") - val transactionId: Long = 0, - - @ColumnInfo(name = "output_pool") - val outputPool: Int = 0, - - @ColumnInfo(name = "output_index") - val outputIndex: Int = 0, - - @ColumnInfo(name = "from_account") - val account: Int = 0, - - val address: String = "", - - val value: Long = 0, - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - val memo: ByteArray? = byteArrayOf() - -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Sent) return false - - if (id != other.id) return false - if (transactionId != other.transactionId) return false - if (outputPool != other.outputPool) return false - if (outputIndex != other.outputIndex) return false - if (account != other.account) return false - if (address != other.address) return false - if (value != other.value) return false - if (memo != null) { - if (other.memo == null) return false - if (!memo.contentEquals(other.memo)) return false - } else if (other.memo != null) return false - - return true - } - - override fun hashCode(): Int { - var result = id ?: 0 - result = 31 * result + transactionId.hashCode() - result = 31 * result + outputPool - result = 31 * result + outputIndex - result = 31 * result + account - result = 31 * result + address.hashCode() - result = 31 * result + value.hashCode() - result = 31 * result + (memo?.contentHashCode() ?: 0) - return result - } -} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt deleted file mode 100644 index d1047029..00000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Transactions.kt +++ /dev/null @@ -1,417 +0,0 @@ -@file:Suppress("TooManyFunctions") - -package cash.z.ecc.android.sdk.db.entity - -import android.text.format.DateUtils -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.PrimaryKey -import androidx.room.RoomWarnings -import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManager -import cash.z.ecc.android.sdk.model.BlockHeight -import cash.z.ecc.android.sdk.model.Zatoshi - -// -// Entities -// - -@Entity( - primaryKeys = ["id_tx"], - tableName = "transactions", - foreignKeys = [ - ForeignKey( - entity = Block::class, - parentColumns = ["height"], - childColumns = ["block"] - ) - ] -) -@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD) -data class TransactionEntity( - @ColumnInfo(name = "id_tx") - val id: Long?, - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "txid") - val transactionId: ByteArray, - - @ColumnInfo(name = "tx_index") - val transactionIndex: Int?, - - val created: String?, - - @ColumnInfo(name = "expiry_height") - val expiryHeight: Int?, - - @ColumnInfo(name = "block") - val minedHeight: Int?, - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - val raw: ByteArray? -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is TransactionEntity) return false - - if (id != other.id) return false - if (!transactionId.contentEquals(other.transactionId)) return false - if (transactionIndex != other.transactionIndex) return false - if (created != other.created) return false - if (expiryHeight != other.expiryHeight) return false - if (minedHeight != other.minedHeight) return false - if (raw != null) { - if (other.raw == null) return false - if (!raw.contentEquals(other.raw)) return false - } else if (other.raw != null) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + transactionId.contentHashCode() - result = 31 * result + (transactionIndex ?: 0) - result = 31 * result + (created?.hashCode() ?: 0) - result = 31 * result + (expiryHeight ?: 0) - result = 31 * result + (minedHeight ?: 0) - result = 31 * result + (raw?.contentHashCode() ?: 0) - return result - } -} - -@Entity(tableName = "pending_transactions") -data class PendingTransactionEntity( - @PrimaryKey(autoGenerate = true) - override val id: Long = 0, - override val toAddress: String = "", - override val value: Long = -1, - override val memo: ByteArray? = byteArrayOf(), - override val accountIndex: Int, - override val minedHeight: Long = -1, - override val expiryHeight: Long = -1, - - override val cancelled: Int = 0, - override val encodeAttempts: Int = -1, - override val submitAttempts: Int = -1, - override val errorMessage: String? = null, - override val errorCode: Int? = null, - override val createTime: Long = System.currentTimeMillis(), - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - override val raw: ByteArray = byteArrayOf(), - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - override val rawTransactionId: ByteArray? = byteArrayOf() -) : PendingTransaction { - - val valueZatoshi: Zatoshi - get() = Zatoshi(value) - - @Suppress("ComplexMethod") - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is PendingTransactionEntity) return false - - if (id != other.id) return false - if (toAddress != other.toAddress) return false - if (value != other.value) return false - if (memo != null) { - if (other.memo == null) return false - if (!memo.contentEquals(other.memo)) return false - } else if (other.memo != null) return false - if (accountIndex != other.accountIndex) return false - if (minedHeight != other.minedHeight) return false - if (expiryHeight != other.expiryHeight) return false - if (cancelled != other.cancelled) return false - if (encodeAttempts != other.encodeAttempts) return false - if (submitAttempts != other.submitAttempts) return false - if (errorMessage != other.errorMessage) return false - if (errorCode != other.errorCode) return false - if (createTime != other.createTime) return false - if (!raw.contentEquals(other.raw)) return false - if (rawTransactionId != null) { - if (other.rawTransactionId == null) return false - if (!rawTransactionId.contentEquals(other.rawTransactionId)) return false - } else if (other.rawTransactionId != null) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + toAddress.hashCode() - result = 31 * result + value.hashCode() - result = 31 * result + (memo?.contentHashCode() ?: 0) - result = 31 * result + accountIndex - result = 31 * result + minedHeight.hashCode() - result = 31 * result + expiryHeight.hashCode() - result = 31 * result + cancelled - result = 31 * result + encodeAttempts - result = 31 * result + submitAttempts - result = 31 * result + (errorMessage?.hashCode() ?: 0) - result = 31 * result + (errorCode ?: 0) - result = 31 * result + createTime.hashCode() - result = 31 * result + raw.contentHashCode() - result = 31 * result + (rawTransactionId?.contentHashCode() ?: 0) - return result - } -} - -// -// Query Objects -// - -/** - * A mined, shielded transaction. Since this is a [MinedTransaction], it represents data - * on the blockchain. - */ -data class ConfirmedTransaction( - override val id: Long = 0L, - override val value: Long = 0L, - override val memo: ByteArray? = ByteArray(0), - override val noteId: Long = 0L, - override val blockTimeInSeconds: Long = 0L, - override val minedHeight: Long = -1, - override val transactionIndex: Int, - override val rawTransactionId: ByteArray = ByteArray(0), - - // properties that differ from received transactions - val toAddress: String? = null, - val expiryHeight: Int? = null, - override val raw: ByteArray? = byteArrayOf() -) : MinedTransaction, SignedTransaction { - - val minedBlockHeight - get() = if (minedHeight == -1L) { - null - } else { - BlockHeight(minedHeight) - } - - @Suppress("ComplexMethod") - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ConfirmedTransaction) return false - - if (id != other.id) return false - if (value != other.value) return false - if (memo != null) { - if (other.memo == null) return false - if (!memo.contentEquals(other.memo)) return false - } else if (other.memo != null) return false - if (noteId != other.noteId) return false - if (blockTimeInSeconds != other.blockTimeInSeconds) return false - if (minedHeight != other.minedHeight) return false - if (transactionIndex != other.transactionIndex) return false - if (!rawTransactionId.contentEquals(other.rawTransactionId)) return false - if (toAddress != other.toAddress) return false - if (expiryHeight != other.expiryHeight) return false - if (raw != null) { - if (other.raw == null) return false - if (!raw.contentEquals(other.raw)) return false - } else if (other.raw != null) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + value.hashCode() - result = 31 * result + (memo?.contentHashCode() ?: 0) - result = 31 * result + noteId.hashCode() - result = 31 * result + blockTimeInSeconds.hashCode() - result = 31 * result + minedHeight.hashCode() - result = 31 * result + transactionIndex - result = 31 * result + rawTransactionId.contentHashCode() - result = 31 * result + (toAddress?.hashCode() ?: 0) - result = 31 * result + (expiryHeight ?: 0) - result = 31 * result + (raw?.contentHashCode() ?: 0) - return result - } -} - -val ConfirmedTransaction.valueInZatoshi - get() = Zatoshi(value) - -data class EncodedTransaction(val txId: ByteArray, override val raw: ByteArray, val expiryHeight: Long?) : - SignedTransaction { - - val expiryBlockHeight - get() = expiryHeight?.let { BlockHeight(it) } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is EncodedTransaction) return false - - if (!txId.contentEquals(other.txId)) return false - if (!raw.contentEquals(other.raw)) return false - if (expiryHeight != other.expiryHeight) return false - - return true - } - - override fun hashCode(): Int { - var result = txId.contentHashCode() - result = 31 * result + raw.contentHashCode() - result = 31 * result + (expiryHeight?.hashCode() ?: 0) - return result - } -} - -// -// Transaction Interfaces -// - -/** - * Common interface between confirmed transactions on the blockchain and pending transactions being - * constructed. - */ -interface Transaction { - val id: Long - val value: Long - val memo: ByteArray? -} - -/** - * Interface for anything that's able to provide signed transaction bytes. - */ -interface SignedTransaction { - val raw: ByteArray? -} - -/** - * Parent type for transactions that have been mined. This is useful for putting all transactions in - * one list for things like history. A mined tx should have all properties, except possibly a memo. - */ -interface MinedTransaction : Transaction { - val minedHeight: Long - val noteId: Long - val blockTimeInSeconds: Long - val transactionIndex: Int - val rawTransactionId: ByteArray -} - -interface PendingTransaction : SignedTransaction, Transaction { - override val id: Long - override val value: Long - override val memo: ByteArray? - val toAddress: String - val accountIndex: Int - val minedHeight: Long // apparently this can be -1 as an uninitialized value - val expiryHeight: Long // apparently this can be -1 as an uninitialized value - val cancelled: Int - val encodeAttempts: Int - val submitAttempts: Int - val errorMessage: String? - val errorCode: Int? - val createTime: Long - val rawTransactionId: ByteArray? -} - -// -// Extension-oriented design -// - -fun PendingTransaction.isSameTxId(other: MinedTransaction): Boolean { - return rawTransactionId != null && rawTransactionId!!.contentEquals(other.rawTransactionId) -} - -fun PendingTransaction.isSameTxId(other: PendingTransaction): Boolean { - return rawTransactionId != null && other.rawTransactionId != null && - rawTransactionId!!.contentEquals(other.rawTransactionId!!) -} - -fun PendingTransaction.hasRawTransactionId(): Boolean { - return rawTransactionId != null && (rawTransactionId?.isNotEmpty() == true) -} - -fun PendingTransaction.isCreating(): Boolean { - return (raw?.isEmpty() != false) && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding() -} - -fun PendingTransaction.isCreated(): Boolean { - return (raw?.isEmpty() == false) && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding() -} - -fun PendingTransaction.isFailedEncoding(): Boolean { - return (raw?.isEmpty() != false) && encodeAttempts > 0 -} - -fun PendingTransaction.isFailedSubmit(): Boolean { - return errorMessage != null || (errorCode != null && errorCode!! < 0) -} - -fun PendingTransaction.isFailure(): Boolean { - return isFailedEncoding() || isFailedSubmit() -} - -fun PendingTransaction.isCancelled(): Boolean { - return cancelled > 0 -} - -fun PendingTransaction.isMined(): Boolean { - return minedHeight > 0 -} - -fun PendingTransaction.isSubmitted(): Boolean { - return submitAttempts > 0 -} - -fun PendingTransaction.isExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean { - // TODO [#687]: test for off-by-one error here. Should we use <= or < - // TODO [#687]: https://github.com/zcash/zcash-android-wallet-sdk/issues/687 - if (latestHeight == null || - latestHeight.value < saplingActivationHeight.value || - expiryHeight < saplingActivationHeight.value - ) { - return false - } - return expiryHeight < latestHeight.value -} - -// if we don't have info on a pendingtx after 100 blocks then it's probably safe to stop polling! -@Suppress("MagicNumber") -fun PendingTransaction.isLongExpired(latestHeight: BlockHeight?, saplingActivationHeight: BlockHeight): Boolean { - if (latestHeight == null || - latestHeight.value < saplingActivationHeight.value || - expiryHeight < saplingActivationHeight.value - ) { - return false - } - return (latestHeight.value - expiryHeight) > 100 -} - -fun PendingTransaction.isMarkedForDeletion(): Boolean { - return rawTransactionId == null && - (errorCode ?: 0) == PersistentTransactionManager.SAFE_TO_DELETE_ERROR_CODE -} - -@Suppress("MagicNumber") -fun PendingTransaction.isSafeToDiscard(): Boolean { - // invalid dates shouldn't happen or should be temporary - if (createTime < 0) return false - - val age = System.currentTimeMillis() - createTime - val smallThreshold = 30 * DateUtils.MINUTE_IN_MILLIS - val hugeThreshold = 30 * DateUtils.DAY_IN_MILLIS - return when { - // if it is mined, then it is not pending so it can be deleted fairly quickly from this db - isMined() && age > smallThreshold -> true - // if a tx fails to encode, then there's not much we can do with it - isFailedEncoding() && age > smallThreshold -> true - // don't delete failed submissions until they've been cleaned up, properly, or else we lose - // the ability to remove them in librustzcash prior to expiration - isFailedSubmit() && isMarkedForDeletion() -> true - !isMined() && age > hugeThreshold -> true - else -> false - } -} - -fun PendingTransaction.isPending(currentHeight: BlockHeight?): Boolean { - // not mined and not expired and successfully created - return !isSubmitSuccess() && minedHeight == -1L && - (expiryHeight == -1L || expiryHeight > (currentHeight?.value ?: 0L)) && raw != null -} - -fun PendingTransaction.isSubmitSuccess(): Boolean { - return submitAttempts > 0 && (errorCode != null && errorCode!! >= 0) && errorMessage == null -} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Utxo.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Utxo.kt deleted file mode 100644 index d94499d9..00000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/Utxo.kt +++ /dev/null @@ -1,73 +0,0 @@ -package cash.z.ecc.android.sdk.db.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.RoomWarnings - -@Entity( - tableName = "utxos", - primaryKeys = ["id_utxo"], - foreignKeys = [ - ForeignKey( - entity = TransactionEntity::class, - parentColumns = ["id_tx"], - childColumns = ["spent_in_tx"] - ) - ] -) -@SuppressWarnings(RoomWarnings.MISSING_INDEX_ON_FOREIGN_KEY_CHILD) -data class Utxo( - @ColumnInfo(name = "id_utxo") - val id: Long? = 0L, - - val address: String = "", - - @ColumnInfo(name = "prevout_txid", typeAffinity = ColumnInfo.BLOB) - val txid: ByteArray = byteArrayOf(), - - @ColumnInfo(name = "prevout_idx") - val transactionIndex: Int = -1, - - @ColumnInfo(typeAffinity = ColumnInfo.BLOB) - val script: ByteArray = byteArrayOf(), - - @ColumnInfo(name = "value_zat") - val value: Long = 0L, - - val height: Int = -1, - - /** - * A reference to the transaction this note was later spent in - */ - @ColumnInfo(name = "spent_in_tx") - val spent: Int? = 0 -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Utxo) return false - - if (id != other.id) return false - if (address != other.address) return false - if (!txid.contentEquals(other.txid)) return false - if (transactionIndex != other.transactionIndex) return false - if (!script.contentEquals(other.script)) return false - if (value != other.value) return false - if (height != other.height) return false - if (spent != other.spent) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + address.hashCode() - result = 31 * result + txid.contentHashCode() - result = 31 * result + transactionIndex - result = 31 * result + script.contentHashCode() - result = 31 * result + value.hashCode() - result = 31 * result + height - result = 31 * result + (spent ?: 0) - return result - } -} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt index bb5adfd4..e39fa3a3 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/exception/Exceptions.kt @@ -177,30 +177,30 @@ sealed class BirthdayException(message: String, cause: Throwable? = null) : SdkE /** * Exceptions thrown by the initializer. */ -sealed class InitializerException(message: String, cause: Throwable? = null) : SdkException(message, cause) { - object SeedRequired : InitializerException( +sealed class InitializeException(message: String, cause: Throwable? = null) : SdkException(message, cause) { + object SeedRequired : InitializeException( "A pending database migration requires the wallet's seed. Call this initialization " + "method again with the seed." ) - class FalseStart(cause: Throwable?) : InitializerException("Failed to initialize accounts due to: $cause", cause) - class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializerException( + class FalseStart(cause: Throwable?) : InitializeException("Failed to initialize accounts due to: $cause", cause) + class AlreadyInitializedException(cause: Throwable, dbPath: String) : InitializeException( "Failed to initialize the blocks table" + " because it already exists in $dbPath", cause ) - object MissingBirthdayException : InitializerException( + object MissingBirthdayException : InitializeException( "Expected a birthday for this wallet but failed to find one. This usually means that " + "wallet setup did not happen correctly. A workaround might be to interpret the " + "birthday, based on the contents of the wallet data but it is probably better " + "not to mask this error because the root issue should be addressed." ) - object MissingViewingKeyException : InitializerException( + object MissingViewingKeyException : InitializeException( "Expected a unified viewingKey for this wallet but failed to find one. This usually means" + " that wallet setup happened incorrectly. A workaround might be to derive the" + " unified viewingKey from the seed or seedPhrase, if they exist, but it is probably" + " better not to mask this error because the root issue should be addressed." ) - class MissingAddressException(description: String, cause: Throwable? = null) : InitializerException( + class MissingAddressException(description: String, cause: Throwable? = null) : InitializeException( "Expected a $description address for this wallet but failed to find one. This usually" + " means that wallet setup happened incorrectly. If this problem persists, a" + " workaround might be to go to settings and WIPE the wallet and rescan. Doing so" + @@ -209,18 +209,18 @@ sealed class InitializerException(message: String, cause: Throwable? = null) : S if (cause != null) "\nCaused by: $cause" else "" ) object DatabasePathException : - InitializerException( + InitializeException( "Critical failure to locate path for storing databases. Perhaps this device prevents" + " apps from storing data? We cannot initialize the wallet unless we can store" + " data." ) - class InvalidBirthdayHeightException(birthday: BlockHeight?, network: ZcashNetwork) : InitializerException( + class InvalidBirthdayHeightException(birthday: BlockHeight?, network: ZcashNetwork) : InitializeException( "Invalid birthday height of ${birthday?.value}. The birthday height must be at least the height of" + " Sapling activation on ${network.networkName} (${network.saplingActivationHeight})." ) - object MissingDefaultBirthdayException : InitializerException( + object MissingDefaultBirthdayException : InitializeException( "The birthday height is missing and it is unclear which value to use as a default." ) } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/NoBackupContextWrapper.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/NoBackupContextWrapper.kt index 2d36f52d..2bc89294 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/NoBackupContextWrapper.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/NoBackupContextWrapper.kt @@ -2,8 +2,6 @@ package cash.z.ecc.android.sdk.internal import android.content.Context import android.content.ContextWrapper -import android.os.Build -import androidx.annotation.RequiresApi import java.io.File /** @@ -18,7 +16,6 @@ import java.io.File * @param parentDir The directory in which is the database file placed. * @return Wrapped context class. */ -@RequiresApi(Build.VERSION_CODES.O_MR1) internal class NoBackupContextWrapper( context: Context, private val parentDir: File diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDownloader.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDownloader.kt index fd2edd9d..89918c61 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDownloader.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDownloader.kt @@ -2,6 +2,7 @@ package cash.z.ecc.android.sdk.internal.block import cash.z.ecc.android.sdk.exception.LightWalletException import cash.z.ecc.android.sdk.internal.ext.retryUpTo import cash.z.ecc.android.sdk.internal.ext.tryWarn +import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.BlockHeight @@ -21,17 +22,17 @@ import kotlinx.coroutines.withContext * data; although, by default the SDK uses gRPC and SQL. * * @property lightWalletService the service used for requesting compact blocks - * @property compactBlockStore responsible for persisting the compact blocks that are received + * @property compactBlockRepository responsible for persisting the compact blocks that are received */ -open class CompactBlockDownloader private constructor(val compactBlockStore: CompactBlockStore) { +open class CompactBlockDownloader private constructor(val compactBlockRepository: CompactBlockRepository) { lateinit var lightWalletService: LightWalletService private set constructor( lightWalletService: LightWalletService, - compactBlockStore: CompactBlockStore - ) : this(compactBlockStore) { + compactBlockRepository: CompactBlockRepository + ) : this(compactBlockRepository) { this.lightWalletService = lightWalletService } @@ -46,7 +47,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com */ suspend fun downloadBlockRange(heightRange: ClosedRange): Int = withContext(IO) { val result = lightWalletService.getBlockRange(heightRange) - compactBlockStore.write(result) + compactBlockRepository.write(result) } /** @@ -58,7 +59,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com suspend fun rewindToHeight(height: BlockHeight) = // TODO [#685]: cancel anything in flight // TODO [#685]: https://github.com/zcash/zcash-android-wallet-sdk/issues/685 - compactBlockStore.rewindTo(height) + compactBlockRepository.rewindTo(height) /** * Return the latest block height known by the lightwalletService. @@ -69,12 +70,12 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com lightWalletService.getLatestBlockHeight() /** - * Return the latest block height that has been persisted into the [CompactBlockStore]. + * Return the latest block height that has been persisted into the [CompactBlockRepository]. * * @return the latest block height that has been persisted. */ suspend fun getLastDownloadedHeight() = - compactBlockStore.getLatestHeight() + compactBlockRepository.getLatestHeight() suspend fun getServerInfo(): Service.LightdInfo = withContext(IO) { lateinit var result: Service.LightdInfo @@ -126,7 +127,7 @@ open class CompactBlockDownloader private constructor(val compactBlockStore: Com withContext(Dispatchers.IO) { lightWalletService.shutdown() } - compactBlockStore.close() + compactBlockRepository.close() } /** diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CursorExt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CursorExt.kt new file mode 100644 index 00000000..9dd01e51 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CursorExt.kt @@ -0,0 +1,12 @@ +@file:Suppress("ktlint:filename") + +package cash.z.ecc.android.sdk.internal.db + +import android.database.Cursor + +internal fun Cursor.optLong(columnIndex: Int): Long? = + if (isNull(columnIndex)) { + null + } else { + getLong(columnIndex) + } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CursorParser.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CursorParser.kt new file mode 100644 index 00000000..1dcd3be1 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CursorParser.kt @@ -0,0 +1,16 @@ +package cash.z.ecc.android.sdk.internal.db + +import android.database.Cursor + +fun interface CursorParser { + /** + * Extracts an object from a Cursor. This method assumes that the Cursor contains all the needed columns and + * that the Cursor is positioned to a row that is ready to be read. This method, in turn, will not mutate + * the Cursor or move the Cursor position. + * + * @param cursor Cursor from a query to a contract this parser can handle. + * @return a new Object. + * @throws AssertionError If the cursor is closed or the cursor is out of range. + */ + fun newObject(cursor: Cursor): T +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/DatabaseCoordinator.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DatabaseCoordinator.kt similarity index 98% rename from sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/DatabaseCoordinator.kt rename to sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DatabaseCoordinator.kt index dd02b0ba..45b6c25a 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/DatabaseCoordinator.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DatabaseCoordinator.kt @@ -1,10 +1,10 @@ -package cash.z.ecc.android.sdk.db +package cash.z.ecc.android.sdk.internal.db import android.content.Context import androidx.annotation.VisibleForTesting import androidx.room.Room import androidx.room.RoomDatabase -import cash.z.ecc.android.sdk.exception.InitializerException +import cash.z.ecc.android.sdk.exception.InitializeException import cash.z.ecc.android.sdk.ext.ZcashSdk import cash.z.ecc.android.sdk.internal.AndroidApiVersion import cash.z.ecc.android.sdk.internal.Files @@ -325,7 +325,7 @@ internal class DatabaseCoordinator private constructor(context: Context) { */ private suspend fun getDatabaseParentDir(appContext: Context): File { return appContext.getDatabasePathSuspend("unused.db").parentFile - ?: throw InitializerException.DatabasePathException + ?: throw InitializeException.DatabasePathException } /** @@ -401,7 +401,7 @@ internal fun commonDatabaseBuilder( Room.databaseBuilder( NoBackupContextWrapper( context, - databaseFile.parentFile ?: throw InitializerException.DatabasePathException + databaseFile.parentFile ?: throw InitializeException.DatabasePathException ), klass, databaseFile.name diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt deleted file mode 100644 index bca20157..00000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/DerivedDataDb.kt +++ /dev/null @@ -1,541 +0,0 @@ -package cash.z.ecc.android.sdk.internal.db - -import androidx.paging.DataSource -import androidx.room.Dao -import androidx.room.Database -import androidx.room.Query -import androidx.room.RoomDatabase -import androidx.room.RoomWarnings -import androidx.room.Transaction -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import cash.z.ecc.android.sdk.db.entity.Account -import cash.z.ecc.android.sdk.db.entity.Block -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction -import cash.z.ecc.android.sdk.db.entity.EncodedTransaction -import cash.z.ecc.android.sdk.db.entity.Received -import cash.z.ecc.android.sdk.db.entity.Sent -import cash.z.ecc.android.sdk.db.entity.TransactionEntity -import cash.z.ecc.android.sdk.db.entity.Utxo -import cash.z.ecc.android.sdk.internal.twig - -// -// Database -// - -/** - * The "Data DB," where all data derived from the compact blocks is stored. Most importantly, this - * database contains transaction information and can be queried for the current balance. The - * "blocks" table contains a copy of everything that has been scanned. In the future, that table can - * be truncated up to the last scanned block, for storage efficiency. Wallets should only read from, - * but never write to, this database. - */ -@Database( - entities = [ - TransactionEntity::class, - Block::class, - Received::class, - Account::class, - Sent::class, - Utxo::class - ], - version = 7, - exportSchema = true -) -abstract class DerivedDataDb : RoomDatabase() { - abstract fun transactionDao(): TransactionDao - abstract fun blockDao(): BlockDao - abstract fun receivedDao(): ReceivedDao - abstract fun sentDao(): SentDao - abstract fun accountDao(): AccountDao - - // - // Migrations - // - - @Suppress("MagicNumber") - companion object { - - val MIGRATION_3_4 = object : Migration(3, 4) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("PRAGMA foreign_keys = OFF;") - database.execSQL( - """ - CREATE TABLE IF NOT EXISTS received_notes_new ( - id_note INTEGER PRIMARY KEY, tx INTEGER NOT NULL, - output_index INTEGER NOT NULL, account INTEGER NOT NULL, - diversifier BLOB NOT NULL, value INTEGER NOT NULL, - rcm BLOB NOT NULL, nf BLOB NOT NULL UNIQUE, - is_change INTEGER NOT NULL, memo BLOB, - spent INTEGER, - FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (account) REFERENCES accounts(account), - FOREIGN KEY (spent) REFERENCES transactions(id_tx), - CONSTRAINT tx_output UNIQUE (tx, output_index) - ); - """.trimIndent() - ) - database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;") - database.execSQL("DROP TABLE received_notes;") - database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;") - database.execSQL("PRAGMA foreign_keys = ON;") - } - } - - val MIGRATION_4_3 = object : Migration(4, 3) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("PRAGMA foreign_keys = OFF;") - database.execSQL( - """ - CREATE TABLE IF NOT EXISTS received_notes_new ( - id_note INTEGER PRIMARY KEY, - tx INTEGER NOT NULL, - output_index INTEGER NOT NULL, - account INTEGER NOT NULL, - diversifier BLOB NOT NULL, - value INTEGER NOT NULL, - rcm BLOB NOT NULL, - nf BLOB NOT NULL UNIQUE, - is_change INTEGER NOT NULL, - memo BLOB, - spent INTEGER, - FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (account) REFERENCES accounts(account), - FOREIGN KEY (spent) REFERENCES transactions(id_tx), - CONSTRAINT tx_output UNIQUE (tx, output_index) - ); - """.trimIndent() - ) - database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;") - database.execSQL("DROP TABLE received_notes;") - database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;") - database.execSQL("PRAGMA foreign_keys = ON;") - } - } - - val MIGRATION_4_5 = object : Migration(4, 5) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("PRAGMA foreign_keys = OFF;") - database.execSQL( - """ - CREATE TABLE IF NOT EXISTS received_notes_new ( - id_note INTEGER PRIMARY KEY, - tx INTEGER NOT NULL, - output_index INTEGER NOT NULL, - account INTEGER NOT NULL, - diversifier BLOB NOT NULL, - value INTEGER NOT NULL, - rcm BLOB NOT NULL, - nf BLOB NOT NULL UNIQUE, - is_change INTEGER NOT NULL, - memo BLOB, - spent INTEGER, - FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (account) REFERENCES accounts(account), - FOREIGN KEY (spent) REFERENCES transactions(id_tx), - CONSTRAINT tx_output UNIQUE (tx, output_index) - ); - """.trimIndent() - ) - database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;") - database.execSQL("DROP TABLE received_notes;") - database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;") - database.execSQL("PRAGMA foreign_keys = ON;") - } - } - - val MIGRATION_5_6 = object : Migration(5, 6) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL( - """ - CREATE TABLE IF NOT EXISTS utxos ( - id_utxo INTEGER PRIMARY KEY, - address TEXT NOT NULL, - prevout_txid BLOB NOT NULL, - prevout_idx INTEGER NOT NULL, - script BLOB NOT NULL, - value_zat INTEGER NOT NULL, - height INTEGER NOT NULL, - spent_in_tx INTEGER, - FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx), - CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx) - ); - """.trimIndent() - ) - } - } - - val MIGRATION_6_7 = object : Migration(6, 7) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("PRAGMA foreign_keys = OFF;") - database.execSQL( - """ - CREATE TABLE IF NOT EXISTS accounts_new ( - account INTEGER PRIMARY KEY, - extfvk TEXT NOT NULL, - address TEXT NOT NULL, - transparent_address TEXT NOT NULL - ); - """.trimIndent() - ) - database.execSQL("DROP TABLE accounts;") - database.execSQL("ALTER TABLE accounts_new RENAME TO accounts;") - database.execSQL("PRAGMA foreign_keys = ON;") - } - } - } -} - -// -// Data Access Objects -// - -/** - * The data access object for blocks, used for determining the last scanned height. - */ -@Dao -interface BlockDao { - @Query("SELECT COUNT(height) FROM blocks") - suspend fun count(): Int - - @Query("SELECT MAX(height) FROM blocks") - suspend fun lastScannedHeight(): Long - - @Query("SELECT MIN(height) FROM blocks") - suspend fun firstScannedHeight(): Long - - @Query("SELECT hash FROM BLOCKS WHERE height = :height") - suspend fun findHashByHeight(height: Long): ByteArray? -} - -/** - * The data access object for notes, used for determining whether transactions exist. - */ -@Dao -interface ReceivedDao { - @Query("SELECT COUNT(tx) FROM received_notes") - suspend fun count(): Int -} - -/** - * The data access object for sent notes, used for determining whether outbound transactions exist. - */ -@Dao -interface SentDao { - @Query("SELECT COUNT(tx) FROM sent_notes") - suspend fun count(): Int -} - -@Dao -interface AccountDao { - @Query("SELECT COUNT(account) FROM accounts") - suspend fun count(): Int -} - -/** - * The data access object for transactions, used for querying all transaction information, including - * whether transactions are mined. - */ -@Dao -@Suppress("TooManyFunctions") -interface TransactionDao { - @Query("SELECT COUNT(id_tx) FROM transactions") - suspend fun count(): Int - - @Query("SELECT COUNT(block) FROM transactions WHERE block IS NULL") - suspend fun countUnmined(): Int - - @Query( - """ - SELECT transactions.txid AS txId, - transactions.raw AS raw, - transactions.expiry_height AS expiryHeight - FROM transactions - WHERE id_tx = :id AND raw is not null - """ - ) - suspend fun findEncodedTransactionById(id: Long): EncodedTransaction? - - @Query( - """ - SELECT transactions.block - FROM transactions - WHERE txid = :rawTransactionId - LIMIT 1 - """ - ) - suspend fun findMinedHeight(rawTransactionId: ByteArray): Long? - - /** - * Query sent transactions that have been mined, sorted so the newest data is at the top. - */ - @Query( - """ - SELECT transactions.id_tx AS id, - transactions.block AS minedHeight, - transactions.tx_index AS transactionIndex, - transactions.txid AS rawTransactionId, - transactions.expiry_height AS expiryHeight, - transactions.raw AS raw, - sent_notes.address AS toAddress, - sent_notes.value AS value, - sent_notes.memo AS memo, - sent_notes.id_note AS noteId, - blocks.time AS blockTimeInSeconds - FROM transactions - LEFT JOIN sent_notes - ON transactions.id_tx = sent_notes.tx - LEFT JOIN blocks - ON transactions.block = blocks.height - WHERE transactions.raw IS NOT NULL - AND minedheight > 0 - ORDER BY block IS NOT NULL, height DESC, time DESC, txid DESC - LIMIT :limit - """ - ) - @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - fun getSentTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory - - /** - * Query transactions, aggregating information on send/receive, sorted carefully so the newest - * data is at the top and the oldest transactions are at the bottom. - */ - @Query( - """ - SELECT transactions.id_tx AS id, - transactions.block AS minedHeight, - transactions.tx_index AS transactionIndex, - transactions.txid AS rawTransactionId, - received_notes.value AS value, - received_notes.memo AS memo, - received_notes.id_note AS noteId, - blocks.time AS blockTimeInSeconds - FROM transactions - LEFT JOIN received_notes - ON transactions.id_tx = received_notes.tx - LEFT JOIN blocks - ON transactions.block = blocks.height - WHERE received_notes.is_change != 1 - ORDER BY minedheight DESC, blocktimeinseconds DESC, id DESC - LIMIT :limit - """ - ) - @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) - fun getReceivedTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory - - /** - * Query all transactions, joining outbound and inbound transactions into the same table. - */ - @Query( - """ - SELECT transactions.id_tx AS id, - transactions.block AS minedHeight, - transactions.tx_index AS transactionIndex, - transactions.txid AS rawTransactionId, - transactions.expiry_height AS expiryHeight, - transactions.raw AS raw, - sent_notes.address AS toAddress, - CASE - WHEN sent_notes.value IS NOT NULL THEN sent_notes.value - ELSE received_notes.value - end AS value, - CASE - WHEN sent_notes.memo IS NOT NULL THEN sent_notes.memo - ELSE received_notes.memo - end AS memo, - CASE - WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note - ELSE received_notes.id_note - end AS noteId, - blocks.time AS blockTimeInSeconds - FROM transactions - LEFT JOIN received_notes - ON transactions.id_tx = received_notes.tx - LEFT JOIN sent_notes - ON transactions.id_tx = sent_notes.tx - LEFT JOIN blocks - ON transactions.block = blocks.height - /* we want all received txs except those that are change and all sent transactions (even those that haven't been - mined yet). Note: every entry in the 'send_notes' table has a non-null value for 'address' */ - WHERE ( sent_notes.address IS NULL - AND received_notes.is_change != 1 ) - OR sent_notes.address IS NOT NULL - ORDER BY ( minedheight IS NOT NULL ), - minedheight DESC, - blocktimeinseconds DESC, - id DESC - LIMIT :limit - """ - ) - fun getAllTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory - - /** - * Query the transactions table over the given block range, this includes transactions that - * should not show up in most UIs. The intended purpose of this request is to find new - * transactions that need to be enhanced via follow-up requests to the server. - */ - @Query( - """ - SELECT transactions.id_tx AS id, - transactions.block AS minedHeight, - transactions.tx_index AS transactionIndex, - transactions.txid AS rawTransactionId, - transactions.expiry_height AS expiryHeight, - transactions.raw AS raw, - sent_notes.address AS toAddress, - CASE - WHEN sent_notes.value IS NOT NULL THEN sent_notes.value - ELSE received_notes.value - end AS value, - CASE - WHEN sent_notes.memo IS NOT NULL THEN sent_notes.memo - ELSE received_notes.memo - end AS memo, - CASE - WHEN sent_notes.id_note IS NOT NULL THEN sent_notes.id_note - ELSE received_notes.id_note - end AS noteId, - blocks.time AS blockTimeInSeconds - FROM transactions - LEFT JOIN received_notes - ON transactions.id_tx = received_notes.tx - LEFT JOIN sent_notes - ON transactions.id_tx = sent_notes.tx - LEFT JOIN blocks - ON transactions.block = blocks.height - WHERE :blockRangeStart <= minedheight - AND minedheight <= :blockRangeEnd - ORDER BY ( minedheight IS NOT NULL ), - minedheight ASC, - blocktimeinseconds DESC, - id DESC - LIMIT :limit - """ - ) - suspend fun findAllTransactionsByRange( - blockRangeStart: Long, - blockRangeEnd: Long = blockRangeStart, - limit: Int = Int.MAX_VALUE - ): List - - // Experimental: cleanup cancelled transactions - // This should probably be a rust call but there's not a lot of bandwidth for this - // work to happen in librustzcash. So prove the concept on our side, first - // then move the logic to the right place. Especially since the data access API is - // coming soon - @Transaction - suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean { - var success = false - @Suppress("TooGenericExceptionCaught") - try { - var hasInitialMatch = false - twig("[cleanup] cleanupCancelledTx starting...") - findUnminedTransactionIds(rawTransactionId).also { - twig("[cleanup] cleanupCancelledTx found ${it.size} matching transactions to cleanup") - }.forEach { transactionId -> - hasInitialMatch = true - removeInvalidOutboundTransaction(transactionId) - } - val hasFinalMatch = findMatchingTransactionId(rawTransactionId) != null - success = hasInitialMatch && !hasFinalMatch - twig("[cleanup] cleanupCancelledTx Done. success? $success") - } catch (t: Throwable) { - twig("[cleanup] failed to cleanup transaction due to: $t") - } - return success - } - - @Transaction - suspend fun removeInvalidOutboundTransaction(transactionId: Long): Boolean { - var success = false - @Suppress("TooGenericExceptionCaught") - try { - twig("[cleanup] removing invalid transactionId:$transactionId") - val result = unspendTransactionNotes(transactionId) - twig("[cleanup] unspent ($result) notes matching transaction $transactionId") - findSentNoteIds(transactionId)?.forEach { noteId -> - twig("[cleanup] WARNING: deleting invalid sent noteId:$noteId") - deleteSentNote(noteId) - } - - // delete the UTXOs because these are effectively cached and we don't have a good way of knowing whether - // they're spent - deleteUtxos(transactionId).let { count -> - twig("[cleanup] removed $count UTXOs matching transactionId $transactionId") - } - - twig("[cleanup] WARNING: deleting invalid transactionId $transactionId") - success = deleteTransaction(transactionId) != 0 - twig("[cleanup] removeInvalidTransaction Done. success? $success") - } catch (t: Throwable) { - twig("[cleanup] failed to remove Invalid Transaction due to: $t") - } - return success - } - - @Transaction - suspend fun deleteExpired(lastHeight: Long): Int { - var count = 0 - findExpiredTxs(lastHeight).forEach { transactionId -> - if (removeInvalidOutboundTransaction(transactionId)) count++ - } - return count - } - - // - // Private-ish functions (these will move to rust, or the data access API eventually) - // - - @Query( - """ - SELECT transactions.id_tx AS id - FROM transactions - WHERE txid = :rawTransactionId - AND block IS NULL - """ - ) - suspend fun findUnminedTransactionIds(rawTransactionId: ByteArray): List - - @Query( - """ - SELECT transactions.id_tx AS id - FROM transactions - WHERE txid = :rawTransactionId - LIMIT 1 - """ - ) - suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long? - - @Query( - """ - SELECT sent_notes.id_note AS id - FROM sent_notes - WHERE tx = :transactionId - """ - ) - suspend fun findSentNoteIds(transactionId: Long): List? - - @Query("DELETE FROM sent_notes WHERE id_note = :id") - suspend fun deleteSentNote(id: Int): Int - - @Query("DELETE FROM transactions WHERE id_tx = :id") - suspend fun deleteTransaction(id: Long): Int - - @Query("UPDATE received_notes SET spent = null WHERE spent = :transactionId") - suspend fun unspendTransactionNotes(transactionId: Long): Int - - @Query("DELETE FROM utxos WHERE spent_in_tx = :utxoId") - suspend fun deleteUtxos(utxoId: Long): Int - - @Query( - """ - SELECT transactions.id_tx - FROM transactions - WHERE created IS NOT NULL - AND block IS NULL - AND tx_index IS NULL - AND expiry_height < :lastheight - """ - ) - suspend fun findExpiredTxs(lastheight: Long): List -} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/ReadOnlySqliteOpenHelper.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/ReadOnlySqliteOpenHelper.kt new file mode 100644 index 00000000..a5a2144d --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/ReadOnlySqliteOpenHelper.kt @@ -0,0 +1,46 @@ +package cash.z.ecc.android.sdk.internal.db + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +internal class ReadOnlySqliteOpenHelper( + context: Context, + name: String, + version: Int +) : SQLiteOpenHelper(context, name, null, version) { + + override fun onCreate(db: SQLiteDatabase?) { + error("Database should be created by Rust libraries") + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + error("Database should be upgraded by Rust libraries") + } + + internal companion object { + /** + * Opens a database that has already been initialized by something else. + * + * @param context Application context. + * @param name Database file name. + * @param databaseVersion Version of the database as set in https://sqlite.org/pragma.html#pragma_user_version + * This is required to bypass database creation/migration logic in Android. + */ + suspend fun openExistingDatabaseAsReadOnly( + context: Context, + name: String, + databaseVersion: Int + ): SQLiteDatabase { + return withContext(Dispatchers.IO) { + ReadOnlySqliteOpenHelper( + context, + name, + databaseVersion + ).readableDatabase + } + } + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/ReadOnlySupportSqliteOpenHelper.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/ReadOnlySupportSqliteOpenHelper.kt new file mode 100644 index 00000000..ddbfefbe --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/ReadOnlySupportSqliteOpenHelper.kt @@ -0,0 +1,61 @@ +package cash.z.ecc.android.sdk.internal.db + +import android.content.Context +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import cash.z.ecc.android.sdk.exception.InitializeException +import cash.z.ecc.android.sdk.internal.AndroidApiVersion +import cash.z.ecc.android.sdk.internal.NoBackupContextWrapper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File + +object ReadOnlySupportSqliteOpenHelper { + + /** + * Opens a database that has already been initialized by something else. + * + * @param context Application context. + * @param file Database file name. + * @param databaseVersion Version of the database as set in https://sqlite.org/pragma.html#pragma_user_version + * This is required to bypass database creation/migration logic in Android. + */ + suspend fun openExistingDatabaseAsReadOnly( + context: Context, + file: File, + databaseVersion: Int + ): SupportSQLiteDatabase { + return withContext(Dispatchers.IO) { + val config = if (AndroidApiVersion.isAtLeastO_MR1) { + val contextWrapper = NoBackupContextWrapper( + context, + file.parentFile ?: throw InitializeException.DatabasePathException + ) + SupportSQLiteOpenHelper.Configuration.builder(contextWrapper) + .apply { + name(file.name) + callback(ReadOnlyCallback(databaseVersion)) + }.build() + } else { + SupportSQLiteOpenHelper.Configuration.builder(context) + .apply { + name(file.absolutePath) + callback(ReadOnlyCallback(databaseVersion)) + }.build() + } + + FrameworkSQLiteOpenHelperFactory().create(config).readableDatabase + } + } +} + +private class ReadOnlyCallback(version: Int) : SupportSQLiteOpenHelper.Callback(version) { + override fun onCreate(db: SupportSQLiteDatabase) { + error("Database ${db.path} should be created by Rust libraries") + } + + override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { + error("Database ${db.path} should be upgraded by Rust libraries") + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/SQLiteDatabaseExt.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/SQLiteDatabaseExt.kt new file mode 100644 index 00000000..10ed9856 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/SQLiteDatabaseExt.kt @@ -0,0 +1,109 @@ +@file:Suppress("ktlint:filename") + +package cash.z.ecc.android.sdk.internal.db + +import android.database.sqlite.SQLiteDatabase +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteQueryBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import java.util.Locale +import kotlin.coroutines.CoroutineContext + +/** + * Performs a query on a background thread. + * + * Note that this method is best for small queries, as Cursor has an in-memory window of cached data. If iterating + * through a large number of items that exceeds the window, the Cursor may perform additional IO. + */ +@Suppress("LongParameterList") +internal fun SQLiteDatabase.queryAndMap( + table: String, + columns: Array? = null, + selection: String? = null, + selectionArgs: Array? = null, + groupBy: String? = null, + having: String? = null, + orderBy: String? = null, + limit: String? = null, + offset: String? = null, + coroutineContext: CoroutineContext = Dispatchers.IO, + cursorParser: CursorParser +) = flow { + // TODO [#703]: Support blobs for argument binding + // https://github.com/zcash/zcash-android-wallet-sdk/issues/703 + val mappedSelectionArgs = selectionArgs?.onEach { + if (it is ByteArray) { + throw IllegalArgumentException("ByteArray is not supported") + } + }?.map { it.toString() }?.toTypedArray() + + // Counterintuitive but correct. When using the comma syntax, offset comes first. + // When using the keyword syntax, "LIMIT 1 OFFSET 2" then the offset comes second. + val limitAndOffset = if (null == offset) { + limit + } else { + String.format(Locale.ROOT, "%s,%s", offset, limit) // NON-NLS + } + + query( + table, + columns, + selection, + mappedSelectionArgs, + groupBy, + having, + orderBy, + limitAndOffset + ).use { + it.moveToPosition(-1) + while (it.moveToNext()) { + emit(cursorParser.newObject(it)) + } + } +}.flowOn(coroutineContext) + +/** + * Performs a query on a background thread. + * + * Note that this method is best for small queries, as Cursor has an in-memory window of cached data. If iterating + * through a large number of items that exceeds the window, the Cursor may perform additional IO. + */ +@Suppress("LongParameterList") +internal fun SupportSQLiteDatabase.queryAndMap( + table: String, + columns: Array? = null, + selection: String? = null, + selectionArgs: Array? = null, + groupBy: String? = null, + having: String? = null, + orderBy: String? = null, + limit: String? = null, + offset: String? = null, + coroutineContext: CoroutineContext = Dispatchers.IO, + cursorParser: CursorParser +) = flow { + val qb = SupportSQLiteQueryBuilder.builder(table).apply { + columns(columns) + selection(selection, selectionArgs) + having(having) + groupBy(groupBy) + orderBy(orderBy) + + // Counterintuitive but correct. When using the comma syntax, offset comes first. + // When using the keyword syntax, "LIMIT 1 OFFSET 2" then the offset comes second. + if (null == offset) { + limit(limit) + } else { + limit(String.format(Locale.ROOT, "%s,%s", offset, limit)) // NON-NLS + } + } + + query(qb.create()).use { + it.moveToPosition(-1) + while (it.moveToNext()) { + emit(cursorParser.newObject(it)) + } + } +}.flowOn(coroutineContext) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CompactBlockDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/block/CompactBlockDb.kt similarity index 94% rename from sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CompactBlockDb.kt rename to sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/block/CompactBlockDb.kt index 6fcdbc97..ef5e0e94 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/CompactBlockDb.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/block/CompactBlockDb.kt @@ -1,4 +1,4 @@ -package cash.z.ecc.android.sdk.internal.db +package cash.z.ecc.android.sdk.internal.db.block import androidx.room.Dao import androidx.room.Database @@ -7,7 +7,6 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.RoomDatabase import androidx.room.Transaction -import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity // // Database diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/CompactBlockEntity.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/block/CompactBlockEntity.kt similarity index 53% rename from sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/CompactBlockEntity.kt rename to sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/block/CompactBlockEntity.kt index 50fd9495..b3244d47 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/db/entity/CompactBlockEntity.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/block/CompactBlockEntity.kt @@ -1,7 +1,11 @@ -package cash.z.ecc.android.sdk.db.entity +package cash.z.ecc.android.sdk.internal.db.block import androidx.room.ColumnInfo import androidx.room.Entity +import cash.z.ecc.android.sdk.internal.model.CompactBlock +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray +import cash.z.ecc.android.sdk.model.ZcashNetwork @Entity(primaryKeys = ["height"], tableName = "compactblocks") data class CompactBlockEntity( @@ -24,4 +28,14 @@ data class CompactBlockEntity( result = 31 * result + data.contentHashCode() return result } + + internal fun toCompactBlock(zcashNetwork: ZcashNetwork) = CompactBlock( + BlockHeight.new(zcashNetwork, height), + FirstClassByteArray(data) + ) + + companion object { + internal fun fromCompactBlock(compactBlock: CompactBlock) = + CompactBlockEntity(compactBlock.height.value, compactBlock.data.byteArray) + } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDbStore.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/block/DbCompactBlockRepository.kt similarity index 86% rename from sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDbStore.kt rename to sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/block/DbCompactBlockRepository.kt index 80f8d87e..d7cfd072 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockDbStore.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/block/DbCompactBlockRepository.kt @@ -1,12 +1,11 @@ -package cash.z.ecc.android.sdk.internal.block +package cash.z.ecc.android.sdk.internal.db.block import android.content.Context import androidx.room.RoomDatabase -import cash.z.ecc.android.sdk.db.commonDatabaseBuilder -import cash.z.ecc.android.sdk.db.entity.CompactBlockEntity import cash.z.ecc.android.sdk.internal.SdkDispatchers import cash.z.ecc.android.sdk.internal.SdkExecutors -import cash.z.ecc.android.sdk.internal.db.CompactBlockDb +import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder +import cash.z.ecc.android.sdk.internal.repository.CompactBlockRepository import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.wallet.sdk.rpc.CompactFormats @@ -17,10 +16,10 @@ import java.io.File * An implementation of CompactBlockStore that persists information to a database in the given * path. This represents the "cache db" or local cache of compact blocks waiting to be scanned. */ -class CompactBlockDbStore private constructor( +class DbCompactBlockRepository private constructor( private val network: ZcashNetwork, private val cacheDb: CompactBlockDb -) : CompactBlockStore { +) : CompactBlockRepository { private val cacheDao = cacheDb.compactBlockDao() @@ -52,10 +51,10 @@ class CompactBlockDbStore private constructor( appContext: Context, zcashNetwork: ZcashNetwork, databaseFile: File - ): CompactBlockDbStore { + ): DbCompactBlockRepository { val cacheDb = createCompactBlockCacheDb(appContext.applicationContext, databaseFile) - return CompactBlockDbStore(zcashNetwork, cacheDb) + return DbCompactBlockRepository(zcashNetwork, cacheDb) } private fun createCompactBlockCacheDb( diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AccountTable.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AccountTable.kt new file mode 100644 index 00000000..edf7c4c3 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AccountTable.kt @@ -0,0 +1,22 @@ +package cash.z.ecc.android.sdk.internal.db.derived + +import androidx.sqlite.db.SupportSQLiteDatabase +import cash.z.ecc.android.sdk.internal.db.queryAndMap +import kotlinx.coroutines.flow.first + +internal class AccountTable(private val sqliteDatabase: SupportSQLiteDatabase) { + companion object { + + private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS + } + + suspend fun count() = sqliteDatabase.queryAndMap( + AccountTableDefinition.TABLE_NAME, + columns = PROJECTION_COUNT, + cursorParser = { it.getLong(0) } + ).first() +} + +object AccountTableDefinition { + const val TABLE_NAME = "accounts" // $NON-NLS +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AllTransactionView.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AllTransactionView.kt new file mode 100644 index 00000000..f29d5efc --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/AllTransactionView.kt @@ -0,0 +1,142 @@ +package cash.z.ecc.android.sdk.internal.db.derived + +import androidx.sqlite.db.SupportSQLiteDatabase +import cash.z.ecc.android.sdk.internal.db.CursorParser +import cash.z.ecc.android.sdk.internal.db.queryAndMap +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray +import cash.z.ecc.android.sdk.model.TransactionOverview +import cash.z.ecc.android.sdk.model.Zatoshi +import cash.z.ecc.android.sdk.model.ZcashNetwork +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import java.util.Locale +import kotlin.math.absoluteValue + +internal class AllTransactionView( + private val zcashNetwork: ZcashNetwork, + private val sqliteDatabase: SupportSQLiteDatabase +) { + companion object { + + private val ORDER_BY = String.format( + Locale.ROOT, + "%s DESC, %s DESC", // $NON-NLS + AllTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT, + AllTransactionViewDefinition.COLUMN_INTEGER_ID + ) + + private val SELECTION_BLOCK_RANGE = String.format( + Locale.ROOT, + "%s >= ? AND %s <= ?", // $NON-NLS + AllTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT, + AllTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT + ) + + private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS + } + + private val cursorParser: CursorParser = CursorParser { + val idColumnIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_ID) + val minedHeightColumnIndex = + it.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT) + val transactionIndexColumnIndex = it.getColumnIndex( + AllTransactionViewDefinition.COLUMN_INTEGER_TRANSACTION_INDEX + ) + val rawTransactionIdIndex = + it.getColumnIndex(AllTransactionViewDefinition.COLUMN_BLOB_RAW_TRANSACTION_ID) + val expiryHeightIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT) + val rawIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_BLOB_RAW) + val netValueIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_LONG_VALUE) + val feePaidIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_LONG_FEE_PAID) + val isChangeIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_BOOLEAN_IS_CHANGE) + val isWalletInternalIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_BOOLEAN_IS_WALLET_INTERNAL) + val receivedNoteCountIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_RECEIVED_NOTE_COUNT) + val sentNoteCountIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_SENT_NOTE_COUNT) + val memoCountIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_MEMO_COUNT) + val blockTimeIndex = it.getColumnIndex(AllTransactionViewDefinition.COLUMN_INTEGER_BLOCK_TIME) + + val netValueLong = it.getLong(netValueIndex) + val isSent = netValueLong < 0 + + TransactionOverview( + id = it.getLong(idColumnIndex), + rawId = FirstClassByteArray(it.getBlob(rawTransactionIdIndex)), + minedHeight = BlockHeight.new(zcashNetwork, it.getLong(minedHeightColumnIndex)), + expiryHeight = BlockHeight.new(zcashNetwork, it.getLong(expiryHeightIndex)), + index = it.getLong(transactionIndexColumnIndex), + raw = FirstClassByteArray(it.getBlob(rawIndex)), + isSentTransaction = isSent, + netValue = Zatoshi(netValueLong.absoluteValue), + feePaid = Zatoshi(it.getLong(feePaidIndex)), + isChange = it.getInt(isChangeIndex) != 0, + isWalletInternal = it.getInt(isWalletInternalIndex) != 0, + receivedNoteCount = it.getInt(receivedNoteCountIndex), + sentNoteCount = it.getInt(sentNoteCountIndex), + memoCount = it.getInt(memoCountIndex), + blockTimeEpochSeconds = it.getLong(blockTimeIndex) + ) + } + + suspend fun count() = sqliteDatabase.queryAndMap( + AllTransactionViewDefinition.VIEW_NAME, + columns = PROJECTION_COUNT, + cursorParser = { it.getLong(0) } + ).first() + + fun getAllTransactions() = + sqliteDatabase.queryAndMap( + table = AllTransactionViewDefinition.VIEW_NAME, + orderBy = ORDER_BY, + cursorParser = cursorParser + ) + + fun getTransactionRange(blockHeightRange: ClosedRange) = + sqliteDatabase.queryAndMap( + table = AllTransactionViewDefinition.VIEW_NAME, + orderBy = ORDER_BY, + selection = SELECTION_BLOCK_RANGE, + selectionArgs = arrayOf(blockHeightRange.start.value, blockHeightRange.endInclusive.value), + cursorParser = cursorParser + ) + + suspend fun getOldestTransaction() = + sqliteDatabase.queryAndMap( + table = AllTransactionViewDefinition.VIEW_NAME, + orderBy = ORDER_BY, + limit = "1", + cursorParser = cursorParser + ).firstOrNull() +} + +internal object AllTransactionViewDefinition { + const val VIEW_NAME = "v_transactions" // $NON-NLS + + const val COLUMN_INTEGER_ID = "id_tx" // $NON-NLS + + const val COLUMN_INTEGER_MINED_HEIGHT = "mined_height" // $NON-NLS + + const val COLUMN_INTEGER_TRANSACTION_INDEX = "tx_index" // $NON-NLS + + const val COLUMN_BLOB_RAW_TRANSACTION_ID = "txid" // $NON-NLS + + const val COLUMN_INTEGER_EXPIRY_HEIGHT = "expiry_height" // $NON-NLS + + const val COLUMN_BLOB_RAW = "raw" // $NON-NLS + + const val COLUMN_LONG_VALUE = "net_value" // $NON-NLS + + const val COLUMN_LONG_FEE_PAID = "fee_paid" // $NON-NLS + + const val COLUMN_BOOLEAN_IS_WALLET_INTERNAL = "is_wallet_internal" // $NON-NLS + + const val COLUMN_BOOLEAN_IS_CHANGE = "has_change" // $NON-NLS + + const val COLUMN_INTEGER_SENT_NOTE_COUNT = "sent_note_count" // $NON-NLS + + const val COLUMN_INTEGER_RECEIVED_NOTE_COUNT = "received_note_count" // $NON-NLS + + const val COLUMN_INTEGER_MEMO_COUNT = "memo_count" // $NON-NLS + + const val COLUMN_INTEGER_BLOCK_TIME = "block_time" // $NON-NLS +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/BlockTable.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/BlockTable.kt new file mode 100644 index 00000000..261d3066 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/BlockTable.kt @@ -0,0 +1,88 @@ +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() + } +} + +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 +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt new file mode 100644 index 00000000..8f26577c --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DbDerivedDataRepository.kt @@ -0,0 +1,69 @@ +package cash.z.ecc.android.sdk.internal.db.derived + +import cash.z.ecc.android.sdk.internal.model.EncodedTransaction +import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.Transaction +import cash.z.ecc.android.sdk.model.TransactionOverview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import java.util.UUID + +@Suppress("TooManyFunctions") +internal class DbDerivedDataRepository( + private val derivedDataDb: DerivedDataDb +) : DerivedDataRepository { + private val invalidatingFlow = MutableStateFlow(UUID.randomUUID()) + + override suspend fun lastScannedHeight(): BlockHeight { + return derivedDataDb.blockTable.lastScannedHeight() + } + + override suspend fun firstScannedHeight(): BlockHeight { + return derivedDataDb.blockTable.firstScannedHeight() + } + + override suspend fun isInitialized(): Boolean { + return derivedDataDb.blockTable.count() > 0 + } + + override suspend fun findEncodedTransactionById(txId: Long): EncodedTransaction? { + return derivedDataDb.transactionTable.findEncodedTransactionById(txId) + } + + override suspend fun findNewTransactions(blockHeightRange: ClosedRange): List = + derivedDataDb.allTransactionView.getTransactionRange(blockHeightRange).toList() + + override suspend fun getOldestTransaction() = derivedDataDb.allTransactionView.getOldestTransaction() + + override suspend fun findMinedHeight(rawTransactionId: ByteArray) = derivedDataDb.transactionTable + .findMinedHeight(rawTransactionId) + + override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray) = derivedDataDb.transactionTable + .findDatabaseId(rawTransactionId) + + override suspend fun findBlockHash(height: BlockHeight) = derivedDataDb.blockTable.findBlockHash(height) + + override suspend fun getTransactionCount() = derivedDataDb.transactionTable.count() + + override fun invalidate() { + invalidatingFlow.value = UUID.randomUUID() + } + + override suspend fun getAccountCount() = derivedDataDb.accountTable.count() + // toInt() should be safe because we expect very few accounts + .toInt() + + override val receivedTransactions: Flow> + get() = invalidatingFlow.map { derivedDataDb.receivedTransactionView.getReceivedTransactions().toList() } + override val sentTransactions: Flow> + get() = invalidatingFlow.map { derivedDataDb.sentTransactionView.getSentTransactions().toList() } + override val allTransactions: Flow> + get() = invalidatingFlow.map { derivedDataDb.allTransactionView.getAllTransactions().toList() } + + override suspend fun close() { + derivedDataDb.close() + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt new file mode 100644 index 00000000..ec5d063b --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/DerivedDataDb.kt @@ -0,0 +1,81 @@ +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.NoBackupContextWrapper +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.model.Checkpoint +import cash.z.ecc.android.sdk.jni.RustBackend +import cash.z.ecc.android.sdk.model.ZcashNetwork +import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +internal class DerivedDataDb private constructor( + private val zcashNetwork: ZcashNetwork, + private val sqliteDatabase: SupportSQLiteDatabase +) { + val accountTable = AccountTable(sqliteDatabase) + + val blockTable = BlockTable(zcashNetwork, sqliteDatabase) + + val transactionTable = TransactionTable(zcashNetwork, sqliteDatabase) + + val allTransactionView = AllTransactionView(zcashNetwork, sqliteDatabase) + + val sentTransactionView = SentTransactionView(zcashNetwork, sqliteDatabase) + + val receivedTransactionView = ReceivedTransactionView(zcashNetwork, sqliteDatabase) + + suspend fun close() { + withContext(Dispatchers.IO) { + sqliteDatabase.close() + } + } + + companion object { + // Database migrations are managed by librustzcash. This is a hard-coded value to ensure that Android's + // SqliteOpenHelper is happy + private const val DATABASE_VERSION = 8 + + @Suppress("LongParameterList", "SpreadOperator") + suspend fun new( + context: Context, + rustBackend: RustBackend, + zcashNetwork: ZcashNetwork, + checkpoint: Checkpoint, + seed: ByteArray?, + viewingKeys: List + ): DerivedDataDb { + rustBackend.initDataDb(seed) + + // TODO [#681]: consider converting these to typed exceptions in the welding layer + // TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681 + tryWarn( + "Did not initialize the blocks table. It probably was already initialized.", + ifContains = "table is not empty" + ) { + rustBackend.initBlocksTable(checkpoint) + } + + tryWarn( + "Did not initialize the accounts table. It probably was already initialized.", + ifContains = "table is not empty" + ) { + rustBackend.initAccountsTable(*viewingKeys.toTypedArray()) + } + + val database = ReadOnlySupportSqliteOpenHelper.openExistingDatabaseAsReadOnly( + NoBackupContextWrapper( + context, + rustBackend.dataDbFile.parentFile!! + ), + rustBackend.dataDbFile, + DATABASE_VERSION + ) + + return DerivedDataDb(zcashNetwork, database) + } + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedTransactionView.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedTransactionView.kt new file mode 100644 index 00000000..107f1c12 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/ReceivedTransactionView.kt @@ -0,0 +1,105 @@ +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.Account +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray +import cash.z.ecc.android.sdk.model.Transaction +import cash.z.ecc.android.sdk.model.Zatoshi +import cash.z.ecc.android.sdk.model.ZcashNetwork +import kotlinx.coroutines.flow.first +import java.util.Locale + +internal class ReceivedTransactionView( + private val zcashNetwork: ZcashNetwork, + private val sqliteDatabase: SupportSQLiteDatabase +) { + companion object { + + private val ORDER_BY = String.format( + Locale.ROOT, + "%s DESC, %s DESC", // $NON-NLS + ReceivedTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT, + ReceivedTransactionViewDefinition.COLUMN_INTEGER_ID + ) + + private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS + } + + suspend fun count() = sqliteDatabase.queryAndMap( + AccountTableDefinition.TABLE_NAME, + columns = PROJECTION_COUNT, + cursorParser = { it.getLong(0) } + ).first() + + fun getReceivedTransactions() = + sqliteDatabase.queryAndMap( + table = ReceivedTransactionViewDefinition.VIEW_NAME, + orderBy = ORDER_BY, + cursorParser = { + val idColumnIndex = it.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_ID) + val minedHeightColumnIndex = + it.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT) + val transactionIndexColumnIndex = it.getColumnIndex( + ReceivedTransactionViewDefinition + .COLUMN_INTEGER_TRANSACTION_INDEX + ) + val rawTransactionIdIndex = + it.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_BLOB_RAW_TRANSACTION_ID) + val expiryHeightIndex = it.getColumnIndex( + ReceivedTransactionViewDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT + ) + val rawIndex = it.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_BLOB_RAW) + val receivedAccountIndex = it.getColumnIndex( + ReceivedTransactionViewDefinition.COLUMN_INTEGER_RECEIVED_BY_ACCOUNT + ) + val receivedTotalIndex = + it.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_RECEIVED_TOTAL) + val receivedNoteCountIndex = + it.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_RECEIVED_NOTE_COUNT) + val memoCountIndex = it.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_MEMO_COUNT) + val blockTimeIndex = it.getColumnIndex(ReceivedTransactionViewDefinition.COLUMN_INTEGER_BLOCK_TIME) + + Transaction.Received( + id = it.getLong(idColumnIndex), + rawId = FirstClassByteArray(it.getBlob(rawTransactionIdIndex)), + minedHeight = BlockHeight.new(zcashNetwork, it.getLong(minedHeightColumnIndex)), + expiryHeight = BlockHeight.new(zcashNetwork, it.getLong(expiryHeightIndex)), + index = it.getLong(transactionIndexColumnIndex), + raw = FirstClassByteArray(it.getBlob(rawIndex)), + receivedByAccount = Account(it.getInt(receivedAccountIndex)), + receivedTotal = Zatoshi(it.getLong(receivedTotalIndex)), + receivedNoteCount = it.getInt(receivedNoteCountIndex), + memoCount = it.getInt(memoCountIndex), + blockTimeEpochSeconds = it.getLong(blockTimeIndex) + ) + } + ) +} + +internal object ReceivedTransactionViewDefinition { + const val VIEW_NAME = "v_tx_received" // $NON-NLS + + const val COLUMN_INTEGER_ID = "id_tx" // $NON-NLS + + const val COLUMN_INTEGER_MINED_HEIGHT = "mined_height" // $NON-NLS + + const val COLUMN_INTEGER_TRANSACTION_INDEX = "tx_index" // $NON-NLS + + const val COLUMN_BLOB_RAW_TRANSACTION_ID = "txid" // $NON-NLS + + const val COLUMN_INTEGER_EXPIRY_HEIGHT = "expiry_height" // $NON-NLS + + const val COLUMN_BLOB_RAW = "raw" // $NON-NLS + + const val COLUMN_INTEGER_RECEIVED_BY_ACCOUNT = "received_by_account" // $NON-NLS + + const val COLUMN_INTEGER_RECEIVED_TOTAL = "received_total" // $NON-NLS + + const val COLUMN_INTEGER_RECEIVED_NOTE_COUNT = "received_note_count" // $NON-NLS + + const val COLUMN_INTEGER_MEMO_COUNT = "memo_count" // $NON-NLS + + const val COLUMN_INTEGER_BLOCK_TIME = "block_time" // $NON-NLS +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/SentTransactionView.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/SentTransactionView.kt new file mode 100644 index 00000000..c35b1962 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/SentTransactionView.kt @@ -0,0 +1,99 @@ +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.Account +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray +import cash.z.ecc.android.sdk.model.Transaction +import cash.z.ecc.android.sdk.model.Zatoshi +import cash.z.ecc.android.sdk.model.ZcashNetwork +import kotlinx.coroutines.flow.first +import java.util.Locale + +internal class SentTransactionView( + private val zcashNetwork: ZcashNetwork, + private val sqliteDatabase: SupportSQLiteDatabase +) { + companion object { + + private val ORDER_BY = String.format( + Locale.ROOT, + "%s DESC, %s DESC", // $NON-NLS + SentTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT, + SentTransactionViewDefinition.COLUMN_INTEGER_ID + ) + + private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS + } + + suspend fun count() = sqliteDatabase.queryAndMap( + SentTransactionViewDefinition.VIEW_NAME, + columns = PROJECTION_COUNT, + cursorParser = { it.getLong(0) } + ).first() + + fun getSentTransactions() = + sqliteDatabase.queryAndMap( + table = SentTransactionViewDefinition.VIEW_NAME, + orderBy = ORDER_BY, + cursorParser = { + val idColumnIndex = it.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_ID) + val minedHeightColumnIndex = + it.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_MINED_HEIGHT) + val transactionIndexColumnIndex = it.getColumnIndex( + SentTransactionViewDefinition + .COLUMN_INTEGER_TRANSACTION_INDEX + ) + val rawTransactionIdIndex = + it.getColumnIndex(SentTransactionViewDefinition.COLUMN_BLOB_RAW_TRANSACTION_ID) + val expiryHeightIndex = it.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT) + val rawIndex = it.getColumnIndex(SentTransactionViewDefinition.COLUMN_BLOB_RAW) + val sentFromAccount = it.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_SENT_FROM_ACCOUNT) + val sentTotalIndex = it.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_SENT_TOTAL) + val sentNoteCountIndex = it.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_SENT_NOTE_COUNT) + val memoCountIndex = it.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_MEMO_COUNT) + val blockTimeIndex = it.getColumnIndex(SentTransactionViewDefinition.COLUMN_INTEGER_BLOCK_TIME) + + Transaction.Sent( + id = it.getLong(idColumnIndex), + rawId = FirstClassByteArray(it.getBlob(rawTransactionIdIndex)), + minedHeight = BlockHeight.new(zcashNetwork, it.getLong(minedHeightColumnIndex)), + expiryHeight = BlockHeight.new(zcashNetwork, it.getLong(expiryHeightIndex)), + index = it.getLong(transactionIndexColumnIndex), + raw = FirstClassByteArray(it.getBlob(rawIndex)), + sentFromAccount = Account(it.getInt(sentFromAccount)), + sentTotal = Zatoshi(it.getLong(sentTotalIndex)), + sentNoteCount = it.getInt(sentNoteCountIndex), + memoCount = it.getInt(memoCountIndex), + blockTimeEpochSeconds = it.getLong(blockTimeIndex) + ) + } + ) +} + +internal object SentTransactionViewDefinition { + const val VIEW_NAME = "v_tx_sent" // $NON-NLS + + const val COLUMN_INTEGER_ID = "id_tx" // $NON-NLS + + const val COLUMN_INTEGER_MINED_HEIGHT = "mined_height" // $NON-NLS + + const val COLUMN_INTEGER_TRANSACTION_INDEX = "tx_index" // $NON-NLS + + const val COLUMN_BLOB_RAW_TRANSACTION_ID = "txid" // $NON-NLS + + const val COLUMN_INTEGER_EXPIRY_HEIGHT = "expiry_height" // $NON-NLS + + const val COLUMN_BLOB_RAW = "raw" // $NON-NLS + + const val COLUMN_INTEGER_SENT_FROM_ACCOUNT = "sent_from_account" // $NON-NLS + + const val COLUMN_INTEGER_SENT_TOTAL = "sent_total" // $NON-NLS + + const val COLUMN_INTEGER_SENT_NOTE_COUNT = "sent_note_count" // $NON-NLS + + const val COLUMN_INTEGER_MEMO_COUNT = "memo_count" // $NON-NLS + + const val COLUMN_INTEGER_BLOCK_TIME = "block_time" // $NON-NLS +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TransactionTable.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TransactionTable.kt new file mode 100644 index 00000000..8e7f7f71 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/derived/TransactionTable.kt @@ -0,0 +1,137 @@ +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.internal.model.EncodedTransaction +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray +import cash.z.ecc.android.sdk.model.ZcashNetwork +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withContext +import java.util.Locale + +internal class TransactionTable( + private val zcashNetwork: ZcashNetwork, + private val sqliteDatabase: SupportSQLiteDatabase +) { + + companion object { + private val SELECTION_BLOCK_IS_NULL = String.format( + Locale.ROOT, + "%s IS NULL", // $NON-NLS + TransactionTableDefinition.COLUMN_INTEGER_BLOCK + ) + + private val PROJECTION_COUNT = arrayOf("COUNT(*)") // $NON-NLS + + private val PROJECTION_BLOCK = arrayOf(TransactionTableDefinition.COLUMN_INTEGER_BLOCK) + + private val PROJECTION_PRIMARY_KEY_ID = arrayOf(TransactionTableDefinition.COLUMN_INTEGER_ID) + + private val PROJECTION_ENCODED_TRANSACTION = arrayOf( + TransactionTableDefinition.COLUMN_BLOB_TRANSACTION_ID, + TransactionTableDefinition.COLUMN_BLOB_RAW, + TransactionTableDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT + ) + + private val SELECTION_RAW_TRANSACTION_ID = String.format( + Locale.ROOT, + "%s = ?", // $NON-NLS + TransactionTableDefinition.COLUMN_BLOB_TRANSACTION_ID + ) + + private val SELECTION_TRANSACTION_ID_AND_RAW_NOT_NULL = String.format( + Locale.ROOT, + "%s = ? AND %s IS NOT NULL", // $NON-NLS + TransactionTableDefinition.COLUMN_INTEGER_ID, + TransactionTableDefinition.COLUMN_BLOB_RAW + ) + } + + suspend fun count() = withContext(Dispatchers.IO) { + sqliteDatabase.queryAndMap( + table = TransactionTableDefinition.TABLE_NAME, + columns = PROJECTION_COUNT, + cursorParser = { it.getLong(0) } + ).first() + } + + suspend fun countUnmined() = + sqliteDatabase.queryAndMap( + table = TransactionTableDefinition.TABLE_NAME, + columns = PROJECTION_COUNT, + selection = SELECTION_BLOCK_IS_NULL, + cursorParser = { it.getLong(0) } + ).first() + + suspend fun findEncodedTransactionById(id: Long): EncodedTransaction? { + return sqliteDatabase.queryAndMap( + table = TransactionTableDefinition.TABLE_NAME, + columns = PROJECTION_ENCODED_TRANSACTION, + selection = SELECTION_TRANSACTION_ID_AND_RAW_NOT_NULL, + selectionArgs = arrayOf(id) + ) { + val txIdIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_BLOB_TRANSACTION_ID) + val rawIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_BLOB_RAW) + val heightIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_INTEGER_EXPIRY_HEIGHT) + + val txid = it.getBlob(txIdIndex) + val raw = it.getBlob(rawIndex) + val expiryHeight = if (it.isNull(heightIndex)) { + null + } else { + BlockHeight.new(zcashNetwork, it.getLong(heightIndex)) + } + + EncodedTransaction( + FirstClassByteArray(txid), + FirstClassByteArray(raw), + expiryHeight + ) + }.firstOrNull() + } + + suspend fun findMinedHeight(rawTransactionId: ByteArray): BlockHeight? { + return sqliteDatabase.queryAndMap( + table = TransactionTableDefinition.TABLE_NAME, + columns = PROJECTION_BLOCK, + selection = SELECTION_RAW_TRANSACTION_ID, + selectionArgs = arrayOf(rawTransactionId) + ) { + val blockIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_INTEGER_BLOCK) + BlockHeight.new(zcashNetwork, it.getLong(blockIndex)) + }.firstOrNull() + } + + suspend fun findDatabaseId(rawTransactionId: ByteArray): Long? { + return sqliteDatabase.queryAndMap( + table = TransactionTableDefinition.TABLE_NAME, + columns = PROJECTION_PRIMARY_KEY_ID, + selection = SELECTION_RAW_TRANSACTION_ID, + selectionArgs = arrayOf(rawTransactionId) + ) { + val idIndex = it.getColumnIndexOrThrow(TransactionTableDefinition.COLUMN_INTEGER_ID) + it.getLong(idIndex) + }.firstOrNull() + } +} + +object TransactionTableDefinition { + const val TABLE_NAME = "transactions" // $NON-NLS + + const val COLUMN_INTEGER_ID = "id_tx" // $NON-NLS + + const val COLUMN_BLOB_TRANSACTION_ID = "txid" // $NON-NLS + + const val COLUMN_TEXT_CREATED = "created" // $NON-NLS + + const val COLUMN_INTEGER_BLOCK = "block" // $NON-NLS + + const val COLUMN_INTEGER_TX_INDEX = "tx_index" // $NON-NLS + + const val COLUMN_INTEGER_EXPIRY_HEIGHT = "expiry_height" // $NON-NLS + + const val COLUMN_BLOB_RAW = "raw" // $NON-NLS +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/PendingTransactionDb.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/pending/PendingTransactionDb.kt similarity index 96% rename from sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/PendingTransactionDb.kt rename to sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/pending/PendingTransactionDb.kt index fe17babb..4bede7be 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/PendingTransactionDb.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/pending/PendingTransactionDb.kt @@ -1,4 +1,4 @@ -package cash.z.ecc.android.sdk.internal.db +package cash.z.ecc.android.sdk.internal.db.pending import androidx.room.Dao import androidx.room.Database @@ -8,7 +8,6 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.RoomDatabase import androidx.room.Update -import cash.z.ecc.android.sdk.db.entity.PendingTransactionEntity import kotlinx.coroutines.flow.Flow // diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/pending/PendingTransactionEntity.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/pending/PendingTransactionEntity.kt new file mode 100644 index 00000000..e9a5a137 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/db/pending/PendingTransactionEntity.kt @@ -0,0 +1,142 @@ +package cash.z.ecc.android.sdk.internal.db.pending + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray +import cash.z.ecc.android.sdk.model.PendingTransaction +import cash.z.ecc.android.sdk.model.Zatoshi +import cash.z.ecc.android.sdk.model.ZcashNetwork + +@Entity(tableName = "pending_transactions") +data class PendingTransactionEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val toAddress: String, + val value: Long, + val memo: ByteArray?, + val accountIndex: Int, + val minedHeight: Long = NO_BLOCK_HEIGHT, + val expiryHeight: Long = NO_BLOCK_HEIGHT, + + val cancelled: Int = 0, + val encodeAttempts: Int = -1, + val submitAttempts: Int = -1, + val errorMessage: String? = null, + val errorCode: Int? = null, + val createTime: Long = System.currentTimeMillis(), + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + val raw: ByteArray = byteArrayOf(), + @ColumnInfo(typeAffinity = ColumnInfo.BLOB) + val rawTransactionId: ByteArray? = byteArrayOf() +) { + fun toPendingTransaction(zcashNetwork: ZcashNetwork) = PendingTransaction( + id = id, + value = Zatoshi(value), + memo = memo?.let { FirstClassByteArray(it) }, + raw = FirstClassByteArray(raw), + toAddress = toAddress, + accountIndex = accountIndex, + minedHeight = if (minedHeight == NO_BLOCK_HEIGHT) { + null + } else { + BlockHeight.new(zcashNetwork, minedHeight) + }, + expiryHeight = if (expiryHeight == NO_BLOCK_HEIGHT) { + null + } else { + BlockHeight.new(zcashNetwork, expiryHeight) + }, + cancelled = cancelled, + encodeAttempts = encodeAttempts, + submitAttempts = submitAttempts, + errorMessage = errorMessage, + errorCode = errorCode, + createTime = createTime, + rawTransactionId = rawTransactionId?.let { FirstClassByteArray(it) } + ) + + @Suppress("ComplexMethod") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PendingTransactionEntity + + if (id != other.id) return false + if (toAddress != other.toAddress) return false + if (value != other.value) return false + if (memo != null) { + if (other.memo == null) return false + if (!memo.contentEquals(other.memo)) return false + } else if (other.memo != null) return false + if (accountIndex != other.accountIndex) return false + if (minedHeight != other.minedHeight) return false + if (expiryHeight != other.expiryHeight) return false + if (cancelled != other.cancelled) return false + if (encodeAttempts != other.encodeAttempts) return false + if (submitAttempts != other.submitAttempts) return false + if (errorMessage != other.errorMessage) return false + if (errorCode != other.errorCode) return false + if (createTime != other.createTime) return false + if (!raw.contentEquals(other.raw)) return false + if (rawTransactionId != null) { + if (other.rawTransactionId == null) return false + if (!rawTransactionId.contentEquals(other.rawTransactionId)) return false + } else if (other.rawTransactionId != null) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + toAddress.hashCode() + result = 31 * result + value.hashCode() + result = 31 * result + (memo?.contentHashCode() ?: 0) + result = 31 * result + accountIndex + result = 31 * result + minedHeight.hashCode() + result = 31 * result + expiryHeight.hashCode() + result = 31 * result + cancelled + result = 31 * result + encodeAttempts + result = 31 * result + submitAttempts + result = 31 * result + (errorMessage?.hashCode() ?: 0) + result = 31 * result + (errorCode ?: 0) + result = 31 * result + createTime.hashCode() + result = 31 * result + raw.contentHashCode() + result = 31 * result + (rawTransactionId?.contentHashCode() ?: 0) + return result + } + + companion object { + const val NO_BLOCK_HEIGHT = -1L + + fun from(pendingTransaction: PendingTransaction) = PendingTransactionEntity( + id = pendingTransaction.id, + value = pendingTransaction.value.value, + memo = pendingTransaction.memo?.byteArray, + raw = pendingTransaction.raw.byteArray, + toAddress = pendingTransaction.toAddress, + accountIndex = pendingTransaction.accountIndex, + minedHeight = pendingTransaction.minedHeight?.value ?: NO_BLOCK_HEIGHT, + expiryHeight = pendingTransaction.expiryHeight?.value ?: NO_BLOCK_HEIGHT, + cancelled = pendingTransaction.cancelled, + encodeAttempts = pendingTransaction.encodeAttempts, + submitAttempts = pendingTransaction.submitAttempts, + errorMessage = pendingTransaction.errorMessage, + errorCode = pendingTransaction.errorCode, + createTime = pendingTransaction.createTime, + rawTransactionId = pendingTransaction.rawTransactionId?.byteArray + ) + } +} + +fun PendingTransactionEntity.isSubmitted(): Boolean { + return submitAttempts > 0 +} + +fun PendingTransactionEntity.isFailedEncoding() = raw.isNotEmpty() && encodeAttempts > 0 + +fun PendingTransactionEntity.isCancelled(): Boolean { + return cancelled > 0 +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/Block.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/Block.kt new file mode 100644 index 00000000..a8f3ee00 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/Block.kt @@ -0,0 +1,11 @@ +package cash.z.ecc.android.sdk.internal.model + +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray + +internal data class Block( + val height: BlockHeight, + val hash: FirstClassByteArray, + val time: Int, + val saplingTree: FirstClassByteArray +) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/CompactBlock.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/CompactBlock.kt new file mode 100644 index 00000000..a371e98b --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/CompactBlock.kt @@ -0,0 +1,9 @@ +package cash.z.ecc.android.sdk.internal.model + +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray + +internal data class CompactBlock( + val height: BlockHeight, + val data: FirstClassByteArray +) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/EncodedTransaction.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/EncodedTransaction.kt new file mode 100644 index 00000000..d5e08298 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/model/EncodedTransaction.kt @@ -0,0 +1,10 @@ +package cash.z.ecc.android.sdk.internal.model + +import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.FirstClassByteArray + +internal data class EncodedTransaction( + val txId: FirstClassByteArray, + val raw: FirstClassByteArray, + val expiryHeight: BlockHeight? +) diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockStore.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/CompactBlockRepository.kt similarity index 92% rename from sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockStore.kt rename to sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/CompactBlockRepository.kt index ba7123e2..f924a4fd 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/block/CompactBlockStore.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/CompactBlockRepository.kt @@ -1,4 +1,4 @@ -package cash.z.ecc.android.sdk.internal.block +package cash.z.ecc.android.sdk.internal.repository import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.wallet.sdk.rpc.CompactFormats @@ -6,7 +6,7 @@ import cash.z.wallet.sdk.rpc.CompactFormats /** * Interface for storing compact blocks. */ -interface CompactBlockStore { +interface CompactBlockRepository { /** * Gets the highest block that is currently stored. * diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt similarity index 66% rename from sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt rename to sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt index aa1f2cb6..25d6966e 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionRepository.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/repository/DerivedDataRepository.kt @@ -1,15 +1,16 @@ -package cash.z.ecc.android.sdk.internal.transaction +package cash.z.ecc.android.sdk.internal.repository -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction -import cash.z.ecc.android.sdk.db.entity.EncodedTransaction +import cash.z.ecc.android.sdk.internal.model.EncodedTransaction import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.Transaction +import cash.z.ecc.android.sdk.model.TransactionOverview import kotlinx.coroutines.flow.Flow /** * Repository of wallet transactions, providing an agnostic interface to the underlying information. */ @Suppress("TooManyFunctions") -interface TransactionRepository { +internal interface DerivedDataRepository { /** * The last height scanned by this repository. @@ -26,8 +27,6 @@ interface TransactionRepository { suspend fun firstScannedHeight(): BlockHeight /** - * Returns true when this repository has been initialized and seeded with the initial checkpoint. - * * @return true when this repository has been initialized and seeded with the initial checkpoint. */ suspend fun isInitialized(): Boolean @@ -52,7 +51,9 @@ interface TransactionRepository { * * @return a list of transactions that were mined in the given range, inclusive. */ - suspend fun findNewTransactions(blockHeightRange: ClosedRange): List + suspend fun findNewTransactions(blockHeightRange: ClosedRange): List + + suspend fun getOldestTransaction(): TransactionOverview? /** * Find the mined height that matches the given raw tx_id in bytes. This is useful for matching @@ -66,36 +67,42 @@ interface TransactionRepository { 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 + /** * Provides a way for other components to signal that the underlying data has been modified. */ fun invalidate() - /** - * When a transaction has been cancelled by the user, we need a bridge to clean it up from the - * dataDb. This function will safely remove everything related to that transaction in the right - * order to satisfy foreign key constraints, even if cascading isn't setup in the DB. - * - * @return true when an unmined transaction was found and then successfully removed - */ - suspend fun cleanupCancelledTx(rawTransactionId: ByteArray): Boolean - - suspend fun deleteExpired(lastScannedHeight: BlockHeight): Int - - suspend fun count(): Int - suspend fun getAccountCount(): Int // // Transactions // + /* + * Note there are two big limitations with this implementation: + * 1. Clients don't receive notification if the underlying data changes. A flow of flows could help there. + * 2. Pagination isn't supported. Although flow does a good job of allowing the data to be processed as a stream, + * that doesn't work so well in UI when users might scroll forwards/backwards. + * + * We'll come back to this and improve it in the future. This implementation is already an improvement over + * prior versions. + */ + /** A flow of all the inbound confirmed transactions */ - val receivedTransactions: Flow> + val receivedTransactions: Flow> /** A flow of all the outbound confirmed transactions */ - val sentTransactions: Flow> + val sentTransactions: Flow> /** A flow of all the inbound and outbound confirmed transactions */ - val allTransactions: Flow> + val allTransactions: Flow> + + suspend fun close() } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt deleted file mode 100644 index 75e3f5f7..00000000 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PagedTransactionRepository.kt +++ /dev/null @@ -1,249 +0,0 @@ -package cash.z.ecc.android.sdk.internal.transaction - -import android.content.Context -import androidx.paging.PagedList -import androidx.room.RoomDatabase -import cash.z.ecc.android.sdk.db.commonDatabaseBuilder -import cash.z.ecc.android.sdk.db.entity.ConfirmedTransaction -import cash.z.ecc.android.sdk.exception.InitializerException.SeedRequired -import cash.z.ecc.android.sdk.ext.ZcashSdk -import cash.z.ecc.android.sdk.internal.SdkDispatchers -import cash.z.ecc.android.sdk.internal.SdkExecutors -import cash.z.ecc.android.sdk.internal.db.DerivedDataDb -import cash.z.ecc.android.sdk.internal.ext.android.toFlowPagedList -import cash.z.ecc.android.sdk.internal.ext.android.toRefreshable -import cash.z.ecc.android.sdk.internal.ext.tryWarn -import cash.z.ecc.android.sdk.internal.model.Checkpoint -import cash.z.ecc.android.sdk.internal.twig -import cash.z.ecc.android.sdk.jni.RustBackend -import cash.z.ecc.android.sdk.model.BlockHeight -import cash.z.ecc.android.sdk.model.ZcashNetwork -import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.withContext -import java.io.File - -/** - * Example of a repository that leverages the Room paging library to return a [PagedList] of - * transactions. Consumers can register as a page listener and receive an interface that allows for - * efficiently paging data. - * - * @param pageSize transactions per page. This influences pre-fetch and memory configuration. - */ -@Suppress("TooManyFunctions") -internal class PagedTransactionRepository private constructor( - private val zcashNetwork: ZcashNetwork, - private val db: DerivedDataDb, - private val pageSize: Int -) : TransactionRepository { - - // DAOs - private val blocks = db.blockDao() - private val accounts = db.accountDao() - private val transactions = db.transactionDao() - - // Transaction Flows - private val allTransactionsFactory = transactions.getAllTransactions().toRefreshable() - - override val receivedTransactions - get() = flow> { - emitAll( - transactions.getReceivedTransactions().toRefreshable().toFlowPagedList(pageSize) - ) - } - override val sentTransactions - get() = flow> { - emitAll(transactions.getSentTransactions().toRefreshable().toFlowPagedList(pageSize)) - } - override val allTransactions - get() = flow> { - emitAll(allTransactionsFactory.toFlowPagedList(pageSize)) - } - - // - // TransactionRepository API - // - - override fun invalidate() = allTransactionsFactory.refresh() - - override suspend fun lastScannedHeight() = BlockHeight.new(zcashNetwork, blocks.lastScannedHeight()) - - override suspend fun firstScannedHeight() = BlockHeight.new(zcashNetwork, blocks.firstScannedHeight()) - - override suspend fun isInitialized() = blocks.count() > 0 - - override suspend fun findEncodedTransactionById(txId: Long) = - transactions.findEncodedTransactionById(txId) - - override suspend fun findNewTransactions(blockHeightRange: ClosedRange): List = - transactions.findAllTransactionsByRange(blockHeightRange.start.value, blockHeightRange.endInclusive.value) - - override suspend fun findMinedHeight(rawTransactionId: ByteArray) = - transactions.findMinedHeight(rawTransactionId)?.let { BlockHeight.new(zcashNetwork, it) } - - override suspend fun findMatchingTransactionId(rawTransactionId: ByteArray): Long? = - transactions.findMatchingTransactionId(rawTransactionId) - - override suspend fun cleanupCancelledTx(rawTransactionId: ByteArray) = - transactions.cleanupCancelledTx(rawTransactionId) - - // let expired transactions linger in the UI for a little while - override suspend fun deleteExpired(lastScannedHeight: BlockHeight) = - transactions.deleteExpired(lastScannedHeight.value - (ZcashSdk.EXPIRY_OFFSET / 2)) - - override suspend fun count() = transactions.count() - - override suspend fun getAccountCount() = accounts.count() - - /** - * Close the underlying database. - */ - suspend fun close() { - withContext(SdkDispatchers.DATABASE_IO) { - db.close() - } - } - - // 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? = blocks.findHashByHeight(height.value) - suspend fun getTransactionCount(): Int = transactions.count() - - // TODO [#681]: convert this into a wallet repository rather than "transaction repository" - // TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681 - - companion object { - @Suppress("LongParameterList") - suspend fun new( - appContext: Context, - zcashNetwork: ZcashNetwork, - pageSize: Int = 10, - rustBackend: RustBackend, - seed: ByteArray?, - birthday: Checkpoint, - viewingKeys: List, - overwriteVks: Boolean = false - ): PagedTransactionRepository { - initMissingDatabases(rustBackend, seed, birthday, viewingKeys) - - val db = buildDatabase(appContext.applicationContext, rustBackend.dataDbFile) - applyKeyMigrations(rustBackend, overwriteVks, viewingKeys) - - return PagedTransactionRepository(zcashNetwork, db, pageSize) - } - - /** - * Build the database and apply migrations. - */ - private suspend fun buildDatabase(context: Context, databaseFile: File): DerivedDataDb { - twig("Building dataDb and applying migrations") - return commonDatabaseBuilder( - context, - DerivedDataDb::class.java, - databaseFile - ) - .setJournalMode(RoomDatabase.JournalMode.TRUNCATE) - .setQueryExecutor(SdkExecutors.DATABASE_IO) - .setTransactionExecutor(SdkExecutors.DATABASE_IO) - .addMigrations(DerivedDataDb.MIGRATION_3_4) - .addMigrations(DerivedDataDb.MIGRATION_4_3) - .addMigrations(DerivedDataDb.MIGRATION_4_5) - .addMigrations(DerivedDataDb.MIGRATION_5_6) - .addMigrations(DerivedDataDb.MIGRATION_6_7) - .build().also { - // TODO [#681]: document why we do this. My guess is to catch database issues early or to trigger - // migrations--I forget why it was added but there was a good reason? - // TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681 - withContext(SdkDispatchers.DATABASE_IO) { - // TODO [#649]: StrictMode policy violation: LeakedClosableViolation - // TODO [#649]: https://github.com/zcash/zcash-android-wallet-sdk/issues/649 - it.openHelper.writableDatabase.beginTransaction() - it.openHelper.writableDatabase.endTransaction() - } - } - } - - /** - * Create any databases that don't already exist via Rust. Originally, this was done on the Rust - * side because Rust was intended to own the "dataDb" and Kotlin just reads from it. Since then, - * it has been more clear that Kotlin should own the data and just let Rust use it. - */ - private suspend fun initMissingDatabases( - rustBackend: RustBackend, - seed: ByteArray?, - birthday: Checkpoint, - viewingKeys: List - ) { - maybeCreateDataDb(rustBackend, seed) - maybeInitBlocksTable(rustBackend, birthday) - maybeInitAccountsTable(rustBackend, viewingKeys) - } - - /** - * Create the dataDb and its table, if it doesn't exist. - */ - private suspend fun maybeCreateDataDb(rustBackend: RustBackend, seed: ByteArray?) { - tryWarn( - "Warning: did not create dataDb. It probably already exists.", - unlessContains = "requires the wallet's seed" - ) { - val res = rustBackend.initDataDb(seed) - if (res == 1) { - throw SeedRequired - } - twig("Initialized wallet for first run file: ${rustBackend.dataDbFile}") - } - } - - /** - * Initialize the blocks table with the given birthday, if needed. - */ - private suspend fun maybeInitBlocksTable( - rustBackend: RustBackend, - checkpoint: Checkpoint - ) { - // TODO [#681]: consider converting these to typed exceptions in the welding layer - // TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681 - tryWarn( - "Warning: did not initialize the blocks table. It probably was already initialized.", - ifContains = "table is not empty" - ) { - rustBackend.initBlocksTable(checkpoint) - twig("seeded the database with sapling tree at height ${checkpoint.height}") - } - twig("database file: ${rustBackend.dataDbFile}") - } - - /** - * Initialize the accounts table with the given viewing keys. - */ - private suspend fun maybeInitAccountsTable( - rustBackend: RustBackend, - viewingKeys: List - ) { - // TODO [#681]: consider converting these to typed exceptions in the welding layer - // TODO [#681]: https://github.com/zcash/zcash-android-wallet-sdk/issues/681 - tryWarn( - "Warning: did not initialize the accounts table. It probably was already initialized.", - ifContains = "table is not empty" - ) { - @Suppress("SpreadOperator") - rustBackend.initAccountsTable(*viewingKeys.toTypedArray()) - twig("Initialized the accounts table with ${viewingKeys.size} viewingKey(s)") - } - } - - private suspend fun applyKeyMigrations( - rustBackend: RustBackend, - overwriteVks: Boolean, - viewingKeys: List - ) { - if (overwriteVks) { - twig("applying key migrations . . .") - maybeInitAccountsTable(rustBackend, viewingKeys) - } - } - } -} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt index 6ef3e1e4..66b5afa9 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/PersistentTransactionManager.kt @@ -2,23 +2,25 @@ package cash.z.ecc.android.sdk.internal.transaction import android.content.Context import androidx.room.RoomDatabase -import cash.z.ecc.android.sdk.db.commonDatabaseBuilder -import cash.z.ecc.android.sdk.db.entity.PendingTransaction -import cash.z.ecc.android.sdk.db.entity.PendingTransactionEntity -import cash.z.ecc.android.sdk.db.entity.isCancelled -import cash.z.ecc.android.sdk.db.entity.isFailedEncoding -import cash.z.ecc.android.sdk.db.entity.isSubmitted -import cash.z.ecc.android.sdk.internal.db.PendingTransactionDao -import cash.z.ecc.android.sdk.internal.db.PendingTransactionDb +import cash.z.ecc.android.sdk.internal.db.commonDatabaseBuilder +import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDao +import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionDb +import cash.z.ecc.android.sdk.internal.db.pending.PendingTransactionEntity +import cash.z.ecc.android.sdk.internal.db.pending.isCancelled +import cash.z.ecc.android.sdk.internal.db.pending.isFailedEncoding +import cash.z.ecc.android.sdk.internal.db.pending.isSubmitted import cash.z.ecc.android.sdk.internal.service.LightWalletService import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.PendingTransaction import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi +import cash.z.ecc.android.sdk.model.ZcashNetwork import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext @@ -37,8 +39,9 @@ import kotlin.math.max * @property service the lightwallet service used to submit transactions. */ @Suppress("TooManyFunctions") -class PersistentTransactionManager( +internal class PersistentTransactionManager( db: PendingTransactionDb, + private val zcashNetwork: ZcashNetwork, internal val encoder: TransactionEncoder, private val service: LightWalletService ) : OutboundTransactionManager { @@ -51,24 +54,6 @@ class PersistentTransactionManager( */ private val _dao: PendingTransactionDao = db.pendingTransactionDao() - /** - * Constructor that creates the database and then executes a callback on it. - */ - constructor( - appContext: Context, - encoder: TransactionEncoder, - service: LightWalletService, - databaseFile: File - ) : this( - commonDatabaseBuilder( - appContext, - PendingTransactionDb::class.java, - databaseFile - ).setJournalMode(RoomDatabase.JournalMode.TRUNCATE).build(), - encoder, - service - ) - // // OutboundTransactionManager implementation // @@ -81,8 +66,8 @@ class PersistentTransactionManager( ): PendingTransaction = withContext(Dispatchers.IO) { twig("constructing a placeholder transaction") var tx = PendingTransactionEntity( - value = zatoshi.value, toAddress = toAddress, + value = zatoshi.value, memo = memo.toByteArray(), accountIndex = account.value ) @@ -99,7 +84,7 @@ class PersistentTransactionManager( ) } - tx + tx.toPendingTransaction(zcashNetwork) } override suspend fun applyMinedHeight(pendingTx: PendingTransaction, minedHeight: BlockHeight) { @@ -114,30 +99,34 @@ class PersistentTransactionManager( pendingTx: PendingTransaction ): PendingTransaction = withContext(Dispatchers.IO) { twig("managing the creation of a transaction") - var tx = pendingTx as PendingTransactionEntity - if (tx.accountIndex != usk.account.value) { - throw java.lang.IllegalArgumentException("usk is not for the same account as pendingTx") - } + + var tx = PendingTransactionEntity.from(pendingTx) @Suppress("TooGenericExceptionCaught") try { twig("beginning to encode transaction with : $encoder") val encodedTx = encoder.createTransaction( usk, - tx.valueZatoshi, - tx.toAddress, - tx.memo + pendingTx.value, + pendingTx.toAddress, + pendingTx.memo?.byteArray ) twig("successfully encoded transaction!") safeUpdate("updating transaction encoding", -1) { - updateEncoding(tx.id, encodedTx.raw, encodedTx.txId, encodedTx.expiryHeight) + updateEncoding( + pendingTx.id, + encodedTx.raw.byteArray, + encodedTx.txId.byteArray, + encodedTx + .expiryHeight?.value + ) } } catch (t: Throwable) { var message = "failed to encode transaction due to : ${t.message}" t.cause?.let { message += " caused by: $it" } twig(message) safeUpdate("updating transaction error info") { - updateError(tx.id, message, ERROR_ENCODING) + updateError(pendingTx.id, message, ERROR_ENCODING) } } finally { safeUpdate("incrementing transaction encodeAttempts (from: ${tx.encodeAttempts})", -1) { @@ -146,7 +135,7 @@ class PersistentTransactionManager( } } - tx + tx.toPendingTransaction(zcashNetwork) } // TODO(str4d): This uses operator overloading to distinguish between it and createToAddress, which breaks when @@ -157,7 +146,7 @@ class PersistentTransactionManager( pendingTx: PendingTransaction ): PendingTransaction { twig("managing the creation of a shielding transaction") - var tx = pendingTx as PendingTransactionEntity + var tx = PendingTransactionEntity.from(pendingTx) @Suppress("TooGenericExceptionCaught") try { twig("beginning to encode shielding transaction with : $encoder") @@ -167,7 +156,7 @@ class PersistentTransactionManager( ) twig("successfully encoded shielding transaction!") safeUpdate("updating shielding transaction encoding") { - updateEncoding(tx.id, encodedTx.raw, encodedTx.txId, encodedTx.expiryHeight) + updateEncoding(tx.id, encodedTx.raw.byteArray, encodedTx.txId.byteArray, encodedTx.expiryHeight?.value) } } catch (t: Throwable) { var message = "failed to encode auto-shielding transaction due to : ${t.message}" @@ -183,7 +172,7 @@ class PersistentTransactionManager( } } - return tx + return tx.toPendingTransaction(zcashNetwork) } override suspend fun submit(pendingTx: PendingTransaction): PendingTransaction = withContext(Dispatchers.IO) { @@ -236,11 +225,11 @@ class PersistentTransactionManager( } } - tx + tx.toPendingTransaction(zcashNetwork) } override suspend fun monitorById(id: Long): Flow { - return pendingTransactionDao { monitorById(id) } + return pendingTransactionDao { monitorById(id) }.map { it.toPendingTransaction(zcashNetwork) } } override suspend fun isValidShieldedAddress(address: String) = @@ -268,7 +257,7 @@ class PersistentTransactionManager( override suspend fun findById(id: Long) = pendingTransactionDao { findById(id) - } + }?.toPendingTransaction(zcashNetwork) override suspend fun markForDeletion(id: Long) = pendingTransactionDao { withContext(IO) { @@ -292,11 +281,11 @@ class PersistentTransactionManager( override suspend fun abort(transaction: PendingTransaction): Int { return pendingTransactionDao { twig("[cleanup] Deleting pendingTxId: ${transaction.id}") - delete(transaction as PendingTransactionEntity) + delete(PendingTransactionEntity.from(transaction)) } } - override fun getAll() = _dao.getAll() + override fun getAll() = _dao.getAll().map { list -> list.map { it.toPendingTransaction(zcashNetwork) } } // // Helper functions @@ -343,5 +332,22 @@ class PersistentTransactionManager( private const val SAFE_TO_DELETE_ERROR_MESSAGE = "safe to delete" const val SAFE_TO_DELETE_ERROR_CODE = -9090 + + fun new( + appContext: Context, + zcashNetwork: ZcashNetwork, + encoder: TransactionEncoder, + service: LightWalletService, + databaseFile: File + ) = PersistentTransactionManager( + commonDatabaseBuilder( + appContext, + PendingTransactionDb::class.java, + databaseFile + ).setJournalMode(RoomDatabase.JournalMode.TRUNCATE).build(), + zcashNetwork, + encoder, + service + ) } } diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt index 1e981aac..a49ee46f 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionEncoder.kt @@ -1,10 +1,10 @@ package cash.z.ecc.android.sdk.internal.transaction -import cash.z.ecc.android.sdk.db.entity.EncodedTransaction +import cash.z.ecc.android.sdk.internal.model.EncodedTransaction import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi -interface TransactionEncoder { +internal interface TransactionEncoder { /** * Creates a transaction, throwing an exception whenever things are missing. When the provided * wallet implementation doesn't throw an exception, we wrap the issue into a descriptive diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt index 33643f4f..a16f68a9 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/TransactionManager.kt @@ -1,8 +1,8 @@ package cash.z.ecc.android.sdk.internal.transaction -import cash.z.ecc.android.sdk.db.entity.PendingTransaction import cash.z.ecc.android.sdk.model.Account import cash.z.ecc.android.sdk.model.BlockHeight +import cash.z.ecc.android.sdk.model.PendingTransaction import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi import kotlinx.coroutines.flow.Flow diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt index 20581870..bc3a7b4c 100644 --- a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/internal/transaction/WalletTransactionEncoder.kt @@ -1,9 +1,10 @@ package cash.z.ecc.android.sdk.internal.transaction -import cash.z.ecc.android.sdk.db.entity.EncodedTransaction import cash.z.ecc.android.sdk.exception.TransactionEncoderException import cash.z.ecc.android.sdk.ext.masked import cash.z.ecc.android.sdk.internal.SaplingParamTool +import cash.z.ecc.android.sdk.internal.model.EncodedTransaction +import cash.z.ecc.android.sdk.internal.repository.DerivedDataRepository import cash.z.ecc.android.sdk.internal.twig import cash.z.ecc.android.sdk.internal.twigTask import cash.z.ecc.android.sdk.jni.RustBackendWelding @@ -22,7 +23,7 @@ import cash.z.ecc.android.sdk.model.Zatoshi internal class WalletTransactionEncoder( private val rustBackend: RustBackendWelding, private val saplingParamTool: SaplingParamTool, - private val repository: TransactionRepository + private val repository: DerivedDataRepository ) : TransactionEncoder { /** diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/PendingTransaction.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/PendingTransaction.kt new file mode 100644 index 00000000..0195dd34 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/PendingTransaction.kt @@ -0,0 +1,139 @@ +@file:Suppress("TooManyFunctions") + +package cash.z.ecc.android.sdk.model + +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.minutes + +data class PendingTransaction( + val id: Long, + val value: Zatoshi, + val memo: FirstClassByteArray?, + val raw: FirstClassByteArray, + val toAddress: String, + val accountIndex: Int, + val minedHeight: BlockHeight?, + val expiryHeight: BlockHeight?, + val cancelled: Int, + val encodeAttempts: Int, + val submitAttempts: Int, + val errorMessage: String?, + val errorCode: Int?, + val createTime: Long, + val rawTransactionId: FirstClassByteArray? +) + +// Note there are some commented out methods which aren't being removed yet, as they might be needed before the +// Roomoval draft PR is completed + +// fun PendingTransaction.isSameTxId(other: MinedTransaction) = +// rawTransactionId == other.rawTransactionId +// +// fun PendingTransaction.isSameTxId(other: PendingTransaction) = +// rawTransactionId == other.rawTransactionId + +internal fun PendingTransaction.hasRawTransactionId() = rawTransactionId?.byteArray?.isEmpty() == false + +fun PendingTransaction.isCreating() = + raw.byteArray.isNotEmpty() && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding() + +fun PendingTransaction.isCreated() = + raw.byteArray.isNotEmpty() && submitAttempts <= 0 && !isFailedSubmit() && !isFailedEncoding() + +fun PendingTransaction.isFailedEncoding() = raw.byteArray.isNotEmpty() && encodeAttempts > 0 + +fun PendingTransaction.isFailedSubmit(): Boolean { + return errorMessage != null || (errorCode != null && errorCode < 0) +} + +fun PendingTransaction.isFailure(): Boolean { + return isFailedEncoding() || isFailedSubmit() +} + +// fun PendingTransaction.isCancelled(): Boolean { +// return cancelled > 0 +// } + +fun PendingTransaction.isMined(): Boolean { + return minedHeight != null +} + +internal fun PendingTransaction.isSubmitted(): Boolean { + return submitAttempts > 0 +} + +@Suppress("ReturnCount") +internal fun PendingTransaction.isExpired( + latestHeight: BlockHeight?, + saplingActivationHeight: BlockHeight +): Boolean { + val expiryHeightLocal = expiryHeight + + if (latestHeight == null || expiryHeightLocal == null) { + return false + } + // TODO [#687]: test for off-by-one error here. Should we use <= or < + // TODO [#687]: https://github.com/zcash/zcash-android-wallet-sdk/issues/687 + if (latestHeight.value < saplingActivationHeight.value || expiryHeightLocal < saplingActivationHeight) { + return false + } + + return expiryHeightLocal < latestHeight +} + +private const val EXPIRY_BLOCK_COUNT = 100 + +// if we don't have info on a pendingtx after 100 blocks then it's probably safe to stop polling! +@Suppress("ReturnCount") +internal fun PendingTransaction.isLongExpired( + latestHeight: BlockHeight?, + saplingActivationHeight: BlockHeight +): Boolean { + val expiryHeightLocal = expiryHeight + + if (latestHeight == null || expiryHeightLocal == null) { + return false + } + + if (latestHeight.value < saplingActivationHeight.value || expiryHeightLocal < saplingActivationHeight) { + return false + } + return (latestHeight.value - expiryHeightLocal.value) > EXPIRY_BLOCK_COUNT +} + +private const val ERROR_CODE_MARKED_FOR_DELETION = -9090 + +internal fun PendingTransaction.isMarkedForDeletion(): Boolean { + return rawTransactionId == null && (errorCode ?: 0) == ERROR_CODE_MARKED_FOR_DELETION +} + +private val smallThreshold = 30.minutes +private val hugeThreshold = 30.days + +internal fun PendingTransaction.isSafeToDiscard(): Boolean { + // invalid dates shouldn't happen or should be temporary + if (createTime < 0) return false + + val ageInMilliseconds = System.currentTimeMillis() - createTime + return when { + // if it is mined, then it is not pending so it can be deleted fairly quickly from this db + isMined() && ageInMilliseconds > smallThreshold.inWholeMilliseconds -> true + // if a tx fails to encode, then there's not much we can do with it + isFailedEncoding() && ageInMilliseconds > smallThreshold.inWholeMilliseconds -> true + // don't delete failed submissions until they've been cleaned up, properly, or else we lose + // the ability to remove them in librustzcash prior to expiration + isFailedSubmit() && isMarkedForDeletion() -> true + !isMined() && ageInMilliseconds > hugeThreshold.inWholeMilliseconds -> true + else -> false + } +} + +fun PendingTransaction.isPending(currentHeight: BlockHeight?): Boolean { + // not mined and not expired and successfully created + return !isSubmitSuccess() && minedHeight == null && + (expiryHeight == null || expiryHeight.value > (currentHeight?.value ?: 0L)) +} + +fun PendingTransaction.isSubmitSuccess(): Boolean { + return submitAttempts > 0 && (errorCode != null && errorCode >= 0) && errorMessage == null +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Transaction.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Transaction.kt new file mode 100644 index 00000000..e00f7c6b --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/Transaction.kt @@ -0,0 +1,35 @@ +package cash.z.ecc.android.sdk.model + +sealed class Transaction { + data class Received( + val id: Long, + val rawId: FirstClassByteArray, + val minedHeight: BlockHeight, + val expiryHeight: BlockHeight, + val index: Long, + val raw: FirstClassByteArray, + val receivedByAccount: Account, + val receivedTotal: Zatoshi, + val receivedNoteCount: Int, + val memoCount: Int, + val blockTimeEpochSeconds: Long + ) : Transaction() { + override fun toString() = "ReceivedTransaction" + } + + data class Sent( + val id: Long, + val rawId: FirstClassByteArray, + val minedHeight: BlockHeight, + val expiryHeight: BlockHeight, + val index: Long, + val raw: FirstClassByteArray, + val sentFromAccount: Account, + val sentTotal: Zatoshi, + val sentNoteCount: Int, + val memoCount: Int, + val blockTimeEpochSeconds: Long + ) : Transaction() { + override fun toString() = "SentTransaction" + } +} diff --git a/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionOverview.kt b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionOverview.kt new file mode 100644 index 00000000..a824a776 --- /dev/null +++ b/sdk-lib/src/main/java/cash/z/ecc/android/sdk/model/TransactionOverview.kt @@ -0,0 +1,26 @@ +package cash.z.ecc.android.sdk.model + +/** + * High level transaction information, suitable for mapping to a display of transaction history. + * + * Note that both sent and received transactions will have a positive net value. Consumers of this class must + */ +data class TransactionOverview( + val id: Long, + val rawId: FirstClassByteArray, + val minedHeight: BlockHeight, + val expiryHeight: BlockHeight, + val index: Long, + val raw: FirstClassByteArray, + val isSentTransaction: Boolean, + val netValue: Zatoshi, + val feePaid: Zatoshi, + val isChange: Boolean, + val isWalletInternal: Boolean, + val receivedNoteCount: Int, + val sentNoteCount: Int, + val memoCount: Int, + val blockTimeEpochSeconds: Long +) { + override fun toString() = "TransactionOverview" +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c3bd3bc0..d18c30ed 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -68,6 +68,7 @@ dependencyResolutionManagement { val androidxAppcompatVersion = extra["ANDROIDX_APPCOMPAT_VERSION"].toString() val androidxConstraintLayoutVersion = extra["ANDROIDX_CONSTRAINT_LAYOUT_VERSION"].toString() val androidxCoreVersion = extra["ANDROIDX_CORE_VERSION"].toString() + val androidxDatabaseVersion = extra["ANDROIDX_DATABASE_VERSION"].toString() val androidxEspressoVersion = extra["ANDROIDX_ESPRESSO_VERSION"].toString() val androidxLifecycleVersion = extra["ANDROIDX_LIFECYCLE_VERSION"].toString() val androidxMultidexVersion = extra["ANDROIDX_MULTIDEX_VERSION"].toString() @@ -126,6 +127,8 @@ dependencyResolutionManagement { library("androidx-paging", "androidx.paging:paging-runtime-ktx:$androidxPagingVersion") library("androidx-room-compiler", "androidx.room:room-compiler:$androidxRoomVersion") library("androidx-room-core", "androidx.room:room-ktx:$androidxRoomVersion") + library("androidx-sqlite", "androidx.sqlite:sqlite-ktx:${androidxDatabaseVersion}") + library("androidx-sqlite-framework", "androidx.sqlite:sqlite-framework:${androidxDatabaseVersion}") library("bip39", "cash.z.ecc.android:kotlin-bip39:$bip39Version") library("grpc-android", "io.grpc:grpc-android:$grpcVersion") library("grpc-okhttp", "io.grpc:grpc-okhttp:$grpcVersion")