ZcashLightClientKit/Example/ZcashLightClientSample/ZcashLightClientSample/Send/SendViewController.swift

383 lines
12 KiB
Swift
Raw Normal View History

//
// SendViewController.swift
// ZcashLightClientSample
//
// Created by Francisco Gindre on 12/3/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import UIKit
import ZcashLightClientKit
import KRProgressHUD
class SendViewController: UIViewController {
@IBOutlet weak var addressLabel: UILabel!
@IBOutlet weak var amountLabel: UILabel!
@IBOutlet weak var addressTextField: UITextField!
@IBOutlet weak var amountTextField: UITextField!
@IBOutlet weak var balanceLabel: UILabel!
2020-01-14 14:25:14 -08:00
@IBOutlet weak var verifiedBalanceLabel: UILabel!
@IBOutlet weak var maxFunds: UISwitch!
@IBOutlet weak var sendButton: UIButton!
@IBOutlet weak var synchronizerStatusLabel: UILabel!
2020-06-18 16:53:11 -07:00
@IBOutlet weak var memoField: UITextView!
@IBOutlet weak var charactersLeftLabel: UILabel!
let characterLimit: Int = 512
2021-09-17 06:49:58 -07:00
var wallet = Initializer.shared
// swiftlint:disable:next implicitly_unwrapped_optional
var synchronizer: Synchronizer!
override func viewDidLoad() {
super.viewDidLoad()
synchronizer = AppDelegate.shared.sharedSynchronizer
2022-10-31 16:45:58 -07:00
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(viewTapped(_:)))
self.view.addGestureRecognizer(tapRecognizer)
setUp()
Task { @MainActor in
// swiftlint:disable:next force_try
try! await synchronizer.prepare()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Task { @MainActor in
do {
try await synchronizer.start(retry: false)
self.synchronizerStatusLabel.text = SDKSynchronizer.textFor(state: synchronizer.status)
} catch {
self.synchronizerStatusLabel.text = SDKSynchronizer.textFor(state: synchronizer.status)
fail(error)
}
}
}
@objc func viewTapped(_ recognizer: UITapGestureRecognizer) {
let point = recognizer.location(in: self.view)
if addressTextField.isFirstResponder && !addressTextField.frame.contains(point) {
addressTextField.resignFirstResponder()
2021-09-17 06:49:58 -07:00
} else if amountTextField.isFirstResponder && !amountTextField.frame.contains(point) {
amountTextField.resignFirstResponder()
2020-06-18 16:53:11 -07:00
} else if memoField.isFirstResponder &&
!memoField.frame.contains(point) {
memoField.resignFirstResponder()
}
}
func setUp() {
balanceLabel.text = format(balance: wallet.getBalance())
2020-01-14 14:25:14 -08:00
verifiedBalanceLabel.text = format(balance: wallet.getVerifiedBalance())
toggleSendButton()
2020-06-18 16:53:11 -07:00
memoField.text = ""
memoField.layer.borderColor = UIColor.gray.cgColor
memoField.layer.borderWidth = 1
memoField.layer.cornerRadius = 5
charactersLeftLabel.text = textForCharacterCount(0)
let center = NotificationCenter.default
2021-09-17 06:49:58 -07:00
center.addObserver(
self,
selector: #selector(synchronizerStarted(_:)),
name: Notification.Name.synchronizerStarted,
object: synchronizer
)
center.addObserver(
self,
selector: #selector(synchronizerSynced(_:)),
name: Notification.Name.synchronizerSynced,
object: synchronizer
)
center.addObserver(
self,
selector: #selector(synchronizerStopped(_:)),
name: Notification.Name.synchronizerStopped,
object: synchronizer
)
center.addObserver(
self,
selector: #selector(synchronizerUpdated(_:)),
name: Notification.Name.synchronizerProgressUpdated,
object: synchronizer
)
}
func format(balance: Zatoshi = Zatoshi()) -> String {
"Zec \(balance.formattedString ?? "0.0")"
}
func toggleSendButton() {
sendButton.isEnabled = isFormValid()
}
func maxFundsOn() {
let fee = Zatoshi(10000)
let max: Zatoshi = wallet.getVerifiedBalance() - fee
amountTextField.text = format(balance: max)
amountTextField.isEnabled = false
}
func maxFundsOff() {
amountTextField.isEnabled = true
}
func isFormValid() -> Bool {
switch synchronizer.status {
case .synced:
return isBalanceValid() && isAmountValid() && isRecipientValid()
default:
return false
2021-09-17 06:49:58 -07:00
}
}
func isBalanceValid() -> Bool {
wallet.getVerifiedBalance() > .zero
}
func isAmountValid() -> Bool {
2021-09-17 06:49:58 -07:00
guard
let value = amountTextField.text,
let amount = NumberFormatter.zcashNumberFormatter.number(from: value).flatMap({ Zatoshi($0.int64Value) }),
amount <= wallet.getVerifiedBalance()
2021-09-17 06:49:58 -07:00
else {
return false
}
2021-09-17 06:49:58 -07:00
return true
}
func isRecipientValid() -> Bool {
guard let addr = self.addressTextField.text else {
return false
}
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
return wallet.isValidSaplingAddress(addr) || wallet.isValidTransparentAddress(addr)
}
@IBAction func maxFundsValueChanged(_ sender: Any) {
2020-06-18 16:53:11 -07:00
if maxFunds.isOn {
maxFundsOn()
} else {
maxFundsOff()
}
}
@IBAction func send(_ sender: Any) {
guard isFormValid() else {
loggerProxy.warn("WARNING: Form is invalid")
return
}
2021-09-17 06:49:58 -07:00
let alert = UIAlertController(
title: "About To send funds!",
// swiftlint:disable:next line_length
message: "This is an ugly confirmation message. You should come up with something fancier that lets the user be sure about sending funds without disturbing the user experience with an annoying alert like this one",
preferredStyle: UIAlertController.Style.alert
)
let sendAction = UIAlertAction(
title: "Send!",
style: UIAlertAction.Style.default,
handler: { _ in
self.send()
}
)
let cancelAction = UIAlertAction(
title: "Go back! I'm not sure about this.",
style: UIAlertAction.Style.destructive,
handler: { _ in
self.cancel()
}
)
alert.addAction(sendAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
func send() {
guard
isFormValid(),
let amount = amountTextField.text,
let zec = NumberFormatter.zcashNumberFormatter.number(from: amount).flatMap({ Zatoshi($0.int64Value) }),
let recipient = addressTextField.text
else {
loggerProxy.warn("WARNING: Form is invalid")
return
}
guard let spendingKey = try? DerivationTool(
networkType: kZcashNetwork.networkType
)
.deriveUnifiedSpendingKey(
seed: DemoAppConfig.seed,
accountIndex: 0
)
else {
[#461] Adopt a Type-Safe Keys and Addresses API This PR creates data types for Addresses and Keys so that they are not represented by Strings anymore. This avoids mistakenly use the wrong keys because they are all alike for the type system. New Protocols: ============= StringEncoded -> Protocol that makes a type can be expressed in an string-encoded fashion either for UI or Interchange purposes. Undescribable -> A protocol that implements methods that override default decriptions used by debuggers, loggers and event trackers to avoid types conforming to it to be leaked to logs. Deleted Protocols: ================== UnifiedFullViewingKey --> turned into a struct. UnifiedAddress --> turned into a struct new Error Type: ================ ```` enum KeyEncodingError: Error { case invalidEncoding } ```` This error is thrown when an Address or Key type (addresses are public keys in the end) can be decoded from their String representation, typically upon initialization from a User input. New Types: ========= SaplingExtendedSpendingKey -> Type for Sapling Extended Full Viewing Keys this type will be replaced with Unified Spending Keys soon. SaplingExtendedFullViewingKey -> Extended Full Viewing Key for Sapling. Maintains existing funcionality. Will be probably deprecated in favor of UFVK. TransparentAccountPrivKey -> Private key for transparent account. Used only for shielding operations. Note: this will probably be deprecated soon. UnifiedFullViewingKey -> Replaces the protocol that had the same name. TransparentAddress -> Replaces a type alias with a struct SaplingAddress --> Represents a Sapling receiver address. Comonly called zAddress. This address corresponds to the Zcash Sapling shielded pool. Although this it is fully functional, we encourage developers to choose `UnifiedAddress` before Sapling or Transparent ones. UnifiedAddress -> Represents a UA. String-encodable and Equatable. Use of UAs must be favored instead of individual receivers for different pools. This type can't be decomposed into their Receiver types yet. Recipient -> This represents all valid receiver types to be used as inputs for outgoing transactions. ```` public enum Recipient: Equatable, StringEncoded { case transparent(TransparentAddress) case sapling(SaplingAddress) case unified(UnifiedAddress) ```` The wrapped concrete receiver is a valid receiver type. Deleted Type Aliases: ===================== The following aliases were deleted and turned into types ```` public typealias TransparentAddress = String public typealias SaplingShieldedAddress = String ```` Changes to Derivation Tool ========================== DerivationTool has been changed to accomodate this new types and remove Strings whenever possible. Changes to Synchronizer and CompactBlockProcessor ================================================= Accordingly these to components have been modified to accept the new types intead of strings when possible. Changes to Demo App =================== The demo App has been patch to compile and work with the new types. Developers must consider that the use (and abuse) of forced_try and forced unwrapping is a "license" that maintainers are using for the sake of brevity. We consider that clients of this SDK do know how to handle Errors and Optional and it is not the objective of the demo code to show good practices on those matters. Closes #461
2022-08-20 15:10:22 -07:00
loggerProxy.error("NO SPENDING KEY")
return
}
KRProgressHUD.show()
Task { @MainActor in
do {
let pendingTransaction = try await synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: zec,
// swiftlint:disable:next force_try
toAddress: try! Recipient(recipient, network: kZcashNetwork.networkType),
memo: try! self.memoField.text.asMemo()
)
KRProgressHUD.dismiss()
loggerProxy.info("transaction created: \(pendingTransaction)")
} catch {
fail(error)
loggerProxy.error("SEND FAILED: \(error)")
}
}
}
func fail(_ error: Error) {
2021-09-17 06:49:58 -07:00
let alert = UIAlertController(
title: "Send failed!",
message: "\(error)",
preferredStyle: UIAlertController.Style.alert
)
let action = UIAlertAction(title: "OK :(", style: UIAlertAction.Style.default, handler: nil)
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
2021-09-17 06:49:58 -07:00
func cancel() {}
// MARK: synchronizer notifications
@objc func synchronizerUpdated(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.synchronizerStatusLabel.text = SDKSynchronizer.textFor(state: self.synchronizer.status)
}
}
@objc func synchronizerStarted(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.synchronizerStatusLabel.text = SDKSynchronizer.textFor(state: self.synchronizer.status)
}
}
@objc func synchronizerStopped(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.synchronizerStatusLabel.text = SDKSynchronizer.textFor(state: self.synchronizer.status)
}
}
@objc func synchronizerSynced(_ notification: Notification) {
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.synchronizerStatusLabel.text = SDKSynchronizer.textFor(state: self.synchronizer.status)
}
}
2020-06-18 16:53:11 -07:00
func textForCharacterCount(_ count: Int) -> String {
"\(count) of \(characterLimit) bytes left"
}
}
extension SendViewController: UITextFieldDelegate {
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
if textField == amountTextField {
2020-06-18 16:53:11 -07:00
return !maxFunds.isOn
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
textField.resignFirstResponder()
toggleSendButton()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == amountTextField {
addressTextField.becomeFirstResponder()
return false
}
textField.resignFirstResponder()
return true
}
}
2020-06-18 16:53:11 -07:00
extension SendViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let userPressedDelete = text.isEmpty && range.length > 0
return textView.text.utf8.count < characterLimit || userPressedDelete
}
func textViewDidChange(_ textView: UITextView) {
self.charactersLeftLabel.text = textForCharacterCount(textView.text.utf8.count)
}
}
extension SDKSynchronizer {
2021-06-14 15:56:32 -07:00
static func textFor(state: SyncStatus) -> String {
2020-06-18 16:53:11 -07:00
switch state {
2021-06-14 15:56:32 -07:00
case .downloading(let progress):
return "Downloading \(progress.progressHeight)/\(progress.targetHeight)"
2021-09-17 06:49:58 -07:00
2021-06-14 15:56:32 -07:00
case .enhancing(let enhanceProgress):
return "Enhancing tx \(enhanceProgress.enhancedTransactions) of \(enhanceProgress.totalTransactions)"
2021-09-17 06:49:58 -07:00
2021-06-14 15:56:32 -07:00
case .fetching:
return "fetching UTXOs"
2021-09-17 06:49:58 -07:00
2021-06-14 15:56:32 -07:00
case .scanning(let scanProgress):
return "Scanning: \(scanProgress.progressHeight)/\(scanProgress.targetHeight)"
2021-09-17 06:49:58 -07:00
2020-06-18 16:53:11 -07:00
case .disconnected:
return "disconnected 💔"
2021-09-17 06:49:58 -07:00
2020-06-18 16:53:11 -07:00
case .stopped:
return "Stopped 🚫"
2021-09-17 06:49:58 -07:00
2020-06-18 16:53:11 -07:00
case .synced:
return "Synced 😎"
2021-09-17 06:49:58 -07:00
2021-05-19 14:49:08 -07:00
case .unprepared:
return "Unprepared 😅"
2021-09-17 06:49:58 -07:00
2021-06-14 15:56:32 -07:00
case .validating:
return "Validating"
2021-09-17 06:49:58 -07:00
2021-06-14 15:56:32 -07:00
case .error(let e):
return "Error: \(e.localizedDescription)"
2020-06-18 16:53:11 -07:00
}
}
}
extension Optional where Wrapped == String {
func asMemo() throws -> Memo {
switch self {
case .some(let string):
return try Memo(string: string)
case .none:
return .empty
}
}
}