ZcashLightClientKit/Sources/ZcashLightClientKit/Initializer.swift

434 lines
17 KiB
Swift

//
// Initializer.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 13/09/2019.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Foundation
/**
Represents a lightwallet instance endpoint to connect to
*/
public struct LightWalletEndpoint {
public let host: String
public let port: Int
public let secure: Bool
public let singleCallTimeoutInMillis: Int64
public let 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
}
}
/// This contains URLs from which can the SDK fetch files that contain sapling parameters.
/// Use `SaplingParamsSourceURL.default` when initilizing the SDK.
public struct SaplingParamsSourceURL {
public let spendParamFileURL: URL
public let outputParamFileURL: URL
public static var `default`: SaplingParamsSourceURL {
return SaplingParamsSourceURL(spendParamFileURL: ZcashSDK.spendParamFileURL, outputParamFileURL: ZcashSDK.outputParamFileURL)
}
}
/// This identifies different instances of the synchronizer. It is usefull when the client app wants to support multiple wallets (with different
/// seeds) in one app. If the client app support only one wallet then it doesn't have to care about alias atall.
///
/// When custom alias is used to create instance of the synchronizer then paths to all resources (databases, storages...) are updated accordingly to
/// be sure that each instance is using unique paths to resources.
///
/// Custom alias identifiers shouldn't contain any confidential information because it may be logged. It also should have a reasonable length and
/// form. It will be part of the paths to the files (databases, storage...)
///
/// IMPORTANT: Always use `default` alias for one of the instances of the synchronizer.
public enum ZcashSynchronizerAlias: Hashable {
case `default`
case custom(String)
}
extension ZcashSynchronizerAlias: CustomStringConvertible {
public var description: String {
switch self {
case .`default`:
return "default"
case let .custom(alias):
return "c_\(alias)"
}
}
}
/**
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.
*/
// swiftlint:disable:next type_body_length
public class Initializer {
struct URLs {
let fsBlockDbRoot: URL
let dataDbURL: URL
let spendParamsURL: URL
let outputParamsURL: URL
}
public enum InitializationResult {
case success
case seedRequired
}
public enum LoggingPolicy {
case `default`(OSLogger.LogLevel)
case custom(Logger)
case noLogging
}
// This is used to uniquely identify instance of the SDKSynchronizer. It's used when checking if the Alias is already used or not.
let id = UUID()
let container: DIContainer
let alias: ZcashSynchronizerAlias
let endpoint: LightWalletEndpoint
let fsBlockDbRoot: URL
let dataDbURL: URL
let spendParamsURL: URL
let outputParamsURL: URL
let saplingParamsSourceURL: SaplingParamsSourceURL
let lightWalletService: LightWalletService
let transactionRepository: TransactionRepository
let accountRepository: AccountRepository
let storage: CompactBlockRepository
let blockDownloaderService: BlockDownloaderService
let network: ZcashNetwork
let logger: Logger
let rustBackend: ZcashRustBackendWelding
/// The effective birthday of the wallet based on the height provided when initializing and the checkpoints available on this SDK.
///
/// This contains valid value only after `initialize` function is called.
public private(set) var walletBirthday: BlockHeight
/// The purpose of this to migrate from cacheDb to fsBlockDb
private let cacheDbURL: URL?
/// Error that can be created when updating URLs according to alias. If this error is created then it is thrown from `SDKSynchronizer.prepare()`
/// or `SDKSynchronizer.wipe()`.
var urlsParsingError: ZcashError?
/// Constructs the Initializer and migrates an old cacheDb to the new file system block cache if a `cacheDbURL` is provided.
/// - Parameters:
/// - cacheDbURL: previous location of the cacheDb. If you don't know what a cacheDb is and you are adopting this SDK for the first time then
/// just pass `nil` here.
/// - fsBlockDbRoot: location of the compact blocks cache
/// - dataDbURL: Location of the data db
/// - 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?,
fsBlockDbRoot: URL,
dataDbURL: URL,
endpoint: LightWalletEndpoint,
network: ZcashNetwork,
spendParamsURL: URL,
outputParamsURL: URL,
saplingParamsSourceURL: SaplingParamsSourceURL,
alias: ZcashSynchronizerAlias = .default,
loggingPolicy: LoggingPolicy = .default(.debug)
) {
let container = DIContainer()
// It's not possible to fail from constructor. Technically it's possible but it can be pain for the client apps to handle errors thrown
// from constructor. So `parsingError` is just stored in initializer and `SDKSynchronizer.prepare()` throw this error if it exists.
let (updatedURLs, parsingError) = Self.setup(
container: container,
cacheDbURL: cacheDbURL,
fsBlockDbRoot: fsBlockDbRoot,
dataDbURL: dataDbURL,
endpoint: endpoint,
network: network,
spendParamsURL: spendParamsURL,
outputParamsURL: outputParamsURL,
saplingParamsSourceURL: saplingParamsSourceURL,
alias: alias,
loggingPolicy: loggingPolicy
)
self.init(
container: container,
cacheDbURL: cacheDbURL,
urls: updatedURLs,
endpoint: endpoint,
network: network,
saplingParamsSourceURL: saplingParamsSourceURL,
alias: alias,
urlsParsingError: parsingError,
loggingPolicy: loggingPolicy
)
}
/// Internal for dependency injection purposes.
convenience init(
container: DIContainer,
cacheDbURL: URL?,
fsBlockDbRoot: URL,
dataDbURL: URL,
endpoint: LightWalletEndpoint,
network: ZcashNetwork,
spendParamsURL: URL,
outputParamsURL: URL,
saplingParamsSourceURL: SaplingParamsSourceURL,
alias: ZcashSynchronizerAlias = .default,
loggingPolicy: LoggingPolicy = .default(.debug)
) {
// It's not possible to fail from constructor. Technically it's possible but it can be pain for the client apps to handle errors thrown
// from constructor. So `parsingError` is just stored in initializer and `SDKSynchronizer.prepare()` throw this error if it exists.
let (updatedURLs, parsingError) = Self.setup(
container: container,
cacheDbURL: cacheDbURL,
fsBlockDbRoot: fsBlockDbRoot,
dataDbURL: dataDbURL,
endpoint: endpoint,
network: network,
spendParamsURL: spendParamsURL,
outputParamsURL: outputParamsURL,
saplingParamsSourceURL: saplingParamsSourceURL,
alias: alias,
loggingPolicy: loggingPolicy
)
self.init(
container: container,
cacheDbURL: cacheDbURL,
urls: updatedURLs,
endpoint: endpoint,
network: network,
saplingParamsSourceURL: saplingParamsSourceURL,
alias: alias,
urlsParsingError: parsingError,
loggingPolicy: loggingPolicy
)
}
private init(
container: DIContainer,
cacheDbURL: URL?,
urls: URLs,
endpoint: LightWalletEndpoint,
network: ZcashNetwork,
saplingParamsSourceURL: SaplingParamsSourceURL,
alias: ZcashSynchronizerAlias,
urlsParsingError: ZcashError?,
loggingPolicy: LoggingPolicy = .default(.debug)
) {
self.container = container
self.cacheDbURL = cacheDbURL
self.rustBackend = container.resolve(ZcashRustBackendWelding.self)
self.fsBlockDbRoot = urls.fsBlockDbRoot
self.dataDbURL = urls.dataDbURL
self.endpoint = endpoint
self.spendParamsURL = urls.spendParamsURL
self.outputParamsURL = urls.outputParamsURL
self.saplingParamsSourceURL = saplingParamsSourceURL
self.alias = alias
self.lightWalletService = container.resolve(LightWalletService.self)
self.transactionRepository = container.resolve(TransactionRepository.self)
self.accountRepository = AccountRepositoryBuilder.build(
dataDbURL: urls.dataDbURL,
readOnly: true,
caching: true,
logger: container.resolve(Logger.self)
)
self.storage = container.resolve(CompactBlockRepository.self)
self.blockDownloaderService = container.resolve(BlockDownloaderService.self)
self.network = network
self.walletBirthday = Checkpoint.birthday(with: 0, network: network).height
self.urlsParsingError = urlsParsingError
self.logger = container.resolve(Logger.self)
}
private static func makeLightWalletServiceFactory(endpoint: LightWalletEndpoint) -> LightWalletServiceFactory {
return LightWalletServiceFactory(endpoint: endpoint)
}
// swiftlint:disable:next function_parameter_count
private static func setup(
container: DIContainer,
cacheDbURL: URL?,
fsBlockDbRoot: URL,
dataDbURL: URL,
endpoint: LightWalletEndpoint,
network: ZcashNetwork,
spendParamsURL: URL,
outputParamsURL: URL,
saplingParamsSourceURL: SaplingParamsSourceURL,
alias: ZcashSynchronizerAlias,
loggingPolicy: LoggingPolicy = .default(.debug)
) -> (URLs, ZcashError?) {
let urls = URLs(
fsBlockDbRoot: fsBlockDbRoot,
dataDbURL: dataDbURL,
spendParamsURL: spendParamsURL,
outputParamsURL: outputParamsURL
)
// It's not possible to fail from constructor. Technically it's possible but it can be pain for the client apps to handle errors thrown
// from constructor. So `parsingError` is just stored in initializer and `SDKSynchronizer.prepare()` throw this error if it exists.
let (updatedURLs, parsingError) = Self.tryToUpdateURLs(with: alias, urls: urls)
Dependencies.setup(
in: container,
urls: updatedURLs,
alias: alias,
networkType: network.networkType,
endpoint: endpoint,
loggingPolicy: loggingPolicy
)
return (updatedURLs, parsingError)
}
/// Try to update URLs with `alias`.
///
/// If the `default` alias is used then the URLs are changed at all.
/// If the `custom("anotherInstance")` is used then last path component or the URL is updated like this:
/// - /some/path/to.file -> /some/path/c_anotherInstance_to.file
/// - /some/path/to/directory -> /some/path/to/c_anotherInstance_directory
///
/// If any of the URLs can't be parsed then returned error isn't nil.
static func tryToUpdateURLs(
with alias: ZcashSynchronizerAlias,
urls: URLs
) -> (URLs, ZcashError?) {
let updatedURLsResult = Self.updateURLs(with: alias, urls: urls)
let parsingError: ZcashError?
let updatedURLs: URLs
switch updatedURLsResult {
case let .success(updated):
parsingError = nil
updatedURLs = updated
case let .failure(error):
parsingError = error
// When failure happens just use original URLs because something must be used. But this shouldn't be a problem because
// `SDKSynchronizer.prepare()` handles this error. And the SDK won't work if it isn't switched from `unprepared` state.
updatedURLs = urls
}
return (updatedURLs, parsingError)
}
private static func updateURLs(
with alias: ZcashSynchronizerAlias,
urls: URLs
) -> Result<URLs, ZcashError> {
guard let updatedFsBlockDbRoot = urls.fsBlockDbRoot.updateLastPathComponent(with: alias) else {
return .failure(.initializerCantUpdateURLWithAlias(urls.fsBlockDbRoot))
}
guard let updatedDataDbURL = urls.dataDbURL.updateLastPathComponent(with: alias) else {
return .failure(.initializerCantUpdateURLWithAlias(urls.dataDbURL))
}
guard let updatedSpendParamsURL = urls.spendParamsURL.updateLastPathComponent(with: alias) else {
return .failure(.initializerCantUpdateURLWithAlias(urls.spendParamsURL))
}
guard let updateOutputParamsURL = urls.outputParamsURL.updateLastPathComponent(with: alias) else {
return .failure(.initializerCantUpdateURLWithAlias(urls.outputParamsURL))
}
return .success(
URLs(
fsBlockDbRoot: updatedFsBlockDbRoot,
dataDbURL: updatedDataDbURL,
spendParamsURL: updatedSpendParamsURL,
outputParamsURL: updateOutputParamsURL
)
)
}
/// 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.
func initialize(with seed: [UInt8]?, viewingKeys: [UnifiedFullViewingKey], walletBirthday: BlockHeight) async throws -> InitializationResult {
try await storage.create()
if case .seedRequired = try await rustBackend.initDataDb(seed: seed) {
return .seedRequired
}
let checkpoint = Checkpoint.birthday(with: walletBirthday, network: network)
do {
try await rustBackend.initBlocksTable(
height: Int32(checkpoint.height),
hash: checkpoint.hash,
time: checkpoint.time,
saplingTree: checkpoint.saplingTree
)
} catch ZcashError.rustInitBlocksTableDataDbNotEmpty {
// this is fine
} catch {
throw error
}
self.walletBirthday = checkpoint.height
do {
try await rustBackend.initAccountsTable(ufvks: viewingKeys)
} catch ZcashError.rustInitAccountsTableDataDbNotEmpty {
// this is fine
} catch {
throw error
}
return .success
}
/**
checks if the provided address is a valid sapling address
*/
public func isValidSaplingAddress(_ address: String) -> Bool {
DerivationTool(networkType: network.networkType).isValidSaplingAddress(address)
}
/**
checks if the provided address is a transparent zAddress
*/
public func isValidTransparentAddress(_ address: String) -> Bool {
DerivationTool(networkType: network.networkType).isValidTransparentAddress(address)
}
}