Transaction data access improvements + Create Spend + Get Balance Screen (#33)

* Transaction encoder implementation tests WIP

* Create Spend and Create Transaction + Test [WIP]

* New! Get Balance Screen

* Send to address
This commit is contained in:
Francisco Gindre 2019-11-26 19:32:20 -03:00 committed by GitHub
parent 908d8ac823
commit c772934d3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 609 additions and 103 deletions

View File

@ -1,4 +1,4 @@
platform :ios, '11.0'
platform :ios, '12.0'
target 'ZcashLightClientSample' do
# Comment the next line if you don't want to use dynamic frameworks

View File

@ -56,8 +56,8 @@ SPEC CHECKSUMS:
SQLite.swift: d2b4642190917051ce6bd1d49aab565fe794eea3
SwiftGRPC: f8fcfecb547c96cc6913de619f95fa3cd09838ee
SwiftProtobuf: 4fd9645e69b72cbae6ec8da5be0cdd20ca6565dd
ZcashLightClientKit: 5c5b46a9c1f5b293d493241d542215155e9bbfd8
ZcashLightClientKit: 3f257088ec27eae318ad2448775593f93334e43c
PODFILE CHECKSUM: bc6f10f9eb1279ca097cfa020229bb9cfb768223
PODFILE CHECKSUM: 1e92cf7111b50439bcbf88bd8118908750cc6e7c
COCOAPODS: 1.8.4

View File

@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
0D2343EE238C91B900606F71 /* sapling-output.params in Resources */ = {isa = PBXBuildFile; fileRef = 0D2343EC238C91B900606F71 /* sapling-output.params */; };
0D2343EF238C91B900606F71 /* sapling-spend.params in Resources */ = {isa = PBXBuildFile; fileRef = 0D2343ED238C91B900606F71 /* sapling-spend.params */; };
0D756A94236C761E009B041B /* GetAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D756A93236C761E009B041B /* GetAddressViewController.swift */; };
0D7A4A83236CCD88001F4DD8 /* SyncBlocksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D7A4A82236CCD88001F4DD8 /* SyncBlocksViewController.swift */; };
0D907F162322CC5900D641FE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D907F152322CC5900D641FE /* AppDelegate.swift */; };
@ -16,6 +18,7 @@
0D907F202322CC5B00D641FE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0D907F1E2322CC5B00D641FE /* LaunchScreen.storyboard */; };
0D907F2B2322CC5B00D641FE /* ZcashLightClientSampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D907F2A2322CC5B00D641FE /* ZcashLightClientSampleTests.swift */; };
0D907F362322CC5B00D641FE /* ZcashLightClientSampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D907F352322CC5B00D641FE /* ZcashLightClientSampleUITests.swift */; };
0DCD3DC7238D88B100DD3EC4 /* GetBalanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DCD3DC6238D88B100DD3EC4 /* GetBalanceViewController.swift */; };
0DDFB33C236B743000AED892 /* LatestHeightViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DDFB33B236B743000AED892 /* LatestHeightViewController.swift */; };
0DDFB33E236B844900AED892 /* DemoAppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DDFB33D236B844900AED892 /* DemoAppConfig.swift */; };
30AB89006A2D6891F32BFBF0 /* Pods_ZcashLightClientSample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17A69D88CA6DB45FA9D21E75 /* Pods_ZcashLightClientSample.framework */; };
@ -41,6 +44,8 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
0D2343EC238C91B900606F71 /* sapling-output.params */ = {isa = PBXFileReference; lastKnownFileType = file; name = "sapling-output.params"; path = "../../../ZcashLightClientKitTests/sapling-output.params"; sourceTree = "<group>"; };
0D2343ED238C91B900606F71 /* sapling-spend.params */ = {isa = PBXFileReference; lastKnownFileType = file; name = "sapling-spend.params"; path = "../../../ZcashLightClientKitTests/sapling-spend.params"; sourceTree = "<group>"; };
0D756A93236C761E009B041B /* GetAddressViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetAddressViewController.swift; sourceTree = "<group>"; };
0D7A4A82236CCD88001F4DD8 /* SyncBlocksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBlocksViewController.swift; sourceTree = "<group>"; };
0D907F122322CC5900D641FE /* ZcashLightClientSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ZcashLightClientSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -56,6 +61,7 @@
0D907F312322CC5B00D641FE /* ZcashLightClientSampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ZcashLightClientSampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
0D907F352322CC5B00D641FE /* ZcashLightClientSampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashLightClientSampleUITests.swift; sourceTree = "<group>"; };
0D907F372322CC5B00D641FE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0DCD3DC6238D88B100DD3EC4 /* GetBalanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GetBalanceViewController.swift; path = "Get Balance/GetBalanceViewController.swift"; sourceTree = "<group>"; };
0DDFB33B236B743000AED892 /* LatestHeightViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestHeightViewController.swift; sourceTree = "<group>"; };
0DDFB33D236B844900AED892 /* DemoAppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAppConfig.swift; sourceTree = "<group>"; };
17A69D88CA6DB45FA9D21E75 /* Pods_ZcashLightClientSample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ZcashLightClientSample.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -138,6 +144,10 @@
0D907F142322CC5900D641FE /* ZcashLightClientSample */ = {
isa = PBXGroup;
children = (
0DCD3DC5238D888B00DD3EC4 /* Get Balance */,
0DCD3DC4238D886400DD3EC4 /* Send Funds */,
0D2343EC238C91B900606F71 /* sapling-output.params */,
0D2343ED238C91B900606F71 /* sapling-spend.params */,
0D7A4A81236CCCDB001F4DD8 /* Sync Blocks */,
0D756A92236C75FE009B041B /* Get Address */,
0DDFB33A236B733700AED892 /* Latest Block Height */,
@ -148,6 +158,7 @@
0D907F1E2322CC5B00D641FE /* LaunchScreen.storyboard */,
0D907F212322CC5B00D641FE /* Info.plist */,
0DDFB33D236B844900AED892 /* DemoAppConfig.swift */,
0DCD3DC6238D88B100DD3EC4 /* GetBalanceViewController.swift */,
);
path = ZcashLightClientSample;
sourceTree = "<group>";
@ -170,6 +181,20 @@
path = ZcashLightClientSampleUITests;
sourceTree = "<group>";
};
0DCD3DC4238D886400DD3EC4 /* Send Funds */ = {
isa = PBXGroup;
children = (
);
path = "Send Funds";
sourceTree = "<group>";
};
0DCD3DC5238D888B00DD3EC4 /* Get Balance */ = {
isa = PBXGroup;
children = (
);
path = "Get Balance";
sourceTree = "<group>";
};
0DDFB33A236B733700AED892 /* Latest Block Height */ = {
isa = PBXGroup;
children = (
@ -313,6 +338,8 @@
0D907F202322CC5B00D641FE /* LaunchScreen.storyboard in Resources */,
0D907F1D2322CC5B00D641FE /* Assets.xcassets in Resources */,
0D907F1B2322CC5900D641FE /* Main.storyboard in Resources */,
0D2343EE238C91B900606F71 /* sapling-output.params in Resources */,
0D2343EF238C91B900606F71 /* sapling-spend.params in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -440,6 +467,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0DCD3DC7238D88B100DD3EC4 /* GetBalanceViewController.swift in Sources */,
0D756A94236C761E009B041B /* GetAddressViewController.swift in Sources */,
0D907F182322CC5900D641FE /* ViewController.swift in Sources */,
0DDFB33C236B743000AED892 /* LatestHeightViewController.swift in Sources */,

View File

@ -13,12 +13,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private var wallet: Initializer?
@UserDefaults var addresses: [String]?
var addresses: [String]?
var sharedWallet: Initializer {
if let wallet = wallet {
return wallet
} else {
let wallet = Initializer(cacheDbURL:try! __cacheDbURL() , dataDbURL: try! __dataDbURL(), endpoint: DemoAppConfig.endpoint)
let wallet = Initializer(cacheDbURL:try! __cacheDbURL() , dataDbURL: try! __dataDbURL(), endpoint: DemoAppConfig.endpoint, spendParamsURL: try! __spendParamsURL(), outputParamsURL: try! __outputParamsURL())
self.addresses = try! wallet.initialize(seedProvider: DemoAppConfig(), walletBirthdayHeight: BlockHeight(DemoAppConfig.birthdayHeight)) // Init or DIE
self.wallet = wallet
return wallet
@ -94,3 +94,13 @@ func __cacheDbURL() throws -> URL {
func __dataDbURL() throws -> URL {
try __documentsDirectory().appendingPathComponent("data.db", isDirectory: false)
}
func __spendParamsURL() throws -> URL {
try __documentsDirectory().appendingPathComponent("sapling-spend.params", isDirectory: false)
}
func __outputParamsURL() throws -> URL {
try __documentsDirectory().appendingPathComponent("sapling-output.params", isDirectory: false)
}

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Ewq-Xy-xHb">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Ewq-Xy-xHb">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@ -79,6 +79,26 @@
<segue destination="eja-yc-RHW" kind="show" id="fZ3-Vb-Oxe"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="v8L-AQ-cAg" style="IBUITableViewCellStyleDefault" id="RKO-CX-5oF">
<rect key="frame" x="0.0" y="158.5" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="RKO-CX-5oF" id="WdD-zf-ng7">
<rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Get Balance" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="v8L-AQ-cAg">
<rect key="frame" x="20" y="0.0" width="374" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="r9r-pi-Z4o" kind="show" id="dvg-Kp-7LV"/>
</connections>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
@ -291,5 +311,38 @@
</objects>
<point key="canvasLocation" x="-92" y="40"/>
</scene>
<!--Get Balance View Controller-->
<scene sceneID="jf5-6v-Lfw">
<objects>
<viewController id="r9r-pi-Z4o" customClass="GetBalanceViewController" customModule="ZcashLightClientSample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="n2N-jn-4DV">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GbA-U2-oep">
<rect key="frame" x="0.0" y="88" width="414" height="774"/>
<fontDescription key="fontDescription" type="system" pointSize="32"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="GbA-U2-oep" firstAttribute="leading" secondItem="5sH-IW-gs5" secondAttribute="leading" id="FaP-La-2qn"/>
<constraint firstItem="5sH-IW-gs5" firstAttribute="trailing" secondItem="GbA-U2-oep" secondAttribute="trailing" id="FaU-Yc-Sfo"/>
<constraint firstItem="5sH-IW-gs5" firstAttribute="bottom" secondItem="GbA-U2-oep" secondAttribute="bottom" id="XAB-SE-nvz"/>
<constraint firstItem="GbA-U2-oep" firstAttribute="top" secondItem="5sH-IW-gs5" secondAttribute="top" id="icf-gK-HIL"/>
</constraints>
<viewLayoutGuide key="safeArea" id="5sH-IW-gs5"/>
</view>
<navigationItem key="navigationItem" id="HmT-B1-R92"/>
<connections>
<outlet property="balance" destination="GbA-U2-oep" id="Y61-Hu-0LP"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="R73-h3-yvk" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1830" y="1623"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,38 @@
//
// GetBalanceViewController.swift
// ZcashLightClientSample
//
// Created by Francisco Gindre on 11/26/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import UIKit
import ZcashLightClientKit
class GetBalanceViewController: UIViewController {
@IBOutlet weak var balance: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Account 0 Balance"
self.balance.text = "\(Initializer.shared.getBalance().asHumanReadableZecBalance()) ZEC"
// Do any additional setup after loading the view.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
extension Int64 {
func asHumanReadableZecBalance() -> Double {
Double(self) / Double(ZATOSHI_PER_ZEC)
}
}

View File

@ -18,7 +18,7 @@ Pod::Spec.new do |s|
s.public_header_files = 'ZcashLightClientKit/**/*.h'
s.source_files = 'ZcashLightClientKit/**/*.{swift,h,a}'
s.module_map = 'ZcashLightClientKit.modulemap'
s.swift_version = '5.0'
s.swift_version = '5.1'
s.ios.deployment_target = '11.0'
s.dependency 'SwiftGRPC'
s.dependency 'SQLite.swift'
@ -34,8 +34,8 @@ Pod::Spec.new do |s|
CMD
s.test_spec 'Tests' do | test_spec |
test_spec.source_files = 'ZcashLightClientKitTests/**/*.{swift,db}'
test_spec.ios.resources = 'ZcashLightClientKitTests/**/*.{db}'
test_spec.source_files = 'ZcashLightClientKitTests/**/*.{swift}'
test_spec.ios.resources = 'ZcashLightClientKitTests/**/*.{db,params}'
test_spec.dependency 'SwiftGRPC'
test_spec.dependency 'SQLite.swift'
end

View File

@ -0,0 +1,42 @@
//
// PendingTransactionDao.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/19/19.
//
import Foundation
class PendingTransactionSQLDAO: PendingTransactionRepository {
var dbProvider: ConnectionProvider
init(dbProvider: ConnectionProvider) {
self.dbProvider = dbProvider
}
func create(_ transaction: PendingTransactionEntity) throws -> Int64 {
-1
}
func update(_ transaction: PendingTransactionEntity) throws {
}
func delete(_ transaction: PendingTransactionEntity) throws {
}
func cancel(_ transaction: PendingTransactionEntity) throws {
}
func find(by id: Int64) throws -> PendingTransactionEntity? {
nil
}
func getAll() throws -> [PendingTransactionEntity] {
[]
}
}

View File

@ -45,7 +45,7 @@ struct TransactionBuilder {
// Optional values
var toAddress: String?
if let to = bindings[ConfirmedColumns.toAddress.rawValue] as? String {
if let to = bindings[ConfirmedColumns.toAddress.rawValue] as? String {
toAddress = to
}
@ -60,7 +60,7 @@ struct TransactionBuilder {
}
var memo: Data?
if let memoBlob = bindings[ConfirmedColumns.memo.rawValue] as? Blob {
if let memoBlob = bindings[ConfirmedColumns.memo.rawValue] as? Blob {
memo = Data(blob: memoBlob)
}
@ -94,7 +94,7 @@ struct TransactionBuilder {
// Optional values
var memo: Data?
if let memoBlob = bindings[ReceivedColumns.memo.rawValue] as? Blob {
if let memoBlob = bindings[ReceivedColumns.memo.rawValue] as? Blob {
memo = Data(blob: memoBlob)
}

View File

@ -42,8 +42,6 @@ struct ConfirmedTransaction: ConfirmedTransactionEntity {
var rawTransactionId: Data?
}
class TransactionSQLDAO: TransactionRepository {
struct TableStructure {
@ -72,9 +70,10 @@ class TransactionSQLDAO: TransactionRepository {
try dbProvider.connection().scalar(transactions.filter(TableStructure.minedHeight == nil).count)
}
func findBy(id: Int) throws -> TransactionEntity? {
func findBy(id: Int64) throws -> TransactionEntity? {
let query = transactions.filter(TableStructure.id == Int64(id)).limit(1)
let entity: Transaction? = try dbProvider.connection().prepare(query).map({ try $0.decode() }).first
let sequence = try dbProvider.connection().prepare(query)
let entity: Transaction? = try sequence.map({ try $0.decode() }).first
return entity
}
@ -198,7 +197,7 @@ extension Data {
}
extension Array where Element == UInt8 {
var data : Data{
var data: Data {
return Data(self)
}
}

View File

@ -8,7 +8,7 @@
import Foundation
public protocol AccountEntity: Hashable {
var account:Int { get set }
var account: Int { get set }
var extfvk: String { get set }
var address: String { get set }
}
@ -28,5 +28,3 @@ public extension AccountEntity {
return true
}
}

View File

@ -0,0 +1,26 @@
//
// EncodedTransactionEntity.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/19/19.
//
import Foundation
struct EncodedTransaction: SignedTransactionEntity {
var transactionId: Data
var raw: Data?
}
extension EncodedTransaction: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(transactionId)
hasher.combine(raw)
}
static func == (lhs: Self, rhs: Self) -> Bool {
guard lhs.transactionId == rhs.transactionId else { return false }
guard lhs.raw == rhs.raw else { return false }
return true
}
}

View File

@ -0,0 +1,77 @@
//
// PendingTransactionEntity.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/19/19.
//
import Foundation
public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTransaction, RawIdentifiable {
var toAddress: String { get set }
var accountIndex: Int { get set }
var minedHeight: BlockHeight { get set }
var expiryHeight: BlockHeight { get set }
var cancelled: Int { get set }
var encodeAttempts: Int { get set }
var submitAttempts: Int { get set }
var errorMesssage: String? { get set }
var errorCode: Int? { get set }
var createTime: TimeInterval { get set }
func isSameTransactionId<T: RawIdentifiable> (other: T) -> Bool
func isPending(currentHeight: Int) -> Bool
var isCreating: Bool { get }
var isFailedEncoding: Bool { get }
var isFailedSubmit: Bool { get }
var isFailure: Bool { get }
var isCancelled: Bool { get }
var isMined: Bool { get }
var isSubmitted: Bool { get }
var isSubmitSuccess: Bool { get }
}
public extension PendingTransactionEntity {
func isSameTransaction<T: RawIdentifiable>(other: T) -> Bool {
guard let selfId = self.rawTransactionId, let otherId = other.rawTransactionId else { return false }
return selfId == otherId
}
var isCreating: Bool {
(raw?.isEmpty ?? true) != false && submitAttempts <= 0 && !isFailedSubmit && !isFailedEncoding
}
var isFailedEncoding: Bool {
(raw?.isEmpty ?? true) != false && encodeAttempts > 0
}
var isFailedSubmit: Bool {
errorMesssage != nil || (errorCode != nil && (errorCode ?? 0) < 0)
}
var isFailure: Bool {
isFailedEncoding || isFailedSubmit
}
var isCancelled: Bool {
cancelled > 0
}
var isMined: Bool {
minedHeight > 0
}
var isSubmitted: Bool {
submitAttempts > 0
}
func isPending(currentHeight: Int = -1) -> Bool {
// not mined and not expired and successfully created
!isSubmitSuccess && minedHeight == -1 && (expiryHeight == -1 || expiryHeight > currentHeight) && raw != nil
}
var isSubmitSuccess: Bool {
submitAttempts > 0 && (errorCode != nil && (errorCode ?? -1) >= 0) && errorMesssage == nil
}
}

View File

@ -66,73 +66,3 @@ public protocol ConfirmedTransactionEntity: MinedTransactionEntity, SignedTransa
var toAddress: String? { get set }
var expiryHeight: BlockHeight? { get set }
}
public protocol PendingTransactionEntity: SignedTransactionEntity, AbstractTransaction, RawIdentifiable {
var toAddress: String { get set }
var accountIndex: Int { get set }
var minedHeight: BlockHeight { get set }
var expiryHeight: BlockHeight { get set }
var cancelled: Int { get set }
var encodeAttempts: Int { get set }
var submitAttempts: Int { get set }
var errorMesssage: String? { get set }
var errorCode: Int? { get set }
var createTime: TimeInterval { get set }
func isSameTransactionId<T: RawIdentifiable> (other: T) -> Bool
func isPending(currentHeight: Int) -> Bool
var isCreating: Bool { get }
var isFailedEncoding: Bool { get }
var isFailedSubmit: Bool { get }
var isFailure: Bool { get }
var isCancelled: Bool { get }
var isMined: Bool { get }
var isSubmitted: Bool { get }
var isSubmitSuccess: Bool { get }
}
public extension PendingTransactionEntity {
func isSameTransaction<T: RawIdentifiable>(other: T) -> Bool {
guard let selfId = self.rawTransactionId, let otherId = other.rawTransactionId else { return false }
return selfId == otherId
}
var isCreating: Bool {
(raw?.isEmpty ?? true) != false && submitAttempts <= 0 && !isFailedSubmit && !isFailedEncoding
}
var isFailedEncoding: Bool {
(raw?.isEmpty ?? true) != false && encodeAttempts > 0
}
var isFailedSubmit: Bool {
errorMesssage != nil || (errorCode != nil && (errorCode ?? 0) < 0)
}
var isFailure: Bool {
isFailedEncoding || isFailedSubmit
}
var isCancelled: Bool {
cancelled > 0
}
var isMined: Bool {
minedHeight > 0
}
var isSubmitted: Bool {
submitAttempts > 0
}
func isPending(currentHeight: Int = -1) -> Bool {
// not mined and not expired and successfully created
!isSubmitSuccess && minedHeight == -1 && (expiryHeight == -1 || expiryHeight > currentHeight) && raw != nil
}
var isSubmitSuccess: Bool {
submitAttempts > 0 && (errorCode != nil && (errorCode ?? -1) >= 0) && errorMesssage == nil
}
}

View File

@ -45,17 +45,20 @@ public class Initializer {
private var rustBackend: ZcashRustBackendWelding.Type = ZcashRustBackend.self
private var lowerBoundHeight: BlockHeight = SAPLING_ACTIVATION_HEIGHT
private var cacheDbURL: URL
private var dataDbURL: URL
private(set) var cacheDbURL: URL
private(set) var dataDbURL: URL
private(set) var spendParamsURL: URL
private(set) var outputParamsURL: URL
private var walletBirthday: WalletBirthday?
public private(set) var endpoint: LightWalletEndpoint
public init (cacheDbURL: URL, dataDbURL: URL, endpoint: LightWalletEndpoint) {
public init (cacheDbURL: URL, dataDbURL: URL, endpoint: LightWalletEndpoint, spendParamsURL: URL, outputParamsURL: URL) {
self.cacheDbURL = cacheDbURL
self.dataDbURL = dataDbURL
self.endpoint = endpoint
self.spendParamsURL = spendParamsURL
self.outputParamsURL = outputParamsURL
}
/**
@ -113,7 +116,11 @@ public class Initializer {
}
public func getAddress(index account: Int = 0) -> String? {
return rustBackend.getAddress(dbData: dataDbURL, account: Int32(account))
rustBackend.getAddress(dbData: dataDbURL, account: Int32(account))
}
public func getBalance(account index: Int = 0) -> Int64 {
rustBackend.getBalance(dbData: dataDbURL, account: Int32(index))
}
// TODO: make internal

View File

@ -0,0 +1,17 @@
//
// PendingTransactionRepository.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/19/19.
//
import Foundation
protocol PendingTransactionRepository {
func create(_ transaction: PendingTransactionEntity) throws -> Int64
func update(_ transaction: PendingTransactionEntity) throws
func delete(_ transaction: PendingTransactionEntity) throws
func cancel(_ transaction: PendingTransactionEntity) throws
func find(by id: Int64) throws -> PendingTransactionEntity?
func getAll() throws -> [PendingTransactionEntity]
}

View File

@ -12,9 +12,9 @@ enum TransactionRepositoryError: Error {
}
protocol TransactionRepository {
func countAll() throws -> Int
func countAll() throws -> Int
func countUnmined() throws -> Int
func findBy(id: Int) throws -> TransactionEntity?
func findBy(id: Int64) throws -> TransactionEntity?
func findBy(rawId: Data) throws -> TransactionEntity?
func findAllSentTransactions(limit: Int) throws -> [ConfirmedTransactionEntity]?
func findAllReceivedTransactions(limit: Int) throws -> [ConfirmedTransactionEntity]?

View File

@ -7,3 +7,15 @@
//
import Foundation
extension Data {
func asZcashTransactionMemo() -> String? {
return nil
}
}
extension String {
func encodeAsZcashTransactionMemo() -> Data? {
return nil
}
}

View File

@ -12,6 +12,10 @@ class ZcashRustBackend: ZcashRustBackendWelding {
static func lastError() -> RustWeldingError? {
guard let message = getLastError() else { return nil }
if message.contains("couldn't load Sapling spend parameters") {
return RustWeldingError.saplingSpendParametersNotFound
}
return RustWeldingError.genericError(message: message)
}
@ -126,7 +130,9 @@ class ZcashRustBackend: ZcashRustBackendWelding {
let dbData = dbData.osStr()
let spendParams = spendParams.osStr()
let outputParams = outputParams.osStr()
return zcashlc_send_to_address(dbData.0, dbData.1, account, extsk, to, value, memo, spendParams.0, spendParams.1, outputParams.0, outputParams.1)
let memoBytes = memo ?? ""
return zcashlc_send_to_address(dbData.0, dbData.1, account, extsk, to, value, memoBytes, spendParams.0, spendParams.1, outputParams.0, outputParams.1)
}
}

View File

@ -12,6 +12,7 @@ public enum RustWeldingError: Error {
case genericError(message: String)
case dataDbInitFailed(message: String)
case dataDbNotEmpty
case saplingSpendParametersNotFound
}
public struct ZcashRustBackendWeldingConstants {

View File

@ -0,0 +1,37 @@
//
// TransactionEncoder.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/20/19.
//
import Foundation
typealias TransactionEncoderResultBlock = (_ result: Result<EncodedTransaction,Error>) -> Void
public enum TransactionEncoderError: Error {
case notFound(transactionId: Int64)
case NotEncoded(transactionId: Int64)
case missingParams
case spendingKeyWrongNetwork
}
protocol TransactionEncoder {
/**
Creates a transaction, throwing an exception whenever things are missing. When the provided wallet implementation
doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather than using
double-bangs for things).
Blocking
*/
func createTransaction(spendingKey: String, zatoshi: Int64, to: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction
/**
Creates a transaction, throwing an exception whenever things are missing. When the provided wallet implementation
doesn't throw an exception, we wrap the issue into a descriptive exception ourselves (rather than using
double-bangs for things).
Non-blocking
*/
func createTransaction(spendingKey: String, zatoshi: Int64, to: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock)
}

View File

@ -0,0 +1,73 @@
//
// WalletTransactionEncoder.swift
// ZcashLightClientKit
//
// Created by Francisco Gindre on 11/20/19.
//
import Foundation
class WalletTransactionEncoder: TransactionEncoder {
var rustBackend: ZcashRustBackend.Type
var repository: TransactionRepository
var initializer: Initializer
init(rust: ZcashRustBackend.Type, repository: TransactionRepository, initializer: Initializer) {
self.rustBackend = rust
self.repository = repository
self.initializer = initializer
}
func createTransaction(spendingKey: String, zatoshi: Int64, to: String, memo: String?, from accountIndex: Int) throws -> EncodedTransaction {
let txId = try createSpend(spendingKey: spendingKey, zatoshi: zatoshi, to: to, memo: memo, from: accountIndex)
do {
let transaction = try repository.findBy(id: txId)
guard let tx = transaction else {
throw TransactionEncoderError.notFound(transactionId: txId)
}
print("sentTransaction id: \(txId)")
return EncodedTransaction(transactionId: tx.transactionId , raw: tx.raw)
} catch {
throw TransactionEncoderError.notFound(transactionId: txId)
}
}
func createTransaction(spendingKey: String, zatoshi: Int64, to: String, memo: String?, from accountIndex: Int, result: @escaping TransactionEncoderResultBlock) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
do {
result(.success(try self.createTransaction(spendingKey: spendingKey, zatoshi: zatoshi, to: to, memo: memo, from: accountIndex)))
} catch {
result(.failure(error))
}
}
}
func createSpend(spendingKey: String, zatoshi: Int64, to address: String, memo: String?, from accountIndex: Int) throws -> Int64 {
guard ensureParams(spend: initializer.spendParamsURL, output: initializer.spendParamsURL),
let spend = URL(string: initializer.spendParamsURL.path), let output = URL(string: initializer.outputParamsURL.path) else {
throw TransactionEncoderError.missingParams
}
let txId = rustBackend.sendToAddress(dbData: initializer.dataDbURL, account: Int32(accountIndex), extsk: spendingKey, to: address, value: Int64(zatoshi), memo: memo, spendParams: spend, outputParams: output)
guard txId > 0 else {
throw rustBackend.lastError() ?? RustWeldingError.genericError(message: "create spend failed")
}
return txId
}
func ensureParams(spend: URL, output: URL) -> Bool {
let readableSpend = FileManager.default.isReadableFile(atPath: spend.path)
let readableOutput = FileManager.default.isReadableFile(atPath: output.path)
return readableSpend && readableOutput // Todo: change this to something that makes sense
}
}

View File

@ -31,7 +31,7 @@ class WalletTests: XCTestCase {
func testWalletInitialization() {
let wallet = Initializer(cacheDbURL: cacheData, dataDbURL: dbData, endpoint: LightWalletEndpoint(address: "localhost", port: "9067", secure: false))
let wallet = Initializer(cacheDbURL: cacheData, dataDbURL: dbData, endpoint: LightWalletEndpointBuilder.default, spendParamsURL: try! __spendParamsURL(), outputParamsURL: try! __outputParamsURL())
XCTAssertNoThrow(try wallet.initialize(seedProvider: SampleSeedProvider(), walletBirthdayHeight: SAPLING_ACTIVATION_HEIGHT))

View File

@ -0,0 +1,81 @@
//
// WalletTransactionEncoderTests.swift
// ZcashLightClientKit-Unit-Tests
//
// Created by Francisco Gindre on 11/20/19.
//
import XCTest
@testable import ZcashLightClientKit
class WalletTransactionEncoderTests: XCTestCase {
var repository: TransactionRepository!
var rustBackend = ZcashRustBackend.self
var transactionEncoder: WalletTransactionEncoder!
var dataDbHandle = TestDbHandle(originalDb: TestDbBuilder.prePopulatedDataDbURL()!)
var cacheDbHandle = TestDbHandle(originalDb: TestDbBuilder.prePopulatedCacheDbURL()!)
var initializer: Initializer!
let spendingKey = "secret-extended-key-test1qvpevftsqqqqpqy52ut2vv24a2qh7nsukew7qg9pq6djfwyc3xt5vaxuenshp2hhspp9qmqvdh0gs2ljpwxders5jkwgyhgln0drjqaguaenfhehz4esdl4kwlm5t9q0l6wmzcrvcf5ed6dqzvct3e2ge7f6qdvzhp02m7sp5a0qjssrwpdh7u6tq89hl3wchuq8ljq8r8rwd6xdwh3nry9at80z7amnj3s6ah4jevnvfr08gxpws523z95g6dmn4wm6l3658kd4xcq9rc0qn"
let recipientAddress = "ztestsapling1ctuamfer5xjnnrdr3xdazenljx0mu0gutcf9u9e74tr2d3jwjnt0qllzxaplu54hgc2tyjdc2p6"
let zpend: Int64 = 500_000
override func setUp() {
try! dataDbHandle.setUp()
try! cacheDbHandle.setUp()
initializer = Initializer(cacheDbURL: cacheDbHandle.readWriteDb, dataDbURL: dataDbHandle.readWriteDb, endpoint: LightWalletEndpointBuilder.default, spendParamsURL: try! __spendParamsURL(), outputParamsURL: try! __outputParamsURL())
repository = TransactionSQLDAO(dbProvider: dataDbHandle.connectionProvider(readwrite: false))
transactionEncoder = WalletTransactionEncoder(rust: rustBackend.self, repository: repository, initializer: initializer)
}
override func tearDown() {
repository = nil
dataDbHandle.dispose()
cacheDbHandle.dispose()
}
func testCreateTransaction() {
var transaction: EncodedTransaction?
XCTAssertNoThrow(try { transaction = try transactionEncoder.createTransaction(spendingKey: spendingKey, zatoshi: zpend, to: recipientAddress, memo: nil, from: 0)}())
guard let tx = transaction else {
XCTFail("transaction is nil. error: \(String(describing: rustBackend.getLastError()))")
return
}
var retrievedTx: TransactionEntity?
XCTAssertNoThrow(try { retrievedTx = try repository.findBy(rawId: tx.transactionId) }())
XCTAssertNotNil(retrievedTx, "transaction not found")
}
func testCreateSpend() {
XCTAssert(initializer.getBalance() >= zpend)
var spendId: Int64?
XCTAssertNoThrow(try { spendId = try transactionEncoder.createSpend(spendingKey: self.spendingKey, zatoshi: self.zpend, to: self.recipientAddress, memo: nil, from: 0) }())
guard let id = spendId else {
XCTFail("failed to create spend. error: \(String(describing: rustBackend.getLastError()))")
return
}
var tx: TransactionEntity?
XCTAssertNoThrow(try { tx = try repository.findBy(id: id)}())
XCTAssertNotNil(tx, "Transaction Id: \(id), not found. rust error: \(String(describing: rustBackend.getLastError()))")
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measure {
_ = try! transactionEncoder.createSpend(spendingKey: self.spendingKey, zatoshi: self.zpend, to: self.recipientAddress, memo: nil, from: 0)
}
}
}

Binary file not shown.

View File

@ -10,6 +10,28 @@ import Foundation
import SQLite
@testable import ZcashLightClientKit
struct TestDbHandle {
var originalDb: URL
var readWriteDb: URL
init(originalDb: URL) {
self.originalDb = originalDb
self.readWriteDb = FileManager.default.temporaryDirectory.appendingPathComponent(self.originalDb.lastPathComponent.appending("_\(Date().timeIntervalSince1970)")) // avoid files clashing because crashing tests failed to remove previous ones by incrementally changing the filename
}
func setUp() throws {
try FileManager.default.copyItem(at: originalDb, to: readWriteDb)
}
func dispose() {
try? FileManager.default.removeItem(at: readWriteDb)
}
func connectionProvider(readwrite: Bool = true) -> ConnectionProvider {
SimpleConnectionProvider(path: self.readWriteDb.absoluteString, readonly: !readwrite)
}
}
class TestDbBuilder {
enum TestBuilderError: Error {
@ -28,6 +50,14 @@ class TestDbBuilder {
return compactBlockDao
}
static func prePopulatedCacheDbURL() -> URL? {
Bundle(for: TestDbBuilder.self).url(forResource: "cache", withExtension: "db")
}
static func prePopulatedDataDbURL() -> URL? {
Bundle(for: TestDbBuilder.self).url(forResource: "test_data", withExtension: "db")
}
static func prepopulatedDataDbProvider() -> ConnectionProvider? {
let bundle = Bundle(for: TestDbBuilder.self)
guard let url = bundle.url(forResource: "ZcashSdk_Data", withExtension: "db") else { return nil }

View File

@ -8,8 +8,15 @@
import Foundation
import SwiftGRPC
import ZcashLightClientKit
import XCTest
class LightWalletEndpointBuilder {
static var `default`: LightWalletEndpoint {
LightWalletEndpoint(address: "localhost", port: "9067", secure: false)
}
}
class ChannelProvider {
func channel() -> SwiftGRPC.Channel {
Channel(address: Constants.address, secure: false)
@ -55,13 +62,47 @@ func __dataDbURL() throws -> URL {
try __documentsDirectory().appendingPathComponent("data.db", isDirectory: false)
}
func __spendParamsURL() throws -> URL {
Bundle.testBundle.url(forResource: "sapling-spend", withExtension: "params")!
}
func __outputParamsURL() throws -> URL {
Bundle.testBundle.url(forResource: "sapling-output", withExtension: "params")!
}
func copyParametersToDocuments() throws -> (spend: URL, output: URL) {
let spendURL = try __documentsDirectory().appendingPathComponent("sapling-spend.params", isDirectory: false)
let outputURL = try __documentsDirectory().appendingPathComponent("sapling-output.params", isDirectory: false)
try FileManager.default.copyItem(at: try __spendParamsURL(), to: spendURL)
try FileManager.default.copyItem(at: try __outputParamsURL(), to: outputURL)
return (spendURL, outputURL)
}
func deleteParametersFromDocuments() throws {
let documents = try __documentsDirectory()
deleteParamsFrom(spend: documents.appendingPathComponent("sapling-spend.params"), output: documents.appendingPathComponent("sapling-output.params"))
}
func deleteParamsFrom(spend: URL, output: URL) {
try? FileManager.default.removeItem(at: spend)
try? FileManager.default.removeItem(at: output)
}
func parametersReady() -> Bool {
guard let output = try? __documentsDirectory().appendingPathComponent("sapling-output.params", isDirectory: false),
let spend = try? __documentsDirectory().appendingPathComponent("sapling-spend.params", isDirectory: false),
guard let output = try? __outputParamsURL(),
let spend = try? __spendParamsURL(),
FileManager.default.isReadableFile(atPath: output.absoluteString),
FileManager.default.isReadableFile(atPath: spend.absoluteString) else {
return false
}
return true
}
class StubTest: XCTestCase {}
extension Bundle {
static var testBundle: Bundle {
Bundle(for: StubTest.self)
}
}