From 3295436270778e3470c39a67fa76a319390d57ed Mon Sep 17 00:00:00 2001 From: Christopher Pinski Date: Sun, 10 Nov 2019 21:22:52 -0600 Subject: [PATCH 1/7] Adding a user option for selecting their fee limit for payments --- .../Localizable/ExpiryTime+Localizable.swift | 20 +-- ...aymentFeeLimitPercentage+Localizable.swift | 20 +++ Library/Extensions/UIAlertController.swift | 16 +++ Library/Generated/strings.swift | 31 +++++ .../ModalDetail/Send/SendViewController.swift | 115 +++++++++++++----- .../ModalDetail/Send/SendViewModel.swift | 95 +++++++++++---- ...LightningPaymentFeeLimitSettingsItem.swift | 50 ++++++++ .../LightningRequestExpirySettingsItem.swift | 6 +- Library/Scenes/Settings/Settings.swift | 9 ++ .../Settings/SettingsViewController.swift | 3 +- Library/en.lproj/Localizable.strings | 11 ++ Lightning/Services/TransactionService.swift | 4 +- SwiftLnd/Api/LightningApi.swift | 4 +- SwiftLnd/Extensions/Protobuf+Extensions.swift | 13 +- .../Model/PaymentFeeLimitPercentage.swift | 16 +++ Zap.xcodeproj/project.pbxproj | 12 ++ 16 files changed, 356 insertions(+), 69 deletions(-) create mode 100644 Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift create mode 100644 Library/Scenes/Settings/Items/LightningPaymentFeeLimitSettingsItem.swift create mode 100644 SwiftLnd/Model/PaymentFeeLimitPercentage.swift diff --git a/Library/Extensions/Localizable/ExpiryTime+Localizable.swift b/Library/Extensions/Localizable/ExpiryTime+Localizable.swift index 43f610070..013d2ad19 100644 --- a/Library/Extensions/Localizable/ExpiryTime+Localizable.swift +++ b/Library/Extensions/Localizable/ExpiryTime+Localizable.swift @@ -12,23 +12,23 @@ extension ExpiryTime: Localizable { public var localized: String { switch self { case .oneMinute: - return L10n.ExpiryTime.oneMinute + return L10n.ExpiryTime.oneMinute case .tenMinutes: - return L10n.ExpiryTime.tenMinutes + return L10n.ExpiryTime.tenMinutes case .thirtyMinutes: - return L10n.ExpiryTime.thirtyMinutes + return L10n.ExpiryTime.thirtyMinutes case .oneHour: - return L10n.ExpiryTime.oneHour + return L10n.ExpiryTime.oneHour case .sixHours: - return L10n.ExpiryTime.sixHours + return L10n.ExpiryTime.sixHours case .oneDay: - return L10n.ExpiryTime.oneDay + return L10n.ExpiryTime.oneDay case .oneWeek: - return L10n.ExpiryTime.oneWeek + return L10n.ExpiryTime.oneWeek case .thirtyDays: - return L10n.ExpiryTime.thirtyDays + return L10n.ExpiryTime.thirtyDays case .oneYear: - return L10n.ExpiryTime.oneYear - } + return L10n.ExpiryTime.oneYear + } } } diff --git a/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift b/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift new file mode 100644 index 000000000..0e5ab5409 --- /dev/null +++ b/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift @@ -0,0 +1,20 @@ +// +// Library +// +// Created by Christopher Pinski on 10/26/19. +// Copyright © 2019 Zap. All rights reserved. +// + +import Foundation +import SwiftLnd + +extension PaymentFeeLimitPercentage: Localizable { + public var localized: String { + switch self { + case .zero: + return L10n.PaymentFeeLimitPercentage.none + default: + return "\(self.rawValue)%" + } + } +} diff --git a/Library/Extensions/UIAlertController.swift b/Library/Extensions/UIAlertController.swift index af37f02c8..744ba2431 100644 --- a/Library/Extensions/UIAlertController.swift +++ b/Library/Extensions/UIAlertController.swift @@ -45,4 +45,20 @@ extension UIAlertController { return alertController } + + static func feeLimitAlertController(message: String, sendAction: @escaping () -> Void) -> UIAlertController { + let title: String = L10n.Scene.Send.FeeAlert.title + let confirmButtonTitle: String = L10n.Scene.Send.FeeAlert.ConfirmButton.title + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let cancelAlertAction = UIAlertAction(title: L10n.Scene.Send.FeeAlert.CancelButton.title, style: .cancel, handler: nil) + + let confirmAlertAction = UIAlertAction(title: confirmButtonTitle, style: .default) { _ in + sendAction() + } + alertController.addAction(cancelAlertAction) + alertController.addAction(confirmAlertAction) + + return alertController + } } diff --git a/Library/Generated/strings.swift b/Library/Generated/strings.swift index 9a9b724df..9401bb416 100644 --- a/Library/Generated/strings.swift +++ b/Library/Generated/strings.swift @@ -130,6 +130,11 @@ internal enum L10n { } } } + + internal enum PaymentFeeLimitPercentage { + /// None + internal static let none = L10n.tr("Localizable", "payment_fee_limit_percentage.none") + } internal enum RpcConnectQrcodeError { /// Could not read BTCPay configurations file. @@ -588,15 +593,39 @@ internal enum L10n { internal static let successLabel = L10n.tr("Localizable", "scene.send.success_label") /// Send internal static let title = L10n.tr("Localizable", "scene.send.title") + /// The fee for this payment (%@) exceeds the limit specified in the settings (%@). + internal static func feeExceedsUserLimit(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "scene.send.fee_exceeds_user_limit", p1, p2) + } + /// The fee for this payment (%@ sats) will be higher than the payment amount (%@ sats). + internal static func feeExceedsPayment(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "scene.send.fee_exceeds_payment_amount", p1, p2) + } + internal enum FeeAlert { + /// Fee Limit Alert + internal static let title = L10n.tr("Localizable", "scene.send.fee_alert.title") + internal enum CancelButton { + /// No + internal static let title = L10n.tr("Localizable", "scene.send.fee_alert.cancel_button.title") + } + internal enum ConfirmButton { + /// Yes + internal static let title = L10n.tr("Localizable", "scene.send.fee_alert.confirm_button.title") + } + } internal enum Lightning { /// Send Lightning Payment internal static let title = L10n.tr("Localizable", "scene.send.lightning.title") + /// Do you really want to pay this invoice? + internal static let paymentConfirmation = L10n.tr("Localizable", "scene.send.lightning.payment_confirmation") } internal enum OnChain { /// Fee: internal static let fee = L10n.tr("Localizable", "scene.send.on_chain.fee") /// Send On-Chain internal static let title = L10n.tr("Localizable", "scene.send.on_chain.title") + /// Do you really want to send this payment? + internal static let paymentConfirmation = L10n.tr("Localizable", "scene.send.on_chain.payment_confirmation") internal enum Fee { /// Estimated Delivery: %@ internal static func estimatedDelivery(_ p1: String) -> String { @@ -649,6 +678,8 @@ internal enum L10n { internal static let currency = L10n.tr("Localizable", "scene.settings.item.currency") /// Need Help? internal static let help = L10n.tr("Localizable", "scene.settings.item.help") + /// Lightning Payment Fee Limit + internal static let lightningPaymentFeeLimit = L10n.tr("Localizable", "scene.settings.item.lightning_payment_fee_limit") /// Lightning Request Expiry internal static let lightningRequestExpiry = L10n.tr("Localizable", "scene.settings.item.lightning_request_expiry") /// Show lnd Log diff --git a/Library/Scenes/ModalDetail/Send/SendViewController.swift b/Library/Scenes/ModalDetail/Send/SendViewController.swift index 77f08a15b..ca918a4b7 100644 --- a/Library/Scenes/ModalDetail/Send/SendViewController.swift +++ b/Library/Scenes/ModalDetail/Send/SendViewController.swift @@ -11,6 +11,29 @@ import SwiftBTC import SwiftLnd import UIKit +extension Formatter { + static let asPercentage: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .percent + formatter.minimumSignificantDigits = 1 + formatter.maximumSignificantDigits = 3 + formatter.multiplier = 1 + return formatter + }() +} + +extension Int { + var formattedAsPercentage: String { + return Formatter.asPercentage.string(for: self) ?? "" + } +} + +extension Decimal { + var formattedAsPercentage: String { + return Formatter.asPercentage.string(for: self) ?? "" + } +} + final class SendViewController: ModalDetailViewController { private let viewModel: SendViewModel private let authenticationViewModel: AuthenticationViewModel @@ -87,6 +110,19 @@ final class SendViewController: ModalDetailViewController { self?.amountInputView?.subtitleTextColor = $0 } .dispose(in: reactive.bag) + + viewModel.sendStatus.observeNext { [weak self] feeLimitPercent in + self?.triggerSend(feeLimitPercent: feeLimitPercent) + }.dispose(in: reactive.bag) + + viewModel.sendStatus.observeFailed { [weak self] error in + switch error { + case .feeGreaterThanPayment(let feeInfo): + self?.showFeeLimitAlert(feePercentage: feeInfo.feePercentage, userFeeLimitPercent: feeInfo.userFeeLimitPercentage, sendFeeLimitPercent: feeInfo.sendFeeLimitPercentage) + case .feePercentageGreaterThanUserLimit(let feeInfo): + self?.showFeeLimitAlert(feePercentage: feeInfo.feePercentage, userFeeLimitPercent: feeInfo.userFeeLimitPercentage, sendFeeLimitPercent: feeInfo.sendFeeLimitPercentage) + } + }.dispose(in: reactive.bag) } private func addAmountInputView() { @@ -160,14 +196,29 @@ final class SendViewController: ModalDetailViewController { } private func sendButtonTapped() { - authenticate { [weak self] result in - switch result { - case .success: - self?.send() - case .failure: - Toast.presentError(L10n.Scene.Send.authenticationFailed) - } + viewModel.determineSendStatus() + } + + private func showFeeLimitAlert(feePercentage: Decimal, userFeeLimitPercent: Int, sendFeeLimitPercent: Int?) { + let message: String + switch viewModel.method { + case .onChain: + message = """ + \(L10n.Scene.Send.feeExceedsUserLimit(feePercentage.formattedAsPercentage, userFeeLimitPercent.formattedAsPercentage)) + + \(L10n.Scene.Send.OnChain.paymentConfirmation) + """ + case .lightning: + message = """ + \(L10n.Scene.Send.feeExceedsUserLimit(feePercentage.formattedAsPercentage, userFeeLimitPercent.formattedAsPercentage)) + + \(L10n.Scene.Send.Lightning.paymentConfirmation) + """ + } + let controller = UIAlertController.feeLimitAlertController(message: message) { [weak self] in + self?.triggerSend(feeLimitPercent: sendFeeLimitPercent) } + self.present(controller, animated: true) } private func presentLoading() { @@ -252,29 +303,35 @@ final class SendViewController: ModalDetailViewController { self?.dismissParent() } } - - private func send() { - let sendStartTime = Date() - - presentLoading() - - viewModel.send { [weak self] result in - let minimumLoadingTime: TimeInterval = 1 - let sendingTime = Date().timeIntervalSince(sendStartTime) - let delay = max(0, minimumLoadingTime - sendingTime) - - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay) { - switch result { - case .success: - - self?.presentSuccess() - case .failure(let error): - UINotificationFeedbackGenerator().notificationOccurred(.error) - - Toast.presentError(error.localizedDescription) - self?.amountInputView?.isEnabled = true - self?.recoverFromLoadingState() + + private func triggerSend(feeLimitPercent: Int?) { + authenticate { [weak self] result in + switch result { + case .success: + let sendStartTime = Date() + + self?.presentLoading() + self?.viewModel.send(feeLimitPercent: feeLimitPercent) { [weak self] result in + let minimumLoadingTime: TimeInterval = 1 + let sendingTime = Date().timeIntervalSince(sendStartTime) + let delay = max(0, minimumLoadingTime - sendingTime) + + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay) { + switch result { + case .success: + + self?.presentSuccess() + case .failure(let error): + UINotificationFeedbackGenerator().notificationOccurred(.error) + + Toast.presentError(error.localizedDescription) + self?.amountInputView?.isEnabled = true + self?.recoverFromLoadingState() + } + } } + case .failure: + Toast.presentError(L10n.Scene.Send.authenticationFailed) } } } diff --git a/Library/Scenes/ModalDetail/Send/SendViewModel.swift b/Library/Scenes/ModalDetail/Send/SendViewModel.swift index 5f76233cc..5d343b4fa 100644 --- a/Library/Scenes/ModalDetail/Send/SendViewModel.swift +++ b/Library/Scenes/ModalDetail/Send/SendViewModel.swift @@ -8,6 +8,7 @@ import Bond import Foundation import Lightning +import ReactiveKit import SwiftBTC import SwiftLnd @@ -47,13 +48,26 @@ final class SendViewModel: NSObject { } } } + + struct FeeInfo { + var feePercentage: Decimal + var userFeeLimitPercentage: Int + var sendFeeLimitPercentage: Int? + } + + enum SendError: Error { + case feeGreaterThanPayment(FeeInfo) + case feePercentageGreaterThanUserLimit(FeeInfo) + } + let paymentFeeThreshold: Satoshi = 100 let fee = Observable>(.loading) - let method: SendMethod - let subtitleText = Observable(nil) let isSubtitleTextWarning = Observable(false) + let sendStatus = Subject() + + private var feePercent: Decimal? var amount: Satoshi? { didSet { @@ -121,6 +135,58 @@ final class SendViewModel: NSObject { updateSubtitle() } + func send(feeLimitPercent: Int?, completion: @escaping ApiCompletion) { + guard let amount = amount else { return } + + isSending = true + + let internalComplection: ApiCompletion = { [weak self] in + if case .failure = $0 { + self?.isSending = false + } + completion($0) + } + + switch method { + case .lightning(let paymentRequest): + lightningService.transactionService.sendPayment(paymentRequest, amount: amount, feeLimitPercent: feeLimitPercent, completion: internalComplection) + case .onChain(let bitcoinURI): + lightningService.transactionService.sendCoins(bitcoinURI: bitcoinURI, amount: amount, confirmationTarget: confirmationTarget, completion: internalComplection) + } + } + + func determineSendStatus() { + guard let amount = amount, let feePercent = feePercent else { return } + + let actualFee: Satoshi + + switch fee.value { + case .element(let fee): + guard let fee = fee else { return } + actualFee = fee + default: + return + } + + if amount <= paymentFeeThreshold { + if actualFee >= amount { + self.sendStatus.receive(event: .failed(.feeGreaterThanPayment(FeeInfo(feePercentage: feePercent, userFeeLimitPercentage: Settings.shared.lightningPaymentFeeLimit.value.rawValue, sendFeeLimitPercentage: nil)))) + } else { + self.sendStatus.receive(100) + } + } else if actualFee >= amount { + self.sendStatus.receive(event: .failed(.feeGreaterThanPayment(FeeInfo(feePercentage: feePercent, userFeeLimitPercentage: Settings.shared.lightningPaymentFeeLimit.value.rawValue, sendFeeLimitPercentage: nil)))) + } else if Settings.shared.lightningPaymentFeeLimit.value == .zero { + self.sendStatus.receive(nil) + } else { + if feePercent > Decimal(Settings.shared.lightningPaymentFeeLimit.value.rawValue) { + self.sendStatus.receive(event: .failed(.feePercentageGreaterThanUserLimit(FeeInfo(feePercentage: feePercent, userFeeLimitPercentage: Settings.shared.lightningPaymentFeeLimit.value.rawValue, sendFeeLimitPercentage: 100)))) + } else { + self.sendStatus.receive(Settings.shared.lightningPaymentFeeLimit.value.rawValue) + } + } + } + private func updateSubtitle() { Settings.shared.primaryCurrency .compactMap { [method, maxPaymentAmount] in @@ -152,10 +218,12 @@ final class SendViewModel: NSObject { private func updateFee() { if isAmountValid { fee.value = .loading + feePercent = nil updateIsUIEnabled() debounceFetchFee() } else { fee.value = .element(nil) + feePercent = nil } } @@ -169,6 +237,7 @@ final class SendViewModel: NSObject { else { return } self.fee.value = .element(result.fee) + self.calculateFeePercent(fee: result.fee ?? result.amount, amount: result.amount) self.updateIsUIEnabled() } @@ -179,24 +248,8 @@ final class SendViewModel: NSObject { lightningService.transactionService.onChainFees(address: bitcoinURI.bitcoinAddress, amount: amount, confirmationTarget: confirmationTarget, completion: feeCompletion) } } - - func send(completion: @escaping ApiCompletion) { - guard let amount = amount else { return } - - isSending = true - - let internalComplection: ApiCompletion = { [weak self] in - if case .failure = $0 { - self?.isSending = false - } - completion($0) - } - - switch method { - case .lightning(let paymentRequest): - lightningService.transactionService.sendPayment(paymentRequest, amount: amount, completion: internalComplection) - case .onChain(let bitcoinURI): - lightningService.transactionService.sendCoins(bitcoinURI: bitcoinURI, amount: amount, confirmationTarget: confirmationTarget, completion: internalComplection) - } + + private func calculateFeePercent(fee: Satoshi, amount: Satoshi) { + feePercent = ( fee / amount ) * 100 } } diff --git a/Library/Scenes/Settings/Items/LightningPaymentFeeLimitSettingsItem.swift b/Library/Scenes/Settings/Items/LightningPaymentFeeLimitSettingsItem.swift new file mode 100644 index 000000000..0fb8fa82d --- /dev/null +++ b/Library/Scenes/Settings/Items/LightningPaymentFeeLimitSettingsItem.swift @@ -0,0 +1,50 @@ +// +// Library +// +// Created by Christopher Pinski on 10/12/19. +// Copyright © 2019 Zap. All rights reserved. +// + +import Bond +import Foundation +import SwiftLnd + +final class LightningPaymentFeeLimitSelectionSettingsItem: DetailDisclosureSettingsItem, SubtitleSettingsItem { + let subtitle = Settings.shared.lightningPaymentFeeLimit.map { Optional($0.localized) } + + let title = L10n.Scene.Settings.Item.lightningPaymentFeeLimit + + func didSelectItem(from fromViewController: UIViewController) { + let items: [SettingsItem] = PaymentFeeLimitPercentage.allCases.map { LightningPaymentFeeLimitSettingsItem(percentage: $0) } + let section = Section(title: nil, rows: items) + + let viewController = GroupedTableViewController(sections: [section]) + viewController.title = title + viewController.navigationItem.largeTitleDisplayMode = .never + + fromViewController.navigationController?.show(viewController, sender: nil) + } +} + +final class LightningPaymentFeeLimitSettingsItem: NSObject, SelectableSettingsItem { + var isSelectedOption = Observable(false) + + let title: String + private let percentage: PaymentFeeLimitPercentage + + init(percentage: PaymentFeeLimitPercentage) { + self.percentage = percentage + title = percentage.localized + super.init() + + Settings.shared.lightningPaymentFeeLimit + .observeNext { [isSelectedOption] currentPercentage in + isSelectedOption.value = currentPercentage == percentage + } + .dispose(in: reactive.bag) + } + + func didSelectItem(from fromViewController: UIViewController) { + Settings.shared.lightningPaymentFeeLimit.value = percentage + } +} diff --git a/Library/Scenes/Settings/Items/LightningRequestExpirySettingsItem.swift b/Library/Scenes/Settings/Items/LightningRequestExpirySettingsItem.swift index d5b29bf3e..8dec71fd6 100644 --- a/Library/Scenes/Settings/Items/LightningRequestExpirySettingsItem.swift +++ b/Library/Scenes/Settings/Items/LightningRequestExpirySettingsItem.swift @@ -34,12 +34,12 @@ final class LightningRequestExpirySettingsItem: NSObject, SelectableSettingsItem init(expiryTime: ExpiryTime) { self.expiryTime = expiryTime - title = "\(expiryTime.localized)" + title = expiryTime.localized super.init() Settings.shared.lightningRequestExpiry - .observeNext { [isSelectedOption] currenteExpiryTime in - isSelectedOption.value = currenteExpiryTime == expiryTime + .observeNext { [isSelectedOption] currentExpiryTime in + isSelectedOption.value = currentExpiryTime == expiryTime } .dispose(in: reactive.bag) } diff --git a/Library/Scenes/Settings/Settings.swift b/Library/Scenes/Settings/Settings.swift index 03cf2a87e..5e4ba2230 100644 --- a/Library/Scenes/Settings/Settings.swift +++ b/Library/Scenes/Settings/Settings.swift @@ -14,6 +14,7 @@ import SwiftLnd public final class Settings: NSObject, Persistable { // Persistable public typealias Value = SettingsData + public var data: SettingsData = SettingsData() { didSet { savePersistable() @@ -28,6 +29,7 @@ public final class Settings: NSObject, Persistable { var blockExplorer: BlockExplorer? var onChainRequestAddressType: OnChainRequestAddressType? var lightningRequestExpiry: ExpiryTime? + var lightningPaymentFeeLimit: PaymentFeeLimitPercentage? } public let primaryCurrency: Observable @@ -38,6 +40,7 @@ public final class Settings: NSObject, Persistable { let blockExplorer: Observable let onChainRequestAddressType: Observable let lightningRequestExpiry: Observable + let lightningPaymentFeeLimit: Observable public static let shared = Settings() @@ -55,6 +58,7 @@ public final class Settings: NSObject, Persistable { blockExplorer = Observable(data?.blockExplorer ?? .blockstream) onChainRequestAddressType = Observable(data?.onChainRequestAddressType ?? .witnessPubkeyHash) lightningRequestExpiry = Observable(data?.lightningRequestExpiry ?? .oneHour) + lightningPaymentFeeLimit = Observable(data?.lightningPaymentFeeLimit ?? .one) super.init() @@ -95,6 +99,11 @@ public final class Settings: NSObject, Persistable { .skip(first: 1) .observeNext { [weak self] in self?.data.lightningRequestExpiry = $0 + }, + lightningPaymentFeeLimit + .skip(first: 1) + .observeNext { [weak self] in + self?.data.lightningPaymentFeeLimit = $0 } ].dispose(in: reactive.bag) } diff --git a/Library/Scenes/Settings/SettingsViewController.swift b/Library/Scenes/Settings/SettingsViewController.swift index 85d6e230d..a79e3b7c2 100644 --- a/Library/Scenes/Settings/SettingsViewController.swift +++ b/Library/Scenes/Settings/SettingsViewController.swift @@ -40,7 +40,8 @@ final class SettingsViewController: GroupedTableViewController { BitcoinUnitSelectionSettingsItem(), OnChainRequestAddressTypeSelectionSettingsItem(), BlockExplorerSelectionSettingsItem(), - LightningRequestExpirySelectionSettingsItem() + LightningRequestExpirySelectionSettingsItem(), + LightningPaymentFeeLimitSelectionSettingsItem() ]) ] sections.append(contentsOf: [ diff --git a/Library/en.lproj/Localizable.strings b/Library/en.lproj/Localizable.strings index 904b38f0f..fd8920213 100644 --- a/Library/en.lproj/Localizable.strings +++ b/Library/en.lproj/Localizable.strings @@ -54,6 +54,8 @@ "expiry_time.thirty_days" = "30 Days"; "expiry_time.one_year" = "1 Year"; +"payment_fee_limit_percentage.none" = "None"; + "scene.history.title" = "Activity"; "scene.history.empty_state_label" = "0 transactions 🙁"; @@ -63,6 +65,7 @@ "scene.settings.section.wallet" = "Wallet"; "scene.settings.item.version_warning" = "Your lnd is outdated (%@). Zap iOS works best with lnd version %@ or above."; "scene.settings.item.bitcoin_unit" = "Bitcoin Unit"; +"scene.settings.item.lightning_payment_fee_limit" = "Lightning Payment Fee Limit"; "scene.settings.item.lightning_request_expiry" = "Lightning Request Expiry"; "scene.settings.item.currency" = "Currency"; "scene.settings.item.currency.popular" = "Popular Currencies"; @@ -161,6 +164,14 @@ "scene.send.subtitle.lightning_can_send_balance" = "Can send: %@"; "scene.send.success_label" = "Payment Successful"; "scene.send.paste_button.title" = "Paste Address"; +"scene.send.fee_exceeds_user_limit" = "The fee for this payment (%@) exceeds the limit specified in the settings (%@)."; +"scene.send.fee_exceeds_payment_amount" = "The fee for this payment (%@ sats) will be higher than the payment amount (%@ sats)."; +"scene.send.lightning.payment_confirmation" = "Do you really want to pay this invoice?"; +"scene.send.on_chain.payment_confirmation" = "Do you really want to send this payment?"; + +"scene.send.fee_alert.title" = "Fee Limit Alert"; +"scene.send.fee_alert.cancel_button.title" = "No"; +"scene.send.fee_alert.confirm_button.title" = "Yes"; "scene.connect_remote_node.title" = "Connect Remote Node"; "scene.connect_remote_node.empty_state" = "Scan the QR Code generated by lndconnect, BTCPay Server, or paste the link you get from running 'lndconnect -j' to connect to your node."; diff --git a/Lightning/Services/TransactionService.swift b/Lightning/Services/TransactionService.swift index 73f25b8bd..734a388aa 100644 --- a/Lightning/Services/TransactionService.swift +++ b/Lightning/Services/TransactionService.swift @@ -54,8 +54,8 @@ public final class TransactionService { } } - public func sendPayment(_ paymentRequest: PaymentRequest, amount: Satoshi, completion: @escaping ApiCompletion) { - api.sendPayment(paymentRequest, amount: amount) { [balanceService, paymentListUpdater] in + public func sendPayment(_ paymentRequest: PaymentRequest, amount: Satoshi, feeLimitPercent: Int?, completion: @escaping ApiCompletion) { + api.sendPayment(paymentRequest, amount: amount, feeLimitPercent: feeLimitPercent) { [balanceService, paymentListUpdater] in if case .success(let payment) = $0 { balanceService.update() paymentListUpdater.add(payment: payment) diff --git a/SwiftLnd/Api/LightningApi.swift b/SwiftLnd/Api/LightningApi.swift index 1050f5b35..b1d0cef4d 100644 --- a/SwiftLnd/Api/LightningApi.swift +++ b/SwiftLnd/Api/LightningApi.swift @@ -118,8 +118,8 @@ public final class LightningApi { connection.listPayments(Lnrpc_ListPaymentsRequest(), completion: run(completion) { $0.payments.map(Payment.init) }) } - public func sendPayment(_ paymentRequest: PaymentRequest, amount: Satoshi?, completion: @escaping ApiCompletion) { - let request = Lnrpc_SendRequest(paymentRequest: paymentRequest.raw, amount: amount) + public func sendPayment(_ paymentRequest: PaymentRequest, amount: Satoshi?, feeLimitPercent: Int?, completion: @escaping ApiCompletion) { + let request = Lnrpc_SendRequest(paymentRequest: paymentRequest.raw, amount: amount, feeLimitPercent: feeLimitPercent) connection.sendPaymentSync(request) { result in switch result { case .success(let value): diff --git a/SwiftLnd/Extensions/Protobuf+Extensions.swift b/SwiftLnd/Extensions/Protobuf+Extensions.swift index a3d0362ec..852fac9c4 100644 --- a/SwiftLnd/Extensions/Protobuf+Extensions.swift +++ b/SwiftLnd/Extensions/Protobuf+Extensions.swift @@ -90,13 +90,24 @@ extension Lnrpc_SendCoinsRequest { } extension Lnrpc_SendRequest { - init(paymentRequest: String, amount: Satoshi?) { + init(paymentRequest: String, amount: Satoshi?, feeLimitPercent: Int?) { self.init() self.paymentRequest = paymentRequest if let amount = amount { self.amt = amount.int64 } + if let feeLimitPercent = feeLimitPercent { + self.feeLimit = Lnrpc_FeeLimit(percent: feeLimitPercent) + } + } +} + +extension Lnrpc_FeeLimit { + init(percent: Int) { + self.init() + + self.percent = Int64(percent) } } diff --git a/SwiftLnd/Model/PaymentFeeLimitPercentage.swift b/SwiftLnd/Model/PaymentFeeLimitPercentage.swift new file mode 100644 index 000000000..72a0bd994 --- /dev/null +++ b/SwiftLnd/Model/PaymentFeeLimitPercentage.swift @@ -0,0 +1,16 @@ +// +// SwiftLnd +// +// Created by Christopher Pinski on 10/26/19. +// Copyright © 2019 Zap. All rights reserved. +// + +import Foundation + +public enum PaymentFeeLimitPercentage: Int, Codable, CaseIterable { + case zero = 0 + case one = 1 + case three = 3 + case five = 5 + case ten = 10 +} diff --git a/Zap.xcodeproj/project.pbxproj b/Zap.xcodeproj/project.pbxproj index cf935301b..825ea7f88 100644 --- a/Zap.xcodeproj/project.pbxproj +++ b/Zap.xcodeproj/project.pbxproj @@ -12,9 +12,12 @@ 37DC0DB072FDF3A74598EA06 /* Pods_RPC_Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71E4E79032A371505A0E58C1 /* Pods_RPC_Library.framework */; }; 3FCED3EA112663089C273764 /* *.bitcoinaverage.com.cer in Resources */ = {isa = PBXBuildFile; fileRef = 3FCED45A10ACE912173010E1 /* *.bitcoinaverage.com.cer */; }; 4295D217D276D875A94F4622 /* Pods_RPC_SwiftLnd.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B55A0B343498CAAB75872BD /* Pods_RPC_SwiftLnd.framework */; }; + 5C10EAD92352DE5900FB80DA /* LightningPaymentFeeLimitSettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10EAD82352DE5900FB80DA /* LightningPaymentFeeLimitSettingsItem.swift */; }; 5C18BB6F234C015100BCF9D9 /* ExpiryTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C18BB6E234C015100BCF9D9 /* ExpiryTime.swift */; }; 5C18BB71234C09EB00BCF9D9 /* LightningRequestExpirySettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C18BB70234C09EB00BCF9D9 /* LightningRequestExpirySettingsItem.swift */; }; 5C18BB77234C0BF400BCF9D9 /* ExpiryTime+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C18BB76234C0BF400BCF9D9 /* ExpiryTime+Localizable.swift */; }; + 5CA13678236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA13677236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift */; }; + 5CD423AD2364FB160031194F /* PaymentFeeLimitPercentage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD423AC2364FB160031194F /* PaymentFeeLimitPercentage.swift */; }; 5E7CF0840E8C74EB58F80DB5 /* Pods_SnapshotUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6AEA9B7EDFEBE4BBEB56DAD /* Pods_SnapshotUITests.framework */; }; 6A9518DA222DCFCA0008FE4F /* Array2DElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9518D9222DCFCA0008FE4F /* Array2DElement.swift */; }; 7BE67678020C235E19A3D64E /* Pods_SwiftLndTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71876EABB08A5425C9DA33C7 /* Pods_SwiftLndTests.framework */; }; @@ -551,9 +554,12 @@ 4D052C7425BFF1AED850782A /* Pods-LibraryTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LibraryTests.debug.xcconfig"; path = "Target Support Files/Pods-LibraryTests/Pods-LibraryTests.debug.xcconfig"; sourceTree = ""; }; 4E372F564A61EB8E70FA424B /* Pods-SnapshotUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SnapshotUITests.release.xcconfig"; path = "Target Support Files/Pods-SnapshotUITests/Pods-SnapshotUITests.release.xcconfig"; sourceTree = ""; }; 5284CC239994D6B844AD6ACE /* Pods-Zap.debugremote.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Zap.debugremote.xcconfig"; path = "Target Support Files/Pods-Zap/Pods-Zap.debugremote.xcconfig"; sourceTree = ""; }; + 5C10EAD82352DE5900FB80DA /* LightningPaymentFeeLimitSettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightningPaymentFeeLimitSettingsItem.swift; sourceTree = ""; }; 5C18BB6E234C015100BCF9D9 /* ExpiryTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiryTime.swift; sourceTree = ""; }; 5C18BB70234C09EB00BCF9D9 /* LightningRequestExpirySettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightningRequestExpirySettingsItem.swift; sourceTree = ""; }; 5C18BB76234C0BF400BCF9D9 /* ExpiryTime+Localizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExpiryTime+Localizable.swift"; sourceTree = ""; }; + 5CA13677236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaymentFeeLimitPercentage+Localizable.swift"; sourceTree = ""; }; + 5CD423AC2364FB160031194F /* PaymentFeeLimitPercentage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentFeeLimitPercentage.swift; sourceTree = ""; }; 5E8DFC750E5DE2109A4B90CB /* Pods-RPC-Lightning.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RPC-Lightning.debug.xcconfig"; path = "Target Support Files/Pods-RPC-Lightning/Pods-RPC-Lightning.debug.xcconfig"; sourceTree = ""; }; 5F7C5272C0C2909B7C7987FF /* Pods-SnapshotUITests.debugremote.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SnapshotUITests.debugremote.xcconfig"; path = "Target Support Files/Pods-SnapshotUITests/Pods-SnapshotUITests.debugremote.xcconfig"; sourceTree = ""; }; 6A9518D9222DCFCA0008FE4F /* Array2DElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array2DElement.swift; sourceTree = ""; }; @@ -1293,6 +1299,7 @@ AD28CB7A211DE93300A31004 /* BlockExplorerSettingsItem.swift */, ADCD5B0E20D7E40C0037F156 /* ChangePinSettingsItem.swift */, A091914A203B297D00FA525A /* CurrencySettingsItem.swift */, + 5C10EAD82352DE5900FB80DA /* LightningPaymentFeeLimitSettingsItem.swift */, 5C18BB70234C09EB00BCF9D9 /* LightningRequestExpirySettingsItem.swift */, AD6FFCA520AB0A3700B57330 /* OnChainRequestAddressTypeSettingsItem.swift */, ADFE2E4B216CF4A800988243 /* PushViewControllerSettingsItem.swift */, @@ -1412,6 +1419,7 @@ ADECC36A20F257BE00D00546 /* Transaction.swift */, AD511B272253744800EDFC7C /* Version.swift */, AD248627225B7E130049532C /* WalletBalance.swift */, + 5CD423AC2364FB160031194F /* PaymentFeeLimitPercentage.swift */, ); path = Model; sourceTree = ""; @@ -1777,6 +1785,7 @@ A0C2EABD206F8B5B003AE56E /* Localizable.swift */, AD77464D2076162300EC596B /* Network+Localizable.swift */, AD967FEF21062AB20048085B /* RPCConnectQRCodeError+Localizable.swift */, + 5CA13677236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift */, ); path = Localizable; sourceTree = ""; @@ -3145,6 +3154,7 @@ AD931BD122DE23CC0048431C /* Password.swift in Sources */, ADA2DD03225CDAED007482D9 /* ChannelQRCodeScannerViewController.swift in Sources */, ADEEB5362134669B00D2F992 /* ModalPinViewController.swift in Sources */, + 5C10EAD92352DE5900FB80DA /* LightningPaymentFeeLimitSettingsItem.swift in Sources */, AD391CDB20D16FE1007EE22A /* HistoryViewController.swift in Sources */, AD667E572136E622007B9160 /* TimeLockStore.swift in Sources */, AD4B4BE122EAF0CE0029773A /* EmptyStateView.swift in Sources */, @@ -3291,6 +3301,7 @@ ADCEAFD522579C2A004F605B /* LoadingAnimationView.swift in Sources */, AD391C5F20D16F7A007EE22A /* RecoverWalletViewController.swift in Sources */, AD391C6B20D16F89007EE22A /* InputNumberFormatter.swift in Sources */, + 5CA13678236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift in Sources */, AD391C4920D16F68007EE22A /* ModalPresentationController.swift in Sources */, AD391CA820D16FA8007EE22A /* SendQRCodeScannerStrategy.swift in Sources */, AD391C5520D16F7A007EE22A /* ConnectRemoteNodeViewController.swift in Sources */, @@ -3425,6 +3436,7 @@ AD79E424219D90EF002589CA /* String+Extensions.swift in Sources */, ADADAD8D2143B3C900A48E1F /* Info.swift in Sources */, ADFB36D72143B73C00146FCC /* LndConstants.swift in Sources */, + 5CD423AD2364FB160031194F /* PaymentFeeLimitPercentage.swift in Sources */, ADADAD8C2143B3C900A48E1F /* GraphTopologyUpdate.swift in Sources */, AD24862A225B7F210049532C /* ChannelBalance.swift in Sources */, AD5AA2E72271B64500AEDDD7 /* rpc.pb.swift in Sources */, From 82a0b65d04fab7ee8f891ef8c469f6dc52769cd3 Mon Sep 17 00:00:00 2001 From: Christopher Pinski Date: Mon, 11 Nov 2019 14:25:57 -0600 Subject: [PATCH 2/7] Removing unnecessary weak self declaration --- Library/Scenes/ModalDetail/Send/SendViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Scenes/ModalDetail/Send/SendViewController.swift b/Library/Scenes/ModalDetail/Send/SendViewController.swift index ca918a4b7..1ab0cd07d 100644 --- a/Library/Scenes/ModalDetail/Send/SendViewController.swift +++ b/Library/Scenes/ModalDetail/Send/SendViewController.swift @@ -311,7 +311,7 @@ final class SendViewController: ModalDetailViewController { let sendStartTime = Date() self?.presentLoading() - self?.viewModel.send(feeLimitPercent: feeLimitPercent) { [weak self] result in + self?.viewModel.send(feeLimitPercent: feeLimitPercent) { result in let minimumLoadingTime: TimeInterval = 1 let sendingTime = Date().timeIntervalSince(sendStartTime) let delay = max(0, minimumLoadingTime - sendingTime) From 74dddb7520cda71d92878fbda3ffea8e7afc45d9 Mon Sep 17 00:00:00 2001 From: Christopher Pinski Date: Mon, 11 Nov 2019 14:56:35 -0600 Subject: [PATCH 3/7] Moving PaymentFeeLimitPercentage to Library --- .../PaymentFeeLimitPercentage+Localizable.swift | 1 - .../ModalDetail/Send/SendViewController.swift | 4 +--- .../LightningPaymentFeeLimitSettingsItem.swift | 1 - .../Model/PaymentFeeLimitPercentage.swift | 2 +- Zap.xcodeproj/project.pbxproj | 16 ++++++++++++---- 5 files changed, 14 insertions(+), 10 deletions(-) rename {SwiftLnd => Library/Scenes/Settings}/Model/PaymentFeeLimitPercentage.swift (95%) diff --git a/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift b/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift index 0e5ab5409..b05c58f87 100644 --- a/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift +++ b/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift @@ -6,7 +6,6 @@ // import Foundation -import SwiftLnd extension PaymentFeeLimitPercentage: Localizable { public var localized: String { diff --git a/Library/Scenes/ModalDetail/Send/SendViewController.swift b/Library/Scenes/ModalDetail/Send/SendViewController.swift index 1ab0cd07d..614673e7e 100644 --- a/Library/Scenes/ModalDetail/Send/SendViewController.swift +++ b/Library/Scenes/ModalDetail/Send/SendViewController.swift @@ -308,9 +308,9 @@ final class SendViewController: ModalDetailViewController { authenticate { [weak self] result in switch result { case .success: + self?.presentLoading() let sendStartTime = Date() - self?.presentLoading() self?.viewModel.send(feeLimitPercent: feeLimitPercent) { result in let minimumLoadingTime: TimeInterval = 1 let sendingTime = Date().timeIntervalSince(sendStartTime) @@ -319,11 +319,9 @@ final class SendViewController: ModalDetailViewController { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delay) { switch result { case .success: - self?.presentSuccess() case .failure(let error): UINotificationFeedbackGenerator().notificationOccurred(.error) - Toast.presentError(error.localizedDescription) self?.amountInputView?.isEnabled = true self?.recoverFromLoadingState() diff --git a/Library/Scenes/Settings/Items/LightningPaymentFeeLimitSettingsItem.swift b/Library/Scenes/Settings/Items/LightningPaymentFeeLimitSettingsItem.swift index 0fb8fa82d..c886212e8 100644 --- a/Library/Scenes/Settings/Items/LightningPaymentFeeLimitSettingsItem.swift +++ b/Library/Scenes/Settings/Items/LightningPaymentFeeLimitSettingsItem.swift @@ -7,7 +7,6 @@ import Bond import Foundation -import SwiftLnd final class LightningPaymentFeeLimitSelectionSettingsItem: DetailDisclosureSettingsItem, SubtitleSettingsItem { let subtitle = Settings.shared.lightningPaymentFeeLimit.map { Optional($0.localized) } diff --git a/SwiftLnd/Model/PaymentFeeLimitPercentage.swift b/Library/Scenes/Settings/Model/PaymentFeeLimitPercentage.swift similarity index 95% rename from SwiftLnd/Model/PaymentFeeLimitPercentage.swift rename to Library/Scenes/Settings/Model/PaymentFeeLimitPercentage.swift index 72a0bd994..f342159bf 100644 --- a/SwiftLnd/Model/PaymentFeeLimitPercentage.swift +++ b/Library/Scenes/Settings/Model/PaymentFeeLimitPercentage.swift @@ -1,5 +1,5 @@ // -// SwiftLnd +// Library // // Created by Christopher Pinski on 10/26/19. // Copyright © 2019 Zap. All rights reserved. diff --git a/Zap.xcodeproj/project.pbxproj b/Zap.xcodeproj/project.pbxproj index 825ea7f88..306778059 100644 --- a/Zap.xcodeproj/project.pbxproj +++ b/Zap.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ 5C18BB71234C09EB00BCF9D9 /* LightningRequestExpirySettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C18BB70234C09EB00BCF9D9 /* LightningRequestExpirySettingsItem.swift */; }; 5C18BB77234C0BF400BCF9D9 /* ExpiryTime+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C18BB76234C0BF400BCF9D9 /* ExpiryTime+Localizable.swift */; }; 5CA13678236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA13677236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift */; }; - 5CD423AD2364FB160031194F /* PaymentFeeLimitPercentage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD423AC2364FB160031194F /* PaymentFeeLimitPercentage.swift */; }; + 5CC505812379FF5E00FB1C19 /* PaymentFeeLimitPercentage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD423AC2364FB160031194F /* PaymentFeeLimitPercentage.swift */; }; 5E7CF0840E8C74EB58F80DB5 /* Pods_SnapshotUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6AEA9B7EDFEBE4BBEB56DAD /* Pods_SnapshotUITests.framework */; }; 6A9518DA222DCFCA0008FE4F /* Array2DElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9518D9222DCFCA0008FE4F /* Array2DElement.swift */; }; 7BE67678020C235E19A3D64E /* Pods_SwiftLndTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71876EABB08A5425C9DA33C7 /* Pods_SwiftLndTests.framework */; }; @@ -1183,6 +1183,14 @@ path = Pods; sourceTree = ""; }; + 5CC505802379FF2800FB1C19 /* Model */ = { + isa = PBXGroup; + children = ( + 5CD423AC2364FB160031194F /* PaymentFeeLimitPercentage.swift */, + ); + path = Model; + sourceTree = ""; + }; A01009322014BD960001EF94 /* Scenes */ = { isa = PBXGroup; children = ( @@ -1419,7 +1427,6 @@ ADECC36A20F257BE00D00546 /* Transaction.swift */, AD511B272253744800EDFC7C /* Version.swift */, AD248627225B7E130049532C /* WalletBalance.swift */, - 5CD423AC2364FB160031194F /* PaymentFeeLimitPercentage.swift */, ); path = Model; sourceTree = ""; @@ -1468,6 +1475,7 @@ isa = PBXGroup; children = ( A06660F8202E070D00EE32FA /* Items */, + 5CC505802379FF2800FB1C19 /* Model */, A0919144203B1CF400FA525A /* GroupedTableViewController.swift */, A0DDC06D2018C08800AEFF94 /* Section.swift */, A0A05A5620149A0D0007D1C9 /* Settings.swift */, @@ -1784,8 +1792,8 @@ 5C18BB76234C0BF400BCF9D9 /* ExpiryTime+Localizable.swift */, A0C2EABD206F8B5B003AE56E /* Localizable.swift */, AD77464D2076162300EC596B /* Network+Localizable.swift */, - AD967FEF21062AB20048085B /* RPCConnectQRCodeError+Localizable.swift */, 5CA13677236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift */, + AD967FEF21062AB20048085B /* RPCConnectQRCodeError+Localizable.swift */, ); path = Localizable; sourceTree = ""; @@ -3139,6 +3147,7 @@ AD391C3920D16F0B007EE22A /* BockExplorer.swift in Sources */, ADA81E9E210DE14100B7C6F2 /* UIView.swift in Sources */, ADDD13B62119CE740035D2F9 /* StackViewElement.swift in Sources */, + 5CC505812379FF5E00FB1C19 /* PaymentFeeLimitPercentage.swift in Sources */, ADA58F9C22049470009A5494 /* WalletConfigurationStore.swift in Sources */, AD27021F22E06E5000D4BF27 /* Keychain.swift in Sources */, AD391C9A20D16FA8007EE22A /* QRCodeScannerView.swift in Sources */, @@ -3436,7 +3445,6 @@ AD79E424219D90EF002589CA /* String+Extensions.swift in Sources */, ADADAD8D2143B3C900A48E1F /* Info.swift in Sources */, ADFB36D72143B73C00146FCC /* LndConstants.swift in Sources */, - 5CD423AD2364FB160031194F /* PaymentFeeLimitPercentage.swift in Sources */, ADADAD8C2143B3C900A48E1F /* GraphTopologyUpdate.swift in Sources */, AD24862A225B7F210049532C /* ChannelBalance.swift in Sources */, AD5AA2E72271B64500AEDDD7 /* rpc.pb.swift in Sources */, From 1008c3c0d6864601db12ecf569ad01f61a891cc2 Mon Sep 17 00:00:00 2001 From: Christopher Pinski Date: Thu, 14 Nov 2019 22:57:51 -0600 Subject: [PATCH 4/7] Addressing review comments and utilizing the correct error message depending upon the type of fee limit error --- Library/Extensions/UIAlertController.swift | 2 +- Library/Generated/strings.swift | 6 --- .../ModalDetail/Send/SendViewController.swift | 49 +++++++++++-------- .../ModalDetail/Send/SendViewModel.swift | 9 ++++ Library/en.lproj/Localizable.strings | 4 +- 5 files changed, 39 insertions(+), 31 deletions(-) diff --git a/Library/Extensions/UIAlertController.swift b/Library/Extensions/UIAlertController.swift index 744ba2431..8a561a1a1 100644 --- a/Library/Extensions/UIAlertController.swift +++ b/Library/Extensions/UIAlertController.swift @@ -51,7 +51,7 @@ extension UIAlertController { let confirmButtonTitle: String = L10n.Scene.Send.FeeAlert.ConfirmButton.title let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let cancelAlertAction = UIAlertAction(title: L10n.Scene.Send.FeeAlert.CancelButton.title, style: .cancel, handler: nil) + let cancelAlertAction = UIAlertAction(title: L10n.Generic.cancel, style: .cancel, handler: nil) let confirmAlertAction = UIAlertAction(title: confirmButtonTitle, style: .default) { _ in sendAction() diff --git a/Library/Generated/strings.swift b/Library/Generated/strings.swift index 24cdc428b..731b8d5c5 100644 --- a/Library/Generated/strings.swift +++ b/Library/Generated/strings.swift @@ -649,10 +649,6 @@ internal enum L10n { internal enum FeeAlert { /// Fee Limit Alert internal static let title = L10n.tr("Localizable", "scene.send.fee_alert.title") - internal enum CancelButton { - /// No - internal static let title = L10n.tr("Localizable", "scene.send.fee_alert.cancel_button.title") - } internal enum ConfirmButton { /// Yes internal static let title = L10n.tr("Localizable", "scene.send.fee_alert.confirm_button.title") @@ -669,8 +665,6 @@ internal enum L10n { internal static let fee = L10n.tr("Localizable", "scene.send.on_chain.fee") /// Send On-Chain internal static let title = L10n.tr("Localizable", "scene.send.on_chain.title") - /// Do you really want to send this payment? - internal static let paymentConfirmation = L10n.tr("Localizable", "scene.send.on_chain.payment_confirmation") internal enum Fee { /// Estimated Delivery: %@ internal static func estimatedDelivery(_ p1: String) -> String { diff --git a/Library/Scenes/ModalDetail/Send/SendViewController.swift b/Library/Scenes/ModalDetail/Send/SendViewController.swift index 614673e7e..218050d13 100644 --- a/Library/Scenes/ModalDetail/Send/SendViewController.swift +++ b/Library/Scenes/ModalDetail/Send/SendViewController.swift @@ -116,12 +116,7 @@ final class SendViewController: ModalDetailViewController { }.dispose(in: reactive.bag) viewModel.sendStatus.observeFailed { [weak self] error in - switch error { - case .feeGreaterThanPayment(let feeInfo): - self?.showFeeLimitAlert(feePercentage: feeInfo.feePercentage, userFeeLimitPercent: feeInfo.userFeeLimitPercentage, sendFeeLimitPercent: feeInfo.sendFeeLimitPercentage) - case .feePercentageGreaterThanUserLimit(let feeInfo): - self?.showFeeLimitAlert(feePercentage: feeInfo.feePercentage, userFeeLimitPercent: feeInfo.userFeeLimitPercentage, sendFeeLimitPercent: feeInfo.sendFeeLimitPercentage) - } + self?.showFeeLimitAlert(sendError: error) }.dispose(in: reactive.bag) } @@ -199,26 +194,38 @@ final class SendViewController: ModalDetailViewController { viewModel.determineSendStatus() } - private func showFeeLimitAlert(feePercentage: Decimal, userFeeLimitPercent: Int, sendFeeLimitPercent: Int?) { + private func showFeeLimitAlert(sendError: SendViewModel.SendError) { let message: String switch viewModel.method { - case .onChain: - message = """ - \(L10n.Scene.Send.feeExceedsUserLimit(feePercentage.formattedAsPercentage, userFeeLimitPercent.formattedAsPercentage)) - - \(L10n.Scene.Send.OnChain.paymentConfirmation) - """ case .lightning: - message = """ - \(L10n.Scene.Send.feeExceedsUserLimit(feePercentage.formattedAsPercentage, userFeeLimitPercent.formattedAsPercentage)) + let sendFeeLimitPercentage: Int? - \(L10n.Scene.Send.Lightning.paymentConfirmation) - """ - } - let controller = UIAlertController.feeLimitAlertController(message: message) { [weak self] in - self?.triggerSend(feeLimitPercent: sendFeeLimitPercent) + switch sendError { + case .feeGreaterThanPayment(let feeInfo): + message = """ + \(L10n.Scene.Send.feeExceedsPayment(feeInfo.feePercentage.formattedAsPercentage, feeInfo.userFeeLimitPercentage.formattedAsPercentage)) + + \(L10n.Scene.Send.Lightning.paymentConfirmation) + """ + + sendFeeLimitPercentage = feeInfo.sendFeeLimitPercentage + case .feePercentageGreaterThanUserLimit(let feeInfo): + message = """ + \(L10n.Scene.Send.feeExceedsUserLimit(feeInfo.feePercentage.formattedAsPercentage, feeInfo.userFeeLimitPercentage.formattedAsPercentage)) + + \(L10n.Scene.Send.Lightning.paymentConfirmation) + """ + + sendFeeLimitPercentage = feeInfo.sendFeeLimitPercentage + } + + let controller = UIAlertController.feeLimitAlertController(message: message) { [weak self] in + self?.triggerSend(feeLimitPercent: sendFeeLimitPercentage) + } + self.present(controller, animated: true) + default: + return } - self.present(controller, animated: true) } private func presentLoading() { diff --git a/Library/Scenes/ModalDetail/Send/SendViewModel.swift b/Library/Scenes/ModalDetail/Send/SendViewModel.swift index 61bcd359b..416227703 100644 --- a/Library/Scenes/ModalDetail/Send/SendViewModel.swift +++ b/Library/Scenes/ModalDetail/Send/SendViewModel.swift @@ -164,6 +164,15 @@ final class SendViewModel: NSObject { } func determineSendStatus() { + switch method { + case .lightning: + self.determineLightningSendStatus() + default: + self.sendStatus.receive(nil) + } + } + + private func determineLightningSendStatus() { guard let amount = amount, let feePercent = feePercent else { return } let actualFee: Satoshi diff --git a/Library/en.lproj/Localizable.strings b/Library/en.lproj/Localizable.strings index 973a725e2..5bcc50c1f 100644 --- a/Library/en.lproj/Localizable.strings +++ b/Library/en.lproj/Localizable.strings @@ -180,11 +180,9 @@ "scene.send.fee_exceeds_user_limit" = "The fee for this payment (%@) exceeds the limit specified in the settings (%@)."; "scene.send.fee_exceeds_payment_amount" = "The fee for this payment (%@ sats) will be higher than the payment amount (%@ sats)."; "scene.send.lightning.payment_confirmation" = "Do you really want to pay this invoice?"; -"scene.send.on_chain.payment_confirmation" = "Do you really want to send this payment?"; "scene.send.fee_alert.title" = "Fee Limit Alert"; -"scene.send.fee_alert.cancel_button.title" = "No"; -"scene.send.fee_alert.confirm_button.title" = "Yes"; +"scene.send.fee_alert.confirm_button.title" = "Send"; "scene.connect_remote_node.title" = "Connect Remote Node"; "scene.connect_remote_node.empty_state" = "Scan the QR Code generated by lndconnect, BTCPay Server, or paste the link you get from running 'lndconnect -j' to connect to your node."; From 022f7d84ac4d68ec2df4d29facb95e7e1ff59335 Mon Sep 17 00:00:00 2001 From: Christopher Pinski Date: Fri, 15 Nov 2019 16:41:26 -0600 Subject: [PATCH 5/7] Adding unit tests for the SendViewModel --- .../ModalDetail/Send/SendViewModel.swift | 2 +- Library/Tests/SendViewModelTests.swift | 346 ++++++++++++++++++ Zap.xcodeproj/project.pbxproj | 4 + 3 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 Library/Tests/SendViewModelTests.swift diff --git a/Library/Scenes/ModalDetail/Send/SendViewModel.swift b/Library/Scenes/ModalDetail/Send/SendViewModel.swift index 416227703..0537a2938 100644 --- a/Library/Scenes/ModalDetail/Send/SendViewModel.swift +++ b/Library/Scenes/ModalDetail/Send/SendViewModel.swift @@ -67,7 +67,7 @@ final class SendViewModel: NSObject { let isSubtitleTextWarning = Observable(false) let sendStatus = Subject() - private var feePercent: Decimal? + var feePercent: Decimal? var amount: Satoshi? { didSet { diff --git a/Library/Tests/SendViewModelTests.swift b/Library/Tests/SendViewModelTests.swift new file mode 100644 index 000000000..b2e80b700 --- /dev/null +++ b/Library/Tests/SendViewModelTests.swift @@ -0,0 +1,346 @@ +// +// LibraryTests +// +// Created by Christopher Pinski on 11/11/19. +// Copyright © 2019 Zap. All rights reserved. +// + +@testable import Library +@testable import Lightning +import SwiftBTC +@testable import SwiftLnd +import XCTest + +final class MockBackupService: StaticChannelBackupServiceType { + func save(data: Result, nodePubKey: String, fileName: String) {} +} + +extension RPCCredentials { + // swiftlint:disable:next force_unwrapping + static var mock: RPCCredentials = RPCCredentials(certificate: nil, macaroon: Macaroon(hexadecimalString: "deadbeef")!, host: URL(string: "127.0.0.1")!) +} + +// swiftlint:disable force_unwrapping +// swiftlint:disable implicitly_unwrapped_optional +final class SendViewModelTests: XCTestCase { + + private var mockService: LightningService! + + override func setUp() { + super.setUp() + + let api = LightningApi(connection: MockLightningConnection()) + let testConnection = LightningConnection.remote(RPCCredentials.mock) + + mockService = LightningService(api: api, connection: testConnection, backupService: MockBackupService()) + } + + func testBitcoinTransactionSendStatus() { + let expectation = self.expectation(description: "Send Status") + + let bitcoinURI = BitcoinURI(address: BitcoinAddress(string: "mv4rnyY3Su5gjcDNzbMLKBQkBicCtHUtFB")!, amount: 1234.0, memo: nil, lightningFallback: nil) + let bitcoinInvoice = BitcoinInvoice(lightningPaymentRequest: nil, bitcoinURI: bitcoinURI) + + let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) + + sendViewModel.sendStatus.observeNext { feeLimitPercent in + XCTAssertEqual(nil, feeLimitPercent) + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.sendStatus.observeFailed { _ in + XCTFail("Shouldn't observe a fee limit error") + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.determineSendStatus() + + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testLightningPaymentUnderThresholdSendStatus() { + let expectation = self.expectation(description: "Send Status") + + let paymentAmount: Satoshi = 90.0 + let feeAmount: Satoshi = 1.0 + let feePercentage: Decimal = 1.111 + + let currentDate = Date() + let paymentRequest = PaymentRequest(paymentHash: "c1dbb4c26256205edb85105422c30d6570c1f7e136574d2bc4c9a1d526b26a3a", destination: "test", amount: paymentAmount, memo: nil, date: currentDate, expiryDate: Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!, raw: "test", fallbackAddress: nil, network: .testnet) + let bitcoinInvoice = BitcoinInvoice(lightningPaymentRequest: paymentRequest, bitcoinURI: nil) + + let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) + + sendViewModel.fee.value = .element(.success(feeAmount)) + sendViewModel.feePercent = feePercentage + + sendViewModel.sendStatus.observeNext { feeLimitPercent in + XCTAssertEqual(100, feeLimitPercent) + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.sendStatus.observeFailed { _ in + XCTFail("Shouldn't observe a fee limit error") + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.determineSendStatus() + + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testLightningPaymentUnderThresholdWithFeeGreaterThanPaymentSendStatus() { + let expectation = self.expectation(description: "Send Status") + + let paymentAmount: Satoshi = 90.0 + let feeAmount: Satoshi = 95.0 + let feePercentage: Decimal = 105.556 + let userLightningPaymentFeeLimit = PaymentFeeLimitPercentage.one + + let currentDate = Date() + let paymentRequest = PaymentRequest(paymentHash: "c1dbb4c26256205edb85105422c30d6570c1f7e136574d2bc4c9a1d526b26a3a", destination: "test", amount: paymentAmount, memo: nil, date: currentDate, expiryDate: Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!, raw: "test", fallbackAddress: nil, network: .testnet) + let bitcoinInvoice = BitcoinInvoice(lightningPaymentRequest: paymentRequest, bitcoinURI: nil) + + let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) + + Settings.shared.lightningPaymentFeeLimit.value = userLightningPaymentFeeLimit + sendViewModel.fee.value = .element(.success(feeAmount)) + sendViewModel.feePercent = feePercentage + + sendViewModel.sendStatus.observeNext { _ in + XCTFail("Shouldn't observe a fee limit success") + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.sendStatus.observeFailed { sendError in + switch sendError { + case .feeGreaterThanPayment(let feeInfo): + XCTAssertEqual(feePercentage, feeInfo.feePercentage) + XCTAssertEqual(nil, feeInfo.sendFeeLimitPercentage) + XCTAssertEqual(userLightningPaymentFeeLimit.rawValue, feeInfo.userFeeLimitPercentage) + case .feePercentageGreaterThanUserLimit: + XCTFail("Shouldn't observe a fee percentage greater than user limit error") + } + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.determineSendStatus() + + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testLightningPaymentAtThresholdSendStatus() { + let expectation = self.expectation(description: "Send Status") + + let paymentAmount: Satoshi = 100.0 + let feeAmount: Satoshi = 1.0 + let feePercentage: Decimal = 100.0 + + let currentDate = Date() + let paymentRequest = PaymentRequest(paymentHash: "c1dbb4c26256205edb85105422c30d6570c1f7e136574d2bc4c9a1d526b26a3a", destination: "test", amount: paymentAmount, memo: nil, date: currentDate, expiryDate: Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!, raw: "test", fallbackAddress: nil, network: .testnet) + let bitcoinInvoice = BitcoinInvoice(lightningPaymentRequest: paymentRequest, bitcoinURI: nil) + + let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) + + sendViewModel.fee.value = .element(.success(feeAmount)) + sendViewModel.feePercent = feePercentage + + sendViewModel.sendStatus.observeNext { feeLimitPercent in + XCTAssertEqual(100, feeLimitPercent) + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.sendStatus.observeFailed { _ in + XCTFail("Shouldn't observe a fee limit error") + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.determineSendStatus() + + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testLightningPaymentWithFeeUnderUserLimit() { + let expectation = self.expectation(description: "Send Status") + + let paymentAmount: Satoshi = 150.0 + let feeAmount: Satoshi = 1.0 + let feePercentage: Decimal = 0.667 + + let currentDate = Date() + let paymentRequest = PaymentRequest(paymentHash: "c1dbb4c26256205edb85105422c30d6570c1f7e136574d2bc4c9a1d526b26a3a", destination: "test", amount: paymentAmount, memo: nil, date: currentDate, expiryDate: Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!, raw: "test", fallbackAddress: nil, network: .testnet) + let bitcoinInvoice = BitcoinInvoice(lightningPaymentRequest: paymentRequest, bitcoinURI: nil) + + let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) + + Settings.shared.lightningPaymentFeeLimit.value = .one + sendViewModel.fee.value = .element(.success(feeAmount)) + sendViewModel.feePercent = feePercentage + + sendViewModel.sendStatus.observeNext { feeLimitPercent in + XCTAssertEqual(1, feeLimitPercent) + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.sendStatus.observeFailed { _ in + XCTFail("Shouldn't observe a fee limit error") + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.determineSendStatus() + + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testLightningPaymentWithFeeGreaterThanThresholdAndPaymentAmount() { + let expectation = self.expectation(description: "Send Status") + + let paymentAmount: Satoshi = 150.0 + let feeAmount: Satoshi = 160.0 + let feePercentage: Decimal = 106.667 + let userLightningPaymentFeeLimit = PaymentFeeLimitPercentage.one + + let currentDate = Date() + let paymentRequest = PaymentRequest(paymentHash: "c1dbb4c26256205edb85105422c30d6570c1f7e136574d2bc4c9a1d526b26a3a", destination: "test", amount: paymentAmount, memo: nil, date: currentDate, expiryDate: Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!, raw: "test", fallbackAddress: nil, network: .testnet) + let bitcoinInvoice = BitcoinInvoice(lightningPaymentRequest: paymentRequest, bitcoinURI: nil) + + let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) + + Settings.shared.lightningPaymentFeeLimit.value = userLightningPaymentFeeLimit + sendViewModel.fee.value = .element(.success(feeAmount)) + sendViewModel.feePercent = feePercentage + + sendViewModel.sendStatus.observeNext { _ in + XCTFail("Shouldn't observe a fee limit success") + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.sendStatus.observeFailed { sendError in + switch sendError { + case .feeGreaterThanPayment(let feeInfo): + XCTAssertEqual(feePercentage, feeInfo.feePercentage) + XCTAssertEqual(nil, feeInfo.sendFeeLimitPercentage) + XCTAssertEqual(userLightningPaymentFeeLimit.rawValue, feeInfo.userFeeLimitPercentage) + case .feePercentageGreaterThanUserLimit: + XCTFail("Shouldn't observe a fee percentage greater than user limit error") + } + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.determineSendStatus() + + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testLightningPaymentWithFeeGreaterThanThresholdAndEqualToPaymentAmount() { + let expectation = self.expectation(description: "Send Status") + + let paymentAmount: Satoshi = 150.0 + let feeAmount: Satoshi = 150.0 + let feePercentage: Decimal = 100.0 + let userLightningPaymentFeeLimit = PaymentFeeLimitPercentage.one + + let currentDate = Date() + let paymentRequest = PaymentRequest(paymentHash: "c1dbb4c26256205edb85105422c30d6570c1f7e136574d2bc4c9a1d526b26a3a", destination: "test", amount: paymentAmount, memo: nil, date: currentDate, expiryDate: Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!, raw: "test", fallbackAddress: nil, network: .testnet) + let bitcoinInvoice = BitcoinInvoice(lightningPaymentRequest: paymentRequest, bitcoinURI: nil) + + let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) + + Settings.shared.lightningPaymentFeeLimit.value = .one + sendViewModel.fee.value = .element(.success(feeAmount)) + sendViewModel.feePercent = feePercentage + + sendViewModel.sendStatus.observeNext { _ in + XCTFail("Shouldn't observe a fee limit success") + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.sendStatus.observeFailed { sendError in + switch sendError { + case .feeGreaterThanPayment(let feeInfo): + XCTAssertEqual(feePercentage, feeInfo.feePercentage) + XCTAssertEqual(nil, feeInfo.sendFeeLimitPercentage) + XCTAssertEqual(userLightningPaymentFeeLimit.rawValue, feeInfo.userFeeLimitPercentage) + case .feePercentageGreaterThanUserLimit: + XCTFail("Shouldn't observe a fee percentage greater than user limit error") + } + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.determineSendStatus() + + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testLightningPaymentWithFeePercentageGreaterThanUserLimit() { + let expectation = self.expectation(description: "Send Status") + + let paymentAmount: Satoshi = 300 + let feeAmount: Satoshi = 100.0 + let feePercentage: Decimal = 33.333 + let userLightningPaymentFeeLimit = PaymentFeeLimitPercentage.three + + let currentDate = Date() + let paymentRequest = PaymentRequest(paymentHash: "c1dbb4c26256205edb85105422c30d6570c1f7e136574d2bc4c9a1d526b26a3a", destination: "test", amount: paymentAmount, memo: nil, date: currentDate, expiryDate: Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!, raw: "test", fallbackAddress: nil, network: .testnet) + let bitcoinInvoice = BitcoinInvoice(lightningPaymentRequest: paymentRequest, bitcoinURI: nil) + + let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) + + Settings.shared.lightningPaymentFeeLimit.value = userLightningPaymentFeeLimit + sendViewModel.fee.value = .element(.success(feeAmount)) + sendViewModel.feePercent = feePercentage + + sendViewModel.sendStatus.observeNext { _ in + XCTFail("Shouldn't observe a fee limit success") + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.sendStatus.observeFailed { sendError in + switch sendError { + case .feeGreaterThanPayment: + XCTFail("Shouldn't observe a fee greater than payment error") + case .feePercentageGreaterThanUserLimit(let feeInfo): + XCTAssertEqual(feePercentage, feeInfo.feePercentage) + XCTAssertEqual(100, feeInfo.sendFeeLimitPercentage) + XCTAssertEqual(userLightningPaymentFeeLimit.rawValue, feeInfo.userFeeLimitPercentage) + } + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.determineSendStatus() + + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testLightningPaymentWithUserLimitNone() { + let expectation = self.expectation(description: "Send Status") + + let paymentAmount: Satoshi = 200.0 + let feeAmount: Satoshi = 100.0 + let feePercentage: Decimal = 50.0 + + let currentDate = Date() + let paymentRequest = PaymentRequest(paymentHash: "c1dbb4c26256205edb85105422c30d6570c1f7e136574d2bc4c9a1d526b26a3a", destination: "test", amount: paymentAmount, memo: nil, date: currentDate, expiryDate: Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!, raw: "test", fallbackAddress: nil, network: .testnet) + let bitcoinInvoice = BitcoinInvoice(lightningPaymentRequest: paymentRequest, bitcoinURI: nil) + + let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) + + Settings.shared.lightningPaymentFeeLimit.value = .zero + sendViewModel.fee.value = .element(.success(feeAmount)) + sendViewModel.feePercent = feePercentage + + sendViewModel.sendStatus.observeNext { feeLimitPercent in + XCTAssertEqual(nil, feeLimitPercent) + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.sendStatus.observeFailed { _ in + XCTFail("Shouldn't observe a fee limit error") + expectation.fulfill() + }.dispose(in: reactive.bag) + + sendViewModel.determineSendStatus() + + waitForExpectations(timeout: 0.1, handler: nil) + } +} diff --git a/Zap.xcodeproj/project.pbxproj b/Zap.xcodeproj/project.pbxproj index ba78ffad3..69458b1b5 100644 --- a/Zap.xcodeproj/project.pbxproj +++ b/Zap.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 5C18BB77234C0BF400BCF9D9 /* ExpiryTime+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C18BB76234C0BF400BCF9D9 /* ExpiryTime+Localizable.swift */; }; 5CA13678236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA13677236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift */; }; 5CC505812379FF5E00FB1C19 /* PaymentFeeLimitPercentage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD423AC2364FB160031194F /* PaymentFeeLimitPercentage.swift */; }; + 5CF94808237A27D40086DD5B /* SendViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF94807237A27D40086DD5B /* SendViewModelTests.swift */; }; 5E7CF0840E8C74EB58F80DB5 /* Pods_SnapshotUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6AEA9B7EDFEBE4BBEB56DAD /* Pods_SnapshotUITests.framework */; }; 6A9518DA222DCFCA0008FE4F /* Array2DElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A9518D9222DCFCA0008FE4F /* Array2DElement.swift */; }; 7BE67678020C235E19A3D64E /* Pods_SwiftLndTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 71876EABB08A5425C9DA33C7 /* Pods_SwiftLndTests.framework */; }; @@ -579,6 +580,7 @@ 5C18BB76234C0BF400BCF9D9 /* ExpiryTime+Localizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExpiryTime+Localizable.swift"; sourceTree = ""; }; 5CA13677236555A3003D4B98 /* PaymentFeeLimitPercentage+Localizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaymentFeeLimitPercentage+Localizable.swift"; sourceTree = ""; }; 5CD423AC2364FB160031194F /* PaymentFeeLimitPercentage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentFeeLimitPercentage.swift; sourceTree = ""; }; + 5CF94807237A27D40086DD5B /* SendViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendViewModelTests.swift; sourceTree = ""; }; 5E8DFC750E5DE2109A4B90CB /* Pods-RPC-Lightning.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RPC-Lightning.debug.xcconfig"; path = "Target Support Files/Pods-RPC-Lightning/Pods-RPC-Lightning.debug.xcconfig"; sourceTree = ""; }; 5F7C5272C0C2909B7C7987FF /* Pods-SnapshotUITests.debugremote.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SnapshotUITests.debugremote.xcconfig"; path = "Target Support Files/Pods-SnapshotUITests/Pods-SnapshotUITests.debugremote.xcconfig"; sourceTree = ""; }; 6A9518D9222DCFCA0008FE4F /* Array2DElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array2DElement.swift; sourceTree = ""; }; @@ -2374,6 +2376,7 @@ ADFE537F20D693F500B3BA2A /* Info.plist */, ADFCEAE021776E1C00370242 /* EventDetailViewModelTests.swift */, A0F9AEE2206E3E0300E93DCA /* InputNumberFormatterTests.swift */, + 5CF94807237A27D40086DD5B /* SendViewModelTests.swift */, AD81C21A21B86A3100FA3FA9 /* SyncPercentageEstimatorTests.swift */, ); path = Tests; @@ -3591,6 +3594,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5CF94808237A27D40086DD5B /* SendViewModelTests.swift in Sources */, ADFE538520D693FF00B3BA2A /* InputNumberFormatterTests.swift in Sources */, ADFCEAE221776E2100370242 /* EventDetailViewModelTests.swift in Sources */, AD81C21B21B86A3100FA3FA9 /* SyncPercentageEstimatorTests.swift in Sources */, From 6a693398d8f1d424454e9ee7f95b29c47cc91f16 Mon Sep 17 00:00:00 2001 From: Christopher Pinski Date: Sun, 24 Nov 2019 18:45:57 -0600 Subject: [PATCH 6/7] PR comments --- .../Localizable/PaymentFeeLimitPercentage+Localizable.swift | 4 ++-- Library/Scenes/ModalDetail/Send/SendViewModel.swift | 2 +- Library/Scenes/Settings/Model/PaymentFeeLimitPercentage.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift b/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift index b05c58f87..57c2b9553 100644 --- a/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift +++ b/Library/Extensions/Localizable/PaymentFeeLimitPercentage+Localizable.swift @@ -10,10 +10,10 @@ import Foundation extension PaymentFeeLimitPercentage: Localizable { public var localized: String { switch self { - case .zero: + case .none: return L10n.PaymentFeeLimitPercentage.none default: - return "\(self.rawValue)%" + return self.rawValue.formattedAsPercentage } } } diff --git a/Library/Scenes/ModalDetail/Send/SendViewModel.swift b/Library/Scenes/ModalDetail/Send/SendViewModel.swift index 0537a2938..b5b51c6c4 100644 --- a/Library/Scenes/ModalDetail/Send/SendViewModel.swift +++ b/Library/Scenes/ModalDetail/Send/SendViewModel.swift @@ -197,7 +197,7 @@ final class SendViewModel: NSObject { } } else if actualFee >= amount { self.sendStatus.receive(event: .failed(.feeGreaterThanPayment(FeeInfo(feePercentage: feePercent, userFeeLimitPercentage: Settings.shared.lightningPaymentFeeLimit.value.rawValue, sendFeeLimitPercentage: nil)))) - } else if Settings.shared.lightningPaymentFeeLimit.value == .zero { + } else if Settings.shared.lightningPaymentFeeLimit.value == .none { self.sendStatus.receive(nil) } else { if feePercent > Decimal(Settings.shared.lightningPaymentFeeLimit.value.rawValue) { diff --git a/Library/Scenes/Settings/Model/PaymentFeeLimitPercentage.swift b/Library/Scenes/Settings/Model/PaymentFeeLimitPercentage.swift index f342159bf..9ebba3cb5 100644 --- a/Library/Scenes/Settings/Model/PaymentFeeLimitPercentage.swift +++ b/Library/Scenes/Settings/Model/PaymentFeeLimitPercentage.swift @@ -8,7 +8,7 @@ import Foundation public enum PaymentFeeLimitPercentage: Int, Codable, CaseIterable { - case zero = 0 + case none = 0 case one = 1 case three = 3 case five = 5 From eac918ecdc6f91f6400babe7c3c85d4e25739c33 Mon Sep 17 00:00:00 2001 From: Christopher Pinski Date: Sun, 24 Nov 2019 22:53:30 -0600 Subject: [PATCH 7/7] Fixing tests --- Library/Tests/SendViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Tests/SendViewModelTests.swift b/Library/Tests/SendViewModelTests.swift index b2e80b700..a392ae2bd 100644 --- a/Library/Tests/SendViewModelTests.swift +++ b/Library/Tests/SendViewModelTests.swift @@ -325,7 +325,7 @@ final class SendViewModelTests: XCTestCase { let sendViewModel = SendViewModel(invoice: bitcoinInvoice, lightningService: mockService) - Settings.shared.lightningPaymentFeeLimit.value = .zero + Settings.shared.lightningPaymentFeeLimit.value = .none sendViewModel.fee.value = .element(.success(feeAmount)) sendViewModel.feePercent = feePercentage