ZcashLightClientKit/Sources/ZcashLightClientKit/Initializer.swift

387 lines
14 KiB
Swift

//
// Initializer.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 13/09/2019.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
/**
Wrapper for the Rust backend. This class basically represents all the Rust-wallet
capabilities and the supporting data required to exercise those abilities.
*/
public enum InitializerError: Error {
case cacheDbInitFailed(Error)
case dataDbInitFailed(Error)
case accountInitFailed(Error)
case invalidViewingKey(key: String)
}
/**
Represents a lightwallet instance endpoint to connect to
*/
public struct LightWalletEndpoint {
public var host: String
public var port: Int
public var secure: Bool
public var singleCallTimeoutInMillis: Int64
public var streamingCallTimeoutInMillis: Int64
/**
initializes a LightWalletEndpoint
- Parameters:
- address: a String containing the host address
- port: string with the port of the host address
- secure: true if connecting through TLS. Default value is true
- singleCallTimeoutInMillis: timeout for single calls in Milliseconds. Default 30 seconds
- streamingCallTimeoutInMillis: timeout for streaming calls in Milliseconds. Default 100 seconds
*/
public init(
address: String,
port: Int,
secure: Bool = true,
singleCallTimeoutInMillis: Int64 = 30000,
streamingCallTimeoutInMillis: Int64 = 100000
) {
self.host = address
self.port = port
self.secure = secure
self.singleCallTimeoutInMillis = singleCallTimeoutInMillis
self.streamingCallTimeoutInMillis = streamingCallTimeoutInMillis
}
}
/**
Wrapper for all the Rust backend functionality that does not involve processing blocks. This
class initializes the Rust backend and the supporting data required to exercise those abilities.
The [cash.z.wallet.sdk.block.CompactBlockProcessor] handles all the remaining Rust backend
functionality, related to processing blocks.
*/
public class Initializer {
public enum InitializationResult {
case success
case seedRequired
}
private(set) var rustBackend: ZcashRustBackendWelding.Type
private(set) var alias: String
private(set) var endpoint: LightWalletEndpoint
private var lowerBoundHeight: BlockHeight
private(set) var cacheDbURL: URL
private(set) var dataDbURL: URL
private(set) var pendingDbURL: URL
private(set) var spendParamsURL: URL
private(set) var outputParamsURL: URL
private(set) var lightWalletService: LightWalletService
private(set) var transactionRepository: TransactionRepository
private(set) var accountRepository: AccountRepository
private(set) var storage: CompactBlockStorage
private(set) var downloader: CompactBlockDownloader
private(set) var network: ZcashNetwork
private(set) public var viewingKeys: [UnifiedFullViewingKey]
/// The effective birthday of the wallet based on the height provided when initializing
/// and the checkpoints available on this SDK
private(set) public var walletBirthday: BlockHeight
/**
Constructs the Initializer
- Parameters:
- cacheDbURL: location of the compact blocks cache db
- dataDbURL: Location of the data db
- pendingDbURL: location of the pending transactions database
- endpoint: the endpoint representing the lightwalletd instance you want to point to
- spendParamsURL: location of the spend parameters
- outputParamsURL: location of the output parameters
*/
convenience public init (
cacheDbURL: URL,
dataDbURL: URL,
pendingDbURL: URL,
endpoint: LightWalletEndpoint,
network: ZcashNetwork,
spendParamsURL: URL,
outputParamsURL: URL,
viewingKeys: [UnifiedFullViewingKey],
walletBirthday: BlockHeight,
alias: String = "",
loggerProxy: Logger? = nil
) {
let lwdService = LightWalletGRPCService(endpoint: endpoint)
self.init(
rustBackend: ZcashRustBackend.self,
lowerBoundHeight: walletBirthday,
network: network,
cacheDbURL: cacheDbURL,
dataDbURL: dataDbURL,
pendingDbURL: pendingDbURL,
endpoint: endpoint,
service: lwdService,
repository: TransactionRepositoryBuilder.build(dataDbURL: dataDbURL),
accountRepository: AccountRepositoryBuilder.build(
dataDbURL: dataDbURL,
readOnly: true,
caching: true
),
storage: CompactBlockStorage(url: cacheDbURL, readonly: false),
spendParamsURL: spendParamsURL,
outputParamsURL: outputParamsURL,
viewingKeys: viewingKeys,
walletBirthday: walletBirthday,
alias: alias,
loggerProxy: loggerProxy
)
}
/**
Internal for dependency injection purposes
*/
init(
rustBackend: ZcashRustBackendWelding.Type,
lowerBoundHeight: BlockHeight,
network: ZcashNetwork,
cacheDbURL: URL,
dataDbURL: URL,
pendingDbURL: URL,
endpoint: LightWalletEndpoint,
service: LightWalletService,
repository: TransactionRepository,
accountRepository: AccountRepository,
storage: CompactBlockStorage,
spendParamsURL: URL,
outputParamsURL: URL,
viewingKeys: [UnifiedFullViewingKey],
walletBirthday: BlockHeight,
alias: String = "",
loggerProxy: Logger? = nil
) {
logger = loggerProxy
self.rustBackend = rustBackend
self.lowerBoundHeight = lowerBoundHeight
self.cacheDbURL = cacheDbURL
self.dataDbURL = dataDbURL
self.pendingDbURL = pendingDbURL
self.endpoint = endpoint
self.spendParamsURL = spendParamsURL
self.outputParamsURL = outputParamsURL
self.alias = alias
self.lightWalletService = service
self.transactionRepository = repository
self.accountRepository = accountRepository
self.storage = storage
self.downloader = CompactBlockDownloader(service: service, storage: storage)
self.viewingKeys = viewingKeys
self.walletBirthday = walletBirthday
self.network = network
}
/// Initialize the wallet. The ZIP-32 seed bytes can optionally be passed to perform
/// database migrations. most of the times the seed won't be needed. If they do and are
/// not provided this will fail with `InitializationResult.seedRequired`. It could
/// be the case that this method is invoked by a wallet that does not contain the seed phrase
/// and is view-only, or by a wallet that does have the seed but the process does not have the
/// consent of the OS to fetch the keys from the secure storage, like on background tasks.
///
/// 'cache.db' and 'data.db' files are created by this function (if they
/// do not already exist). These files can be given a prefix for scenarios where multiple wallets
///
/// - Parameter seed: ZIP-32 Seed bytes for the wallet that will be initialized
/// - Throws: `InitializerError.dataDbInitFailed` if the creation of the dataDb fails
/// `InitializerError.accountInitFailed` if the account table can't be initialized.
public func initialize(with seed: [UInt8]?) throws -> InitializationResult {
do {
try storage.createTable()
} catch {
throw InitializerError.cacheDbInitFailed(error)
}
do {
if case .seedRequired = try rustBackend.initDataDb(dbData: dataDbURL, seed: seed, networkType: network.networkType) {
return .seedRequired
}
} catch {
throw InitializerError.dataDbInitFailed(error)
}
let checkpoint = Checkpoint.birthday(with: self.walletBirthday, network: network)
do {
try rustBackend.initBlocksTable(
dbData: dataDbURL,
height: Int32(checkpoint.height),
hash: checkpoint.hash,
time: checkpoint.time,
saplingTree: checkpoint.saplingTree,
networkType: network.networkType
)
} catch RustWeldingError.dataDbNotEmpty {
// this is fine
} catch {
throw InitializerError.dataDbInitFailed(error)
}
self.walletBirthday = checkpoint.height
let lastDownloaded = (try? downloader.storage.latestHeight()) ?? walletBirthday
// resume from last downloaded block
lowerBoundHeight = max(walletBirthday, lastDownloaded)
do {
try rustBackend.initAccountsTable(
dbData: dataDbURL,
ufvks: viewingKeys,
networkType: network.networkType
)
} catch RustWeldingError.dataDbNotEmpty {
// this is fine
} catch RustWeldingError.malformedStringInput {
throw RustWeldingError.malformedStringInput
} catch {
throw InitializerError.accountInitFailed(error)
}
let migrationManager = MigrationManager(
cacheDbConnection: SimpleConnectionProvider(path: cacheDbURL.path),
pendingDbConnection: SimpleConnectionProvider(path: pendingDbURL.path),
networkType: self.network.networkType
)
try migrationManager.performMigration()
return .success
}
/// get (unverified) balance from the given account index
/// - Parameter account: the index of the account
@available(*, deprecated, message: "This function will be removed soon. Use the function returning `Zatoshi` instead")
public func getBalance(account index: Int = 0) -> Int64 {
rustBackend.getBalance(dbData: dataDbURL, account: Int32(index), networkType: network.networkType)
}
/// get (unverified) balance from the given account index
/// - Parameter account: the index of the account
/// - Returns: balance in `Zatoshi`
public func getBalance(account index: Int = 0) -> Zatoshi {
Zatoshi(
rustBackend.getBalance(
dbData: dataDbURL,
account: Int32(index),
networkType: network.networkType
)
)
}
/// get verified balance from the given account index
/// - Parameter account: the index of the account
@available(*, deprecated, message: "This function will be removed soon. Use the one returning `Zatoshi` instead")
public func getVerifiedBalance(account index: Int = 0) -> Int64 {
rustBackend.getVerifiedBalance(dbData: dataDbURL, account: Int32(index), networkType: network.networkType)
}
/// get verified balance from the given account index
/// - Parameter account: the index of the account
/// - Returns: balance in `Zatoshi`
public func getVerifiedBalance(account index: Int = 0) -> Zatoshi {
Zatoshi(
rustBackend.getVerifiedBalance(
dbData: dataDbURL,
account: Int32(index),
networkType: network.networkType
)
)
}
/**
checks if the provided address is a valid sapling address
*/
public func isValidSaplingAddress(_ address: String) -> Bool {
rustBackend.isValidSaplingAddress(address, networkType: network.networkType)
}
/**
checks if the provided address is a transparent zAddress
*/
public func isValidTransparentAddress(_ address: String) -> Bool {
rustBackend.isValidTransparentAddress(address, networkType: network.networkType)
}
func isSpendParameterPresent() -> Bool {
FileManager.default.isReadableFile(atPath: self.spendParamsURL.path)
}
func isOutputParameterPresent() -> Bool {
FileManager.default.isReadableFile(atPath: self.outputParamsURL.path)
}
@discardableResult
func downloadParametersIfNeeded() async throws -> Bool {
let spendParameterPresent = isSpendParameterPresent()
let outputParameterPresent = isOutputParameterPresent()
if spendParameterPresent && outputParameterPresent {
return true
}
let outputURL = self.outputParamsURL
let spendURL = self.spendParamsURL
do {
if !outputParameterPresent && !spendParameterPresent {
async let outputURLRequest = SaplingParameterDownloader.downloadOutputParameter(outputURL)
async let spendURLRequest = SaplingParameterDownloader.downloadSpendParameter(spendURL)
_ = try await [outputURLRequest, spendURLRequest]
return false
} else if !outputParameterPresent {
try await SaplingParameterDownloader.downloadOutputParameter(outputURL)
return false
} else if !spendParameterPresent {
try await SaplingParameterDownloader.downloadSpendParameter(spendURL)
return false
}
} catch {
throw error
}
return true
}
}
enum CompactBlockProcessorBuilder {
// swiftlint:disable:next function_parameter_count
static func buildProcessor(
configuration: CompactBlockProcessor.Configuration,
service: LightWalletService,
storage: CompactBlockStorage,
transactionRepository: TransactionRepository,
accountRepository: AccountRepository,
backend: ZcashRustBackendWelding.Type
) -> CompactBlockProcessor {
return CompactBlockProcessor(
service: service,
storage: storage,
backend: backend,
config: configuration,
repository: transactionRepository,
accountRepository: accountRepository
)
}
}
extension InitializerError: LocalizedError {
public var errorDescription: String? {
switch self {
case .invalidViewingKey:
return "The provided viewing key is invalid"
case .cacheDbInitFailed(let error):
return "cacheDb Init failed with error: \(error.localizedDescription)"
case .dataDbInitFailed(let error):
return "dataDb init failed with error: \(error.localizedDescription)"
case .accountInitFailed(let error):
return "account table init failed with error: \(error.localizedDescription)"
}
}
}