ZcashLightClientKit/Sources/ZcashLightClientKit/Synchronizer.swift

400 lines
14 KiB
Swift
Raw Normal View History

//
// Synchronizer.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/5/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
/// Represents errors thrown by a Synchronizer
public enum SynchronizerError: Error {
case initFailed(message: String)
2021-05-05 12:08:57 -07:00
case notPrepared
case syncFailed
case connectionFailed(message: Error)
case generalError(message: String)
case maxRetryAttemptsReached(attempts: Int)
case connectionError(status: Int, message: String)
case networkTimeout
case uncategorized(underlyingError: Error)
case criticalError
2020-10-08 10:00:27 -07:00
case parameterMissing(underlyingError: Error)
case rewindError(underlyingError: Error)
case rewindErrorUnknownArchorHeight
2021-04-08 10:18:16 -07:00
case invalidAccount
case lightwalletdValidationFailed(underlyingError: Error)
}
2020-12-23 15:01:09 -08:00
public enum ShieldFundsError: Error {
case noUTXOFound
case insuficientTransparentFunds
case shieldingFailed(underlyingError: Error)
}
2021-02-15 11:15:50 -08:00
extension ShieldFundsError: LocalizedError {
public var errorDescription: String? {
switch self {
case .noUTXOFound:
return "Could not find UTXOs for the given t-address"
case .insuficientTransparentFunds:
return "You don't have enough confirmed transparent funds to perform a shielding transaction."
case .shieldingFailed(let underlyingError):
return "Shielding transaction failed. Reason: \(underlyingError)"
}
}
}
/// Represent the connection state to the lightwalletd server
2021-06-14 16:38:05 -07:00
public enum ConnectionState {
/// not in use
2021-06-14 16:38:05 -07:00
case idle
2021-09-17 06:49:58 -07:00
/// there's a connection being attempted from a non error state
2021-06-14 16:38:05 -07:00
case connecting
2021-09-17 06:49:58 -07:00
/// connection is established, ready to use or in use
2021-06-14 16:38:05 -07:00
case online
2021-09-17 06:49:58 -07:00
/// the connection is being re-established after losing it temporarily
2021-06-14 16:38:05 -07:00
case reconnecting
2021-09-17 06:49:58 -07:00
/// the connection has been closed
2021-06-14 16:38:05 -07:00
case shutdown
}
/// Primary interface for interacting with the SDK. Defines the contract that specific
/// implementations like SdkSynchronizer fulfill.
public protocol Synchronizer {
/// Value representing the Status of this Synchronizer. As the status changes, it will be also notified
2021-06-14 16:38:05 -07:00
var status: SyncStatus { get }
/// reflects current connection state to LightwalletEndpoint
2021-06-15 14:53:21 -07:00
var connectionState: ConnectionState { get }
/// prepares this initializer to operate. Initializes the internal state with the given
/// Extended Viewing Keys and a wallet birthday found in the initializer object
2021-05-05 12:08:57 -07:00
func prepare() throws
///Starts this synchronizer within the given scope.
///
///Implementations should leverage structured concurrency and
///cancel all jobs when this scope completes.
func start(retry: Bool) throws
/// Stop this synchronizer. Implementations should ensure that calling this method cancels all jobs that were created by this instance.
func stop() throws
/// Gets the sapling shielded address for the given account.
/// - Parameter accountIndex: the optional accountId whose address is of interest. By default, the first account is used.
/// - Returns the address or nil if account index is incorrect
2021-04-08 10:18:16 -07:00
func getShieldedAddress(accountIndex: Int) -> SaplingShieldedAddress?
/// Gets the unified address for the given account.
/// - Parameter accountIndex: the optional accountId whose address is of interest. By default, the first account is used.
/// - Returns the address or nil if account index is incorrect
2021-04-08 10:18:16 -07:00
func getUnifiedAddress(accountIndex: Int) -> UnifiedAddress?
/// Gets the transparent address for the given account.
/// - Parameter accountIndex: the optional accountId whose address is of interest. By default, the first account is used.
/// - Returns the address or nil if account index is incorrect
2021-04-08 10:18:16 -07:00
func getTransparentAddress(accountIndex: Int) -> TransparentAddress?
/// Sends zatoshi.
/// - Parameter spendingKey: the key that allows spends to occur.
/// - Parameter zatoshi: the amount of zatoshi to send.
/// - Parameter toAddress: the recipient's address.
/// - Parameter memo: the optional memo to include as part of the transaction.
/// - Parameter accountIndex: the optional account id to use. By default, the first account is used.
@available(*, deprecated, message: "This function will be removed soon, use the one reveiving a `Zatoshi` value instead")
2021-09-17 06:49:58 -07:00
// swiftlint:disable:next function_parameter_count
func sendToAddress(
spendingKey: String,
zatoshi: Int64,
toAddress: String,
memo: String?,
from accountIndex: Int,
resultBlock: @escaping (_ result: Result<PendingTransactionEntity, Error>) -> Void
)
/// Sends zatoshi.
/// - Parameter spendingKey: the key that allows spends to occur.
/// - Parameter zatoshi: the amount to send in Zatoshi.
/// - Parameter toAddress: the recipient's address.
/// - Parameter memo: the optional memo to include as part of the transaction.
/// - Parameter accountIndex: the optional account id to use. By default, the first account is used.
// swiftlint:disable:next function_parameter_count
func sendToAddress(
spendingKey: String,
zatoshi: Zatoshi,
toAddress: String,
memo: String?,
from accountIndex: Int,
resultBlock: @escaping (_ result: Result<PendingTransactionEntity, Error>) -> Void
)
/// Sends zatoshi.
/// - Parameter spendingKey: the key that allows spends to occur.
/// - Parameter transparentSecretKey: the key that allows to spend transaprent funds
/// - Parameter memo: the optional memo to include as part of the transaction.
/// - Parameter accountIndex: the optional account id that will be used to shield your funds to. By default, the first account is used.
2021-09-17 06:49:58 -07:00
func shieldFunds(
spendingKey: String,
transparentSecretKey: String,
memo: String?,
from accountIndex: Int,
resultBlock: @escaping (_ result: Result<PendingTransactionEntity, Error>) -> Void
)
/// Attempts to cancel a transaction that is about to be sent. Typically, cancellation is only
/// an option if the transaction has not yet been submitted to the server.
/// - Parameter transaction: the transaction to cancel.
/// - Returns: true when the cancellation request was successful. False when it is too late.
func cancelSpend(transaction: PendingTransactionEntity) -> Bool
/// all outbound pending transactions that have been sent but are awaiting confirmations
var pendingTransactions: [PendingTransactionEntity] { get }
/// all the transactions that are on the blockchain
var clearedTransactions: [ConfirmedTransactionEntity] { get }
/// All transactions that are related to sending funds
var sentTransactions: [ConfirmedTransactionEntity] { get }
/// all transactions related to receiving funds
var receivedTransactions: [ConfirmedTransactionEntity] { get }
/// A repository serving transactions in a paginated manner
/// - Parameter kind: Transaction Kind expected from this PaginatedTransactionRepository
func paginatedTransactions(of kind: TransactionKind) -> PaginatedTransactionRepository
/// Returns a list of confirmed transactions that preceed the given transaction with a limit count.
/// - Parameters:
/// - from: the confirmed transaction from which the query should start from or nil to retrieve from the most recent transaction
/// - limit: the maximum amount of items this should return if available
/// - Returns: an array with the given Transactions or nil
func allConfirmedTransactions(from transaction: ConfirmedTransactionEntity?, limit: Int) throws -> [ConfirmedTransactionEntity]?
/// Returns the latest downloaded height from the compact block cache
2020-10-06 16:35:17 -07:00
func latestDownloadedHeight() throws -> BlockHeight
/// Returns the latest block height from the provided Lightwallet endpoint
2020-10-06 16:35:17 -07:00
func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void)
/// Returns the latest block height from the provided Lightwallet endpoint
/// Blocking
2020-10-06 16:35:17 -07:00
func latestHeight() throws -> BlockHeight
2020-12-11 12:15:29 -08:00
/// Returns the latests UTXOs for the given address from the specified height on
2021-09-17 06:49:58 -07:00
func refreshUTXOs(address: String, from height: BlockHeight, result: @escaping (Result<RefreshedUTXOs, Error>) -> Void)
/// Returns the last stored unshielded balance
2021-04-08 10:18:16 -07:00
func getTransparentBalance(accountIndex: Int) throws -> WalletBalance
2021-02-17 15:02:25 -08:00
/// Returns the shielded total balance (includes verified and unverified balance)
@available(*, deprecated, message: "This function will be removed soon, use the one returning a `Zatoshi` value instead")
func getShieldedBalance(accountIndex: Int) -> Int64
/// Returns the shielded total balance (includes verified and unverified balance)
func getShieldedBalance(accountIndex: Int) -> Zatoshi
/// Returns the shielded verified balance (anchor is 10 blocks back)
@available(*, deprecated, message: "This function will be removed soon, use the one returning a `Zatoshi` value instead")
func getShieldedVerifiedBalance(accountIndex: Int) -> Int64
/// Returns the shielded verified balance (anchor is 10 blocks back)
func getShieldedVerifiedBalance(accountIndex: Int) -> Zatoshi
/// Stops the synchronizer and rescans the known blocks with the current keys.
/// - Parameter policy: the rewind policy
/// - Throws rewindErrorUnknownArchorHeight when the rewind points to an invalid height
/// - Throws rewindError for other errors
/// - Note rewind does not trigger notifications as a reorg would. You need to restart the synchronizer afterwards
func rewind(_ policy: RewindPolicy) throws
}
2021-06-14 16:38:05 -07:00
public enum SyncStatus: Equatable {
/// Indicates that this Synchronizer is actively preparing to start,
/// which usually involves setting up database tables, migrations or
/// taking other maintenance steps that need to occur after an upgrade.
2021-05-05 12:08:57 -07:00
case unprepared
2021-09-17 06:49:58 -07:00
/// Indicates that this Synchronizer is actively downloading new blocks from the server.
2021-09-17 06:49:58 -07:00
case downloading(_ status: BlockProgress)
/// Indicates that this Synchronizer is actively validating new blocks that were downloaded
/// from the server. Blocks need to be verified before they are scanned. This confirms that
/// each block is chain-sequential, thereby detecting missing blocks and reorgs.
2021-06-07 16:00:33 -07:00
case validating
2021-09-17 06:49:58 -07:00
/// Indicates that this Synchronizer is actively scanning new valid blocks that were
/// downloaded from the server.
2021-09-17 06:49:58 -07:00
case scanning(_ progress: BlockProgress)
/// Indicates that this Synchronizer is actively enhancing newly scanned blocks
/// with additional transaction details, fetched from the server.
2021-06-14 16:38:05 -07:00
case enhancing(_ progress: EnhancementProgress)
2021-09-17 06:49:58 -07:00
/// fetches the transparent balance and stores it locally
2021-06-07 16:00:33 -07:00
case fetching
2021-09-17 06:49:58 -07:00
/// Indicates that this Synchronizer is fully up to date and ready for all wallet functions.
/// When set, a UI element may want to turn green.
case synced
2021-09-17 06:49:58 -07:00
/// Indicates that [stop] has been called on this Synchronizer and it will no longer be used.
2021-06-14 16:38:05 -07:00
case stopped
2021-09-17 06:49:58 -07:00
/// Indicates that this Synchronizer is disconnected from its lightwalletd server.
/// When set, a UI element may want to turn red.
2021-06-14 16:38:05 -07:00
case disconnected
2021-09-17 06:49:58 -07:00
2021-06-14 16:38:05 -07:00
case error(_ error: Error)
2021-06-07 16:00:33 -07:00
public var isSyncing: Bool {
switch self {
2021-06-14 16:38:05 -07:00
case .downloading, .validating, .scanning, .enhancing, .fetching:
2021-06-07 16:00:33 -07:00
return true
2021-06-14 16:38:05 -07:00
default:
return false
2021-06-07 16:00:33 -07:00
}
}
2021-06-15 14:53:21 -07:00
public var isSynced: Bool {
switch self {
2021-09-17 06:49:58 -07:00
case .synced: return true
default: return false
2021-06-15 14:53:21 -07:00
}
}
}
/// Kind of transactions handled by a Synchronizer
public enum TransactionKind {
case sent
case received
case all
}
/// Type of rewind available
/// -birthday: rewinds the local state to this wallet's birthday
/// -height: rewinds to the nearest blockheight to the one given as argument.
/// -transaction: rewinds to the nearest height based on the anchor of the provided transaction.
public enum RewindPolicy {
case birthday
case height(blockheight: BlockHeight)
case transaction(_ transaction: TransactionEntity)
case quick
}
2021-06-14 16:38:05 -07:00
2021-09-17 06:49:58 -07:00
extension SyncStatus {
// swiftlint:disable cyclomatic_complexity
2021-06-14 16:38:05 -07:00
public static func == (lhs: SyncStatus, rhs: SyncStatus) -> Bool {
switch lhs {
case .unprepared:
if case .unprepared = rhs {
return true
} else {
return false
}
case .disconnected:
if case .disconnected = rhs {
return true
} else {
return false
}
case .downloading:
if case .downloading = rhs {
return true
} else {
return false
}
case .validating:
if case .validating = rhs {
return true
} else {
return false
}
case .scanning:
if case .scanning = rhs {
return true
} else {
return false
}
case .enhancing:
if case .enhancing = rhs {
return true
} else {
return false
}
case .fetching:
if case .fetching = rhs {
return true
} else {
return false
}
case .synced:
if case .synced = rhs {
return true
} else {
return false
}
case .stopped:
if case .stopped = rhs {
return true
} else {
return false
}
case .error:
if case .error = rhs {
return true
} else {
return false
}
}
}
}
extension SyncStatus {
init(_ blockProcessorProgress: CompactBlockProgress) {
switch blockProcessorProgress {
2021-09-17 06:49:58 -07:00
case .download(let progressReport):
self = SyncStatus.downloading(progressReport)
case .validate:
self = .validating
case .scan(let progressReport):
self = .scanning(progressReport)
case .enhance(let enhancingReport):
self = .enhancing(enhancingReport)
case .fetch:
self = .fetching
}
}
}