383 lines
12 KiB
Swift
383 lines
12 KiB
Swift
//
|
|
// 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!
|
|
@IBOutlet weak var verifiedBalanceLabel: UILabel!
|
|
@IBOutlet weak var maxFunds: UISwitch!
|
|
@IBOutlet weak var sendButton: UIButton!
|
|
@IBOutlet weak var synchronizerStatusLabel: UILabel!
|
|
@IBOutlet weak var memoField: UITextView!
|
|
@IBOutlet weak var charactersLeftLabel: UILabel!
|
|
|
|
let characterLimit: Int = 512
|
|
|
|
var wallet = Initializer.shared
|
|
|
|
// swiftlint:disable:next implicitly_unwrapped_optional
|
|
var synchronizer: Synchronizer!
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
synchronizer = AppDelegate.shared.sharedSynchronizer
|
|
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()
|
|
} else if amountTextField.isFirstResponder && !amountTextField.frame.contains(point) {
|
|
amountTextField.resignFirstResponder()
|
|
} else if memoField.isFirstResponder &&
|
|
!memoField.frame.contains(point) {
|
|
memoField.resignFirstResponder()
|
|
}
|
|
}
|
|
|
|
func setUp() {
|
|
balanceLabel.text = format(balance: wallet.getBalance())
|
|
verifiedBalanceLabel.text = format(balance: wallet.getVerifiedBalance())
|
|
toggleSendButton()
|
|
memoField.text = ""
|
|
memoField.layer.borderColor = UIColor.gray.cgColor
|
|
memoField.layer.borderWidth = 1
|
|
memoField.layer.cornerRadius = 5
|
|
charactersLeftLabel.text = textForCharacterCount(0)
|
|
let center = NotificationCenter.default
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
func isBalanceValid() -> Bool {
|
|
wallet.getVerifiedBalance() > .zero
|
|
}
|
|
|
|
func isAmountValid() -> Bool {
|
|
guard
|
|
let value = amountTextField.text,
|
|
let amount = NumberFormatter.zcashNumberFormatter.number(from: value).flatMap({ Zatoshi($0.int64Value) }),
|
|
amount <= wallet.getVerifiedBalance()
|
|
else {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isRecipientValid() -> Bool {
|
|
guard let addr = self.addressTextField.text else {
|
|
return false
|
|
}
|
|
return wallet.isValidSaplingAddress(addr) || wallet.isValidTransparentAddress(addr)
|
|
}
|
|
|
|
@IBAction func maxFundsValueChanged(_ sender: Any) {
|
|
if maxFunds.isOn {
|
|
maxFundsOn()
|
|
} else {
|
|
maxFundsOff()
|
|
}
|
|
}
|
|
|
|
@IBAction func send(_ sender: Any) {
|
|
guard isFormValid() else {
|
|
loggerProxy.warn("WARNING: Form is invalid")
|
|
return
|
|
}
|
|
|
|
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 {
|
|
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) {
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func textForCharacterCount(_ count: Int) -> String {
|
|
"\(count) of \(characterLimit) bytes left"
|
|
}
|
|
}
|
|
|
|
extension SendViewController: UITextFieldDelegate {
|
|
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
|
if textField == amountTextField {
|
|
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
|
|
}
|
|
}
|
|
|
|
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 {
|
|
static func textFor(state: SyncStatus) -> String {
|
|
switch state {
|
|
case .downloading(let progress):
|
|
return "Downloading \(progress.progressHeight)/\(progress.targetHeight)"
|
|
|
|
case .enhancing(let enhanceProgress):
|
|
return "Enhancing tx \(enhanceProgress.enhancedTransactions) of \(enhanceProgress.totalTransactions)"
|
|
|
|
case .fetching:
|
|
return "fetching UTXOs"
|
|
|
|
case .scanning(let scanProgress):
|
|
return "Scanning: \(scanProgress.progressHeight)/\(scanProgress.targetHeight)"
|
|
|
|
case .disconnected:
|
|
return "disconnected 💔"
|
|
|
|
case .stopped:
|
|
return "Stopped 🚫"
|
|
|
|
case .synced:
|
|
return "Synced 😎"
|
|
|
|
case .unprepared:
|
|
return "Unprepared 😅"
|
|
|
|
case .validating:
|
|
return "Validating"
|
|
|
|
case .error(let e):
|
|
return "Error: \(e.localizedDescription)"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Optional where Wrapped == String {
|
|
func asMemo() throws -> Memo {
|
|
switch self {
|
|
case .some(let string):
|
|
return try Memo(string: string)
|
|
case .none:
|
|
return .empty
|
|
}
|
|
}
|
|
}
|