2020-06-10 00:08:19 -07:00
package cash.z.ecc.android.sdk
2019-07-10 11:12:32 -07:00
2020-09-11 00:29:17 -07:00
import android.content.Context
2021-03-10 10:10:03 -08:00
import cash.z.ecc.android.sdk.Synchronizer.Status.DISCONNECTED
import cash.z.ecc.android.sdk.Synchronizer.Status.DOWNLOADING
import cash.z.ecc.android.sdk.Synchronizer.Status.ENHANCING
import cash.z.ecc.android.sdk.Synchronizer.Status.SCANNING
import cash.z.ecc.android.sdk.Synchronizer.Status.STOPPED
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
import cash.z.ecc.android.sdk.Synchronizer.Status.VALIDATING
2020-06-10 00:08:19 -07:00
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
2021-03-10 10:10:03 -08:00
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Disconnected
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Downloading
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Enhancing
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Initialized
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanned
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Scanning
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Stopped
import cash.z.ecc.android.sdk.block.CompactBlockProcessor.State.Validating
import cash.z.ecc.android.sdk.db.entity.PendingTransaction
import cash.z.ecc.android.sdk.db.entity.hasRawTransactionId
import cash.z.ecc.android.sdk.db.entity.isCancelled
import cash.z.ecc.android.sdk.db.entity.isExpired
2021-03-10 19:04:39 -08:00
import cash.z.ecc.android.sdk.db.entity.isFailedSubmit
2021-03-10 10:10:03 -08:00
import cash.z.ecc.android.sdk.db.entity.isLongExpired
import cash.z.ecc.android.sdk.db.entity.isMarkedForDeletion
import cash.z.ecc.android.sdk.db.entity.isMined
import cash.z.ecc.android.sdk.db.entity.isSafeToDiscard
import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
2021-03-10 19:04:39 -08:00
import cash.z.ecc.android.sdk.db.entity.isSubmitted
2020-06-10 00:08:19 -07:00
import cash.z.ecc.android.sdk.exception.SynchronizerException
2021-03-10 10:10:03 -08:00
import cash.z.ecc.android.sdk.ext.ConsensusBranchId
import cash.z.ecc.android.sdk.ext.ZcashSdk
2021-11-18 04:10:30 -08:00
import cash.z.ecc.android.sdk.internal.block.CompactBlockDbStore
import cash.z.ecc.android.sdk.internal.block.CompactBlockDownloader
import cash.z.ecc.android.sdk.internal.block.CompactBlockStore
2021-10-04 04:18:37 -07:00
import cash.z.ecc.android.sdk.internal.ext.toHexReversed
import cash.z.ecc.android.sdk.internal.ext.tryNull
2022-07-12 05:40:09 -07:00
import cash.z.ecc.android.sdk.internal.isEmpty
2021-10-04 04:18:37 -07:00
import cash.z.ecc.android.sdk.internal.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.internal.service.LightWalletService
import cash.z.ecc.android.sdk.internal.transaction.OutboundTransactionManager
import cash.z.ecc.android.sdk.internal.transaction.PagedTransactionRepository
import cash.z.ecc.android.sdk.internal.transaction.PersistentTransactionManager
import cash.z.ecc.android.sdk.internal.transaction.TransactionEncoder
import cash.z.ecc.android.sdk.internal.transaction.TransactionRepository
import cash.z.ecc.android.sdk.internal.transaction.WalletTransactionEncoder
2021-11-18 04:10:30 -08:00
import cash.z.ecc.android.sdk.internal.twig
import cash.z.ecc.android.sdk.internal.twigTask
2022-07-12 05:40:09 -07:00
import cash.z.ecc.android.sdk.model.BlockHeight
2022-07-07 05:52:07 -07:00
import cash.z.ecc.android.sdk.model.WalletBalance
2022-06-21 16:34:42 -07:00
import cash.z.ecc.android.sdk.model.Zatoshi
2021-11-18 04:10:30 -08:00
import cash.z.ecc.android.sdk.tool.DerivationTool
2021-03-31 23:14:57 -07:00
import cash.z.ecc.android.sdk.type.AddressType
import cash.z.ecc.android.sdk.type.AddressType.Shielded
import cash.z.ecc.android.sdk.type.AddressType.Transparent
import cash.z.ecc.android.sdk.type.ConsensusMatchType
2021-04-09 18:43:07 -07:00
import cash.z.ecc.android.sdk.type.ZcashNetwork
2020-06-09 19:05:30 -07:00
import cash.z.wallet.sdk.rpc.Service
import io.grpc.ManagedChannel
2021-03-10 10:10:03 -08:00
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
2019-07-10 11:12:32 -07:00
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
2021-03-10 10:10:03 -08:00
import kotlinx.coroutines.flow.Flow
2021-06-14 21:09:24 -07:00
import kotlinx.coroutines.flow.MutableStateFlow
2021-05-25 08:15:09 -07:00
import kotlinx.coroutines.flow.StateFlow
2021-03-10 10:10:03 -08:00
import kotlinx.coroutines.flow.asFlow
2021-06-14 21:09:24 -07:00
import kotlinx.coroutines.flow.asStateFlow
2021-03-10 10:10:03 -08:00
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
2019-07-10 11:12:32 -07:00
import kotlin.coroutines.CoroutineContext
2020-06-09 19:05:30 -07:00
import kotlin.coroutines.EmptyCoroutineContext
2019-07-10 11:12:32 -07:00
2019-10-21 03:26:02 -07:00
/ * *
2019-11-01 13:25:28 -07:00
* A Synchronizer that attempts to remain operational , despite any number of errors that can occur .
* It acts as the glue that ties all the pieces of the SDK together . Each component of the SDK is
* designed for the potential of stand - alone usage but coordinating all the interactions is non -
* trivial . So the Synchronizer facilitates this , acting as reference that demonstrates how all the
* 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 .
2019-08-30 10:05:02 -07:00
*
2020-06-09 18:35:40 -07:00
* @property storage exposes flows of wallet transaction information .
* @property txManager manages and tracks outbound transactions .
2020-02-27 00:25:07 -08:00
* @property processor saves the downloaded compact blocks to the cache and then scans those blocks for
2019-08-30 10:05:02 -07:00
* data related to this wallet .
2019-07-10 11:12:32 -07:00
* /
@ExperimentalCoroutinesApi
2020-09-11 00:29:17 -07:00
@FlowPreview
2019-11-01 13:25:28 -07:00
class SdkSynchronizer internal constructor (
2020-06-09 18:35:40 -07:00
private val storage : TransactionRepository ,
private val txManager : OutboundTransactionManager ,
2020-01-06 22:26:10 -08:00
val processor : CompactBlockProcessor
2019-07-14 15:13:12 -07:00
) : Synchronizer {
2021-06-14 21:09:24 -07:00
// pools
2022-06-21 16:34:42 -07:00
private val _orchardBalances = MutableStateFlow < WalletBalance ? > ( null )
private val _saplingBalances = MutableStateFlow < WalletBalance ? > ( null )
private val _transparentBalances = MutableStateFlow < WalletBalance ? > ( null )
2021-06-14 21:09:24 -07:00
2019-12-23 11:50:52 -08:00
private val _status = ConflatedBroadcastChannel < Synchronizer . Status > ( DISCONNECTED )
2019-07-10 11:12:32 -07:00
2019-07-12 01:47:17 -07:00
/ * *
2019-11-01 13:25:28 -07:00
* The lifespan of this Synchronizer . This scope is initialized once the Synchronizer starts
* because it will be a child of the parentScope that gets passed into the [ start ] function .
* Everything launched by this Synchronizer will be cancelled once the Synchronizer or its
2020-06-09 19:05:30 -07:00
* parentScope stops . This coordinates with [ isStarted ] so that it fails early
2019-11-01 13:25:28 -07:00
* rather than silently , whenever the scope is used before the Synchronizer has been started .
2019-07-12 01:47:17 -07:00
* /
2020-06-09 19:05:30 -07:00
var coroutineScope : CoroutineScope = CoroutineScope ( EmptyCoroutineContext )
get ( ) {
if ( !is Started ) {
throw SynchronizerException . NotYetStarted
} else {
return field
}
}
set ( value ) {
field = value
if ( value . coroutineContext !is EmptyCoroutineContext ) isStarted = true
}
/ * *
* The channel that this Synchronizer uses to communicate with lightwalletd . In most cases , this
* should not be needed or used . Instead , APIs should be added to the synchronizer to
* enable the desired behavior . In the rare case , such as testing , it can be helpful to share
* the underlying channel to connect to the same service , and use other APIs
* ( such as darksidewalletd ) because channels are heavyweight .
* /
2020-09-23 08:11:45 -07:00
val channel : ManagedChannel get ( ) = ( processor . downloader . lightWalletService as LightWalletGrpcService ) . channel
2020-06-09 19:05:30 -07:00
2021-03-31 05:37:12 -07:00
override var isStarted = false
2019-07-10 11:12:32 -07:00
2021-06-14 21:09:24 -07:00
//
// Balances
//
override val orchardBalances = _orchardBalances . asStateFlow ( )
override val saplingBalances = _saplingBalances . asStateFlow ( )
override val transparentBalances = _transparentBalances . asStateFlow ( )
2019-11-01 13:25:28 -07:00
//
// Transactions
//
2021-05-07 00:59:47 -07:00
override val clearedTransactions get ( ) = storage . allTransactions
2020-06-09 18:35:40 -07:00
override val pendingTransactions = txManager . getAll ( )
2021-05-07 00:59:47 -07:00
override val sentTransactions get ( ) = storage . sentTransactions
override val receivedTransactions get ( ) = storage . receivedTransactions
2019-11-01 13:25:28 -07:00
2019-07-12 01:47:17 -07:00
//
2019-08-30 10:05:02 -07:00
// Status
2019-07-12 01:47:17 -07:00
//
2019-07-10 11:12:32 -07:00
2021-04-09 18:43:07 -07:00
override val network : ZcashNetwork get ( ) = processor . network
2019-08-30 10:05:02 -07:00
/ * *
2019-10-21 03:26:02 -07:00
* Indicates the status of this Synchronizer . This implementation basically simplifies the
2019-11-22 23:18:20 -08:00
* status of the processor to focus only on the high level states that matter most . Whenever the
* processor is finished scanning , the synchronizer updates transaction and balance info and
* then emits a [ SYNCED ] status .
2019-08-30 10:05:02 -07:00
* /
2019-11-22 23:18:20 -08:00
override val status = _status . asFlow ( )
2019-07-12 01:47:17 -07:00
2019-08-30 10:05:02 -07:00
/ * *
2019-11-01 13:25:28 -07:00
* Indicates the download progress of the Synchronizer . When progress reaches 100 , that
* signals that the Synchronizer is in sync with the network . Balances should be considered
2020-01-14 09:52:41 -08:00
* inaccurate and outbound transactions should be prevented until this sync is complete . It is
* a simplified version of [ processorInfo ] .
2019-08-30 10:05:02 -07:00
* /
2019-11-01 13:25:28 -07:00
override val progress : Flow < Int > = processor . progress
2019-07-10 11:12:32 -07:00
2020-01-14 09:52:41 -08:00
/ * *
* Indicates the latest information about the blocks that have been processed by the SDK . This
* is very helpful for conveying detailed progress and status to the user .
* /
override val processorInfo : Flow < CompactBlockProcessor . ProcessorInfo > = processor . processorInfo
2019-07-12 01:47:17 -07:00
2021-06-06 21:18:25 -07:00
/ * *
* The latest height seen on the network while processing blocks . This may differ from the
* latest height scanned and is useful for determining block confirmations and expiration .
* /
2022-07-12 05:40:09 -07:00
override val networkHeight : StateFlow < BlockHeight ? > = processor . networkHeight
2021-05-25 08:15:09 -07:00
2019-07-12 01:47:17 -07:00
//
2019-07-14 15:13:12 -07:00
// Error Handling
2019-07-12 01:47:17 -07:00
//
2019-08-30 10:05:02 -07:00
/ * *
2019-11-01 13:25:28 -07:00
* A callback to invoke whenever an uncaught error is encountered . By definition , the return
* value of the function is ignored because this error is unrecoverable . The only reason the
* function has a return value is so that all error handlers work with the same signature which
* allows one function to handle all errors in simple apps . This callback is not called on the
* main thread so any UI work would need to switch context to the main thread .
2019-07-14 15:13:12 -07:00
* /
override var onCriticalErrorHandler : ( ( Throwable ? ) -> Boolean ) ? = null
2019-08-30 10:05:02 -07:00
/ * *
2019-11-01 13:25:28 -07:00
* A callback to invoke whenever a processor error is encountered . Returning true signals that
* the error was handled and a retry attempt should be made , if possible . This callback is not
* called on the main thread so any UI work would need to switch context to the main thread .
2019-08-30 10:05:02 -07:00
* /
2019-07-14 15:13:12 -07:00
override var onProcessorErrorHandler : ( ( Throwable ? ) -> Boolean ) ? = null
2019-08-30 10:05:02 -07:00
/ * *
2019-11-01 13:25:28 -07:00
* A callback to invoke whenever a server error is encountered while submitting a transaction to
* lightwalletd . Returning true signals that the error was handled and a retry attempt should be
* made , if possible . This callback is not called on the main thread so any UI work would need
* to switch context to the main thread .
2019-08-30 10:05:02 -07:00
* /
2019-07-14 15:13:12 -07:00
override var onSubmissionErrorHandler : ( ( Throwable ? ) -> Boolean ) ? = null
2019-07-12 01:47:17 -07:00
2021-03-31 06:07:37 -07:00
/ * *
* A callback to invoke whenever a processor is not setup correctly . Returning true signals that
* the invalid setup should be ignored . If no handler is set , then any setup error will result
* in a critical error . This callback is not called on the main thread so any UI work would need
* to switch context to the main thread .
* /
override var onSetupErrorHandler : ( ( Throwable ? ) -> Boolean ) ? = null
2020-02-21 15:14:34 -08:00
/ * *
* A callback to invoke whenever a chain error is encountered . These occur whenever the
* processor detects a missing or non - chain - sequential block ( i . e . a reorg ) .
* /
2022-07-12 05:40:09 -07:00
override var onChainErrorHandler : ( ( errorHeight : BlockHeight , rewindHeight : BlockHeight ) -> Any ) ? = null
2020-02-21 15:14:34 -08:00
2019-11-01 13:25:28 -07:00
//
// Public API
//
2020-06-09 19:05:30 -07:00
/ * *
* Convenience function for the latest height . Specifically , this value represents the last
* height that the synchronizer has observed from the lightwalletd server . Instead of using
* this , a wallet will more likely want to consume the flow of processor info using
* [ processorInfo ] .
* /
2022-07-12 05:40:09 -07:00
override val latestHeight
get ( ) = processor . currentInfo . networkBlockHeight
2020-06-09 19:05:30 -07:00
2022-07-12 05:40:09 -07:00
override val latestBirthdayHeight
get ( ) = processor . birthdayHeight
2021-03-31 05:47:04 -07:00
2021-10-21 13:05:02 -07:00
override suspend fun prepare ( ) : Synchronizer = apply {
2022-01-19 10:39:07 -08:00
// Do nothing; this could likely be removed
2021-05-07 00:56:26 -07:00
}
2019-08-30 10:05:02 -07:00
/ * *
2019-11-01 13:25:28 -07:00
* Starts this synchronizer within the given scope . For simplicity , attempting to start an
* instance that has already been started will throw a [ SynchronizerException . FalseStart ]
* exception . This reduces the complexity of managing resources that must be recycled . Instead ,
* each synchronizer is designed to have a long lifespan and should be started from an activity ,
* application or session .
2019-08-30 10:05:02 -07:00
*
2019-11-01 13:25:28 -07:00
* @param parentScope the scope to use for this synchronizer , typically something with a
* lifecycle such as an Activity for single - activity apps or a logged in user session . This
2020-03-26 04:00:04 -07:00
* scope is only used for launching this synchronizer ' s job as a child . If no scope is provided ,
2019-11-26 12:46:31 -08:00
* 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 .
2020-02-27 00:25:07 -08:00
*
* @return an instance of this class so that this function can be used fluidly .
2019-08-30 10:05:02 -07:00
* /
2019-11-26 12:46:31 -08:00
override fun start ( parentScope : CoroutineScope ? ) : Synchronizer {
2020-06-09 19:05:30 -07:00
if ( isStarted ) throw SynchronizerException . FalseStart
2019-11-22 23:18:20 -08:00
// base this scope on the parent so that when the parent's job cancels, everything here
// cancels as well also use a supervisor job so that one failure doesn't bring down the
// whole synchronizer
2019-11-26 12:46:31 -08:00
val supervisorJob = SupervisorJob ( parentScope ?. coroutineContext ?. get ( Job ) )
2020-06-09 19:05:30 -07:00
CoroutineScope ( supervisorJob + Dispatchers . Main ) . let { scope ->
coroutineScope = scope
scope . onReady ( )
}
2019-07-14 15:13:12 -07:00
return this
2019-07-12 01:47:17 -07:00
}
2019-08-30 10:05:02 -07:00
/ * *
2019-11-01 13:25:28 -07:00
* Stop this synchronizer and all of its child jobs . Once a synchronizer has been stopped it
* should not be restarted and attempting to do so will result in an error . Also , this function
* will throw an exception if the synchronizer was never previously started .
2019-08-30 10:05:02 -07:00
* /
2019-07-10 11:12:32 -07:00
override fun stop ( ) {
2019-10-21 03:26:02 -07:00
coroutineScope . launch {
2021-03-31 05:30:51 -07:00
// log everything to help troubleshoot shutdowns that aren't graceful
twig ( " Synchronizer::stop: STARTING " )
twig ( " Synchronizer::stop: processor.stop() " )
2019-10-21 03:26:02 -07:00
processor . stop ( )
2021-03-31 05:30:51 -07:00
twig ( " Synchronizer::stop: coroutineScope.cancel() " )
2019-10-21 03:26:02 -07:00
coroutineScope . cancel ( )
2021-03-31 05:30:51 -07:00
twig ( " Synchronizer::stop: _status.cancel() " )
2019-11-22 23:18:20 -08:00
_status . cancel ( )
2021-03-31 05:30:51 -07:00
twig ( " Synchronizer::stop: COMPLETE " )
2019-10-21 03:26:02 -07:00
}
2019-07-10 11:12:32 -07:00
}
2020-06-09 19:05:30 -07:00
/ * *
* Convenience function that exposes the underlying server information , like its name and
* consensus branch id . Most wallets should already have a different source of truth for the
* server ( s ) with which they operate .
* /
override suspend fun getServerInfo ( ) : Service . LightdInfo = processor . downloader . getServerInfo ( )
2020-09-23 08:11:45 -07:00
/ * *
* Changes the server that is being used to download compact blocks . This will throw an
* exception if it detects that the server change is invalid e . g . switching to testnet from
* mainnet .
* /
override suspend fun changeServer ( host : String , port : Int , errorHandler : ( Throwable ) -> Unit ) {
val info =
( processor . downloader . lightWalletService as LightWalletGrpcService ) . connectionInfo
2020-09-25 06:59:55 -07:00
processor . downloader . changeService (
LightWalletGrpcService ( info . appContext , host , port ) ,
errorHandler
)
2020-09-23 08:11:45 -07:00
}
2022-07-12 05:40:09 -07:00
override suspend fun getNearestRewindHeight ( height : BlockHeight ) : BlockHeight =
2021-04-14 15:44:17 -07:00
processor . getNearestRewindHeight ( height )
2022-07-12 05:40:09 -07:00
override suspend fun rewindToNearestHeight ( height : BlockHeight , alsoClearBlockCache : Boolean ) {
2021-04-14 15:44:17 -07:00
processor . rewindToNearestHeight ( height , alsoClearBlockCache )
2021-03-31 06:16:06 -07:00
}
2021-05-03 19:53:23 -07:00
override suspend fun quickRewind ( ) {
processor . quickRewind ( )
}
2020-06-09 19:05:30 -07:00
//
// Storage APIs
//
// TODO: turn this section into the data access API. For now, just aggregate all the things that we want to do with the underlying data
2022-07-12 05:40:09 -07:00
suspend fun findBlockHash ( height : BlockHeight ) : ByteArray ? {
2020-06-09 19:05:30 -07:00
return ( storage as ? PagedTransactionRepository ) ?. findBlockHash ( height )
}
2022-07-12 05:40:09 -07:00
suspend fun findBlockHashAsHex ( height : BlockHeight ) : String ? {
2020-06-09 19:05:30 -07:00
return findBlockHash ( height ) ?. toHexReversed ( )
}
2021-10-21 13:05:02 -07:00
suspend fun getTransactionCount ( ) : Int {
2020-06-09 19:05:30 -07:00
return ( storage as ? PagedTransactionRepository ) ?. getTransactionCount ( ) ?: 0
}
2020-08-13 17:37:09 -07:00
fun refreshTransactions ( ) {
storage . invalidate ( )
}
2019-07-10 11:12:32 -07:00
//
2019-11-01 13:25:28 -07:00
// Private API
2019-07-10 11:12:32 -07:00
//
2021-04-05 15:37:13 -07:00
suspend fun refreshUtxos ( ) {
2021-06-29 23:24:24 -07:00
twig ( " refreshing utxos " , - 1 )
2021-04-05 15:37:13 -07:00
refreshUtxos ( getTransparentAddress ( ) )
}
2020-02-27 00:25:07 -08:00
/ * *
* Calculate the latest balance , based on the blocks that have been scanned and transmit this
* information into the flow of [ balances ] .
* /
2021-06-14 21:09:24 -07:00
suspend fun refreshAllBalances ( ) {
refreshSaplingBalance ( )
refreshTransparentBalance ( )
// TODO: refresh orchard balance
twig ( " Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place. " )
}
suspend fun refreshSaplingBalance ( ) {
twig ( " refreshing sapling balance " )
_saplingBalances . value = processor . getBalanceInfo ( )
}
suspend fun refreshTransparentBalance ( ) {
twig ( " refreshing transparent balance " )
_transparentBalances . value = processor . getUtxoCacheBalance ( getTransparentAddress ( ) )
2019-07-10 11:12:32 -07:00
}
2021-04-09 18:19:33 -07:00
suspend fun isValidAddress ( address : String ) : Boolean {
try {
return ! validateAddress ( address ) . isNotValid
2022-01-19 10:39:07 -08:00
} catch ( t : Throwable ) {
}
2021-04-09 18:19:33 -07:00
return false
}
2019-07-10 11:12:32 -07:00
private fun CoroutineScope . onReady ( ) = launch ( CoroutineExceptionHandler ( :: onCriticalError ) ) {
2021-05-07 00:56:26 -07:00
twig ( " Preparing to start... " )
prepare ( )
2020-02-11 16:56:31 -08:00
twig ( " Synchronizer ( ${this@SdkSynchronizer} ) Ready. Starting processor! " )
2021-03-31 05:35:09 -07:00
var lastScanTime = 0L
2020-02-21 15:14:34 -08:00
processor . onProcessorErrorListener = :: onProcessorError
2021-03-31 06:07:37 -07:00
processor . onSetupErrorListener = :: onSetupError
2020-02-21 15:14:34 -08:00
processor . onChainErrorListener = :: onChainError
2019-11-22 23:18:20 -08:00
processor . state . onEach {
when ( it ) {
is Scanned -> {
2021-03-31 05:35:09 -07:00
val now = System . currentTimeMillis ( )
2020-01-15 04:10:22 -08:00
// do a bit of housekeeping and then report synced status
2021-03-31 05:35:09 -07:00
onScanComplete ( it . scannedRange , now - lastScanTime )
lastScanTime = now
2019-11-22 23:18:20 -08:00
SYNCED
}
is Stopped -> STOPPED
is Disconnected -> DISCONNECTED
2020-01-15 04:10:22 -08:00
is Downloading , Initialized -> DOWNLOADING
is Validating -> VALIDATING
is Scanning -> SCANNING
2020-03-25 14:58:08 -07:00
is Enhancing -> ENHANCING
2019-11-22 23:18:20 -08:00
} . let { synchronizerStatus ->
2020-03-25 14:58:08 -07:00
// ignore enhancing status for now
// TODO: clean this up and handle enhancing gracefully
if ( synchronizerStatus != ENHANCING ) _status . send ( synchronizerStatus )
2019-11-01 13:25:28 -07:00
}
2019-10-21 03:26:02 -07:00
} . launchIn ( this )
2019-07-10 11:12:32 -07:00
processor . start ( )
twig ( " Synchronizer onReady complete. Processor start has exited! " )
}
2021-04-14 15:44:17 -07:00
private fun onCriticalError ( unused : CoroutineContext ? , error : Throwable ) {
2019-07-10 11:12:32 -07:00
twig ( " ******** " )
twig ( " ******** ERROR: $error " )
2021-05-07 00:59:47 -07:00
twig ( error )
2019-07-10 11:12:32 -07:00
if ( error . cause != null ) twig ( " ******** caused by ${error.cause} " )
if ( error . cause ?. cause != null ) twig ( " ******** caused by ${error.cause?.cause} " )
twig ( " ******** " )
2020-09-25 06:59:55 -07:00
if ( onCriticalErrorHandler == null ) {
twig (
" WARNING: a critical error occurred but no callback is registered to be notified " +
2021-03-10 10:10:03 -08:00
" of critical errors! THIS IS PROBABLY A MISTAKE. To respond to these " +
" errors (perhaps to update the UI or alert the user) set " +
" synchronizer.onCriticalErrorHandler to a non-null value. "
2020-09-25 06:59:55 -07:00
)
}
2019-07-14 15:13:12 -07:00
onCriticalErrorHandler ?. invoke ( error )
2019-07-10 11:12:32 -07:00
}
2019-07-14 15:13:12 -07:00
private fun onFailedSend ( error : Throwable ) : Boolean {
twig ( " ERROR while submitting transaction: $error " )
return onSubmissionErrorHandler ?. invoke ( error ) ?. also {
if ( it ) twig ( " submission error handler signaled that we should try again! " )
} == true
2019-07-10 11:12:32 -07:00
}
2019-07-14 15:13:12 -07:00
private fun onProcessorError ( error : Throwable ) : Boolean {
twig ( " ERROR while processing data: $error " )
2019-10-21 03:26:02 -07:00
if ( onProcessorErrorHandler == null ) {
2019-10-23 22:21:52 -07:00
twig (
" WARNING: falling back to the default behavior for processor errors. To add " +
2021-03-10 10:10:03 -08:00
" custom behavior, set synchronizer.onProcessorErrorHandler to " +
" a non-null value "
2019-10-23 22:21:52 -07:00
)
2019-10-21 03:26:02 -07:00
return true
}
2019-07-14 15:13:12 -07:00
return onProcessorErrorHandler ?. invoke ( error ) ?. also {
2019-10-23 22:21:52 -07:00
twig (
" processor error handler signaled that we should " +
2021-03-10 10:10:03 -08:00
" ${if (it) "try again" else "abort"} ! "
2019-10-23 22:21:52 -07:00
)
2019-07-14 15:13:12 -07:00
} == true
2019-07-10 11:12:32 -07:00
}
2021-03-31 06:07:37 -07:00
private fun onSetupError ( error : Throwable ) : Boolean {
if ( onSetupErrorHandler == null ) {
twig (
" WARNING: falling back to the default behavior for setup errors. To add custom " +
" behavior, set synchronizer.onSetupErrorHandler to a non-null value "
)
return false
}
return onSetupErrorHandler ?. invoke ( error ) == true
}
2019-07-10 11:12:32 -07:00
2022-07-12 05:40:09 -07:00
private fun onChainError ( errorHeight : BlockHeight , rewindHeight : BlockHeight ) {
2020-02-21 15:14:34 -08:00
twig ( " Chain error detected at height: $errorHeight . Rewinding to: $rewindHeight " )
if ( onChainErrorHandler == null ) {
twig (
" WARNING: a chain error occurred but no callback is registered to be notified of " +
2021-03-10 10:10:03 -08:00
" chain errors. To respond to these errors (perhaps to update the UI or alert the " +
" user) set synchronizer.onChainErrorHandler to a non-null value "
2020-02-21 15:14:34 -08:00
)
}
onChainErrorHandler ?. invoke ( errorHeight , rewindHeight )
}
2021-03-31 05:35:09 -07:00
/ * *
* @param elapsedMillis the amount of time that passed since the last scan
* /
2022-07-12 05:40:09 -07:00
private suspend fun onScanComplete ( scannedRange : ClosedRange < BlockHeight > ? , elapsedMillis : Long ) {
2021-03-31 05:35:09 -07:00
// We don't need to update anything if there have been no blocks
// refresh anyway if:
// - if it's the first time we finished scanning
// - if we check for blocks 5 times and find nothing was mined
val shouldRefresh = ! scannedRange . isEmpty ( ) || elapsedMillis > ( ZcashSdk . POLL _INTERVAL * 5 )
val reason = if ( scannedRange . isEmpty ( ) ) " it's been a while " else " new blocks were scanned "
2019-11-22 23:18:20 -08:00
// TRICKY:
// Keep an eye on this section because there is a potential for concurrent DB
// modification. A change in transactions means a change in balance. Calculating the
// balance requires touching transactions. If both are done in separate threads, the
// database can have issues. On Android, this would manifest as a false positive for a
// "malformed database" exception when the database is not actually corrupt but rather
// locked (i.e. it's a bad error message).
// The balance refresh is done first because it is coroutine-based and will fully
// complete by the time the function returns.
// Ultimately, refreshing the transactions just invalidates views of data that
// already exists and it completes on another thread so it should come after the
// balance refresh is complete.
2021-03-31 05:35:09 -07:00
if ( shouldRefresh ) {
2021-06-29 23:24:24 -07:00
twigTask ( " Triggering utxo refresh since $reason ! " , - 1 ) {
2021-03-31 05:35:09 -07:00
refreshUtxos ( )
}
2021-06-29 23:24:24 -07:00
twigTask ( " Triggering balance refresh since $reason ! " , - 1 ) {
2021-06-14 21:09:24 -07:00
refreshAllBalances ( )
2021-03-31 05:35:09 -07:00
}
2021-06-29 23:24:24 -07:00
twigTask ( " Triggering pending transaction refresh since $reason ! " , - 1 ) {
2021-03-31 05:35:09 -07:00
refreshPendingTransactions ( )
}
twigTask ( " Triggering transaction refresh since $reason ! " ) {
refreshTransactions ( )
}
2019-11-22 23:18:20 -08:00
}
}
2022-01-19 10:39:07 -08:00
private suspend fun refreshPendingTransactions ( ) {
2020-07-31 23:13:39 -07:00
twig ( " [cleanup] beginning to refresh and clean up pending transactions " )
2019-11-22 23:18:20 -08:00
// 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).
2020-07-31 23:13:39 -07:00
val allPendingTxs = txManager . getAll ( ) . first ( )
val lastScannedHeight = storage . lastScannedHeight ( )
allPendingTxs . filter { it . isSubmitSuccess ( ) && ! it . isMined ( ) }
2019-11-23 15:07:28 -08:00
. forEach { pendingTx ->
twig ( " checking for updates on pendingTx id: ${pendingTx.id} " )
pendingTx . rawTransactionId ?. let { rawId ->
2020-06-09 18:35:40 -07:00
storage . findMinedHeight ( rawId ) ?. let { minedHeight ->
2019-11-23 15:07:28 -08:00
twig (
" found matching transaction for pending transaction with id " +
2021-03-10 10:10:03 -08:00
" ${pendingTx.id} mined at height $minedHeight ! "
2019-11-23 15:07:28 -08:00
)
2020-06-09 18:35:40 -07:00
txManager . applyMinedHeight ( pendingTx , minedHeight )
2019-11-23 15:07:28 -08:00
}
2019-11-22 23:18:20 -08:00
}
}
2020-07-31 23:13:39 -07:00
2021-06-29 23:24:24 -07:00
twig ( " [cleanup] beginning to cleanup cancelled transactions " , - 1 )
2020-07-31 23:13:39 -07:00
var hasCleaned = false
// Experimental: cleanup cancelled transactions
allPendingTxs . filter { it . isCancelled ( ) && it . hasRawTransactionId ( ) } . let { cancellable ->
cancellable . forEachIndexed { index , pendingTx ->
2021-03-10 10:10:03 -08:00
twig (
" [cleanup] FOUND ( ${index + 1} of ${cancellable.size} ) " +
" CANCELLED pendingTxId: ${pendingTx.id} "
)
2020-07-31 23:13:39 -07:00
hasCleaned = hasCleaned || cleanupCancelledTx ( pendingTx )
}
}
2021-02-23 14:35:22 -08:00
// Experimental: cleanup failed transactions
2022-01-19 10:39:07 -08:00
allPendingTxs . filter { it . isSubmitted ( ) && it . isFailedSubmit ( ) && ! it . isMarkedForDeletion ( ) }
. let { failed ->
failed . forEachIndexed { index , pendingTx ->
twig (
" [cleanup] FOUND ( ${index + 1} of ${failed.size} ) " +
" FAILED pendingTxId: ${pendingTx.id} "
)
cleanupCancelledTx ( pendingTx )
}
2021-02-23 14:35:22 -08:00
}
2021-06-29 23:24:24 -07:00
twig ( " [cleanup] beginning to cleanup expired transactions " , - 1 )
2020-07-31 23:13:39 -07:00
// Experimental: cleanup expired transactions
// note: don't delete the pendingTx until the related data has been scrubbed, or else you
// lose the thing that identifies the other data as invalid
// so we first mark the data for deletion, during the previous "cleanup" step, by removing
// the thing that we're trying to preserve to signal we no longer need it
// sometimes apps crash or things go wrong and we get an orphaned pendingTx that we'll poll
// forever, so maybe just get rid of all of them after a long while
2021-03-10 10:10:03 -08:00
allPendingTxs . filter {
2022-01-19 10:39:07 -08:00
(
it . isExpired (
lastScannedHeight ,
network . saplingActivationHeight
) && it . isMarkedForDeletion ( )
) ||
it . isLongExpired (
lastScannedHeight ,
network . saplingActivationHeight
) || it . isSafeToDiscard ( )
2021-03-10 10:10:03 -08:00
}
2020-07-31 23:13:39 -07:00
. forEach {
val result = txManager . abort ( it )
twig ( " [cleanup] FOUND EXPIRED pendingTX (lastScanHeight: $lastScannedHeight expiryHeight: ${it.expiryHeight} ): and ${it.id} ${if (result > 0) "successfully removed" else "failed to remove"} it " )
}
2021-06-29 23:24:24 -07:00
twig ( " [cleanup] deleting expired transactions from storage " , - 1 )
val expiredCount = storage . deleteExpired ( lastScannedHeight )
if ( expiredCount > 0 ) twig ( " [cleanup] deleted $expiredCount expired transaction(s)! " )
hasCleaned = hasCleaned || ( expiredCount > 0 )
2020-07-31 23:13:39 -07:00
2021-06-14 21:09:24 -07:00
if ( hasCleaned ) refreshAllBalances ( )
2021-06-29 23:24:24 -07:00
twig ( " [cleanup] done refreshing and cleaning up pending transactions " , - 1 )
2019-11-22 23:18:20 -08:00
}
2020-07-31 23:13:39 -07:00
private suspend fun cleanupCancelledTx ( pendingTx : PendingTransaction ) : Boolean {
return if ( storage . cleanupCancelledTx ( pendingTx . rawTransactionId !! ) ) {
txManager . markForDeletion ( pendingTx . id )
true
} else {
twig ( " [cleanup] no matching tx was cleaned so the pendingTx will not be marked for deletion " )
false
}
}
2019-07-10 11:12:32 -07:00
//
// Send / Receive
//
2020-07-31 23:13:39 -07:00
override suspend fun cancelSpend ( pendingId : Long ) = txManager . cancel ( pendingId )
2019-07-14 15:13:12 -07:00
2021-02-17 13:07:57 -08:00
override suspend fun getAddress ( accountId : Int ) : String = getShieldedAddress ( accountId )
2022-01-19 10:39:07 -08:00
override suspend fun getShieldedAddress ( accountId : Int ) : String =
processor . getShieldedAddress ( accountId )
2021-02-17 13:07:57 -08:00
2022-01-19 10:39:07 -08:00
override suspend fun getTransparentAddress ( accountId : Int ) : String =
processor . getTransparentAddress ( accountId )
2019-07-10 11:12:32 -07:00
2019-11-01 13:25:28 -07:00
override fun sendToAddress (
spendingKey : String ,
2022-06-21 16:34:42 -07:00
amount : Zatoshi ,
2019-07-10 11:12:32 -07:00
toAddress : String ,
memo : String ,
2019-11-01 13:25:28 -07:00
fromAccountIndex : Int
2019-11-22 23:18:20 -08:00
) : Flow < PendingTransaction > = flow {
twig ( " Initializing pending transaction " )
// Emit the placeholder transaction, then switch to monitoring the database
2022-06-21 16:34:42 -07:00
txManager . initSpend ( amount , toAddress , memo , fromAccountIndex ) . let { placeHolderTx ->
2019-11-22 23:18:20 -08:00
emit ( placeHolderTx )
2020-06-09 18:35:40 -07:00
txManager . encode ( spendingKey , placeHolderTx ) . let { encodedTx ->
2020-07-31 23:13:39 -07:00
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
if ( encodedTx . isCancelled ( ) ) {
twig ( " [cleanup] this tx has been cancelled so we will cleanup instead of submitting " )
2021-06-14 21:09:24 -07:00
if ( cleanupCancelledTx ( encodedTx ) ) refreshAllBalances ( )
2020-07-31 23:13:39 -07:00
encodedTx
} else {
2020-06-09 18:35:40 -07:00
txManager . submit ( encodedTx )
2019-11-23 15:07:28 -08:00
}
2019-11-22 23:18:20 -08:00
}
2019-11-01 13:25:28 -07:00
}
2019-11-22 23:18:20 -08:00
} . flatMapLatest {
2021-04-05 15:37:13 -07:00
// switch this flow over to monitoring the database for transactions
// so we emit the placeholder TX above, then watch the database for all further updates
2019-11-23 15:07:28 -08:00
twig ( " Monitoring pending transaction (id: ${it.id} ) for updates... " )
2020-06-09 18:35:40 -07:00
txManager . monitorById ( it . id )
2019-11-23 15:07:28 -08:00
} . distinctUntilChanged ( )
2020-01-08 00:57:42 -08:00
2021-02-17 13:07:57 -08:00
override fun shieldFunds (
spendingKey : String ,
transparentSecretKey : String ,
memo : String
) : Flow < PendingTransaction > = flow {
twig ( " Initializing shielding transaction " )
2022-01-19 10:39:07 -08:00
val tAddr =
DerivationTool . deriveTransparentAddressFromPrivateKey ( transparentSecretKey , network )
2021-02-17 13:07:57 -08:00
val tBalance = processor . getUtxoCacheBalance ( tAddr )
val zAddr = getAddress ( 0 )
// Emit the placeholder transaction, then switch to monitoring the database
2022-06-21 16:34:42 -07:00
txManager . initSpend ( tBalance . available , zAddr , memo , 0 ) . let { placeHolderTx ->
2021-02-17 13:07:57 -08:00
emit ( placeHolderTx )
txManager . encode ( spendingKey , transparentSecretKey , placeHolderTx ) . let { encodedTx ->
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
if ( encodedTx . isCancelled ( ) ) {
twig ( " [cleanup] this shielding tx has been cancelled so we will cleanup instead of submitting " )
2021-06-14 21:09:24 -07:00
if ( cleanupCancelledTx ( encodedTx ) ) refreshAllBalances ( )
2021-02-17 13:07:57 -08:00
encodedTx
} else {
txManager . submit ( encodedTx )
}
}
}
} . flatMapLatest {
twig ( " Monitoring shielding transaction (id: ${it.id} ) for updates... " )
txManager . monitorById ( it . id )
} . distinctUntilChanged ( )
2022-07-12 05:40:09 -07:00
override suspend fun refreshUtxos ( address : String , startHeight : BlockHeight ) : Int ? {
2021-04-05 15:37:13 -07:00
return processor . refreshUtxos ( address , startHeight )
2021-02-17 13:07:57 -08:00
}
override suspend fun getTransparentBalance ( tAddr : String ) : WalletBalance {
return processor . getUtxoCacheBalance ( tAddr )
}
2020-07-31 23:13:39 -07:00
override suspend fun isValidShieldedAddr ( address : String ) =
txManager . isValidShieldedAddress ( address )
2020-01-08 00:57:42 -08:00
override suspend fun isValidTransparentAddr ( address : String ) =
2020-06-09 18:35:40 -07:00
txManager . isValidTransparentAddress ( address )
2020-01-08 00:57:42 -08:00
2020-06-09 18:35:40 -07:00
override suspend fun validateAddress ( address : String ) : AddressType {
2020-01-08 00:57:42 -08:00
return try {
2020-01-14 09:57:39 -08:00
if ( isValidShieldedAddr ( address ) ) Shielded else Transparent
2020-01-08 00:57:42 -08:00
} catch ( zError : Throwable ) {
var message = zError . message
try {
2020-01-14 09:57:39 -08:00
if ( isValidTransparentAddr ( address ) ) Transparent else Shielded
2020-01-08 00:57:42 -08:00
} catch ( tError : Throwable ) {
2020-06-09 18:35:40 -07:00
AddressType . Invalid (
2021-03-10 10:10:03 -08:00
if ( message != tError . message ) " $message and ${tError.message} " else (
message
?: " Invalid "
)
2020-01-08 00:57:42 -08:00
)
}
}
}
2019-11-01 13:25:28 -07:00
2020-06-09 18:35:40 -07:00
override suspend fun validateConsensusBranch ( ) : ConsensusMatchType {
val serverBranchId = tryNull { processor . downloader . getServerInfo ( ) . consensusBranchId }
val sdkBranchId = tryNull {
( txManager as PersistentTransactionManager ) . encoder . getConsensusBranchId ( )
2019-11-23 17:47:50 -08:00
}
2020-06-09 18:35:40 -07:00
return ConsensusMatchType (
sdkBranchId ?. let { ConsensusBranchId . fromId ( it ) } ,
serverBranchId ?. let { ConsensusBranchId . fromHex ( it ) }
)
2019-11-23 17:47:50 -08:00
}
2020-09-11 00:29:17 -07:00
2020-10-30 06:20:32 -07:00
interface Erasable {
/ * *
* Erase content related to this SDK .
*
* @param appContext the application context .
2021-04-09 18:43:07 -07:00
* @param network the network corresponding to the data being erased . Data is segmented by
* network in order to prevent contamination .
2020-10-30 06:20:32 -07:00
* @param alias identifier for SDK content . It is possible for multiple synchronizers to
* exist with different aliases .
*
* @return true when content was found for the given alias . False otherwise .
* /
2022-01-19 10:39:07 -08:00
suspend fun erase (
appContext : Context ,
network : ZcashNetwork ,
alias : String = ZcashSdk . DEFAULT _ALIAS
) : Boolean
2020-09-11 00:29:17 -07:00
}
2019-11-23 17:47:50 -08:00
}
2020-09-11 00:29:17 -07:00
/ * *
2022-01-19 10:39:07 -08:00
* Provides a way of constructing a synchronizer where dependencies are injected in .
2020-09-11 00:29:17 -07:00
*
2022-01-19 10:39:07 -08:00
* See the helper methods for generating default values .
2020-09-11 00:29:17 -07:00
* /
2022-01-19 10:39:07 -08:00
object DefaultSynchronizerFactory {
fun new (
repository : TransactionRepository ,
txManager : OutboundTransactionManager ,
2022-06-23 05:31:02 -07:00
processor : CompactBlockProcessor
2022-01-19 10:39:07 -08:00
) : 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
)
}
// TODO [#242]: Don't hard code page size. It is a workaround for Uncaught Exception: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. and is probably related to FlowPagedList
2022-01-14 05:05:55 -08:00
private const val DEFAULT _PAGE _SIZE = 1000
2022-01-19 10:39:07 -08:00
suspend fun defaultTransactionRepository ( initializer : Initializer ) : TransactionRepository =
PagedTransactionRepository . new (
initializer . context ,
2022-07-12 05:40:09 -07:00
initializer . network ,
2022-01-14 05:05:55 -08:00
DEFAULT _PAGE _SIZE ,
2022-01-19 10:39:07 -08:00
initializer . rustBackend ,
2022-07-12 05:40:09 -07:00
initializer . checkpoint ,
2022-01-19 10:39:07 -08:00
initializer . viewingKeys ,
initializer . overwriteVks
)
fun defaultBlockStore ( initializer : Initializer ) : CompactBlockStore =
2022-07-12 05:40:09 -07:00
CompactBlockDbStore . new ( initializer . context , initializer . network , initializer . rustBackend . pathCacheDb )
2022-01-19 10:39:07 -08:00
fun defaultService ( initializer : Initializer ) : LightWalletService =
LightWalletGrpcService ( initializer . context , initializer . host , initializer . port )
fun defaultEncoder (
initializer : Initializer ,
repository : TransactionRepository
) : TransactionEncoder = WalletTransactionEncoder ( initializer . rustBackend , repository )
fun defaultDownloader (
service : LightWalletService ,
blockStore : CompactBlockStore
) : CompactBlockDownloader = CompactBlockDownloader ( service , blockStore )
fun defaultTxManager (
initializer : Initializer ,
encoder : TransactionEncoder ,
service : LightWalletService
) : OutboundTransactionManager =
PersistentTransactionManager ( initializer . context , encoder , service )
fun defaultProcessor (
initializer : Initializer ,
downloader : CompactBlockDownloader ,
repository : TransactionRepository
) : CompactBlockProcessor = CompactBlockProcessor (
downloader ,
2020-09-11 00:29:17 -07:00
repository ,
2022-01-19 10:39:07 -08:00
initializer . rustBackend ,
initializer . rustBackend . birthdayHeight
2020-09-11 00:29:17 -07:00
)
2021-03-10 19:04:39 -08:00
}