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
|
2022-08-12 08:05:00 -07:00
|
|
|
import cash.z.ecc.android.sdk.db.DatabaseCoordinator
|
2021-03-10 10:10:03 -08:00
|
|
|
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
|
2022-10-06 10:48:21 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.ext.getCacheDirSuspend
|
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
|
2022-10-06 10:48:21 -07:00
|
|
|
import cash.z.ecc.android.sdk.internal.model.Checkpoint
|
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-10-06 10:48:21 -07:00
|
|
|
import cash.z.ecc.android.sdk.jni.RustBackend
|
2022-10-06 10:44:34 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.Account
|
2022-07-12 05:40:09 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.BlockHeight
|
2022-10-06 10:48:21 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
|
2022-09-27 06:01:53 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
|
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
|
2022-08-02 06:29:09 -07:00
|
|
|
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
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
|
2022-06-17 05:06:21 -07:00
|
|
|
import cash.z.ecc.android.sdk.type.AddressType.Unified
|
2021-03-31 23:14:57 -07:00
|
|
|
import cash.z.ecc.android.sdk.type.ConsensusMatchType
|
2022-10-06 10:48:21 -07:00
|
|
|
import cash.z.ecc.android.sdk.type.UnifiedFullViewingKey
|
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.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
|
2022-10-06 10:48:21 -07:00
|
|
|
import java.io.File
|
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
|
|
|
*/
|
2022-08-17 06:48:02 -07:00
|
|
|
@OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class)
|
2020-09-11 00:29:17 -07:00
|
|
|
@FlowPreview
|
2022-08-23 06:49:00 -07:00
|
|
|
@Suppress("TooManyFunctions")
|
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
|
|
|
|
2022-08-17 06:48:02 -07:00
|
|
|
// TODO [#288]: Remove Deprecated Usage of ConflatedBroadcastChannel
|
|
|
|
// TODO [#288]: https://github.com/zcash/zcash-android-wallet-sdk/issues/288
|
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 (!isStarted) {
|
|
|
|
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
|
|
|
*/
|
2022-08-17 06:48:02 -07:00
|
|
|
// TODO [#658] Replace ComputableFlow and asFlow() obsolete Coroutine usage
|
|
|
|
// TODO [#658] https://github.com/zcash/zcash-android-wallet-sdk/issues/658
|
|
|
|
@Suppress("DEPRECATION")
|
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()
|
|
|
|
|
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
|
|
|
|
//
|
|
|
|
|
2022-08-23 06:49:00 -07:00
|
|
|
// TODO [#682]: turn this section into the data access API. For now, just aggregate all the things that we want
|
|
|
|
// to do with the underlying data
|
|
|
|
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
2020-06-09 19:05:30 -07:00
|
|
|
|
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)
|
2022-10-06 10:40:49 -07:00
|
|
|
refreshUtxos(getLegacyTransparentAddress())
|
2021-04-05 15:37:13 -07:00
|
|
|
}
|
|
|
|
|
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()
|
2022-08-23 06:49:00 -07:00
|
|
|
// TODO [#682]: refresh orchard balance
|
|
|
|
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
2021-06-14 21:09:24 -07:00
|
|
|
twig("Warning: Orchard balance does not yet refresh. Only some of the plumbing is in place.")
|
|
|
|
}
|
|
|
|
|
|
|
|
suspend fun refreshSaplingBalance() {
|
|
|
|
twig("refreshing sapling balance")
|
2022-10-06 10:44:34 -07:00
|
|
|
_saplingBalances.value = processor.getBalanceInfo(Account.DEFAULT)
|
2021-06-14 21:09:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
suspend fun refreshTransparentBalance() {
|
|
|
|
twig("refreshing transparent balance")
|
2022-10-06 10:40:49 -07:00
|
|
|
_transparentBalances.value = processor.getUtxoCacheBalance(getLegacyTransparentAddress())
|
2019-07-10 11:12:32 -07:00
|
|
|
}
|
|
|
|
|
2021-04-09 18:19:33 -07:00
|
|
|
suspend fun isValidAddress(address: String): Boolean {
|
2022-08-23 06:49:00 -07:00
|
|
|
return !validateAddress(address).isNotValid
|
2021-04-09 18:19:33 -07:00
|
|
|
}
|
|
|
|
|
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
|
2022-08-23 06:49:00 -07:00
|
|
|
// TODO [#682]: clean this up and handle enhancing gracefully
|
|
|
|
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
2020-03-25 14:58:08 -07:00
|
|
|
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!")
|
|
|
|
}
|
|
|
|
|
2022-08-17 06:48:02 -07:00
|
|
|
@Suppress("UNUSED_PARAMETER")
|
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 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
|
2022-08-23 06:49:00 -07:00
|
|
|
@Suppress("MagicNumber")
|
2021-03-31 05:35:09 -07:00
|
|
|
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-08-23 06:49:00 -07:00
|
|
|
@Suppress("LongMethod", "ComplexMethod")
|
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")
|
2022-08-23 06:49:00 -07:00
|
|
|
// TODO [#682]: 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).
|
|
|
|
// TODO [#682]: https://github.com/zcash/zcash-android-wallet-sdk/issues/682
|
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)
|
2022-08-23 06:49:00 -07:00
|
|
|
twig(
|
|
|
|
"[cleanup] FOUND EXPIRED pendingTX (lastScanHeight: $lastScannedHeight " +
|
|
|
|
" expiryHeight: ${it.expiryHeight}): and ${it.id} " +
|
|
|
|
"${if (result > 0) "successfully removed" else "failed to remove"} it"
|
|
|
|
)
|
2020-07-31 23:13:39 -07:00
|
|
|
}
|
|
|
|
|
2021-06-29 23:24:24 -07:00
|
|
|
twig("[cleanup] deleting expired transactions from storage", -1)
|
|
|
|
val expiredCount = storage.deleteExpired(lastScannedHeight)
|
2022-08-23 06:49:00 -07:00
|
|
|
if (expiredCount > 0) {
|
|
|
|
twig("[cleanup] deleted $expiredCount expired transaction(s)!")
|
|
|
|
}
|
2021-06-29 23:24:24 -07:00
|
|
|
hasCleaned = hasCleaned || (expiredCount > 0)
|
2020-07-31 23:13:39 -07:00
|
|
|
|
2022-08-23 06:49:00 -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
|
|
|
|
2022-09-27 06:01:53 -07:00
|
|
|
//
|
|
|
|
// Account management
|
|
|
|
//
|
|
|
|
|
|
|
|
// Not ready to be a public API; internal for testing only
|
|
|
|
internal suspend fun createAccount(seed: ByteArray): UnifiedSpendingKey =
|
|
|
|
processor.createAccount(seed)
|
|
|
|
|
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
|
|
|
|
2022-09-19 11:34:10 -07:00
|
|
|
/**
|
|
|
|
* Returns the current Unified Address for this account.
|
|
|
|
*/
|
2022-10-06 10:44:34 -07:00
|
|
|
override suspend fun getCurrentAddress(account: Account): String =
|
|
|
|
processor.getCurrentAddress(account)
|
2021-02-17 13:07:57 -08:00
|
|
|
|
2022-10-06 10:40:49 -07:00
|
|
|
/**
|
|
|
|
* Returns the legacy Sapling address corresponding to the current Unified Address for this account.
|
|
|
|
*/
|
2022-10-06 10:44:34 -07:00
|
|
|
override suspend fun getLegacySaplingAddress(account: Account): String =
|
|
|
|
processor.getLegacySaplingAddress(account)
|
2021-02-17 13:07:57 -08:00
|
|
|
|
2022-09-19 11:34:10 -07:00
|
|
|
/**
|
|
|
|
* Returns the legacy transparent address corresponding to the current Unified Address for this account.
|
|
|
|
*/
|
2022-10-06 10:44:34 -07:00
|
|
|
override suspend fun getLegacyTransparentAddress(account: Account): String =
|
|
|
|
processor.getTransparentAddress(account)
|
2019-07-10 11:12:32 -07:00
|
|
|
|
2019-11-01 13:25:28 -07:00
|
|
|
override fun sendToAddress(
|
2022-09-29 10:04:00 -07:00
|
|
|
usk: UnifiedSpendingKey,
|
2022-06-21 16:34:42 -07:00
|
|
|
amount: Zatoshi,
|
2019-07-10 11:12:32 -07:00
|
|
|
toAddress: String,
|
2022-10-06 10:44:34 -07:00
|
|
|
memo: String
|
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-09-29 10:04:00 -07:00
|
|
|
txManager.initSpend(amount, toAddress, memo, usk.account).let { placeHolderTx ->
|
2019-11-22 23:18:20 -08:00
|
|
|
emit(placeHolderTx)
|
2022-09-29 10:04:00 -07:00
|
|
|
txManager.encode(usk, 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")
|
2022-08-17 06:48:02 -07:00
|
|
|
if (cleanupCancelledTx(encodedTx)) {
|
|
|
|
refreshAllBalances()
|
|
|
|
}
|
2020-07-31 23:13:39 -07:00
|
|
|
} 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(
|
2022-09-29 10:04:00 -07:00
|
|
|
usk: UnifiedSpendingKey,
|
2021-02-17 13:07:57 -08:00
|
|
|
memo: String
|
|
|
|
): Flow<PendingTransaction> = flow {
|
|
|
|
twig("Initializing shielding transaction")
|
2022-09-29 10:04:00 -07:00
|
|
|
// TODO(str4d): This only shields funds from the current UA's transparent receiver. Fix this once we start
|
|
|
|
// rolling UAs.
|
|
|
|
val tAddr = processor.getTransparentAddress(usk.account)
|
2021-02-17 13:07:57 -08:00
|
|
|
val tBalance = processor.getUtxoCacheBalance(tAddr)
|
2022-09-29 10:04:00 -07:00
|
|
|
val zAddr = getCurrentAddress(usk.account)
|
2021-02-17 13:07:57 -08:00
|
|
|
|
|
|
|
// Emit the placeholder transaction, then switch to monitoring the database
|
2022-09-29 10:04:00 -07:00
|
|
|
txManager.initSpend(tBalance.available, zAddr, memo, usk.account).let { placeHolderTx ->
|
2021-02-17 13:07:57 -08:00
|
|
|
emit(placeHolderTx)
|
2022-09-29 10:04:00 -07:00
|
|
|
txManager.encode("", usk, placeHolderTx).let { encodedTx ->
|
2021-02-17 13:07:57 -08:00
|
|
|
// 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")
|
2022-08-17 06:48:02 -07:00
|
|
|
if (cleanupCancelledTx(encodedTx)) {
|
|
|
|
refreshAllBalances()
|
|
|
|
}
|
2021-02-17 13:07:57 -08:00
|
|
|
} else {
|
|
|
|
txManager.submit(encodedTx)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}.flatMapLatest {
|
|
|
|
twig("Monitoring shielding transaction (id: ${it.id}) for updates...")
|
|
|
|
txManager.monitorById(it.id)
|
|
|
|
}.distinctUntilChanged()
|
|
|
|
|
2022-08-17 06:48:02 -07:00
|
|
|
override suspend fun refreshUtxos(tAddr: String, since: BlockHeight): Int? {
|
|
|
|
return processor.refreshUtxos(tAddr, since)
|
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
|
|
|
|
2022-06-17 05:06:21 -07:00
|
|
|
override suspend fun isValidUnifiedAddr(address: String) =
|
|
|
|
txManager.isValidUnifiedAddress(address)
|
|
|
|
|
2020-06-09 18:35:40 -07:00
|
|
|
override suspend fun validateAddress(address: String): AddressType {
|
2022-08-23 06:49:00 -07:00
|
|
|
@Suppress("TooGenericExceptionCaught")
|
2020-01-08 00:57:42 -08:00
|
|
|
return try {
|
2022-06-17 05:06:21 -07:00
|
|
|
if (isValidShieldedAddr(address)) {
|
|
|
|
Shielded
|
|
|
|
} else if (isValidTransparentAddr(address)) {
|
|
|
|
Transparent
|
|
|
|
} else if (isValidUnifiedAddr(address)) {
|
|
|
|
Unified
|
|
|
|
} else {
|
|
|
|
AddressType.Invalid("Not a Zcash address")
|
2020-01-08 00:57:42 -08:00
|
|
|
}
|
2022-06-17 05:06:21 -07:00
|
|
|
} catch (@Suppress("TooGenericExceptionCaught") error: Throwable) {
|
|
|
|
AddressType.Invalid(error.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
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-10-06 10:48:21 -07:00
|
|
|
internal suspend fun defaultRustBackend(
|
|
|
|
context: Context,
|
|
|
|
network: ZcashNetwork,
|
|
|
|
alias: String,
|
|
|
|
blockHeight: BlockHeight
|
|
|
|
): RustBackend {
|
|
|
|
val coordinator = DatabaseCoordinator.getInstance(context)
|
|
|
|
|
|
|
|
return RustBackend.init(
|
|
|
|
coordinator.cacheDbFile(network, alias).absolutePath,
|
|
|
|
coordinator.dataDbFile(network, alias).absolutePath,
|
|
|
|
File(context.getCacheDirSuspend(), "params").absolutePath,
|
|
|
|
network,
|
|
|
|
blockHeight
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-08-23 06:49:00 -07:00
|
|
|
// TODO [#242]: Don't hard code page size. It is a workaround for Uncaught Exception:
|
|
|
|
// android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy
|
|
|
|
// can touch its views. and is probably related to FlowPagedList
|
|
|
|
// TODO [#242]: https://github.com/zcash/zcash-android-wallet-sdk/issues/242
|
2022-01-14 05:05:55 -08:00
|
|
|
private const val DEFAULT_PAGE_SIZE = 1000
|
2022-10-06 10:48:21 -07:00
|
|
|
|
|
|
|
@Suppress("LongParameterList")
|
|
|
|
internal suspend fun defaultTransactionRepository(
|
|
|
|
context: Context,
|
|
|
|
rustBackend: RustBackend,
|
|
|
|
zcashNetwork: ZcashNetwork,
|
|
|
|
checkpoint: Checkpoint,
|
|
|
|
viewingKeys: List<UnifiedFullViewingKey>,
|
|
|
|
seed: ByteArray?
|
|
|
|
): TransactionRepository =
|
2022-01-19 10:39:07 -08:00
|
|
|
PagedTransactionRepository.new(
|
2022-10-06 10:48:21 -07:00
|
|
|
context,
|
|
|
|
zcashNetwork,
|
2022-01-14 05:05:55 -08:00
|
|
|
DEFAULT_PAGE_SIZE,
|
2022-10-06 10:48:21 -07:00
|
|
|
rustBackend,
|
2022-09-20 17:41:43 -07:00
|
|
|
seed,
|
2022-10-06 10:48:21 -07:00
|
|
|
checkpoint,
|
|
|
|
viewingKeys,
|
|
|
|
false
|
2022-01-19 10:39:07 -08:00
|
|
|
)
|
|
|
|
|
2022-10-06 10:48:21 -07:00
|
|
|
internal fun defaultBlockStore(context: Context, rustBackend: RustBackend, zcashNetwork: ZcashNetwork):
|
|
|
|
CompactBlockStore =
|
2022-08-12 08:05:00 -07:00
|
|
|
CompactBlockDbStore.new(
|
2022-10-06 10:48:21 -07:00
|
|
|
context,
|
|
|
|
zcashNetwork,
|
|
|
|
rustBackend.cacheDbFile
|
2022-08-12 08:05:00 -07:00
|
|
|
)
|
2022-01-19 10:39:07 -08:00
|
|
|
|
2022-10-06 10:48:21 -07:00
|
|
|
fun defaultService(context: Context, lightWalletEndpoint: LightWalletEndpoint): LightWalletService =
|
|
|
|
LightWalletGrpcService.new(context, lightWalletEndpoint)
|
2022-01-19 10:39:07 -08:00
|
|
|
|
2022-10-06 10:48:21 -07:00
|
|
|
internal fun defaultEncoder(
|
|
|
|
rustBackend: RustBackend,
|
2022-01-19 10:39:07 -08:00
|
|
|
repository: TransactionRepository
|
2022-10-06 10:48:21 -07:00
|
|
|
): TransactionEncoder = WalletTransactionEncoder(rustBackend, repository)
|
2022-01-19 10:39:07 -08:00
|
|
|
|
|
|
|
fun defaultDownloader(
|
|
|
|
service: LightWalletService,
|
|
|
|
blockStore: CompactBlockStore
|
|
|
|
): CompactBlockDownloader = CompactBlockDownloader(service, blockStore)
|
|
|
|
|
2022-08-12 08:05:00 -07:00
|
|
|
suspend fun defaultTxManager(
|
2022-10-06 10:48:21 -07:00
|
|
|
context: Context,
|
|
|
|
zcashNetwork: ZcashNetwork,
|
|
|
|
alias: String,
|
2022-01-19 10:39:07 -08:00
|
|
|
encoder: TransactionEncoder,
|
|
|
|
service: LightWalletService
|
2022-08-12 08:05:00 -07:00
|
|
|
): OutboundTransactionManager {
|
2022-10-06 10:48:21 -07:00
|
|
|
val databaseFile = DatabaseCoordinator.getInstance(context).pendingTransactionsDbFile(
|
|
|
|
zcashNetwork,
|
|
|
|
alias
|
2022-08-12 08:05:00 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
return PersistentTransactionManager(
|
2022-10-06 10:48:21 -07:00
|
|
|
context,
|
2022-08-12 08:05:00 -07:00
|
|
|
encoder,
|
|
|
|
service,
|
|
|
|
databaseFile
|
|
|
|
)
|
|
|
|
}
|
2022-01-19 10:39:07 -08:00
|
|
|
|
2022-10-06 10:48:21 -07:00
|
|
|
internal fun defaultProcessor(
|
|
|
|
rustBackend: RustBackend,
|
2022-01-19 10:39:07 -08:00
|
|
|
downloader: CompactBlockDownloader,
|
|
|
|
repository: TransactionRepository
|
|
|
|
): CompactBlockProcessor = CompactBlockProcessor(
|
|
|
|
downloader,
|
2020-09-11 00:29:17 -07:00
|
|
|
repository,
|
2022-10-06 10:48:21 -07:00
|
|
|
rustBackend,
|
|
|
|
rustBackend.birthdayHeight
|
2020-09-11 00:29:17 -07:00
|
|
|
)
|
2021-03-10 19:04:39 -08:00
|
|
|
}
|