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: [