diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Base.lproj/Main.storyboard b/Example/ZcashLightClientSample/ZcashLightClientSample/Base.lproj/Main.storyboard index e99fe7ba..9e1cf82a 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Base.lproj/Main.storyboard +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Base.lproj/Main.storyboard @@ -824,13 +824,19 @@ + - + + + + + + + - + @@ -1452,6 +1475,7 @@ + diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Get Address/GetAddressViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Get Address/GetAddressViewController.swift index b077d004..6d5137cd 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Get Address/GetAddressViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Get Address/GetAddressViewController.swift @@ -9,7 +9,8 @@ import UIKit import ZcashLightClientKit class GetAddressViewController: UIViewController { - @IBOutlet weak var addressLabel: UILabel! + @IBOutlet weak var zAddressLabel: UILabel! + @IBOutlet weak var tAddressLabel: UILabel! @IBOutlet weak var spendingKeyLabel: UILabel! // THIS SHOULD BE SUPER SECRET!!!!! override func viewDidLoad() { @@ -17,11 +18,14 @@ class GetAddressViewController: UIViewController { // Do any additional setup after loading the view. - addressLabel.text = legibleAddresses() ?? "No Addresses found" + zAddressLabel.text = (try? DerivationTool.default.deriveShieldedAddress(seed: DemoAppConfig.seed, accountIndex: 0)) ?? "No Addresses found" + tAddressLabel.text = (try? DerivationTool.default.deriveTransparentAddress(seed: DemoAppConfig.seed)) ?? "could not derive t-address" spendingKeyLabel.text = SampleStorage.shared.privateKey ?? "No Spending Key found" - addressLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(addressTapped(_:)))) - addressLabel.isUserInteractionEnabled = true + zAddressLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(addressTapped(_:)))) + zAddressLabel.isUserInteractionEnabled = true + tAddressLabel.isUserInteractionEnabled = true + tAddressLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tAddressTapped(_:)))) spendingKeyLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(spendingKeyTapped(_:)))) spendingKeyLabel.isUserInteractionEnabled = true loggerProxy.info("Address: \(String(describing: Initializer.shared.getAddress()))") @@ -40,10 +44,6 @@ class GetAddressViewController: UIViewController { } */ - func legibleAddresses() -> String? { - Initializer.shared.getAddress() - } - @IBAction func spendingKeyTapped(_ gesture: UIGestureRecognizer) { guard let key = SampleStorage.shared.privateKey else { loggerProxy.warn("nothing to copy") @@ -60,7 +60,15 @@ class GetAddressViewController: UIViewController { @IBAction func addressTapped(_ gesture: UIGestureRecognizer) { loggerProxy.event("copied to clipboard") - UIPasteboard.general.string = legibleAddresses() + UIPasteboard.general.string = try? DerivationTool.default.deriveShieldedAddress(seed: DemoAppConfig.seed, accountIndex: 0) + let alert = UIAlertController(title: "", message: "Address Copied to clipboard", preferredStyle: UIAlertController.Style.alert) + alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + + @IBAction func tAddressTapped(_ gesture: UIGestureRecognizer) { + loggerProxy.event("copied to clipboard") + UIPasteboard.general.string = try? DerivationTool.default.deriveTransparentAddress(seed: DemoAppConfig.seed) let alert = UIAlertController(title: "", message: "Address Copied to clipboard", preferredStyle: UIAlertController.Style.alert) alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)) self.present(alert, animated: true, completion: nil) diff --git a/Example/ZcashLightClientSample/ZcashLightClientSample/Get UTXOs/GetUTXOsViewController.swift b/Example/ZcashLightClientSample/ZcashLightClientSample/Get UTXOs/GetUTXOsViewController.swift index 03d577cb..a70ce971 100644 --- a/Example/ZcashLightClientSample/ZcashLightClientSample/Get UTXOs/GetUTXOsViewController.swift +++ b/Example/ZcashLightClientSample/ZcashLightClientSample/Get UTXOs/GetUTXOsViewController.swift @@ -12,10 +12,10 @@ import KRProgressHUD class GetUTXOsViewController: UIViewController { @IBOutlet weak var tAddressField: UITextField! @IBOutlet weak var getButton: UIButton! + @IBOutlet weak var getFromCache: UIButton! @IBOutlet weak var validAddressLabel: UILabel! @IBOutlet weak var messageLabel: UILabel! - var service: LightWalletGRPCService = LightWalletGRPCService(endpoint: DemoAppConfig.endpoint) override func viewDidLoad() { super.viewDidLoad() @@ -31,15 +31,17 @@ class GetUTXOsViewController: UIViewController { self.validAddressLabel.textColor = valid ? UIColor.systemGreen : UIColor.systemRed self.getButton.isEnabled = valid + self.getFromCache.isEnabled = valid } @IBAction func getButtonTapped(_ sender: Any) { guard Initializer.shared.isValidTransparentAddress(tAddressField.text ?? ""), let tAddr = tAddressField.text else { + self.messageLabel.text = "Invalid t-Address" return } KRProgressHUD.showMessage("fetching") - service.fetchUTXOs(for: tAddr) { [weak self] (result) in + AppDelegate.shared.sharedSynchronizer.latestUTXOs(address: tAddr) { (result) in DispatchQueue.main.async { [weak self] in KRProgressHUD.dismiss() switch result { @@ -53,6 +55,21 @@ class GetUTXOsViewController: UIViewController { } } + @IBAction func getFromCacheTapped(_ sender: Any) { + guard Initializer.shared.isValidTransparentAddress(tAddressField.text ?? ""), + let tAddr = tAddressField.text else { + self.messageLabel.text = "Invalid t-Address" + return + } + do { + let utxos = try AppDelegate.shared.sharedSynchronizer.cachedUTXOs(address: tAddr) + self.messageLabel.text = "found \(utxos.count) UTXOs for address \(tAddr) on cache" + } catch { + self.messageLabel.text = "Error \(error)" + } + } + + @IBAction func viewTapped(_ recognizer: UITapGestureRecognizer) { self.tAddressField.resignFirstResponder() } diff --git a/ZcashLightClientKit/Block/DatabaseStorage/StorageError.swift b/ZcashLightClientKit/Block/DatabaseStorage/StorageError.swift index f83861db..ef99c059 100644 --- a/ZcashLightClientKit/Block/DatabaseStorage/StorageError.swift +++ b/ZcashLightClientKit/Block/DatabaseStorage/StorageError.swift @@ -15,4 +15,5 @@ enum StorageError: Error { case operationFailed case updateFailed case malformedEntity(fields: [String]?) + case transactionFailed(underlyingError: Error) } diff --git a/ZcashLightClientKit/DAO/UnspentTransactionOutputDAO.swift b/ZcashLightClientKit/DAO/UnspentTransactionOutputDAO.swift deleted file mode 100644 index 8d6f4fc7..00000000 --- a/ZcashLightClientKit/DAO/UnspentTransactionOutputDAO.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// UnspentTransactionOutputDAO.swift -// ZcashLightClientKit -// -// Created by Francisco Gindre on 12/9/20. -// - -import Foundation - -struct UTXO: UnspentTransactionOutputEntity { - var address: String - - var txid: Data - - var index: Int32 - - var script: Data - - var valueZat: Int64 - - var height: UInt64 -} diff --git a/ZcashLightClientKit/DAO/UnspentTransactionOutputDao.swift b/ZcashLightClientKit/DAO/UnspentTransactionOutputDao.swift new file mode 100644 index 00000000..11bb400a --- /dev/null +++ b/ZcashLightClientKit/DAO/UnspentTransactionOutputDao.swift @@ -0,0 +1,132 @@ +// +// UnspentTransactionOutputDAO.swift +// ZcashLightClientKit +// +// Created by Francisco Gindre on 12/9/20. +// + +import Foundation + +struct UTXO: UnspentTransactionOutputEntity, Decodable, Encodable { + + enum CodingKeys: String, CodingKey { + case id + case address + case txid + case index + case script + case valueZat = "value_zat" + case height + } + + var id: Int? + + var address: String + + var txid: Data + + var index: Int + + var script: Data + + var valueZat: Int + + var height: Int + +} + +import SQLite +class UnspentTransactionOutputSQLDAO: UnspentTransactionOutputRepository { + + func store(utxos: [UnspentTransactionOutputEntity]) throws { + do { + + let db = try dbProvider.connection() + try dbProvider.connection().transaction { + for utxo in utxos.map({ (u) -> UTXO in + u as? UTXO ?? UTXO(id: nil, + address: u.address, + txid: u.txid, + index: Int(u.index), + script: u.script, + valueZat: u.valueZat, + height: u.height) + }) { + try db.run(table.insert(utxo)) + } + } + } catch { + throw StorageError.transactionFailed(underlyingError: error) + } + } + + func clearAll(address: String?) throws { + + if let tAddr = address { + do { + try dbProvider.connection().run(table.filter(TableColumns.address == tAddr).delete()) + } catch { + throw StorageError.operationFailed + } + } else { + do { + try dbProvider.connection().run(table.delete()) + } catch { + throw StorageError.operationFailed + } + } + } + + let table = Table("utxos") + + struct TableColumns { + static var id = Expression("id") + static var address = Expression("address") + static var txid = Expression("txid") + static var index = Expression("index") + static var script = Expression("script") + static var valueZat = Expression("value_zat") + static var height = Expression("height") + } + + var dbProvider: ConnectionProvider + + init (dbProvider: ConnectionProvider) { + self.dbProvider = dbProvider + } + + func createTableIfNeeded() throws { + let statement = table.create(ifNotExists: true) { t in + t.column(TableColumns.id, primaryKey: .autoincrement) + t.column(TableColumns.address) + t.column(TableColumns.txid) + t.column(TableColumns.index) + t.column(TableColumns.script) + t.column(TableColumns.valueZat) + t.column(TableColumns.height) + } + try dbProvider.connection().run(statement) + } + + func getAll(address: String?) throws -> [UnspentTransactionOutputEntity] { + if let tAddress = address { + let allTxs: [UTXO] = try dbProvider.connection().prepare(table.filter(TableColumns.address == tAddress)).map({ row in + try row.decode() + }) + return allTxs + } else { + let allTxs: [UTXO] = try dbProvider.connection().prepare(table).map({ row in + try row.decode() + }) + return allTxs + } + } +} + +class UTXORepositoryBuilder { + static func build(initializer: Initializer) throws -> UnspentTransactionOutputRepository { + let dao = UnspentTransactionOutputSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.pendingDbURL.path)) + try dao.createTableIfNeeded() + return dao + } +} diff --git a/ZcashLightClientKit/Entity/UnspentTransactionOutputEntity.swift b/ZcashLightClientKit/Entity/UnspentTransactionOutputEntity.swift index 5562e890..ceef0c4d 100644 --- a/ZcashLightClientKit/Entity/UnspentTransactionOutputEntity.swift +++ b/ZcashLightClientKit/Entity/UnspentTransactionOutputEntity.swift @@ -11,14 +11,14 @@ public protocol UnspentTransactionOutputEntity { var address: String { get set } - var txid: Data {get set} + var txid: Data { get set } - var index: Int32 {get set} + var index: Int { get set } - var script: Data {get set} + var script: Data { get set } - var valueZat: Int64 {get set} + var valueZat: Int { get set } - var height: UInt64 {get set} + var height: Int { get set } } diff --git a/ZcashLightClientKit/Repository/UnspentTransactionOutputRepository.swift b/ZcashLightClientKit/Repository/UnspentTransactionOutputRepository.swift new file mode 100644 index 00000000..e3104465 --- /dev/null +++ b/ZcashLightClientKit/Repository/UnspentTransactionOutputRepository.swift @@ -0,0 +1,17 @@ +// +// UnspentTransactionOutputRepository.swift +// ZcashLightClientKit +// +// Created by Francisco Gindre on 12/11/20. +// + +import Foundation + +protocol UnspentTransactionOutputRepository { + + func getAll(address: String?) throws -> [UnspentTransactionOutputEntity] + + func store(utxos: [UnspentTransactionOutputEntity]) throws + + func clearAll(address: String?) throws +} diff --git a/ZcashLightClientKit/Service/LightWalletGRPCService.swift b/ZcashLightClientKit/Service/LightWalletGRPCService.swift index a752c0e5..e3a035cd 100644 --- a/ZcashLightClientKit/Service/LightWalletGRPCService.swift +++ b/ZcashLightClientKit/Service/LightWalletGRPCService.swift @@ -227,12 +227,13 @@ extension LightWalletGRPCService: LightWalletService { var utxos = [UnspentTransactionOutputEntity]() let response = self.compactTxStreamer.getAddressUtxosStream(arg) { (reply) in utxos.append( - UTXO(address: tAddress, + UTXO(id: nil, + address: tAddress, txid: reply.txid, - index: reply.index, + index: Int(reply.index), script: reply.script, - valueZat: reply.valueZat, - height: UInt64(reply.valueZat) + valueZat: Int(reply.valueZat), + height: Int(reply.valueZat) ) ) } diff --git a/ZcashLightClientKit/Service/LightWalletService.swift b/ZcashLightClientKit/Service/LightWalletService.swift index b901fd4f..621675a9 100644 --- a/ZcashLightClientKit/Service/LightWalletService.swift +++ b/ZcashLightClientKit/Service/LightWalletService.swift @@ -169,6 +169,5 @@ public protocol LightWalletService { */ func fetchTransaction(txId: Data, result: @escaping (Result) -> Void) - func fetchUTXOs(for tAddress: String, result: @escaping(Result<[UnspentTransactionOutputEntity], LightWalletServiceError>) -> Void) } diff --git a/ZcashLightClientKit/Synchronizer.swift b/ZcashLightClientKit/Synchronizer.swift index f740a386..fe73fb90 100644 --- a/ZcashLightClientKit/Synchronizer.swift +++ b/ZcashLightClientKit/Synchronizer.swift @@ -130,6 +130,17 @@ public protocol Synchronizer { Blocking */ func latestHeight() throws -> BlockHeight + + /** + Gets the latest UTXOs for the given t-address and caches the result + */ + func latestUTXOs(address: String, result: @escaping (Result<[UnspentTransactionOutputEntity], Error>) -> Void) + + /** + gets the latest cached UTXOs for the given t-address for the given address + */ + func cachedUTXOs(address: String) throws -> [UnspentTransactionOutputEntity] + } /** diff --git a/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift b/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift index 3609b87d..d581e704 100644 --- a/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift +++ b/ZcashLightClientKit/UIKit/Synchronizer/SDKSynchronizer.swift @@ -90,6 +90,7 @@ public class SDKSynchronizer: Synchronizer { private var transactionManager: OutboundTransactionManager private var transactionRepository: TransactionRepository + private var utxoRepository: UnspentTransactionOutputRepository /** Creates an SDKSynchronizer instance @@ -100,18 +101,21 @@ public class SDKSynchronizer: Synchronizer { self.init(status: .disconnected, initializer: initializer, transactionManager: try OutboundTransactionManagerBuilder.build(initializer: initializer), - transactionRepository: initializer.transactionRepository) + transactionRepository: initializer.transactionRepository, + utxoRepository: try UTXORepositoryBuilder.build(initializer: initializer)) } init(status: Status, initializer: Initializer, transactionManager: OutboundTransactionManager, - transactionRepository: TransactionRepository) { + transactionRepository: TransactionRepository, + utxoRepository: UnspentTransactionOutputRepository) { self.status = status self.initializer = initializer self.transactionManager = transactionManager self.transactionRepository = transactionRepository + self.utxoRepository = utxoRepository } deinit { @@ -415,6 +419,33 @@ public class SDKSynchronizer: Synchronizer { try initializer.downloader.latestBlockHeight() } + public func latestUTXOs(address: String, result: @escaping (Result<[UnspentTransactionOutputEntity], Error>) -> Void) { + guard initializer.isValidTransparentAddress(address) else { + result(.failure(SynchronizerError.generalError(message: "invalid t-address"))) + return + } + + initializer.lightWalletService.fetchUTXOs(for: address, result: { [weak self] r in + guard let self = self else { return } + switch r { + case .success(let utxos): + do { + try self.utxoRepository.clearAll(address: address) + try self.utxoRepository.store(utxos: utxos) + result(.success(utxos)) + } catch { + result(.failure(SynchronizerError.generalError(message: "\(error)"))) + } + case .failure(let error): + result(.failure(SynchronizerError.connectionFailed(message: error))) + } + }) + } + + public func cachedUTXOs(address: String) throws -> [UnspentTransactionOutputEntity] { + try utxoRepository.getAll(address: address) + } + // MARK: notify state private func notify(progress: Float, height: BlockHeight) { NotificationCenter.default.post(name: Notification.Name.synchronizerProgressUpdated, object: self, userInfo: [