From de0d85c20d848d076442346ebfccf8fead2506f6 Mon Sep 17 00:00:00 2001 From: Kevin Gorham Date: Thu, 27 Feb 2020 03:25:07 -0500 Subject: [PATCH] First pass at adding documentation. --- .../demos/getaddress/GetAddressFragment.kt | 4 + .../demos/getblock/GetBlockFragment.kt | 5 + .../getblockrange/GetBlockRangeFragment.kt | 6 + .../GetLatestHeightFragment.kt | 5 + .../getprivatekey/GetPrivateKeyFragment.kt | 12 +- .../sdk/demoapp/demos/home/HomeFragment.kt | 8 +- .../ListTransactionsFragment.kt | 7 +- .../listtransactions/TransactionAdapter.kt | 3 + .../listtransactions/TransactionViewHolder.kt | 3 + .../sdk/demoapp/demos/send/SendFragment.kt | 8 + .../sdk/demoapp/util/SimpleMnemonics.kt | 7 + .../cash/z/wallet/sdk/demoapp/DemoConfig.kt | 1 - .../java/cash/z/wallet/sdk/Initializer.kt | 145 +++++++++++++++++- .../java/cash/z/wallet/sdk/SdkSynchronizer.kt | 57 ++++++- .../java/cash/z/wallet/sdk/Synchronizer.kt | 55 ++++++- .../z/wallet/sdk/block/CompactBlockDbStore.kt | 16 +- .../sdk/block/CompactBlockDownloader.kt | 27 ++++ .../wallet/sdk/block/CompactBlockProcessor.kt | 140 ++++++++++++++++- .../z/wallet/sdk/block/CompactBlockStore.kt | 6 + .../sdk/transaction/TransactionRepository.kt | 3 + 20 files changed, 484 insertions(+), 34 deletions(-) diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getaddress/GetAddressFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getaddress/GetAddressFragment.kt index 941bbee9..a07e094f 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getaddress/GetAddressFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getaddress/GetAddressFragment.kt @@ -6,6 +6,10 @@ import cash.z.wallet.sdk.demoapp.App import cash.z.wallet.sdk.demoapp.BaseDemoFragment import cash.z.wallet.sdk.demoapp.databinding.FragmentGetAddressBinding +/** + * Displays the address associated with the seed defined by the default config. To modify the seed + * that is used, update the `DemoConfig.seedWords` value. + */ class GetAddressFragment : BaseDemoFragment() { private var seed: ByteArray = App.instance.defaultConfig.seed diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblock/GetBlockFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblock/GetBlockFragment.kt index b7ef4452..aaa97a57 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblock/GetBlockFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblock/GetBlockFragment.kt @@ -8,6 +8,11 @@ import cash.z.wallet.sdk.demoapp.databinding.FragmentGetBlockBinding import cash.z.wallet.sdk.service.LightWalletGrpcService import cash.z.wallet.sdk.service.LightWalletService +/** + * Retrieves a compact block from the lightwalletd service and displays basic information about it. + * This demonstrates the basic ability to connect to the server, request a compact block and parse + * the response. + */ class GetBlockFragment : BaseDemoFragment() { private val host = App.instance.defaultConfig.host private val port = App.instance.defaultConfig.port diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt index 7223db43..0b377147 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getblockrange/GetBlockRangeFragment.kt @@ -8,6 +8,12 @@ import cash.z.wallet.sdk.demoapp.databinding.FragmentGetBlockRangeBinding import cash.z.wallet.sdk.service.LightWalletGrpcService import cash.z.wallet.sdk.service.LightWalletService +/** + * Retrieves a range of compact block from the lightwalletd service and displays basic information + * about them. This demonstrates the basic ability to connect to the server, request a range of + * compact block and parse the response. This could be augmented to display metadata about certain + * block ranges for instance, to find the block with the most shielded transactions in a range. + */ class GetBlockRangeFragment : BaseDemoFragment() { private val host = App.instance.defaultConfig.host diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt index 4718d1ed..29f4e598 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getlatestheight/GetLatestHeightFragment.kt @@ -8,6 +8,11 @@ import cash.z.wallet.sdk.demoapp.databinding.FragmentGetLatestHeightBinding import cash.z.wallet.sdk.service.LightWalletGrpcService import cash.z.wallet.sdk.service.LightWalletService +/** + * Retrieves the latest block height from the lightwalletd server. This is the simplest test for + * connectivity with the server. Modify the `host` and the `port` to check the SDK's ability to + * communicate with a given lightwalletd instance. + */ class GetLatestHeightFragment : BaseDemoFragment() { private val host = App.instance.defaultConfig.host private val port = App.instance.defaultConfig.port diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt index 4a595730..eb881296 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/getprivatekey/GetPrivateKeyFragment.kt @@ -6,6 +6,12 @@ import cash.z.wallet.sdk.demoapp.App import cash.z.wallet.sdk.demoapp.BaseDemoFragment import cash.z.wallet.sdk.demoapp.databinding.FragmentGetPrivateKeyBinding +/** + * Displays the viewing key and spending key associated with the seed defined by the default config. + * To modify the seed that is used, update the `DemoConfig.seedWords` value. This demo takes two + * approaches to deriving the seed, one that is stateless and another that is not. In most cases, a + * wallet instance will call `new` on an initializer and then store the result. + */ class GetPrivateKeyFragment : BaseDemoFragment() { private var seed: ByteArray = App.instance.defaultConfig.seed private val initializer: Initializer = Initializer(App.instance) @@ -20,8 +26,10 @@ class GetPrivateKeyFragment : BaseDemoFragment() { /* * Initialize with the seed and retrieve one private key for each account specified (by * default, only 1 account is created). In a normal circumstance, a wallet app would then - * store these keys in its secure storage for retrieval, later. Private keys are only needed - * for sending funds. + * store these keys in its secure storage for retrieval, later. Spending keys are only + * needed when sending funds. Viewing keys can be derived from spending keys. In most cases, + * a call to `initializer.new` or `initializer.import` are the only time a wallet passes the + * seed to the SDK. From that point forward, only spending or viewing keys are needed. */ spendingKeys = initializer.new(seed, birthday) diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/home/HomeFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/home/HomeFragment.kt index 87aa4df8..c848625f 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/home/HomeFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/home/HomeFragment.kt @@ -8,11 +8,15 @@ import android.widget.TextView import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders -import cash.z.wallet.sdk.ext.twig import cash.z.wallet.sdk.demoapp.App import cash.z.wallet.sdk.demoapp.R -import cash.z.wallet.sdk.ext.ZcashSdk +import cash.z.wallet.sdk.ext.twig +/** + * The landing page for the demo. Every time the app returns to this screen, it clears all demo + * data just for sanity. The goal is for each demo to be self-contained so that the behavior is + * repeatable and independent of pre-existing state. + */ class HomeFragment : Fragment() { private lateinit var homeViewModel: HomeViewModel diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt index 2d0a3455..a227cfcb 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/ListTransactionsFragment.kt @@ -18,8 +18,11 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch /** - * List all transactions from a given seed and birthdate, defined in the Injector class which is - * intended to mimic dependency injection. + * List all transactions related to the given seed, since the given birthday. This begins by + * downloading any missing blocks and then validating and scanning their contents. Once scan is + * complete, the transactions are available in the database and can be accessed by any SQL tool. + * By default, the SDK uses a PagedTransactionRepository to provide transaction contents from the + * database in a paged format that works natively with RecyclerViews. */ class ListTransactionsFragment : BaseDemoFragment() { private val config = App.instance.defaultConfig diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/TransactionAdapter.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/TransactionAdapter.kt index 9a96534a..f7b3ddd6 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/TransactionAdapter.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/TransactionAdapter.kt @@ -7,6 +7,9 @@ import androidx.recyclerview.widget.DiffUtil import cash.z.wallet.sdk.demoapp.R import cash.z.wallet.sdk.entity.ConfirmedTransaction +/** + * Simple adapter implementation that knows how to bind a recyclerview to ClearedTransactions. + */ class TransactionAdapter : PagedListAdapter>( object : DiffUtil.ItemCallback() { diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt index c81b355d..6117d673 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/listtransactions/TransactionViewHolder.kt @@ -9,6 +9,9 @@ import cash.z.wallet.sdk.ext.convertZatoshiToZecString import java.text.SimpleDateFormat import java.util.* +/** + * Simple view holder for displaying confirmed transactions in the recyclerview. + */ class TransactionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val amountText = itemView.findViewById(R.id.text_transaction_amount) private val timeText = itemView.findViewById(R.id.text_transaction_timestamp) diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/send/SendFragment.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/send/SendFragment.kt index 31d8b257..50ad0a30 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/send/SendFragment.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/demos/send/SendFragment.kt @@ -15,6 +15,14 @@ import cash.z.wallet.sdk.demoapp.util.SampleStorageBridge import cash.z.wallet.sdk.entity.* import cash.z.wallet.sdk.ext.* +/** + * Demonstrates sending funds to an address. This is the most complex example that puts all of the + * pieces of the SDK together, including monitoring transactions for completion. It begins by + * downloading, validating and scanning any missing blocks. Once that is complete, the wallet is + * in a SYNCED state and available to send funds. Calling `sendToAddress` produces a flow of + * PendingTransaction objects which represent the active state of the transaction that was sent. + * Any time the state of that transaction changes, a new instance will be emitted. + */ class SendFragment : BaseDemoFragment() { private val config = App.instance.defaultConfig private val initializer = Initializer(App.instance, host = config.host, port = config.port) diff --git a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/SimpleMnemonics.kt b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/SimpleMnemonics.kt index 4172826e..d89d784d 100644 --- a/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/SimpleMnemonics.kt +++ b/samples/demo-app/app/src/main/java/cash/z/wallet/sdk/demoapp/util/SimpleMnemonics.kt @@ -7,6 +7,13 @@ import io.github.novacrypto.bip39.Words import io.github.novacrypto.bip39.wordlists.English import java.security.SecureRandom +/** + * A sample implementation of a plugin for handling Mnemonic phrases. Any library can easily be + * plugged into the SDK in this manner. In this case, we are wrapping a few example 3rd party + * libraries with a thin layer that converts from their API to ours via the MnemonicPlugin + * interface. We do not endorse these libraries, rather we just use them as an example of how to + * take existing infrastructure and plug it into the SDK. + */ class SimpleMnemonics : MnemonicPlugin { override fun nextEntropy(): ByteArray { diff --git a/samples/demo-app/app/src/zcashmainnet/java/cash/z/wallet/sdk/demoapp/DemoConfig.kt b/samples/demo-app/app/src/zcashmainnet/java/cash/z/wallet/sdk/demoapp/DemoConfig.kt index 16338f5e..662bcb64 100644 --- a/samples/demo-app/app/src/zcashmainnet/java/cash/z/wallet/sdk/demoapp/DemoConfig.kt +++ b/samples/demo-app/app/src/zcashmainnet/java/cash/z/wallet/sdk/demoapp/DemoConfig.kt @@ -16,7 +16,6 @@ data class DemoConfig( val toAddress: String = "zs1lcdmue7rewgvzh3jd09sfvwq3sumu6hkhpk53q94kcneuffjkdg9e3tyxrugkmpza5c3c5e6eqh" ) { val seed: ByteArray get() = SimpleMnemonics().toSeed(seedWords.toCharArray()) - fun newWalletBirthday() = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance) fun loadBirthday(height: Int = birthdayHeight) = Initializer.DefaultBirthdayStore.loadBirthdayFromAssets(App.instance, height) } \ No newline at end of file diff --git a/src/main/java/cash/z/wallet/sdk/Initializer.kt b/src/main/java/cash/z/wallet/sdk/Initializer.kt index 60dd1138..1e077051 100644 --- a/src/main/java/cash/z/wallet/sdk/Initializer.kt +++ b/src/main/java/cash/z/wallet/sdk/Initializer.kt @@ -19,6 +19,15 @@ import kotlin.reflect.KProperty * synchronizing begins. This begins with one of three actions, a call to either [new], [import] or * [open], where the last option is the most common case--when a user is opening a wallet they have * used before on this device. + * + * @param appContext the application context, used to extract the storage paths for the databases + * and param files. A reference to the context is not held beyond initialization. + * @param host the host that the synchronizer should use. + * @param port the port that the synchronizer should use when connecting to the host. + * @param alias the alias to use for this synchronizer. Think of it as a unique name that allows + * multiple synchronizers to function in the same app. The alias is mapped to database names for the + * cache and data DBs. This value is optional and is usually not required because most apps only + * need one synchronizer. */ class Initializer( appContext: Context, @@ -86,6 +95,8 @@ class Initializer( * DB. * @throws InitializerException.AlreadyInitializedException when the blocks table already exists * and [clearDataDb] is false. + * + * @return the spending key(s) associated with this wallet, for convenience. */ fun new( seed: ByteArray, @@ -116,6 +127,8 @@ class Initializer( * DB. * @throws InitializerException.AlreadyInitializedException when the blocks table already exists * and [clearDataDb] is false. + * + * @return the spending key(s) associated with this wallet, for convenience. */ fun import( seed: ByteArray, @@ -136,6 +149,10 @@ class Initializer( * wallet will use. This height helps with determining where to start downloading as well as how * far back to go during a rewind. Every wallet has a birthday and the initializer depends on * this value but does not own it. + * + * @return an instance of this class so that the function can be used fluidly. Spending keys are + * not returned because the SDK does not store them and this function is for opening a wallet + * that was created previously. */ fun open(birthday: WalletBirthday): Initializer { twig("Opening wallet with birthday ${birthday.height}") @@ -150,6 +167,25 @@ class Initializer( * simply hold the address and viewing key for each account, which simplifies the process of * scanning and decrypting compact blocks. * + * @param seed the seed to use for initializing accounts. We derive the address and the viewing + * key(s) from this seed and also return the related spending key(s). Only the viewing key is + * retained in the database in order to simplify scanning for the wallet. + * @param birthday the birthday to use for this wallet. This is used in order to seed the data + * DB with the first sapling tree, which also determines where the SDK begins downloading and + * scanning. Any blocks lower than the height represented by this birthday can safely be ignored + * since a wallet cannot have transactions prior to it's creation. + * @param numberOfAccounts the number of accounts to create. Only 1 account is tested and + * supported at this time. It is possible, although unlikely that multiple accounts would behave + * as expected. Due to the nature of shielded address, the official Zcash recommendation is to + * only use one address for shielded transactions. Unlike transparent coins, address rotation is + * not necessary for shielded Zcash transactions because the sensitive information is private. + * @param clearCacheDb when true, the cache DB will be deleted prior to initializing accounts. + * This is useful for preventing errors when the database already exists, which happens often + * in tests, demos and proof of concepts. + * @param clearDataDb when true, the cache DB will be deleted prior to initializing accounts. + * This is useful for preventing errors when the database already exists, which happens often + * in tests, demos and proof of concepts. + * * @return the spending keys for each account, ordered by index. These keys are only needed for * spending funds. */ @@ -207,7 +243,9 @@ class Initializer( /** * Internal function used to initialize the [rustBackend] before use. Initialization should only * happen as a result of [new], [import] or [open] being called or as part of stand-alone key - * derivation. + * derivation. This involves loading the shared object file via `System.loadLibrary`. + * + * @return the rustBackend that was loaded by this initializer. */ private fun requireRustBackend(): RustBackend { if (!isInitialized) { @@ -226,6 +264,10 @@ class Initializer( * Given a seed and a number of accounts, return the associated spending keys. These keys can * be used to derive the viewing keys. * + * @param seed the seed from which to derive spending keys. + * @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully + * supported so the default value of 1 is recommended. + * * @return the spending keys that correspond to the seed, formatted as Strings. */ fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array = @@ -234,6 +276,10 @@ class Initializer( /** * Given a seed and a number of accounts, return the associated viewing keys. * + * @param seed the seed from which to derive viewing keys. + * @param numberOfAccounts the number of accounts to use. Multiple accounts are not fully + * supported so the default value of 1 is recommended. + * * @return the viewing keys that correspond to the seed, formatted as Strings. */ fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array = @@ -242,6 +288,8 @@ class Initializer( /** * Given a spending key, return the associated viewing key. * + * @param spendingKey the key from which to derive the viewing key. + * * @return the viewing key that corresponds to the spending key. */ fun deriveViewingKey(spendingKey: String): String = @@ -250,6 +298,10 @@ class Initializer( /** * Given a seed and account index, return the associated address. * + * @param seed the seed from which to derive the address. + * @param accountIndex the index of the account to use for deriving the address. Multiple + * accounts are not fully supported so the default value of 1 is recommended. + * * @return the address that corresponds to the seed and account index. */ fun deriveAddress(seed: ByteArray, accountIndex: Int = 0) = @@ -258,6 +310,9 @@ class Initializer( /** * Given a viewing key string, return the associated address. * + * @param viewingKey the viewing key to use for deriving the address. The viewing key is tied to + * a specific account so no account index is required. + * * @return the address that corresponds to the viewing key. */ fun deriveAddress(viewingKey: String) = @@ -270,9 +325,20 @@ class Initializer( // Path Helpers // + /** + * Returns the path to the cache database that would correspond to the given alias. + * + * @param appContext the application context + * @param alias the alias to convert into a database path + */ fun cacheDbPath(appContext: Context, alias: String): String = aliasToPath(appContext, alias, ZcashSdk.DB_CACHE_NAME) + /** + * Returns the path to the data database that would correspond to the given alias. + * @param appContext the application context + * @param alias the alias to convert into a database path + */ fun dataDbPath(appContext: Context, alias: String): String = aliasToPath(appContext, alias, ZcashSdk.DB_DATA_NAME) @@ -287,7 +353,12 @@ class Initializer( /** - * Model object for holding wallet birthday. It is only used by this class. + * Model object for holding a wallet birthday. It is only used by this class. + * + * @param height the height at the time the wallet was born. + * @param hash the hash of the block at the height. + * @param time the block time at the height. + * @param tree the sapling tree corresponding to the height. */ data class WalletBirthday( val height: Int = -1, @@ -296,27 +367,66 @@ class Initializer( val tree: String = "" ) + /** + * Interface for classes that can handle birthday storage. This makes it possible to bridge into + * existing storage logic. Instances of this interface can also be used as property delegates, + * which enables the syntax `val birthday by birthdayStore` + */ interface WalletBirthdayStore : ReadWriteProperty { val newWalletBirthday: WalletBirthday + /** + * Get the birthday of the wallet, saved in this store. + */ fun getBirthday(): WalletBirthday + + /** + * Set the birthday of the wallet to be saved in this store. + */ fun setBirthday(value: WalletBirthday) + + /** + * Load a birthday matching the given height. This is most commonly used during import to + * find the first available checkpoint that is lower than the requested height. + * + * @param birthdayHeight the height to use as an upper bound for loading. + */ fun loadBirthday(birthdayHeight: Int): WalletBirthday + + /** + * Return true when a birthday has been stored in this instance. + */ fun hasExistingBirthday(): Boolean + + /** + * Return true when a birthday was imported into this instance. + */ fun hasImportedBirthday(): Boolean /* Property implementation that allows this interface to be used as a property delegate */ + /** + * Implement readable interface in order to be able to use instances of this interface as + * property delegates. + */ override fun getValue(thisRef: R, property: KProperty<*>): WalletBirthday { return getBirthday() } + /** + * Implement writable interface in order to be able to use instances of this interface as + * property delegates. + */ override fun setValue(thisRef: R, property: KProperty<*>, value: WalletBirthday) { setBirthday(value) } } + /** + * Default implementation of the [WalletBirthdayStore] interface that loads checkpoints from the + * assets directory, in JSON format and stores the current birthday in shared preferences. + */ class DefaultBirthdayStore( private val appContext: Context, private val importedBirthdayHeight: Int? = null, @@ -370,6 +480,8 @@ class Initializer( * is there, the rest will be too. If that's not the case, a call to this function will * result in an exception. * + * @param prefs the shared preference to use for loading the birthday. + * * @return a birthday from preferences if one exists and null, otherwise null */ private fun loadBirthdayFromPrefs(prefs: SharedPreferences?): WalletBirthday? { @@ -390,7 +502,7 @@ class Initializer( /** * Save the given birthday to the given preferences. * - * @param prefs the shared preferences to use + * @param prefs the shared preferences to use for saving the birthday. * @param birthday the birthday to save. It will be split into primitives. */ private fun saveBirthdayToPrefs(prefs: SharedPreferences, birthday: WalletBirthday) { @@ -423,16 +535,37 @@ class Initializer( */ private const val BIRTHDAY_DIRECTORY = "zcash/saplingtree" + /** + * The default alias to use for naming the preference file used for storage. + */ const val DEFAULT_ALIAS = "default_prefs" - // Constructor function + /** + * A convenience constructor function for creating an instance of this class to use for + * new wallets. It sets the stored birthday to match the `newWalletBirthday` checkpoint + * which is typically the most recent checkpoint available. + * + * @param appContext the application context. + * @param alias the alias to use when naming the preferences file used for storage. + */ fun NewWalletBirthdayStore(appContext: Context, alias: String = DEFAULT_ALIAS): WalletBirthdayStore { return DefaultBirthdayStore(appContext, alias = alias).apply { setBirthday(newWalletBirthday) } } - // Constructor function + /** + * A convenience constructor function for creating an instance of this class to use for + * imported wallets. It sets the stored birthday to match the given + * `importedBirthdayHeight` by finding the highest checkpoint that is below that height. + * + * @param appContext the application context. + * @param importedBirthdayHeight the height corresponding to the birthday of the wallet + * being imported. A checkpoint will be generated that allows scanning to start as close + * to this height as possible because any blocks before this height can safely be + * ignored since a wallet cannot have transactions before it is born. + * @param alias the alias to use when naming the preferences file used for storage. + */ fun ImportedWalletBirthdayStore(appContext: Context, importedBirthdayHeight: Int?, alias: String = DEFAULT_ALIAS): WalletBirthdayStore { return DefaultBirthdayStore(appContext, alias = alias).apply { if (importedBirthdayHeight != null) { @@ -504,6 +637,8 @@ class Initializer( * permit the alias to be used as part of a file name for the preferences and databases. This * enables multiple wallets to exist on one device, which is also helpful for sweeping funds. * + * @param alias the alias to validate. + * * @throws IllegalArgumentException whenever the alias is not less than 100 characters or * contains something other than alphanumeric characters. Underscores are allowed but aliases * must start with a letter. diff --git a/src/main/java/cash/z/wallet/sdk/SdkSynchronizer.kt b/src/main/java/cash/z/wallet/sdk/SdkSynchronizer.kt index 865430b4..99338230 100644 --- a/src/main/java/cash/z/wallet/sdk/SdkSynchronizer.kt +++ b/src/main/java/cash/z/wallet/sdk/SdkSynchronizer.kt @@ -32,9 +32,9 @@ import kotlin.coroutines.CoroutineContext * pieces can be tied together. Its goal is to allow a developer to focus on their app rather than * the nuances of how Zcash works. * - * @param ledger exposes flows of wallet transaction information. - * @param manager manages and tracks outbound transactions. - * @param processor saves the downloaded compact blocks to the cache and then scans those blocks for + * @property ledger exposes flows of wallet transaction information. + * @property manager manages and tracks outbound transactions. + * @property processor saves the downloaded compact blocks to the cache and then scans those blocks for * data related to this wallet. */ @ExperimentalCoroutinesApi @@ -144,6 +144,8 @@ class SdkSynchronizer internal constructor( * scope is only used for launching this synchronzer's job as a child. If no scope is provided, * then this synchronizer and all of its coroutines will run until stop is called, which is not * recommended since it can leak resources. That type of behavior is more useful for tests. + * + * @return an instance of this class so that this function can be used fluidly. */ override fun start(parentScope: CoroutineScope?): Synchronizer { if (::coroutineScope.isInitialized) throw SynchronizerException.FalseStart @@ -181,6 +183,10 @@ class SdkSynchronizer internal constructor( ledger.invalidate() } + /** + * Calculate the latest balance, based on the blocks that have been scanned and transmit this + * information into the flow of [balances]. + */ suspend fun refreshBalance() { twig("refreshing balance") _balances.send(processor.getBalanceInfo()) @@ -357,9 +363,19 @@ class SdkSynchronizer internal constructor( } /** - * Simplest constructor possible. Useful for demos, sample apps or PoC's. Anything more complex + * A convenience constructor that accepts the information most likely to change and uses defaults + * for everything else. This is useful for demos, sample apps or PoC's. Anything more complex * will probably want to handle initialization, directly. + * + * @param appContext the application context. This is mostly used for finding databases and params + * files within the apps secure storage area. + * @param lightwalletdHost the lightwalletd host to use for connections. + * @param lightwalletdPort the lightwalletd port to use for connections. + * @param seed the seed to use for this wallet, when importing. Null when creating a new wallet. + * @param birthdayStore the place to store the birthday of this wallet for future reference, which + * allows something else to manage the state on behalf of the initializer. */ +@Suppress("FunctionName") fun Synchronizer( appContext: Context, lightwalletdHost: String = ZcashSdk.DEFAULT_LIGHTWALLETD_HOST, @@ -387,6 +403,20 @@ fun Synchronizer( return Synchronizer(appContext, initializer) } +/** + * Constructor function to use in most cases. This is a convenience function for when a wallet has + * already created an initializer. Meaning, the basic flow is to call either [Initializer.new] or + * [Initializer.import] on the first run and then [Initializer.open] for all subsequent launches of + * the wallet. From there, the initializer is passed to this function in order to start syncing from + * where the wallet left off. + * + * @param appContext the application context. This is mostly used for finding databases and params + * files within the apps secure storage area. + * @param initializer the helper that is leveraged for creating all the components that the + * Synchronizer requires. It is mainly responsible for initializing the databases associated with + * this synchronizer. + */ +@Suppress("FunctionName") fun Synchronizer( appContext: Context, initializer: Initializer @@ -400,7 +430,24 @@ fun Synchronizer( /** * Constructor function for building a Synchronizer in the most flexible way possible. This allows - * a wallet maker to customize any subcomponent of the Synchronzier. + * a wallet maker to customize any subcomponent of the Synchronzer. + * + * @param appContext the application context. This is mostly used for finding databases and params + * files within the apps secure storage area. + * @param lightwalletdHost the lightwalletd host to use for connections. + * @param lightwalletdPort the lightwalletd port to use for connections. + * @param ledger repository of wallet transactions, providing an agnostic interface to the + * underlying information. + * @param blockStore component responsible for storing compact blocks downloaded from lightwalletd. + * @param service the lightwalletd service that can provide compact blocks and submit transactions. + * @param encoder the component responsible for encoding transactions. + * @param downloader the component responsible for downloading ranges of compact blocks. + * @param manager the component that manages outbound transactions in order to report which ones are + * still pending, particularly after failed attempts or dropped connectivity. The intent is to help + * monitor outbound transactions status through to completion. + * @param processor the component responsible for processing compact blocks. This is effectively the + * brains of the synchronizer that implements most of the high-level business logic and determines + * the current state of the wallet. */ @Suppress("FunctionName") fun Synchronizer( diff --git a/src/main/java/cash/z/wallet/sdk/Synchronizer.kt b/src/main/java/cash/z/wallet/sdk/Synchronizer.kt index a1ca4c0d..dc3c8511 100644 --- a/src/main/java/cash/z/wallet/sdk/Synchronizer.kt +++ b/src/main/java/cash/z/wallet/sdk/Synchronizer.kt @@ -25,6 +25,8 @@ interface Synchronizer { * @param parentScope the scope to use for this synchronizer, typically something with a * lifecycle such as an Activity. Implementations should leverage structured concurrency and * cancel all jobs when this scope completes. + * + * @return an instance of the class so that this function can be used fluidly. */ fun start(parentScope: CoroutineScope? = null): Synchronizer @@ -99,6 +101,8 @@ interface Synchronizer { * * @param accountId the optional accountId whose address is of interest. By default, the first * account is used. + * + * @return the address for the given account. */ suspend fun getAddress(accountId: Int = 0): String @@ -110,6 +114,11 @@ interface Synchronizer { * @param toAddress the recipient's address. * @param memo the optional memo to include as part of the transaction. * @param fromAccountId the optional account id to use. By default, the first account is used. + * + * @return a flow of PendingTransaction objects representing changes to the state of the + * transaction. Any time the state changes a new instance will be emitted by this flow. This is + * 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( spendingKey: String, @@ -122,26 +131,38 @@ interface Synchronizer { /** * Returns true when the given address is a valid z-addr. Invalid addresses will throw an - * exception. Valid z-addresses have these characteristics: //TODO + * exception. Valid z-addresses have these characteristics: //TODO copy info from related ZIP * * @param address the address to validate. + * + * @return true when the given address is a valid z-addr. + * * @throws RuntimeException when the address is invalid. */ suspend fun isValidShieldedAddr(address: String): Boolean /** * Returns true when the given address is a valid t-addr. Invalid addresses will throw an - * exception. Valid t-addresses have these characteristics: //TODO + * exception. Valid t-addresses have these characteristics: //TODO copy info from related ZIP * * @param address the address to validate. + * + * @return true when the given address is a valid t-addr. + * * @throws RuntimeException when the address is invalid. */ suspend fun isValidTransparentAddr(address: String): Boolean /** - * Validates the given address, returning information about why it is invalid. + * Validates the given address, returning information about why it is invalid. This is a + * convenience method that combines the behavior of [isValidShieldedAddr] and + * [isValidTransparentAddr] into one call so that the developer doesn't have to worry about + * handling the exceptions that they throw. Rather, exceptions are converted to + * [AddressType.Invalid] which has a `reason` property describing why it is invalid. * * @param address the address to validate. + * + * @return an instance of [AddressType] providing validation info regarding the given address. */ suspend fun validateAddress(address: String): AddressType @@ -150,6 +171,7 @@ interface Synchronizer { * an option if the transaction has not yet been submitted to the server. * * @param transaction the transaction to cancel. + * * @return true when the cancellation request was successful. False when it is too late. */ suspend fun cancelSpend(transaction: PendingTransaction): Boolean @@ -193,7 +215,9 @@ interface Synchronizer { */ var onChainErrorHandler: ((Int, Int) -> Any)? - + /** + * Represents the status of this Synchronizer, which is useful for communicating to the user. + */ enum class Status { /** * Indicates that [stop] has been called on this Synchronizer and it will no longer be used. @@ -231,12 +255,35 @@ interface Synchronizer { SYNCED } + /** + * Represents the types of addresses, either Shielded, Transparent or Invalid. + */ sealed class AddressType { + /** + * Marker interface for valid [AddressType] instances. + */ interface Valid + + /** + * An instance of [AddressType] corresponding to a valid z-addr. + */ object Shielded : Valid, AddressType() + + /** + * An instance of [AddressType] corresponding to a valid t-addr. + */ object Transparent : Valid, AddressType() + + /** + * An instance of [AddressType] corresponding to an invalid address. + * + * @param reason a descrption of why the address was invalid. + */ class Invalid(val reason: String = "Invalid") : AddressType() + /** + * A convenience method that returns true when an instance of this class is invalid. + */ val isNotValid get() = this !is Valid } diff --git a/src/main/java/cash/z/wallet/sdk/block/CompactBlockDbStore.kt b/src/main/java/cash/z/wallet/sdk/block/CompactBlockDbStore.kt index 1c5357de..a9cfe3d6 100644 --- a/src/main/java/cash/z/wallet/sdk/block/CompactBlockDbStore.kt +++ b/src/main/java/cash/z/wallet/sdk/block/CompactBlockDbStore.kt @@ -6,14 +6,20 @@ import androidx.room.RoomDatabase import cash.z.wallet.sdk.db.CompactBlockDao import cash.z.wallet.sdk.db.CompactBlockDb import cash.z.wallet.sdk.entity.CompactBlockEntity -import cash.z.wallet.sdk.ext.ZcashSdk.DB_CACHE_NAME import cash.z.wallet.sdk.ext.ZcashSdk.SAPLING_ACTIVATION_HEIGHT import cash.z.wallet.sdk.rpc.CompactFormats import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext +/** + * 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. + * + * @param appContext the application context. This is used for creating the database. + * @property dbPath the absolute path to the database. + */ class CompactBlockDbStore( - applicationContext: Context, + appContext: Context, val dbPath: String ) : CompactBlockStore { @@ -21,12 +27,12 @@ class CompactBlockDbStore( private val cacheDb: CompactBlockDb init { - cacheDb = createCompactBlockCacheDb(applicationContext) + cacheDb = createCompactBlockCacheDb(appContext) cacheDao = cacheDb.complactBlockDao() } - private fun createCompactBlockCacheDb(applicationContext: Context): CompactBlockDb { - return Room.databaseBuilder(applicationContext, CompactBlockDb::class.java, dbPath) + private fun createCompactBlockCacheDb(appContext: Context): CompactBlockDb { + return Room.databaseBuilder(appContext, CompactBlockDb::class.java, dbPath) .setJournalMode(RoomDatabase.JournalMode.TRUNCATE) // this is a simple cache of blocks. destroying the db should be benign .fallbackToDestructiveMigration() diff --git a/src/main/java/cash/z/wallet/sdk/block/CompactBlockDownloader.kt b/src/main/java/cash/z/wallet/sdk/block/CompactBlockDownloader.kt index c81625f6..afb775b5 100644 --- a/src/main/java/cash/z/wallet/sdk/block/CompactBlockDownloader.kt +++ b/src/main/java/cash/z/wallet/sdk/block/CompactBlockDownloader.kt @@ -18,25 +18,52 @@ open class CompactBlockDownloader( val compactBlockStore: CompactBlockStore ) { + /** + * Requests the given range of blocks from the lightwalletService and then persists them to the + * compactBlockStore. + * + * @param heightRange the inclusive range of heights to request. For example 10..20 would + * request 11 blocks (including block 10 and block 20). + * + * @return the number of blocks that were returned in the results from the lightwalletService. + */ suspend fun downloadBlockRange(heightRange: IntRange): Int = withContext(IO) { val result = lightwalletService.getBlockRange(heightRange) compactBlockStore.write(result) result.size } + /** + * Rewind the storage to the given height, usually to handle reorgs. + * + * @param height the height to which the data will rewind. + */ suspend fun rewindToHeight(height: Int) = withContext(IO) { // TODO: cancel anything in flight compactBlockStore.rewindTo(height) } + /** + * Return the latest block height known by the lightwalletService. + * + * @return the latest block height. + */ suspend fun getLatestBlockHeight() = withContext(IO) { lightwalletService.getLatestBlockHeight() } + /** + * Return the latest block height that has been persisted into the [CompactBlockStore]. + * + * @return the latest block height that has been persisted. + */ suspend fun getLastDownloadedHeight() = withContext(IO) { compactBlockStore.getLatestHeight() } + /** + * Stop this downloader and cleanup any resources being used. + */ fun stop() { lightwalletService.shutdown() compactBlockStore.close() diff --git a/src/main/java/cash/z/wallet/sdk/block/CompactBlockProcessor.kt b/src/main/java/cash/z/wallet/sdk/block/CompactBlockProcessor.kt index 8d06e206..26cd0a49 100644 --- a/src/main/java/cash/z/wallet/sdk/block/CompactBlockProcessor.kt +++ b/src/main/java/cash/z/wallet/sdk/block/CompactBlockProcessor.kt @@ -33,6 +33,10 @@ import kotlin.math.roundToInt * all the business logic required to validate and scan the blockchain and is therefore tightly coupled with * librustzcash. * + * @property downloader the component responsible for downloading compact blocks and persisting them + * locally for processing. + * @property repository the repository holding transaction information. + * @property rustBackend the librustzcash functionality available and exposed to the SDK. * @param minimumHeight the lowest height that we could care about. This is mostly used during * reorgs as a backstop to make sure we do not rewind beyond sapling activation. It also is factored * in when considering initial range to download. In most cases, this should be the birthday height @@ -45,7 +49,15 @@ class CompactBlockProcessor( private val rustBackend: RustBackendWelding, minimumHeight: Int = SAPLING_ACTIVATION_HEIGHT ) { + /** + * Callback for any critical errors that occur while processing compact blocks. + */ var onProcessorErrorListener: ((Throwable) -> Boolean)? = null + + /** + * Callbaqck for reorgs. This callback is invoked when validation fails with the height at which + * an error was found and the lower bound to which the data will rewind, at most. + */ var onChainErrorListener: ((Int, Int) -> Any)? = null private val consecutiveChainErrors = AtomicInteger(0) @@ -62,12 +74,26 @@ class CompactBlockProcessor( */ private var currentInfo = ProcessorInfo() + /** + * The flow of state values so that a wallet can monitor the state of this class without needing + * to poll. + */ val state = _state.asFlow() + + /** + * The flow of progress values so that a wallet can monitor how much downloading remains + * without needing to poll. + */ val progress = _progress.asFlow() + + /** + * The flow of detailed processorInfo like the range of blocks that shall be downloaded and + * scanned. This gives the wallet a lot of insight into the work of this processor. + */ val processorInfo = _processorInfo.asFlow() /** - * Download compact blocks, verify and scan them. + * Download compact blocks, verify and scan them until [stop] is called. */ suspend fun start() = withContext(IO) { twig("processor starting") @@ -165,6 +191,8 @@ class CompactBlockProcessor( * in ascending order, with no gaps and are also chain-sequential. This means every block's * prevHash value matches the preceding block in the chain. * + * @param lastScanRange the range to be validated and scanned. + * * @return error code or -1 when there is no error. */ private suspend fun validateAndScanNewBlocks(lastScanRange: IntRange): Int = withContext(IO) { @@ -194,7 +222,9 @@ class CompactBlockProcessor( } /** - * Download all blocks in the given range. + * Request all blocks in the given range and persist them locally for processing, later. + * + * @param range the range of blocks to download. */ @VisibleForTesting //allow mocks to verify how this is called, rather than the downloader, which is more complex internal suspend fun downloadNewBlocks(range: IntRange) = withContext(IO) { @@ -234,6 +264,11 @@ class CompactBlockProcessor( * Validate all blocks in the given range, ensuring that the blocks are in ascending order, with * no gaps and are also chain-sequential. This means every block's prevHash value matches the * preceding block in the chain. + * + * @param range the range of blocks to validate. + * + * @return -1 when there is not problem. Otherwise, return the lowest height where an error was + * found. In other words, validation starts at the back of the chain and works toward the tip. */ private fun validateNewBlocks(range: IntRange?): Int { if (range?.isEmpty() != false) { @@ -248,8 +283,13 @@ class CompactBlockProcessor( } /** - * Scan all blocks in the given range, decrypting anything that matches our wallet and storing - * the data. + * Scan all blocks in the given range, decrypting and persisting anything that matches our + * wallet. + * + * @param range the range of blocks to scan. + * + * @return -1 when there is not problem. Otherwise, return the lowest height where an error was + * found. In other words, scanning starts at the back of the chain and works toward the tip. */ private suspend fun scanNewBlocks(range: IntRange?): Boolean = withContext(IO) { if (range?.isEmpty() != false) { @@ -282,6 +322,13 @@ class CompactBlockProcessor( } /** + * Emit an instance of processorInfo, corresponding to the provided data. + * + * @param networkBlockHeight the latest block available to lightwalletd that may or may not be + * downloaded by this wallet yet. + * @param lastScannedHeight the height up to which the wallet last scanned. This determines + * where the next scan will begin. + * @param lastDownloadedHeight the last compact block that was successfully downloaded. * @param lastScanRange the inclusive range to scan. This represents what we most recently * wanted to scan. In most cases, it will be an invalid range because we'd like to scan blocks * that we don't yet have. @@ -326,14 +373,29 @@ class CompactBlockProcessor( } } + /** + * Get the height of the last block that was downloaded by this processor. + * + * @return the last downloaded height reported by the downloader. + */ suspend fun getLastDownloadedHeight() = withContext(IO) { downloader.getLastDownloadedHeight() } + /** + * Get the height of the last block that was scanned by this processor. + * + * @return the last scanned height reported by the repository. + */ suspend fun getLastScannedHeight() = withContext(IO) { repository.lastScannedHeight() } + /** + * Get address corresponding to the given account for this wallet. + * + * @return the address of this wallet. + */ suspend fun getAddress(accountId: Int) = withContext(IO) { rustBackend.getAddress(accountId) } @@ -342,6 +404,8 @@ class CompactBlockProcessor( * Calculates the latest balance info. Defaults to the first account. * * @param accountIndex the account to check for balance info. + * + * @return an instance of WalletBalance containing information about available and total funds. */ suspend fun getBalanceInfo(accountIndex: Int = 0): WalletBalance = withContext(IO) { twigTask("checking balance info") { @@ -358,19 +422,65 @@ class CompactBlockProcessor( } } - suspend fun setState(newState: State) { + /** + * Transmits the given state for this processor. + */ + private suspend fun setState(newState: State) { _state.send(newState) } + /** + * Sealed class representing the various states of this processor. + */ sealed class State { + /** + * Marker interface for [State] instances that represent when the wallet is connected. + */ interface Connected + + /** + * Marker interface for [State] instances that represent when the wallet is syncing. + */ interface Syncing + + /** + * [State] for when the wallet is actively downloading compact blocks because the latest + * block height available from the server is greater than what we have locally. We move out + * of this state once our local height matches the server. + */ object Downloading : Connected, Syncing, State() + + /** + * [State] for when the blocks that have been downloaded are actively being validated to + * ensure that there are no gaps and that every block is chain-sequential to the previous + * block, which determines whether a reorg has happened on our watch. + */ object Validating : Connected, Syncing, State() + + /** + * [State] for when the blocks that have been downloaded are actively being decrypted. + */ object Scanning : Connected, Syncing, State() + + /** + * [State] for when we are done decrypting blocks, for now. + */ class Scanned(val scannedRange:IntRange) : Connected, Syncing, State() + + /** + * [State] for when we have no connection to lightwalletd. + */ object Disconnected : State() + + /** + * [State] for when [stop] has been called. For simplicity, processors should not be + * restarted but they are not prevented from this behavior. + */ object Stopped : State() + + /** + * [State] the initial state of the processor, once it is constructed. + */ object Initialized : State() } @@ -390,6 +500,14 @@ class CompactBlockProcessor( ) /** + * Data class for holding detailed information about the processor. + * + * @param networkBlockHeight the latest block available to lightwalletd that may or may not be + * downloaded by this wallet yet. + * @param lastScannedHeight the height up to which the wallet last scanned. This determines + * where the next scan will begin. + * @param lastDownloadedHeight the last compact block that was successfully downloaded. + * * @param lastDownloadRange inclusive range to download. Meaning, if the range is 10..10, * then we will download exactly block 10. If the range is 11..10, then we want to download * block 11 but can't. @@ -404,7 +522,9 @@ class CompactBlockProcessor( ) { /** - * Returns false when all values match their defaults. + * Determines whether this instance has data. + * + * @return false when all values match their defaults. */ val hasData get() = networkBlockHeight != -1 || lastScannedHeight != -1 @@ -413,13 +533,17 @@ class CompactBlockProcessor( || lastScanRange != 0..-1 /** - * Returns true when there are more than zero blocks remaining to download. + * Determines whether this instance is actively downloading compact blocks. + * + * @return true when there are more than zero blocks remaining to download. */ val isDownloading: Boolean get() = !lastDownloadRange.isEmpty() && lastDownloadedHeight < lastDownloadRange.last /** - * Returns true when downloading has completed and there are more than zero blocks remaining + * Determines whether this instance is actively scanning or validating compact blocks. + * + * @return true when downloading has completed and there are more than zero blocks remaining * to be scanned. */ val isScanning: Boolean get() = !isDownloading diff --git a/src/main/java/cash/z/wallet/sdk/block/CompactBlockStore.kt b/src/main/java/cash/z/wallet/sdk/block/CompactBlockStore.kt index 42d175d6..ffab70a5 100644 --- a/src/main/java/cash/z/wallet/sdk/block/CompactBlockStore.kt +++ b/src/main/java/cash/z/wallet/sdk/block/CompactBlockStore.kt @@ -8,11 +8,15 @@ import cash.z.wallet.sdk.rpc.CompactFormats interface CompactBlockStore { /** * Gets the highest block that is currently stored. + * + * @return the latest block height. */ suspend fun getLatestHeight(): Int /** * Write the given blocks to this store, which may be anything from an in-memory cache to a DB. + * + * @param result the list of compact blocks to persist. */ suspend fun write(result: List) @@ -21,6 +25,8 @@ interface CompactBlockStore { * * After this operation, the data store will look the same as one that has not yet stored the given block height. * Meaning, if max height is 100 block and rewindTo(50) is called, then the highest block remaining will be 49. + * + * @param height the target height to which to rewind. */ suspend fun rewindTo(height: Int) diff --git a/src/main/java/cash/z/wallet/sdk/transaction/TransactionRepository.kt b/src/main/java/cash/z/wallet/sdk/transaction/TransactionRepository.kt index 533385fb..e64ecc58 100644 --- a/src/main/java/cash/z/wallet/sdk/transaction/TransactionRepository.kt +++ b/src/main/java/cash/z/wallet/sdk/transaction/TransactionRepository.kt @@ -4,6 +4,9 @@ import androidx.paging.PagedList import cash.z.wallet.sdk.entity.* import kotlinx.coroutines.flow.Flow +/** + * Repository of wallet transactions, providing an agnostic interface to the underlying information. + */ interface TransactionRepository { fun lastScannedHeight(): Int fun isInitialized(): Boolean