From 83d14e72952818758897a8a1e710e534244b89a6 Mon Sep 17 00:00:00 2001 From: Robbie <304604+robbiehanson@users.noreply.github.com> Date: Tue, 19 Dec 2023 05:08:37 -0500 Subject: [PATCH] (ios) Fix several UI issues (#493) - Fixing compiler issues (latest lightning-kmp changes) - Hide fee bumping button in irrelevant cases (funding and closing). Fixes #488. - Don't allow users to send on-chain transactions with sat/vByte below minimumFee --- phoenix-ios/phoenix-ios/Localizable.xcstrings | 6 +++ .../phoenix-ios/views/inspect/CpfpView.swift | 50 +++++++++++++------ .../views/inspect/SummaryView.swift | 11 +++- .../views/send/MinerFeeSheet.swift | 48 ++++++++++++++---- 4 files changed, 87 insertions(+), 28 deletions(-) diff --git a/phoenix-ios/phoenix-ios/Localizable.xcstrings b/phoenix-ios/phoenix-ios/Localizable.xcstrings index 14a928f15..769509e20 100644 --- a/phoenix-ios/phoenix-ios/Localizable.xcstrings +++ b/phoenix-ios/phoenix-ios/Localizable.xcstrings @@ -8229,6 +8229,12 @@ } } } + }, + "Feerate below minimum allowed by mempool: %@" : { + + }, + "Feerate below minimum allowed by mempool." : { + }, "Fees" : { "comment" : "Label in SummaryInfoGrid", diff --git a/phoenix-ios/phoenix-ios/views/inspect/CpfpView.swift b/phoenix-ios/phoenix-ios/views/inspect/CpfpView.swift index b6e257de9..e918579ef 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/CpfpView.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/CpfpView.swift @@ -26,7 +26,8 @@ struct MinerFeeCPFP { } enum CpfpError: Error { - case feeTooLow + case feeBelowMinimum + case feeNotIncreased case noChannels case errorThrown(message: String) case executeError(problem: SpliceOutProblem) @@ -180,7 +181,7 @@ struct CpfpView: View { minerFeeFormula() .padding(.bottom) - payButton() + footer() } .padding(.horizontal, 40) } @@ -387,7 +388,7 @@ struct CpfpView: View { } @ViewBuilder - func payButton() -> some View { + func footer() -> some View { VStack(alignment: HorizontalAlignment.center, spacing: 10) { @@ -412,7 +413,13 @@ struct CpfpView: View { Group { switch cpfpError { - case .feeTooLow: + case .feeBelowMinimum: + if let mrr = mempoolRecommendedResponse { + Text("Feerate below minimum allowed by mempool: \(satsPerByteString(mrr.minimumFee))") + } else { + Text("Feerate below minimum allowed by mempool.") + } + case .feeNotIncreased: Text( """ This feerate is below what your transactions are already using. \ @@ -527,15 +534,19 @@ struct CpfpView: View { } let doubleValue = mempoolRecommendedResponse.feeForPriority(priority) - + let stringValue = satsPerByteString(doubleValue) + + return (doubleValue, stringValue) + } + + func satsPerByteString(_ value: Double) -> String { + let nf = NumberFormatter() nf.numberStyle = .decimal nf.minimumFractionDigits = 0 nf.maximumFractionDigits = 1 - let stringValue = nf.string(from: NSNumber(value: doubleValue)) ?? "?" - - return (doubleValue, stringValue) + return nf.string(from: NSNumber(value: value)) ?? "?" } func satsPerByteString(_ priority: MinerFeePriority) -> String { @@ -619,26 +630,29 @@ struct CpfpView: View { func satsPerByteChanged() { log.trace("satsPerByteChanged(): \(satsPerByte)") + minerFeeInfo = nil guard let satsPerByte_number = try? parsedSatsPerByte.get(), let peer = Biz.business.peerManager.peerStateValue() else { - minerFeeInfo = nil return } + if let mrr = mempoolRecommendedResponse, mrr.minimumFee > satsPerByte_number.doubleValue { + cpfpError = .feeBelowMinimum + return + } + cpfpError = nil + let originalSatsPerByte = satsPerByte let satsPerByte_satoshi = Bitcoin_kmpSatoshi(sat: satsPerByte_number.int64Value) let effectiveFeerate = Lightning_kmpFeeratePerByte(feerate: satsPerByte_satoshi) let effectiveFeeratePerKw = Lightning_kmpFeeratePerKw(feeratePerByte: effectiveFeerate) - cpfpError = nil - minerFeeInfo = nil - Task { @MainActor in - var pair: KotlinPair? = nil + var pair: KotlinPair? = nil do { pair = try await peer.estimateFeeForSpliceCpfp( channelId: onChainPayment.channelId, @@ -658,7 +672,8 @@ struct CpfpView: View { let cpfpFeeratePerKw: Lightning_kmpFeeratePerKw = pair.first! let cpfpFeerate = Lightning_kmpFeeratePerByte(feeratePerKw: cpfpFeeratePerKw) - let minerFee: Bitcoin_kmpSatoshi = pair.second! + let spliceFees: Lightning_kmpChannelCommand.CommitmentSpliceFees = pair.second! + let minerFee: Bitcoin_kmpSatoshi = spliceFees.miningFee // From the docs (in lightning-kmp): // @@ -679,8 +694,8 @@ struct CpfpView: View { minerFee: minerFee ) } else { - log.error("Error: peer.estimateFeeForSpliceCpfp() => too low") - self.cpfpError = .feeTooLow + log.error("Error: peer.estimateFeeForSpliceCpfp() => fee not increased") + self.cpfpError = .feeNotIncreased } } else { @@ -696,6 +711,9 @@ struct CpfpView: View { // The UI will change, so we need to reset the geometry measurements priorityBoxWidth = nil + + // Might need to display an error (if minimumFee increased) + satsPerByteChanged() } func executePayment() { diff --git a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift index b8e65c62c..ece42c70e 100644 --- a/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift +++ b/phoenix-ios/phoenix-ios/views/inspect/SummaryView.swift @@ -324,7 +324,7 @@ struct SummaryView: View { } } // - if confirmations == 0 { + if confirmations == 0 && supportsBumpFee(onChainPayment) { NavigationLink(destination: cpfpView(onChainPayment)) { Label { Text("Accelerate transaction") @@ -651,6 +651,15 @@ struct SummaryView: View { } } + func supportsBumpFee(_ onChainPayment: Lightning_kmpOnChainOutgoingPayment) -> Bool { + + switch onChainPayment { + case is Lightning_kmpSpliceOutgoingPayment : return true + case is Lightning_kmpSpliceCpfpOutgoingPayment : return true + default : return false + } + } + // -------------------------------------------------- // MARK: Tasks // -------------------------------------------------- diff --git a/phoenix-ios/phoenix-ios/views/send/MinerFeeSheet.swift b/phoenix-ios/phoenix-ios/views/send/MinerFeeSheet.swift index 3635112e6..64a52aca1 100644 --- a/phoenix-ios/phoenix-ios/views/send/MinerFeeSheet.swift +++ b/phoenix-ios/phoenix-ios/views/send/MinerFeeSheet.swift @@ -24,6 +24,7 @@ struct MinerFeeSheet: View { @Binding var mempoolRecommendedResponse: MempoolRecommendedResponse? @State var explicitlySelectedPriority: MinerFeePriority? = nil + @State var feeBelowMinimum: Bool = false @Environment(\.colorScheme) var colorScheme: ColorScheme @EnvironmentObject var smartModalState: SmartModalState @@ -296,8 +297,8 @@ struct MinerFeeSheet: View { @ViewBuilder func footer() -> some View { - HStack(alignment: VerticalAlignment.center, spacing: 0) { - Spacer() + VStack(alignment: HorizontalAlignment.center, spacing: 10) { + Button { reviewTransactionButtonTapped() } label: { @@ -305,7 +306,19 @@ struct MinerFeeSheet: View { } .font(.title3) .disabled(minerFeeInfo == nil) - Spacer() + + if feeBelowMinimum { + Group { + if let mrr = mempoolRecommendedResponse { + Text("Feerate below minimum allowed by mempool: \(satsPerByteString(mrr.minimumFee))") + } else { + Text("Feerate below minimum allowed by mempool.") + } + } + .font(.callout) + .foregroundColor(.appNegative) + .multilineTextAlignment(.center) + } } .padding() .padding(.top) @@ -348,15 +361,19 @@ struct MinerFeeSheet: View { } let doubleValue = mempoolRecommendedResponse.feeForPriority(priority) - + let stringValue = satsPerByteString(doubleValue) + + return (doubleValue, stringValue) + } + + func satsPerByteString(_ value: Double) -> String { + let nf = NumberFormatter() nf.numberStyle = .decimal nf.minimumFractionDigits = 0 nf.maximumFractionDigits = 1 - let stringValue = nf.string(from: NSNumber(value: doubleValue)) ?? "?" - - return (doubleValue, stringValue) + return nf.string(from: NSNumber(value: value)) ?? "?" } func satsPerByteString(_ priority: MinerFeePriority) -> String { @@ -440,15 +457,22 @@ struct MinerFeeSheet: View { func satsPerByteChanged() { log.trace("satsPerByteChanged(): \(satsPerByte)") + minerFeeInfo = nil guard let satsPerByte_number = try? parsedSatsPerByte.get(), let peer = Biz.business.peerManager.peerStateValue(), let scriptBytes = Parser.shared.addressToPublicKeyScript(chain: Biz.business.chain, address: btcAddress) else { - minerFeeInfo = nil return } + if let mrr = mempoolRecommendedResponse, mrr.minimumFee > satsPerByte_number.doubleValue { + feeBelowMinimum = true + return + } else { + feeBelowMinimum = false + } + let originalSatsPerByte = satsPerByte let scriptVector = Bitcoin_kmpByteVector(bytes: scriptBytes) @@ -456,7 +480,6 @@ struct MinerFeeSheet: View { let feePerByte = Lightning_kmpFeeratePerByte(feerate: satsPerByte_satoshi) let feePerKw = Lightning_kmpFeeratePerKw(feeratePerByte: feePerByte) - minerFeeInfo = nil Task { @MainActor in do { let pair = try await peer.estimateFeeForSpliceOut( @@ -467,13 +490,13 @@ struct MinerFeeSheet: View { if let pair = pair, let updatedFeePerKw: Lightning_kmpFeeratePerKw = pair.first, - let fee: Bitcoin_kmpSatoshi = pair.second + let fees: Lightning_kmpChannelCommand.CommitmentSpliceFees = pair.second { if self.satsPerByte == originalSatsPerByte { self.minerFeeInfo = MinerFeeInfo( pubKeyScript: scriptVector, feerate: updatedFeePerKw, - minerFee: fee + minerFee: fees.miningFee ) } } else { @@ -492,6 +515,9 @@ struct MinerFeeSheet: View { // The UI will change, so we need to reset the geometry measurements priorityBoxWidth = nil + + // Might need to display an error (if minimumFee increased) + satsPerByteChanged() } func closeButtonTapped() {