Simplified initialization.

Put more emphasis on creating a synchronizer only with an initializer.
Made it easier to specify an initializer and custom dependencies at once.
Removed confusing constructor from SdkSynchronizer that muddied the waters on how to properly create one.
Added extension function to make it easier to start a synchronizer with an imported wallet.
This commit is contained in:
Kevin Gorham 2020-06-09 21:35:40 -04:00
parent 812e51b0f8
commit 2ee0e9ab3c
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
3 changed files with 118 additions and 114 deletions

View File

@ -6,6 +6,7 @@ import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.Synchronizer.Status.SYNCED
import cash.z.wallet.sdk.entity.isSubmitSuccess
import cash.z.wallet.sdk.ext.*
import cash.z.wallet.sdk.import
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.service.LightWalletGrpcService
import kotlinx.coroutines.delay
@ -80,7 +81,7 @@ class IntegrationTest {
ZcashSdk.MINERS_FEE_ZATOSHI,
toAddress,
"first mainnet tx from the SDK"
).filter { it?.isSubmitSuccess() == true }.onFirst {
).filter { it.isSubmitSuccess() }.onFirst {
log("DONE SENDING!!!")
}
log("returning true from sendFunds")
@ -98,16 +99,15 @@ class IntegrationTest {
const val host = "lightd-main.zecwallet.co"
const val port = 443
val seed = "cash.z.wallet.sdk.integration.IntegrationTest.seed.value.64bytes".toByteArray()
val birthdayHeight = 843_000
val address = "zs1m30y59wxut4zk9w24d6ujrdnfnl42hpy0ugvhgyhr8s0guszutqhdj05c7j472dndjstulph74m"
val toAddress = "zs1vp7kvlqr4n9gpehztr76lcn6skkss9p8keqs3nv8avkdtjrcctrvmk9a7u494kluv756jeee5k0"
private val context = InstrumentationRegistry.getInstrumentation().context
private val synchronizer: Synchronizer = Synchronizer(
context,
host,
443,
seed
)
private val initializer = Initializer(context, host, port).apply {
import(seed, birthdayHeight, overwrite = true)
}
private val synchronizer: Synchronizer = Synchronizer(initializer)
@JvmStatic
@BeforeClass

View File

@ -2,6 +2,7 @@ package cash.z.wallet.sdk
import android.content.Context
import android.content.SharedPreferences
import cash.z.wallet.sdk.Initializer.DefaultBirthdayStore.Companion.ImportedWalletBirthdayStore
import cash.z.wallet.sdk.exception.BirthdayException
import cash.z.wallet.sdk.exception.InitializerException
import cash.z.wallet.sdk.ext.ZcashSdk
@ -59,12 +60,24 @@ class Initializer(
*/
private val pathDataDb: String = dataDbPath(context, alias)
/**
* Backing field for rustBackend, used for giving better error messages whenever the initializer
* is mistakenly used prior to being properly loaded.
*/
private var _rustBackend: RustBackend? = null
/**
* A wrapped version of [cash.z.wallet.sdk.jni.RustBackendWelding] that will be passed to the
* SDK when it is constructed. It provides access to all Librustzcash features and is configured
* based on this initializer.
*/
lateinit var rustBackend: RustBackend
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."
}
return _rustBackend!!
}
/**
* The birthday that was ultimately used for initializing the accounts.
@ -77,7 +90,7 @@ class Initializer(
* setup everything necessary for the Synchronizer to function, which mainly boils down to
* loading the rust backend.
*/
val isInitialized: Boolean get() = ::rustBackend.isInitialized
val isInitialized: Boolean get() = _rustBackend != null
/**
* Initialize a new wallet with the given seed and birthday. It creates the required database
@ -140,8 +153,7 @@ class Initializer(
clearCacheDb: Boolean = false,
clearDataDb: Boolean = false
): Array<String> {
return initializeAccounts(seed, previousWalletBirthday,
clearCacheDb = clearCacheDb, clearDataDb = clearDataDb)
return initializeAccounts(seed, previousWalletBirthday, clearCacheDb = clearCacheDb, clearDataDb = clearDataDb)
}
/**
@ -254,7 +266,7 @@ class Initializer(
private fun requireRustBackend(): RustBackend {
if (!isInitialized) {
twig("Initializing cache: $pathCacheDb data: $pathDataDb params: $pathParams")
rustBackend = RustBackend().init(pathCacheDb, pathDataDb, pathParams)
_rustBackend = RustBackend().init(pathCacheDb, pathDataDb, pathParams)
}
return rustBackend
}
@ -682,6 +694,29 @@ class Initializer(
}
}
/**
* Convenience extension for importing from an integer height, rather than a wallet birthday object.
*
* @param alias the prefix to use for the cache of blocks downloaded and the data decrypted from
* those blocks. Using different names helps for use cases that involve multiple keys.
* @param overwrite when true, this will delete all existing data. Use with caution because this can
* result in a loss of funds, when a user has not backed up their seed. This parameter is most
* useful during testing or proof of concept work, where you want to run the same code each time so
* data must be cleared between runs.
*
* @return the spending keys, derived from the seed, for convenience.
*/
fun Initializer.import(
seed: ByteArray,
birthdayHeight: Int,
alias: String = ZcashSdk.DEFAULT_DB_NAME_PREFIX,
overwrite: Boolean = false
): Array<String> {
return ImportedWalletBirthdayStore(context, birthdayHeight, alias).getBirthday().let {
import(seed, it, clearCacheDb = overwrite, clearDataDb = overwrite)
}
}
/**
* 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

View File

@ -32,15 +32,15 @@ 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.
*
* @property ledger exposes flows of wallet transaction information.
* @property manager manages and tracks outbound transactions.
* @property storage exposes flows of wallet transaction information.
* @property txManager 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
class SdkSynchronizer internal constructor(
private val ledger: TransactionRepository,
private val manager: OutboundTransactionManager,
private val storage: TransactionRepository,
private val txManager: OutboundTransactionManager,
val processor: CompactBlockProcessor
) : Synchronizer {
private val _balances = ConflatedBroadcastChannel(WalletBalance())
@ -61,10 +61,10 @@ class SdkSynchronizer internal constructor(
//
override val balances: Flow<WalletBalance> = _balances.asFlow()
override val clearedTransactions = ledger.allTransactions
override val pendingTransactions = manager.getAll()
override val sentTransactions = ledger.sentTransactions
override val receivedTransactions = ledger.receivedTransactions
override val clearedTransactions = storage.allTransactions
override val pendingTransactions = txManager.getAll()
override val sentTransactions = storage.sentTransactions
override val receivedTransactions = storage.receivedTransactions
//
@ -180,7 +180,7 @@ class SdkSynchronizer internal constructor(
//
private fun refreshTransactions() {
ledger.invalidate()
storage.invalidate()
}
/**
@ -297,16 +297,16 @@ class SdkSynchronizer internal constructor(
// TODO: this would be the place to clear out any stale pending transactions. Remove filter
// logic and then delete any pending transaction with sufficient confirmations (all in one
// db transaction).
manager.getAll().first().filter { it.isSubmitSuccess() && !it.isMined() }
txManager.getAll().first().filter { it.isSubmitSuccess() && !it.isMined() }
.forEach { pendingTx ->
twig("checking for updates on pendingTx id: ${pendingTx.id}")
pendingTx.rawTransactionId?.let { rawId ->
ledger.findMinedHeight(rawId)?.let { minedHeight ->
storage.findMinedHeight(rawId)?.let { minedHeight ->
twig(
"found matching transaction for pending transaction with id" +
" ${pendingTx.id} mined at height ${minedHeight}!"
)
manager.applyMinedHeight(pendingTx, minedHeight)
txManager.applyMinedHeight(pendingTx, minedHeight)
}
}
}
@ -317,7 +317,7 @@ class SdkSynchronizer internal constructor(
// Send / Receive
//
override suspend fun cancelSpend(transaction: PendingTransaction) = manager.cancel(transaction)
override suspend fun cancelSpend(transaction: PendingTransaction) = txManager.cancel(transaction)
override suspend fun getAddress(accountId: Int): String = processor.getAddress(accountId)
@ -330,25 +330,25 @@ class SdkSynchronizer internal constructor(
): Flow<PendingTransaction> = flow {
twig("Initializing pending transaction")
// Emit the placeholder transaction, then switch to monitoring the database
manager.initSpend(zatoshi, toAddress, memo, fromAccountIndex).let { placeHolderTx ->
txManager.initSpend(zatoshi, toAddress, memo, fromAccountIndex).let { placeHolderTx ->
emit(placeHolderTx)
manager.encode(spendingKey, placeHolderTx).let { encodedTx ->
txManager.encode(spendingKey, placeHolderTx).let { encodedTx ->
if (!encodedTx.isFailedEncoding() && !encodedTx.isCancelled()) {
manager.submit(encodedTx)
txManager.submit(encodedTx)
}
}
}
}.flatMapLatest {
twig("Monitoring pending transaction (id: ${it.id}) for updates...")
manager.monitorById(it.id)
txManager.monitorById(it.id)
}.distinctUntilChanged()
override suspend fun isValidShieldedAddr(address: String) = manager.isValidShieldedAddress(address)
override suspend fun isValidShieldedAddr(address: String) = txManager.isValidShieldedAddress(address)
override suspend fun isValidTransparentAddr(address: String) =
manager.isValidTransparentAddress(address)
txManager.isValidTransparentAddress(address)
override suspend fun validateAddress(address: String): Synchronizer.AddressType {
override suspend fun validateAddress(address: String): AddressType {
return try {
if (isValidShieldedAddr(address)) Shielded else Transparent
} catch (zError: Throwable) {
@ -356,91 +356,61 @@ class SdkSynchronizer internal constructor(
try {
if (isValidTransparentAddr(address)) Transparent else Shielded
} catch (tError: Throwable) {
Synchronizer.AddressType.Invalid(
AddressType.Invalid(
if (message != tError.message) "$message and ${tError.message}" else (message
?: "Invalid")
)
}
}
}
}
/**
* 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,
lightwalletdPort: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT,
seed: ByteArray? = null,
birthdayStore: Initializer.WalletBirthdayStore = Initializer.DefaultBirthdayStore(appContext)
): Synchronizer {
val initializer = Initializer(appContext, lightwalletdHost, lightwalletdPort)
if (seed != null && birthdayStore.hasExistingBirthday()) {
twig("Initializing existing wallet")
initializer.open(birthdayStore.getBirthday())
twig("${initializer.rustBackend.pathDataDb}")
} else {
require(seed != null) {
"Failed to initialize. A seed is required when no wallet exists on the device."
}
if (birthdayStore.hasImportedBirthday()) {
twig("Initializing new wallet")
initializer.new(seed, birthdayStore.newWalletBirthday, 1, true, true)
} else {
twig("Initializing imported wallet")
initializer.import(seed, birthdayStore.getBirthday(), true, true)
override suspend fun validateConsensusBranch(): ConsensusMatchType {
val serverBranchId = tryNull { processor.downloader.getServerInfo().consensusBranchId }
val sdkBranchId = tryNull {
(txManager as PersistentTransactionManager).encoder.getConsensusBranchId()
}
return ConsensusMatchType(
sdkBranchId?.let { ConsensusBranchId.fromId(it) },
serverBranchId?.let { ConsensusBranchId.fromHex(it) }
)
}
return Synchronizer(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.
* 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 is mainly responsible for initializing the databases associated with
* this synchronizer.
*/
@Suppress("FunctionName")
fun Synchronizer(initializer: Initializer): Synchronizer {
check(initializer.isInitialized) {
"Error: RustBackend must be loaded before creating the Synchronizer. Verify that either" +
" the 'open', 'new' or 'import' function has been called on the Initializer."
}
return Synchronizer(initializer.context, initializer.rustBackend, initializer.host, initializer.port)
}
/**
* Constructor function for building a Synchronizer in the most flexible way possible. This allows
* 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.
* 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 manager the component that manages outbound transactions in order to report which ones are
* @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
@ -449,25 +419,24 @@ fun Synchronizer(initializer: Initializer): Synchronizer {
*/
@Suppress("FunctionName")
fun Synchronizer(
appContext: Context,
rustBackend: RustBackend,
lightwalletdHost: String = ZcashSdk.DEFAULT_LIGHTWALLETD_HOST,
lightwalletdPort: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT,
ledger: TransactionRepository =
PagedTransactionRepository(appContext, 1000, 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(appContext, rustBackend.pathCacheDb),
service: LightWalletService = LightWalletGrpcService(appContext, lightwalletdHost, lightwalletdPort),
encoder: TransactionEncoder = WalletTransactionEncoder(rustBackend, ledger),
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),
manager: OutboundTransactionManager =
PersistentTransactionManager(appContext, encoder, service),
txManager: OutboundTransactionManager =
PersistentTransactionManager(initializer.context, encoder, service),
processor: CompactBlockProcessor =
CompactBlockProcessor(downloader, ledger, rustBackend, rustBackend.birthdayHeight)
CompactBlockProcessor(downloader, repository, initializer.rustBackend, initializer.rustBackend.birthdayHeight)
): Synchronizer {
// ties everything together
// 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(
ledger,
manager,
repository,
txManager,
processor
)
}