Initializer improvements and iteration.

- Extracted parent interface
- Created ViewingKey Initializer to start synchronizing without the seed (only the viewing key is needed)
- Extracted tools from the existing initializer and simlified it a bit
This commit is contained in:
Kevin Gorham 2020-09-11 03:29:17 -04:00
parent 94c8a18be7
commit 11431f5e5a
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
4 changed files with 403 additions and 223 deletions

View File

@ -0,0 +1,31 @@
package cash.z.ecc.android.sdk
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import org.junit.Assert.assertEquals
import org.junit.Test
class VkInitializerTest {
@Test
fun testInit() {
val height = 1_419_900
val initializer = VkInitializer(context) {
importedWalletBirthday(height)
viewingKeys("zxviews1qvn6j50dqqqqpqxqkvqgx2sp63jccr4k5t8zefadpzsu0yy73vczfznwc794xz6lvy3yp5ucv43lww48zz95ey5vhrsq83dqh0ky9junq0cww2wjp9c3cd45n5l5x8l2g9atnx27e9jgyy8zasjy26gugjtefphan9al3tx208m8ekev5kkx3ug6pd0qk4gq4j4wfuxajn388pfpq54wklwktqkyjz9e6gam0n09xjc35ncd3yah5aa9ezj55lk4u7v7hn0v86vz7ygq4qj2v",
"zxviews1qv886f6hqqqqpqy2ajg9sm22vs4gm4hhajthctfkfws34u45pjtut3qmz0eatpqzvllgsvlk3x0y35ktx5fnzqqzueyph20k3328kx46y3u5xs4750cwuwjuuccfp7la6rh8yt2vjz6tylsrwzy3khtjjzw7etkae6gw3vq608k7quka4nxkeqdxxsr9xxdagv2rhhwugs6w0cquu2ykgzgaln2vyv6ah3ram2h6lrpxuznyczt2xl3lyxcwlk4wfz5rh7wzfd7642c2ae5d7")
alias = "VkInitTest2"
}
assertEquals(height, initializer.birthday.height)
}
companion object {
private val context = InstrumentationRegistry.getInstrumentation().context
init {
Twig.plant(TroubleshootingTwig())
}
}
}

View File

@ -14,6 +14,8 @@ import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool.WalletBirthday
import cash.z.ecc.android.sdk.transaction.*
import com.google.gson.Gson
import com.google.gson.stream.JsonReader
@ -41,11 +43,11 @@ import kotlin.reflect.KProperty
*/
class Initializer(
appContext: Context,
val host: String = ZcashSdk.DEFAULT_LIGHTWALLETD_HOST,
val port: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT,
private val alias: String = ZcashSdk.DEFAULT_ALIAS
) {
val context = appContext.applicationContext
override val host: String = ZcashSdk.DEFAULT_LIGHTWALLETD_HOST,
override val port: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT,
override val alias: String = ZcashSdk.DEFAULT_ALIAS
) : SdkSynchronizer.SdkInitializer {
override val context = appContext.applicationContext
init {
validateAlias(alias)
@ -78,7 +80,7 @@ class Initializer(
* SDK when it is constructed. It provides access to all Librustzcash features and is configured
* based on this initializer.
*/
val rustBackend: RustBackend get() {
override val rustBackend: RustBackend get() {
check(_rustBackend != null) {
"Error: RustBackend must be loaded before it is accessed. Verify that either" +
" the 'open', 'new' or 'import' function has been called on the Initializer."
@ -179,7 +181,7 @@ class Initializer(
*/
fun open(birthday: WalletBirthday): Initializer {
twig("Opening wallet with birthday ${birthday.height}")
requireRustBackend().birthdayHeight = birthday.height
requireRustBackend(birthday)
return this
}
@ -222,16 +224,16 @@ class Initializer(
this.birthday = birthday
twig("Initializing accounts with birthday ${birthday.height}")
try {
requireRustBackend().clear(clearCacheDb, clearDataDb)
requireRustBackend(birthday).clear(clearCacheDb, clearDataDb)
// only creates tables, if they don't exist
requireRustBackend().initDataDb()
requireRustBackend(birthday).initDataDb()
twig("Initialized wallet for first run")
} catch (t: Throwable) {
throw InitializerException.FalseStart(t)
}
try {
requireRustBackend().initBlocksTable(
requireRustBackend(birthday).initBlocksTable(
birthday.height,
birthday.hash,
birthday.time,
@ -247,7 +249,7 @@ class Initializer(
}
try {
return requireRustBackend().initAccountsTable(seed, numberOfAccounts).also {
return requireRustBackend(birthday).initAccountsTable(seed, numberOfAccounts).also {
twig("Initialized the accounts table with ${numberOfAccounts} account(s)")
}
} catch (t: Throwable) {
@ -259,7 +261,7 @@ class Initializer(
* Delete all local data related to this wallet, as though the wallet was never created on this
* device. Simply put, this call deletes the "cache db" and "data db."
*/
fun clear() {
override fun clear() {
rustBackend.clear()
}
@ -270,78 +272,14 @@ class Initializer(
*
* @return the rustBackend that was loaded by this initializer.
*/
private fun requireRustBackend(): RustBackend {
private fun requireRustBackend(walletBirthday: WalletBirthday? = null): RustBackend {
if (!isInitialized) {
twig("Initializing cache: $pathCacheDb data: $pathDataDb params: $pathParams")
_rustBackend = RustBackend().init(pathCacheDb, pathDataDb, pathParams)
_rustBackend = RustBackend().init(pathCacheDb, pathDataDb, pathParams, walletBirthday?.height)
}
return rustBackend
}
//
// Key Derivation Helpers
//
/**
* 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<String> =
requireRustBackend().deriveSpendingKeys(seed, numberOfAccounts)
/**
* 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<String> =
requireRustBackend().deriveViewingKeys(seed, numberOfAccounts)
/**
* 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 =
requireRustBackend().deriveViewingKey(spendingKey)
/**
* 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) =
requireRustBackend().deriveAddress(seed, accountIndex)
/**
* 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) =
requireRustBackend().deriveAddress(viewingKey)
companion object {
//
@ -374,22 +312,6 @@ class Initializer(
}
}
/**
* 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,
val hash: String = "",
val time: Long = -1,
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,
@ -461,7 +383,7 @@ class Initializer(
* significant amounts of startup time. This value is created using the context passed into
* the constructor.
*/
override val newWalletBirthday: WalletBirthday get() = loadBirthdayFromAssets(appContext)
override val newWalletBirthday: WalletBirthday get() = WalletBirthdayTool.loadNearest(appContext)
/**
* Birthday to use whenever no birthday is known, meaning we have to scan from the first
@ -470,7 +392,7 @@ class Initializer(
* the constructor and it is a different value for mainnet and testnet.
*/
private val saplingBirthday: WalletBirthday get() =
loadBirthdayFromAssets(appContext, ZcashSdk.SAPLING_ACTIVATION_HEIGHT)
WalletBirthdayTool.loadExact(appContext, ZcashSdk.SAPLING_ACTIVATION_HEIGHT)
/**
* Preferences where the birthday is stored.
@ -485,7 +407,7 @@ class Initializer(
override fun hasImportedBirthday(): Boolean = importedBirthdayHeight != null
override fun getBirthday(): Initializer.WalletBirthday {
override fun getBirthday(): WalletBirthday {
return loadBirthdayFromPrefs(prefs).apply { twig("Loaded birthday from prefs: ${this?.height}") } ?: saplingBirthday.apply { twig("returning sapling birthday") }
}
@ -495,7 +417,7 @@ class Initializer(
}
override fun loadBirthday(birthdayHeight: Int) =
loadBirthdayFromAssets(appContext, birthdayHeight)
WalletBirthdayTool.loadNearest(appContext, birthdayHeight)
/**
* Retrieves the birthday-related primitives from the given preference object and then uses
@ -587,67 +509,13 @@ class Initializer(
fun ImportedWalletBirthdayStore(appContext: Context, importedBirthdayHeight: Int?, alias: String = ZcashSdk.DEFAULT_ALIAS): WalletBirthdayStore {
return DefaultBirthdayStore(appContext, alias = alias).apply {
if (importedBirthdayHeight != null) {
saveBirthdayToPrefs(prefs, loadBirthdayFromAssets(appContext, importedBirthdayHeight))
saveBirthdayToPrefs(prefs, WalletBirthdayTool.loadNearest(appContext, importedBirthdayHeight))
} else {
setBirthday(newWalletBirthday)
}
}
}
/**
* Load the given birthday file from the assets of the given context. When no height is
* specified, we default to the file with the greatest name.
*
* @param context the context from which to load assets.
* @param birthdayHeight the height file to look for among the file names.
*
* @return a WalletBirthday that reflects the contents of the file or an exception when
* parsing fails.
*/
fun loadBirthdayFromAssets(
context: Context,
birthdayHeight: Int? = null
): WalletBirthday {
twig("loading birthday from assets: $birthdayHeight")
val treeFiles =
context.assets.list(BIRTHDAY_DIRECTORY)?.apply { sortByDescending { fileName ->
try {
fileName.split('.').first().toInt()
} catch (t: Throwable) {
ZcashSdk.SAPLING_ACTIVATION_HEIGHT
}
} }
if (treeFiles.isNullOrEmpty()) throw BirthdayException.MissingBirthdayFilesException(
BIRTHDAY_DIRECTORY
)
twig("found ${treeFiles.size} sapling tree checkpoints: ${Arrays.toString(treeFiles)}")
val file: String
try {
file = if (birthdayHeight == null) treeFiles.first() else {
treeFiles.first {
it.split(".").first().toInt() <= birthdayHeight
}
}
} catch (t: Throwable) {
throw BirthdayException.BirthdayFileNotFoundException(
BIRTHDAY_DIRECTORY,
birthdayHeight
)
}
try {
val reader = JsonReader(
InputStreamReader(context.assets.open("${BIRTHDAY_DIRECTORY}/$file"))
)
return Gson().fromJson(reader, WalletBirthday::class.java)
} catch (t: Throwable) {
throw BirthdayException.MalformattedBirthdayFilesException(
BIRTHDAY_DIRECTORY,
treeFiles[0]
)
}
}
/*
* Helper functions for using SharedPreferences
*/
@ -738,69 +606,3 @@ internal fun validateAlias(alias: String) {
}
}
/**
* Builder function for constructing a Synchronizer with flexibility for adding custom behavior. The
* Initializer is the only thing required because it takes care of loading the Rust libraries
* properly; everything else has a reasonable default. For a wallet, the most common flow is to
* first 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.
*
* The remaining parameters are all optional and they allow a wallet maker to customize any
* subcomponent of the Synchronizer. For example, this function could be used to inject an in-memory
* CompactBlockStore rather than a SQL implementation or a downloader that does not use gRPC:
*
* ```
* val initializer = Initializer(context, host, port).import(seedPhrase, birthdayHeight)
* val synchronizer = Synchronizer(initializer,
* blockStore = MyInMemoryBlockStore(),
* downloader = MyRestfulServiceForBlocks()
* )
* ```
*
* Note: alternatively, all the objects required to build a Synchronizer (the object graph) can be
* supplied by a dependency injection framework like Dagger or Koin. This builder just makes that
* process a bit easier so developers can get started syncing the blockchain without the overhead of
* configuring a bunch of objects, first.
*
* @param initializer the helper that is leveraged for creating all the components that the
* Synchronizer requires. It contains all information necessary to build a synchronizer and it is
* mainly responsible for initializing the databases associated with this synchronizer and loading
* the rust backend.
* @param repository repository of wallet data, providing an interface to the underlying info.
* @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 txManager 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(
initializer: Initializer,
repository: TransactionRepository =
PagedTransactionRepository(initializer.context, 1000, initializer.rustBackend.pathDataDb), // TODO: fix this pagesize bug, small pages should not crash the app. It crashes with: 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
blockStore: CompactBlockStore = CompactBlockDbStore(initializer.context, initializer.rustBackend.pathCacheDb),
service: LightWalletService = LightWalletGrpcService(initializer.context, initializer.host, initializer.port),
encoder: TransactionEncoder = WalletTransactionEncoder(initializer.rustBackend, repository),
downloader: CompactBlockDownloader = CompactBlockDownloader(service, blockStore),
txManager: OutboundTransactionManager =
PersistentTransactionManager(initializer.context, encoder, service),
processor: CompactBlockProcessor =
CompactBlockProcessor(downloader, repository, initializer.rustBackend, initializer.rustBackend.birthdayHeight)
): 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,
processor
)
}

View File

@ -1,17 +1,20 @@
package cash.z.ecc.android.sdk
import android.content.Context
import cash.z.ecc.android.sdk.Synchronizer.Status.*
import cash.z.ecc.android.sdk.block.CompactBlockDbStore
import cash.z.ecc.android.sdk.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.*
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.WalletBalance
import cash.z.ecc.android.sdk.block.CompactBlockStore
import cash.z.ecc.android.sdk.db.entity.*
import cash.z.ecc.android.sdk.exception.SynchronizerException
import cash.z.ecc.android.sdk.ext.*
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.transaction.OutboundTransactionManager
import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository
import cash.z.ecc.android.sdk.transaction.PersistentTransactionManager
import cash.z.ecc.android.sdk.transaction.TransactionRepository
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.transaction.*
import cash.z.ecc.android.sdk.validate.AddressType
import cash.z.ecc.android.sdk.validate.AddressType.Shielded
import cash.z.ecc.android.sdk.validate.AddressType.Transparent
@ -39,6 +42,7 @@ import kotlin.coroutines.EmptyCoroutineContext
* data related to this wallet.
*/
@ExperimentalCoroutinesApi
@FlowPreview
class SdkSynchronizer internal constructor(
private val storage: TransactionRepository,
private val txManager: OutboundTransactionManager,
@ -489,4 +493,80 @@ class SdkSynchronizer internal constructor(
serverBranchId?.let { ConsensusBranchId.fromHex(it) }
)
}
interface SdkInitializer {
val context: Context
val rustBackend: RustBackend
val host: String
val port: Int
val alias: String
fun clear()
}
}
/**
* Builder function for constructing a Synchronizer with flexibility for adding custom behavior. The
* Initializer is the only thing required because it takes care of loading the Rust libraries
* properly; everything else has a reasonable default. For a wallet, the most common flow is to
* first 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.
*
* The remaining parameters are all optional and they allow a wallet maker to customize any
* subcomponent of the Synchronizer. For example, this function could be used to inject an in-memory
* CompactBlockStore rather than a SQL implementation or a downloader that does not use gRPC:
*
* ```
* val initializer = Initializer(context, host, port).import(seedPhrase, birthdayHeight)
* val synchronizer = Synchronizer(initializer,
* blockStore = MyInMemoryBlockStore(),
* downloader = MyRestfulServiceForBlocks()
* )
* ```
*
* Note: alternatively, all the objects required to build a Synchronizer (the object graph) can be
* supplied by a dependency injection framework like Dagger or Koin. This builder just makes that
* process a bit easier so developers can get started syncing the blockchain without the overhead of
* configuring a bunch of objects, first.
*
* @param initializer the helper that is leveraged for creating all the components that the
* Synchronizer requires. It contains all information necessary to build a synchronizer and it is
* mainly responsible for initializing the databases associated with this synchronizer and loading
* the rust backend.
* @param repository repository of wallet data, providing an interface to the underlying info.
* @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 txManager 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(
initializer: SdkSynchronizer.SdkInitializer,
repository: TransactionRepository =
PagedTransactionRepository(initializer.context, 1000, initializer.rustBackend.pathDataDb), // TODO: fix this pagesize bug, small pages should not crash the app. It crashes with: 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
blockStore: CompactBlockStore = CompactBlockDbStore(initializer.context, initializer.rustBackend.pathCacheDb),
service: LightWalletService = LightWalletGrpcService(initializer.context, initializer.host, initializer.port),
encoder: TransactionEncoder = WalletTransactionEncoder(initializer.rustBackend, repository),
downloader: CompactBlockDownloader = CompactBlockDownloader(service, blockStore),
txManager: OutboundTransactionManager =
PersistentTransactionManager(initializer.context, encoder, service),
processor: CompactBlockProcessor =
CompactBlockProcessor(downloader, repository, initializer.rustBackend, initializer.rustBackend.birthdayHeight)
): 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,
processor
)
}

View File

@ -0,0 +1,267 @@
package cash.z.ecc.android.sdk
import android.content.Context
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.ext.tryWarn
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
import java.io.File
import java.lang.IllegalArgumentException
/**
* Simplified Initializer focused on starting from a ViewingKey.
*/
class VkInitializer(appContext: Context, block: Builder.() -> Unit) : SdkSynchronizer.SdkInitializer {
override val context = appContext.applicationContext
override val rustBackend: RustBackend
override val alias: String
override val host: String
override val port: Int
val viewingKeys: Array<out String>
val birthday: WalletBirthdayTool.WalletBirthday
init {
Builder(block).let { builder ->
birthday = builder._birthday
viewingKeys = builder._viewingKeys
alias = builder.alias
host = builder.host
port = builder.port
rustBackend = initRustBackend(birthday)
initMissingDatabases(birthday, *viewingKeys)
}
}
private fun initRustBackend(birthday: WalletBirthdayTool.WalletBirthday): RustBackend {
return RustBackend().init(
cacheDbPath(context, alias),
dataDbPath(context, alias),
"${context.cacheDir.absolutePath}/params",
birthday.height
)
}
private fun initMissingDatabases(
birthday: WalletBirthdayTool.WalletBirthday,
vararg viewingKeys: String
) {
maybeCreateDataDb()
maybeInitBlocksTable(birthday)
maybeInitAccountsTable(*viewingKeys)
}
/**
* Create the dataDb and its table, if it doesn't exist.
*/
private fun maybeCreateDataDb() {
tryWarn("Warning: did not create dataDb. It probably already exists.") {
rustBackend.initDataDb()
twig("Initialized wallet for first run")
}
}
/**
* Initialize the blocks table with the given birthday, if needed.
*/
private fun maybeInitBlocksTable(birthday: WalletBirthdayTool.WalletBirthday) {
tryWarn(
"Warning: did not initialize the blocks table. It probably was already initialized."
) {
rustBackend.initBlocksTable(
birthday.height,
birthday.hash,
birthday.time,
birthday.tree
)
twig("seeded the database with sapling tree at height ${birthday.height}")
}
}
/**
* Initialize the accounts table with the given viewing keys, if needed.
*/
private fun maybeInitAccountsTable(vararg viewingKeys: String) {
tryWarn(
"Warning: did not initialize the accounts table. It probably was already initialized."
) {
rustBackend.initAccountsTable(*viewingKeys)
twig("Initialized the accounts table with ${viewingKeys.size} viewingKey(s)")
}
}
/**
* Delete all local data related to this wallet, as though the wallet was never created on this
* device. Simply put, this call deletes the "cache db" and "data db."
*/
override fun clear() {
rustBackend.clear()
}
//
// 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)
private fun aliasToPath(appContext: Context, alias: String, dbFileName: String): String {
val parentDir: String =
appContext.getDatabasePath("unused.db").parentFile?.absolutePath
?: throw InitializerException.DatabasePathException
val prefix = if (alias.endsWith('_')) alias else "${alias}_"
return File(parentDir, "$prefix$dbFileName").absolutePath
}
/**
* Validate that the alias doesn't contain malicious characters by enforcing simple rules which
* 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.
*/
internal fun validateAlias(alias: String) {
require(alias.length in 1..99 && alias[0].isLetter()
&& alias.all { it.isLetterOrDigit() || it == '_' }) {
"ERROR: Invalid alias ($alias). For security, the alias must be shorter than 100 " +
"characters and only contain letters, digits or underscores and start with a letter"
}
}
inner class Builder(block: Builder.() -> Unit) {
/* lateinit fields that can be set in multiple ways on this builder */
lateinit var _birthday: WalletBirthdayTool.WalletBirthday
private set
lateinit var _viewingKeys: Array<out String>
private set
/* optional fields with default values */
var alias: String = ZcashSdk.DEFAULT_ALIAS
var host: String = ZcashSdk.DEFAULT_LIGHTWALLETD_HOST
var port: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT
var birthdayHeight: Int? = null
set(value) {
field = value
_birthday = WalletBirthdayTool(context).loadNearest(value)
}
init {
block()
validateAlias(alias)
validateViewingKeys()
validateBirthday()
}
fun viewingKeys(vararg extendedFullViewingKeys: String) {
_viewingKeys = extendedFullViewingKeys
}
fun seed(seed: ByteArray, numberOfAccounts: Int = 1) {
_viewingKeys = DerivationTool.deriveViewingKeys(seed, numberOfAccounts)
}
private fun birthday(walletBirthday: WalletBirthdayTool.WalletBirthday) {
_birthday = walletBirthday
}
/**
* Load the most recent checkpoint available. This is useful for new wallets.
*/
fun newWalletBirthday() {
birthdayHeight = null
}
/**
* Load the birthday checkpoint closest to the given wallet birthday. This is useful when
* importing a pre-existing wallet. It is the same as calling
* `birthdayHeight = importedHeight`.
*/
fun importedWalletBirthday(importedHeight: Int) {
birthdayHeight = importedHeight
}
/**
* Theoretically, the oldest possible birthday a wallet could have. Useful for searching
* all transactions on the chain. In reality, no wallets were born at this height.
*/
fun saplingBirthday() {
birthdayHeight = ZcashSdk.SAPLING_ACTIVATION_HEIGHT
}
//
// Convenience functions
//
fun server(host: String, port: Int) {
this.host = host
this.port = port
}
fun import(seed: ByteArray, birthdayHeight: Int) {
seed(seed)
importedWalletBirthday(birthdayHeight)
}
fun new(seed: ByteArray) {
seed(seed)
newWalletBirthday()
}
//
// Validation helpers
//
private fun validateBirthday() {
require(::_birthday.isInitialized) {
"Birthday is required but was not set on this initializer. Verify that a valid" +
" birthday was provided when creating the Initializer such as" +
" WalletBirthdayTool.loadNearest()"
}
require(_birthday.height >= ZcashSdk.SAPLING_ACTIVATION_HEIGHT) {
"Invalid birthday height of ${_birthday.height}. The birthday height must be at" +
" least the height of Sapling activation on ${ZcashSdk.NETWORK}" +
" (${ZcashSdk.SAPLING_ACTIVATION_HEIGHT})."
}
}
private fun validateViewingKeys() {
require(::_viewingKeys.isInitialized && _viewingKeys.isNotEmpty()) {
"Viewing keys are required. Ensure that the viewing keys or seed have been set" +
" on this Initializer."
}
_viewingKeys.forEach {
DerivationTool.validateViewingKey(it)
}
}
}
}