358 lines
14 KiB
Swift
358 lines
14 KiB
Swift
//
|
|
// 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) // ZcashLightClientKit.SynchronizerError error 0.
|
|
case notPrepared // ZcashLightClientKit.SynchronizerError error 9.
|
|
case syncFailed // ZcashLightClientKit.SynchronizerError error 10.
|
|
case connectionFailed(message: Error) // ZcashLightClientKit.SynchronizerError error 1.
|
|
case generalError(message: String) // ZcashLightClientKit.SynchronizerError error 2.
|
|
case maxRetryAttemptsReached(attempts: Int) // ZcashLightClientKit.SynchronizerError error 3.
|
|
case connectionError(status: Int, message: String) // ZcashLightClientKit.SynchronizerError error 4.
|
|
case networkTimeout // ZcashLightClientKit.SynchronizerError error 11.
|
|
case uncategorized(underlyingError: Error) // ZcashLightClientKit.SynchronizerError error 5.
|
|
case criticalError // ZcashLightClientKit.SynchronizerError error 12.
|
|
case parameterMissing(underlyingError: Error) // ZcashLightClientKit.SynchronizerError error 6.
|
|
case rewindError(underlyingError: Error) // ZcashLightClientKit.SynchronizerError error 7.
|
|
case rewindErrorUnknownArchorHeight // ZcashLightClientKit.SynchronizerError error 13.
|
|
case invalidAccount // ZcashLightClientKit.SynchronizerError error 14.
|
|
case lightwalletdValidationFailed(underlyingError: Error) // ZcashLightClientKit.SynchronizerError error 8.
|
|
}
|
|
|
|
public enum ShieldFundsError: Error {
|
|
case noUTXOFound
|
|
case insuficientTransparentFunds
|
|
case shieldingFailed(underlyingError: Error)
|
|
}
|
|
|
|
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
|
|
public enum ConnectionState {
|
|
/// not in use
|
|
case idle
|
|
|
|
/// there's a connection being attempted from a non error state
|
|
case connecting
|
|
|
|
/// connection is established, ready to use or in use
|
|
case online
|
|
|
|
/// the connection is being re-established after losing it temporarily
|
|
case reconnecting
|
|
|
|
/// the connection has been closed
|
|
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
|
|
var status: SyncStatus { get }
|
|
|
|
/// reflects current connection state to LightwalletEndpoint
|
|
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
|
|
func prepare(with seed: [UInt8]?) async throws -> Initializer.InitializationResult
|
|
|
|
///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
|
|
func getSaplingAddress(accountIndex: Int) async -> SaplingAddress?
|
|
|
|
/// 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
|
|
func getUnifiedAddress(accountIndex: Int) async -> 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
|
|
func getTransparentAddress(accountIndex: Int) async -> TransparentAddress?
|
|
|
|
/// Sends zatoshi.
|
|
/// - Parameter spendingKey: the `UnifiedSpendingKey` that allows spends to occur.
|
|
/// - Parameter zatoshi: the amount to send in Zatoshi.
|
|
/// - Parameter toAddress: the recipient's address.
|
|
/// - Parameter memo: an `Optional<Memo>`with the memo to include as part of the transaction. send `nil` when sending to transparent receivers otherwise the function will throw an error
|
|
// swiftlint:disable:next function_parameter_count
|
|
func sendToAddress(
|
|
spendingKey: UnifiedSpendingKey,
|
|
zatoshi: Zatoshi,
|
|
toAddress: Recipient,
|
|
memo: Memo?
|
|
) async throws -> PendingTransactionEntity
|
|
|
|
/// Shields transparent funds from the given private key into the best shielded pool of the account associated to the given `UnifiedSpendingKey`.
|
|
/// - Parameter spendingKey: the `UnifiedSpendingKey` that allows to spend transparent funds
|
|
/// - Parameter memo: the optional memo to include as part of the transaction.
|
|
func shieldFunds(
|
|
spendingKey: UnifiedSpendingKey,
|
|
memo: Memo
|
|
) async throws -> PendingTransactionEntity
|
|
|
|
/// 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
|
|
func latestDownloadedHeight() async throws -> BlockHeight
|
|
|
|
|
|
/// Returns the latest block height from the provided Lightwallet endpoint
|
|
func latestHeight(result: @escaping (Result<BlockHeight, Error>) -> Void)
|
|
|
|
/// Returns the latest block height from the provided Lightwallet endpoint
|
|
/// Blocking
|
|
func latestHeight() async throws -> BlockHeight
|
|
|
|
|
|
/// Returns the latests UTXOs for the given address from the specified height on
|
|
func refreshUTXOs(address: TransparentAddress, from height: BlockHeight) async throws -> RefreshedUTXOs
|
|
|
|
/// Returns the last stored transparent balance
|
|
func getTransparentBalance(accountIndex: Int) async throws -> WalletBalance
|
|
|
|
/// 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) async throws
|
|
}
|
|
|
|
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.
|
|
case unprepared
|
|
|
|
/// Indicates that this Synchronizer is actively downloading new blocks from the server.
|
|
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.
|
|
case validating
|
|
|
|
/// Indicates that this Synchronizer is actively scanning new valid blocks that were
|
|
/// downloaded from the server.
|
|
case scanning(_ progress: BlockProgress)
|
|
|
|
/// Indicates that this Synchronizer is actively enhancing newly scanned blocks
|
|
/// with additional transaction details, fetched from the server.
|
|
case enhancing(_ progress: EnhancementProgress)
|
|
|
|
/// fetches the transparent balance and stores it locally
|
|
case fetching
|
|
|
|
/// 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
|
|
|
|
/// Indicates that [stop] has been called on this Synchronizer and it will no longer be used.
|
|
case stopped
|
|
|
|
/// Indicates that this Synchronizer is disconnected from its lightwalletd server.
|
|
/// When set, a UI element may want to turn red.
|
|
case disconnected
|
|
|
|
case error(_ error: Error)
|
|
|
|
public var isSyncing: Bool {
|
|
switch self {
|
|
case .downloading, .validating, .scanning, .enhancing, .fetching:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
public var isSynced: Bool {
|
|
switch self {
|
|
case .synced: return true
|
|
default: return false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
extension SyncStatus {
|
|
// swiftlint:disable cyclomatic_complexity
|
|
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 {
|
|
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
|
|
}
|
|
}
|
|
}
|