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

341 lines
11 KiB
Swift

//
// SendViewController.swift
// ZcashLightClientSample
//
// Created by Francisco Gindre on 12/3/19.
// Copyright © 2019 Electric Coin Company. All rights reserved.
//
import Combine
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!
// swiftlint:disable:next implicitly_unwrapped_optional
var closureSynchronizer: ClosureSynchronizer!
var cancellables: [AnyCancellable] = []
override func viewDidLoad() {
super.viewDidLoad()
synchronizer = AppDelegate.shared.sharedSynchronizer
closureSynchronizer = ClosureSDKSynchronizer(synchronizer: AppDelegate.shared.sharedSynchronizer)
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(viewTapped(_:)))
self.view.addGestureRecognizer(tapRecognizer)
setUp()
closureSynchronizer.prepare(
with: DemoAppConfig.defaultSeed,
walletBirthday: DemoAppConfig.defaultBirthdayHeight,
for: .existingWallet
) { result in
loggerProxy.debug("Prepare result: \(result)")
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
closureSynchronizer.start(retry: false) { [weak self] error in
DispatchQueue.main.async {
guard let self else { return }
self.synchronizerStatusLabel.text = SDKSynchronizer.textFor(state: self.synchronizer.latestState.syncStatus)
if let error {
self.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() {
Task { @MainActor in
await updateBalance()
await toggleSendButton()
}
memoField.text = ""
memoField.layer.borderColor = UIColor.gray.cgColor
memoField.layer.borderWidth = 1
memoField.layer.cornerRadius = 5
charactersLeftLabel.text = textForCharacterCount(0)
synchronizer.stateStream
.throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true)
.sink(
receiveValue: { [weak self] state in
Task { @MainActor in
await self?.updateBalance()
}
self?.synchronizerStatusLabel.text = SDKSynchronizer.textFor(state: state.syncStatus)
}
)
.store(in: &cancellables)
}
func updateBalance() async {
balanceLabel.text = format(balance: (try? await synchronizer.getAccountBalance(accountIndex: 0))?.saplingBalance.total() ?? .zero)
verifiedBalanceLabel.text = format(balance: (try? await synchronizer.getAccountBalance(accountIndex: 0))?.saplingBalance.spendableValue ?? .zero)
}
func format(balance: Zatoshi = Zatoshi()) -> String {
"Zec \(balance.formattedString ?? "0.0")"
}
func toggleSendButton() async {
sendButton.isEnabled = await isFormValid()
}
func maxFundsOn() {
Task { @MainActor in
let fee = Zatoshi(10000)
let max: Zatoshi = ((try? await synchronizer.getAccountBalance(accountIndex: 0))?.saplingBalance.spendableValue ?? .zero) - fee
amountTextField.text = format(balance: max)
amountTextField.isEnabled = false
}
}
func maxFundsOff() {
amountTextField.isEnabled = true
}
func isFormValid() async -> Bool {
switch synchronizer.latestState.syncStatus {
case .upToDate:
let isBalanceValid = await self.isBalanceValid()
let isAmountValid = await self.isAmountValid()
return isBalanceValid && isAmountValid && isRecipientValid()
default:
return false
}
}
func isBalanceValid() async -> Bool {
let balance = (try? await synchronizer.getAccountBalance(accountIndex: 0))?.saplingBalance.spendableValue ?? .zero
return balance > .zero
}
func isAmountValid() async -> Bool {
let balance = (try? await synchronizer.getAccountBalance(accountIndex: 0))?.saplingBalance.spendableValue ?? .zero
guard
let value = amountTextField.text,
let amount = NumberFormatter.zcashNumberFormatter.number(from: value).flatMap({ Zatoshi($0.int64Value) }),
amount <= balance
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) {
Task { @MainActor in
guard await isFormValid() else {
loggerProxy.warn("WARNING: Form is invalid")
return
}
let alert = UIAlertController(
title: "About To send funds!",
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() {
Task { @MainActor in
guard
await 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
}
let derivationTool = DerivationTool(networkType: kZcashNetwork.networkType)
guard let spendingKey = try? derivationTool.deriveUnifiedSpendingKey(seed: DemoAppConfig.defaultSeed, accountIndex: 0) else {
loggerProxy.error("NO SPENDING KEY")
return
}
KRProgressHUD.show()
do {
let pendingTransaction = try await synchronizer.sendToAddress(
spendingKey: spendingKey,
zatoshi: zec,
// swiftlint:disable:next force_try
toAddress: try! Recipient(recipient, network: kZcashNetwork.networkType),
// swiftlint:disable:next force_try
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() {}
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()
Task { @MainActor in
await 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 .syncing(let progress):
return "Syncing \(progress * 100.0)%"
case .upToDate:
return "Up to Date 😎"
case .unprepared:
return "Unprepared 😅"
case .stopped:
return "Stopped"
case .error(ZcashError.synchronizerDisconnected):
return "disconnected 💔"
case .error(let error):
return "Error: \(error)"
}
}
}
extension Optional where Wrapped == String {
func asMemo() throws -> Memo {
switch self {
case .some(let string):
return try Memo(string: string)
case .none:
return .empty
}
}
}