diff --git a/android/app/src/main/java/org/bitcoinppl/cove/QrCodeScanView.kt b/android/app/src/main/java/org/bitcoinppl/cove/QrCodeScanView.kt index 39096ac02..68162850f 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/QrCodeScanView.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/QrCodeScanView.kt @@ -38,6 +38,7 @@ import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage import org.bitcoinppl.cove_core.AppAlertState import org.bitcoinppl.cove_core.MultiFormat +import org.bitcoinppl.cove_core.MultiQrException import org.bitcoinppl.cove_core.QrScanner import org.bitcoinppl.cove_core.ScanProgress import org.bitcoinppl.cove_core.ScanResult @@ -249,14 +250,14 @@ private fun QrScannerContent( scannedData = data if (!isDisposed) scanner.reset() }, - onError = { error -> + onError = { title, message -> if (!isDisposed) scanner.reset() onDismiss() app.alertState = TaggedItem( AppAlertState.General( - title = "QR Scan Error", - message = error, + title = title, + message = message, ), ) }, @@ -465,7 +466,7 @@ private fun handleQrCode( scanner: QrScanner, onProgress: (ScanProgress) -> Unit, onComplete: (MultiFormat) -> Unit, - onError: (String) -> Unit, + onError: (title: String, message: String) -> Unit, ) { try { // convert barcode to StringOrData (prioritize text, fall back to binary) @@ -490,7 +491,15 @@ private fun handleQrCode( } catch (e: IllegalStateException) { // scanner was destroyed during async callback, ignore Log.d("QrCodeScanView", "Scanner already destroyed, ignoring: ${e.message}") + } catch (e: MultiQrException.SilentPaymentNotSupported) { + onError( + "Unsupported Address", + "Sending to silent payment addresses (sp1...) is not yet supported. Support is coming soon.", + ) } catch (e: Exception) { - onError("Unable to scan QR code: ${e.message ?: "Unknown scanning error"}") + onError( + "QR Scan Error", + "Unable to scan QR code: ${e.message ?: "Unknown scanning error"}", + ) } } diff --git a/android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowPresenter.kt b/android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowPresenter.kt index c410f6561..ea91cc2f0 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowPresenter.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove/flows/SendFlow/SendFlowPresenter.kt @@ -82,6 +82,9 @@ class SendFlowPresenter( is SendFlowException.WrongNetwork, -> "Invalid Address" + is SendFlowException.SilentPaymentNotSupported, + -> "Unsupported Address" + is SendFlowException.InvalidNumber, is SendFlowException.ZeroAmount, -> "Invalid Amount" @@ -152,6 +155,9 @@ class SendFlowPresenter( is SendFlowException.UnableToSaveUnsignedTransaction -> error.v1 + + is SendFlowException.SilentPaymentNotSupported -> + "Sending to silent payment addresses (sp1...) is not yet supported. Support is coming soon." } /** @@ -171,6 +177,7 @@ class SendFlowPresenter( is SendFlowException.EmptyAddress, is SendFlowException.WrongNetwork, is SendFlowException.InvalidAddress, + is SendFlowException.SilentPaymentNotSupported, -> { { alertState = null diff --git a/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt b/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt index d322a3731..d5f35cea0 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt @@ -39997,6 +39997,12 @@ sealed class MultiFormatException: kotlin.Exception() { get() = "" } + class SilentPaymentNotSupported( + ) : MultiFormatException() { + override val message + get() = "" + } + class UnrecognizedFormat( ) : MultiFormatException() { override val message @@ -40055,12 +40061,13 @@ public object FfiConverterTypeMultiFormatError : FfiConverterRustBuffer MultiFormatException.UnsupportedNetworkAddress() - 3 -> MultiFormatException.UnrecognizedFormat() - 4 -> MultiFormatException.InvalidTapSigner( + 3 -> MultiFormatException.SilentPaymentNotSupported() + 4 -> MultiFormatException.UnrecognizedFormat() + 5 -> MultiFormatException.InvalidTapSigner( FfiConverterTypeTapCardParseError.read(buf), ) - 5 -> MultiFormatException.TaprootNotSupported() - 6 -> MultiFormatException.PsbtNotSigned() + 6 -> MultiFormatException.TaprootNotSupported() + 7 -> MultiFormatException.PsbtNotSigned() else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } } @@ -40076,6 +40083,10 @@ public object FfiConverterTypeMultiFormatError : FfiConverterRustBuffer ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) is MultiFormatException.UnrecognizedFormat -> ( // Add the size for the Int that specifies the variant plus the size needed for all fields 4UL @@ -40107,21 +40118,25 @@ public object FfiConverterTypeMultiFormatError : FfiConverterRustBuffer { + is MultiFormatException.SilentPaymentNotSupported -> { buf.putInt(3) Unit } - is MultiFormatException.InvalidTapSigner -> { + is MultiFormatException.UnrecognizedFormat -> { buf.putInt(4) + Unit + } + is MultiFormatException.InvalidTapSigner -> { + buf.putInt(5) FfiConverterTypeTapCardParseError.write(value.v1, buf) Unit } is MultiFormatException.TaprootNotSupported -> { - buf.putInt(5) + buf.putInt(6) Unit } is MultiFormatException.PsbtNotSigned -> { - buf.putInt(6) + buf.putInt(7) Unit } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } @@ -40143,6 +40158,12 @@ sealed class MultiQrException: kotlin.Exception() { get() = "v1=${ v1 }" } + class SilentPaymentNotSupported( + ) : MultiQrException() { + override val message + get() = "" + } + class InvalidUtf8( ) : MultiQrException() { override val message @@ -40208,15 +40229,16 @@ public object FfiConverterTypeMultiQrError : FfiConverterRustBuffer MultiQrException.ParseException( FfiConverterString.read(buf), ) - 2 -> MultiQrException.InvalidUtf8() - 3 -> MultiQrException.RequiresStringData() - 4 -> MultiQrException.InvalidSeedQr( + 2 -> MultiQrException.SilentPaymentNotSupported() + 3 -> MultiQrException.InvalidUtf8() + 4 -> MultiQrException.RequiresStringData() + 5 -> MultiQrException.InvalidSeedQr( FfiConverterTypeSeedQrError.read(buf), ) - 5 -> MultiQrException.Ur( + 6 -> MultiQrException.Ur( FfiConverterTypeUrError.read(buf), ) - 6 -> MultiQrException.BbqrCborNotSupported() + 7 -> MultiQrException.BbqrCborNotSupported() else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } } @@ -40228,6 +40250,10 @@ public object FfiConverterTypeMultiQrError : FfiConverterRustBuffer ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) is MultiQrException.InvalidUtf8 -> ( // Add the size for the Int that specifies the variant plus the size needed for all fields 4UL @@ -40260,26 +40286,30 @@ public object FfiConverterTypeMultiQrError : FfiConverterRustBuffer { + is MultiQrException.SilentPaymentNotSupported -> { buf.putInt(2) Unit } - is MultiQrException.RequiresStringData -> { + is MultiQrException.InvalidUtf8 -> { buf.putInt(3) Unit } - is MultiQrException.InvalidSeedQr -> { + is MultiQrException.RequiresStringData -> { buf.putInt(4) + Unit + } + is MultiQrException.InvalidSeedQr -> { + buf.putInt(5) FfiConverterTypeSeedQrError.write(value.v1, buf) Unit } is MultiQrException.Ur -> { - buf.putInt(5) + buf.putInt(6) FfiConverterTypeUrError.write(value.v1, buf) Unit } is MultiQrException.BbqrCborNotSupported -> { - buf.putInt(6) + buf.putInt(7) Unit } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } @@ -43638,6 +43668,12 @@ sealed class SendFlowException: kotlin.Exception() { get() = "v1=${ v1 }" } + class SilentPaymentNotSupported( + ) : SendFlowException() { + override val message + get() = "" + } + @@ -43696,6 +43732,7 @@ public object FfiConverterTypeSendFlowError : FfiConverterRustBuffer SendFlowException.UnableToGetFeeDetails( FfiConverterString.read(buf), ) + 15 -> SendFlowException.SilentPaymentNotSupported() else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } } @@ -43767,6 +43804,10 @@ public object FfiConverterTypeSendFlowError : FfiConverterRustBuffer ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) } } @@ -43837,6 +43878,10 @@ public object FfiConverterTypeSendFlowError : FfiConverterRustBuffer { + buf.putInt(15) + Unit + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } diff --git a/android/app/src/main/java/org/bitcoinppl/cove_core/types/cove_types.kt b/android/app/src/main/java/org/bitcoinppl/cove_core/types/cove_types.kt index 636ac1748..ed23961b7 100644 --- a/android/app/src/main/java/org/bitcoinppl/cove_core/types/cove_types.kt +++ b/android/app/src/main/java/org/bitcoinppl/cove_core/types/cove_types.kt @@ -9537,6 +9537,12 @@ sealed class AddressException: kotlin.Exception() { get() = "" } + class SilentPaymentNotSupported( + ) : AddressException() { + override val message + get() = "" + } + @@ -9567,6 +9573,7 @@ public object FfiConverterTypeAddressError : FfiConverterRustBuffer AddressException.EmptyAddress() + 7 -> AddressException.SilentPaymentNotSupported() else -> throw RuntimeException("invalid error enum value, something is very wrong!!") } } @@ -9600,6 +9607,10 @@ public object FfiConverterTypeAddressError : FfiConverterRustBuffer ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) } } @@ -9632,6 +9643,10 @@ public object FfiConverterTypeAddressError : FfiConverterRustBuffer { + buf.putInt(7) + Unit + } }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } } diff --git a/ios/Cove/Flows/SendFlow/SendFlowPresenter.swift b/ios/Cove/Flows/SendFlow/SendFlowPresenter.swift index 1af44cd64..8cb916a8d 100644 --- a/ios/Cove/Flows/SendFlow/SendFlowPresenter.swift +++ b/ios/Cove/Flows/SendFlow/SendFlowPresenter.swift @@ -73,6 +73,8 @@ import SwiftUI switch error { case .EmptyAddress, .InvalidAddress, .WrongNetwork: "Invalid Address" + case .SilentPaymentNotSupported: + "Unsupported Address" case .InvalidNumber, .ZeroAmount: "Invalid Amount" case .InsufficientFunds, .NoBalance: "Insufficient Funds" case .SendAmountToLow: "Send Amount Too Low" @@ -129,6 +131,8 @@ import SwiftUI msg case let .UnableToSaveUnsignedTransaction(msg): msg + case .SilentPaymentNotSupported: + "Sending to silent payment addresses (sp1...) is not yet supported. Support is coming soon." } } @@ -145,7 +149,7 @@ import SwiftUI @ViewBuilder private func errorAlertButtons(_ error: SendFlowError) -> some View { switch error { - case .EmptyAddress, .WrongNetwork, .InvalidAddress: + case .EmptyAddress, .WrongNetwork, .InvalidAddress, .SilentPaymentNotSupported: Button("OK") { self.alertState = .none self.focusField = .address diff --git a/ios/Cove/Flows/SendFlow/SetAmountScreen/QrCodeAddressView.swift b/ios/Cove/Flows/SendFlow/SetAmountScreen/QrCodeAddressView.swift index 9514463ef..fb893207e 100644 --- a/ios/Cove/Flows/SendFlow/SetAmountScreen/QrCodeAddressView.swift +++ b/ios/Cove/Flows/SendFlow/SetAmountScreen/QrCodeAddressView.swift @@ -117,6 +117,15 @@ struct QrCodeAddressView: View { haptic.trigger() progress = prog } + } catch MultiQrError.SilentPaymentNotSupported { + scanner.reset() + dismiss() + app.alertState = TaggedItem( + .general( + title: "Unsupported Address", + message: "Sending to silent payment addresses (sp1...) is not yet supported. Support is coming soon." + ) + ) } catch { scanner.reset() dismiss() diff --git a/ios/CoveCore/Sources/CoveCore/generated/cove.swift b/ios/CoveCore/Sources/CoveCore/generated/cove.swift index e0019bf64..e7d9412d9 100644 --- a/ios/CoveCore/Sources/CoveCore/generated/cove.swift +++ b/ios/CoveCore/Sources/CoveCore/generated/cove.swift @@ -23511,6 +23511,7 @@ enum MultiFormatError: Swift.Error, Equatable, Hashable, Foundation.LocalizedErr case InvalidSeedQr(SeedQrError ) case UnsupportedNetworkAddress + case SilentPaymentNotSupported case UnrecognizedFormat case InvalidTapSigner(TapCardParseError ) @@ -23559,12 +23560,13 @@ public struct FfiConverterTypeMultiFormatError: FfiConverterRustBuffer { try FfiConverterTypeSeedQrError.read(from: &buf) ) case 2: return .UnsupportedNetworkAddress - case 3: return .UnrecognizedFormat - case 4: return .InvalidTapSigner( + case 3: return .SilentPaymentNotSupported + case 4: return .UnrecognizedFormat + case 5: return .InvalidTapSigner( try FfiConverterTypeTapCardParseError.read(from: &buf) ) - case 5: return .TaprootNotSupported - case 6: return .PsbtNotSigned + case 6: return .TaprootNotSupported + case 7: return .PsbtNotSigned default: throw UniffiInternalError.unexpectedEnumCase } @@ -23586,21 +23588,25 @@ public struct FfiConverterTypeMultiFormatError: FfiConverterRustBuffer { writeInt(&buf, Int32(2)) - case .UnrecognizedFormat: + case .SilentPaymentNotSupported: writeInt(&buf, Int32(3)) - case let .InvalidTapSigner(v1): + case .UnrecognizedFormat: writeInt(&buf, Int32(4)) + + + case let .InvalidTapSigner(v1): + writeInt(&buf, Int32(5)) FfiConverterTypeTapCardParseError.write(v1, into: &buf) case .TaprootNotSupported: - writeInt(&buf, Int32(5)) + writeInt(&buf, Int32(6)) case .PsbtNotSigned: - writeInt(&buf, Int32(6)) + writeInt(&buf, Int32(7)) } } @@ -23629,6 +23635,7 @@ enum MultiQrError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { case ParseError(String ) + case SilentPaymentNotSupported case InvalidUtf8 case RequiresStringData case InvalidSeedQr(SeedQrError @@ -23678,15 +23685,16 @@ public struct FfiConverterTypeMultiQrError: FfiConverterRustBuffer { case 1: return .ParseError( try FfiConverterString.read(from: &buf) ) - case 2: return .InvalidUtf8 - case 3: return .RequiresStringData - case 4: return .InvalidSeedQr( + case 2: return .SilentPaymentNotSupported + case 3: return .InvalidUtf8 + case 4: return .RequiresStringData + case 5: return .InvalidSeedQr( try FfiConverterTypeSeedQrError.read(from: &buf) ) - case 5: return .Ur( + case 6: return .Ur( try FfiConverterTypeUrError.read(from: &buf) ) - case 6: return .BbqrCborNotSupported + case 7: return .BbqrCborNotSupported default: throw UniffiInternalError.unexpectedEnumCase } @@ -23704,26 +23712,30 @@ public struct FfiConverterTypeMultiQrError: FfiConverterRustBuffer { FfiConverterString.write(v1, into: &buf) - case .InvalidUtf8: + case .SilentPaymentNotSupported: writeInt(&buf, Int32(2)) - case .RequiresStringData: + case .InvalidUtf8: writeInt(&buf, Int32(3)) - case let .InvalidSeedQr(v1): + case .RequiresStringData: writeInt(&buf, Int32(4)) + + + case let .InvalidSeedQr(v1): + writeInt(&buf, Int32(5)) FfiConverterTypeSeedQrError.write(v1, into: &buf) case let .Ur(v1): - writeInt(&buf, Int32(5)) + writeInt(&buf, Int32(6)) FfiConverterTypeUrError.write(v1, into: &buf) case .BbqrCborNotSupported: - writeInt(&buf, Int32(6)) + writeInt(&buf, Int32(7)) } } @@ -26702,6 +26714,7 @@ enum SendFlowError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError ) case UnableToGetFeeDetails(String ) + case SilentPaymentNotSupported @@ -26771,6 +26784,7 @@ public struct FfiConverterTypeSendFlowError: FfiConverterRustBuffer { case 14: return .UnableToGetFeeDetails( try FfiConverterString.read(from: &buf) ) + case 15: return .SilentPaymentNotSupported default: throw UniffiInternalError.unexpectedEnumCase } @@ -26847,6 +26861,10 @@ public struct FfiConverterTypeSendFlowError: FfiConverterRustBuffer { writeInt(&buf, Int32(14)) FfiConverterString.write(v1, into: &buf) + + case .SilentPaymentNotSupported: + writeInt(&buf, Int32(15)) + } } } diff --git a/ios/CoveCore/Sources/CoveCore/generated/cove_types.swift b/ios/CoveCore/Sources/CoveCore/generated/cove_types.swift index de5ea4a66..3a8f938c3 100644 --- a/ios/CoveCore/Sources/CoveCore/generated/cove_types.swift +++ b/ios/CoveCore/Sources/CoveCore/generated/cove_types.swift @@ -4664,6 +4664,7 @@ enum AddressError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { case WrongNetwork(current: Network, validFor: Network ) case EmptyAddress + case SilentPaymentNotSupported @@ -4704,6 +4705,7 @@ public struct FfiConverterTypeAddressError: FfiConverterRustBuffer { validFor: try FfiConverterTypeNetwork.read(from: &buf) ) case 6: return .EmptyAddress + case 7: return .SilentPaymentNotSupported default: throw UniffiInternalError.unexpectedEnumCase } @@ -4742,6 +4744,10 @@ public struct FfiConverterTypeAddressError: FfiConverterRustBuffer { case .EmptyAddress: writeInt(&buf, Int32(6)) + + case .SilentPaymentNotSupported: + writeInt(&buf, Int32(7)) + } } } diff --git a/rust/crates/cove-types/src/address.rs b/rust/crates/cove-types/src/address.rs index 1204aacb2..3b303b774 100644 --- a/rust/crates/cove-types/src/address.rs +++ b/rust/crates/cove-types/src/address.rs @@ -98,6 +98,9 @@ pub enum AddressError { #[error("empty address")] EmptyAddress, + + #[error("silent payment addresses (sp1...) are not yet supported for sending")] + SilentPaymentNotSupported, } impl Clone for AddressInfo { @@ -208,6 +211,28 @@ impl AddressWithNetwork { } } +fn is_silent_payment_address(s: &str) -> bool { + use bitcoin::bech32::{Bech32m, primitives::decode::CheckedHrpstring}; + + let s = s.trim(); + + // Quick prefix check before attempting a full decode. + let lower = s.to_lowercase(); + if !lower.starts_with("sp1") && !lower.starts_with("tsp1") { + return false; + } + + // BIP352 silent payment addresses are bech32m-specifically (not bech32). + // Reject strings that merely look like SP addresses but fail the checksum, + // and reject strings with a valid bech32 (not bech32m) checksum. + let Ok(checked) = CheckedHrpstring::new::(s) else { + return false; + }; + + let hrp = checked.hrp().to_lowercase(); + hrp == "sp" || hrp == "tsp" +} + fn parse_bitcoin_uri(input: &str) -> Result { let input = input.trim(); if input.is_empty() { @@ -223,6 +248,15 @@ fn parse_bitcoin_uri(input: &str) -> Result { _ => format!("bitcoin:{input}"), }; + // Check for silent payment addresses (sp1... / tsp1...) before handing off to the + // payjoin parser, which only accepts valid bech32 bitcoin addresses and would + // otherwise mask the specific error with a generic InvalidAddress. + let candidate = normalized.strip_prefix("bitcoin:").unwrap_or(&normalized); + let candidate = candidate.split('?').next().unwrap_or(candidate); + if is_silent_payment_address(candidate) { + return Err(Error::SilentPaymentNotSupported); + } + // Try full payjoin URI parsing first if let Ok(uri) = payjoin::Uri::try_from(normalized.as_str()) { let amount = uri.amount.map(|a| Amount::from_sat(a.to_sat())); @@ -326,6 +360,11 @@ impl Address { #[uniffi::constructor] pub fn from_string(address: &str, network: Network) -> Result { let address = address.trim(); + + if is_silent_payment_address(address) { + return Err(Error::SilentPaymentNotSupported); + } + let bdk_address = BdkAddress::from_str(address).map_err(|_| Error::InvalidAddress)?; let bitcoin_network: bitcoin::Network = network.into(); @@ -544,6 +583,66 @@ mod tests { assert_eq!(parsed.amount, None); } + /// Helper: encode a valid bech32m string with the given HRP and dummy payload. + fn make_sp_address(hrp_str: &str) -> String { + let hrp = bitcoin::bech32::Hrp::parse(hrp_str).unwrap(); + bitcoin::bech32::encode::(hrp, &[0u8; 66]).unwrap() + } + + #[test] + fn test_silent_payment_address_detection() { + let mainnet = make_sp_address("sp"); + let testnet = make_sp_address("tsp"); + + // valid SP addresses → SilentPaymentNotSupported + assert_eq!( + AddressWithNetwork::try_new(&mainnet), + Err(AddressError::SilentPaymentNotSupported) + ); + assert_eq!( + AddressWithNetwork::try_new(&testnet), + Err(AddressError::SilentPaymentNotSupported) + ); + + // bitcoin: URI with valid SP address + let uri = format!("bitcoin:{mainnet}"); + assert_eq!(AddressWithNetwork::try_new(&uri), Err(AddressError::SilentPaymentNotSupported)); + + // truncated / bad-checksum sp1 strings fall through to InvalidAddress + assert_eq!(AddressWithNetwork::try_new("sp1qqfoobar"), Err(AddressError::InvalidAddress)); + assert_eq!( + AddressWithNetwork::try_new("tsp1qqtruncated"), + Err(AddressError::InvalidAddress) + ); + + // bech32 (not bech32m) encoding with sp HRP must not classify as SP + let sp_hrp = bitcoin::bech32::Hrp::parse("sp").unwrap(); + let bech32_encoded = + bitcoin::bech32::encode::(sp_hrp, &[0u8; 66]).unwrap(); + assert_eq!(AddressWithNetwork::try_new(&bech32_encoded), Err(AddressError::InvalidAddress)); + } + + #[test] + fn test_silent_payment_address_from_string() { + let mainnet = make_sp_address("sp"); + let testnet = make_sp_address("tsp"); + + assert_eq!( + Address::from_string(&mainnet, Network::Bitcoin), + Err(AddressError::SilentPaymentNotSupported) + ); + assert_eq!( + Address::from_string(&testnet, Network::Testnet), + Err(AddressError::SilentPaymentNotSupported) + ); + + // truncated sp1 string → InvalidAddress, not SilentPaymentNotSupported + assert_eq!( + Address::from_string("sp1qqfoobar", Network::Bitcoin), + Err(AddressError::InvalidAddress) + ); + } + #[test] fn test_address_string_spaced_out() { let address = "bc1pkdj04w4lxsv570j5nsd249lqe4w4j608r2nq9997ruh0wv96cnksy5jeny"; diff --git a/rust/src/manager/send_flow_manager.rs b/rust/src/manager/send_flow_manager.rs index d80ad0c7b..23276baae 100644 --- a/rust/src/manager/send_flow_manager.rs +++ b/rust/src/manager/send_flow_manager.rs @@ -419,8 +419,19 @@ impl RustSendFlowManager { pub fn validate_address(self: &Arc, display_alert: bool) -> bool { if self.state.lock().address.is_none() { if display_alert { - let error = - SendFlowError::InvalidAddress(self.state.lock().entering_address.clone()); + let (entering, current) = { + let state = self.state.lock(); + (state.entering_address.clone(), state.metadata.network) + }; + let error = match AddressWithNetwork::try_new(&entering) { + Err(e) => SendFlowError::from_address_error(e, entering), + Ok(awn) if !awn.is_valid_for_network(current) => SendFlowError::WrongNetwork { + address: awn.address.to_string(), + valid_for: awn.network, + current, + }, + Ok(_) => SendFlowError::InvalidAddress(entering), + }; self.reconciler.send(Message::SetAlert(error.into())); } diff --git a/rust/src/manager/send_flow_manager/error.rs b/rust/src/manager/send_flow_manager/error.rs index 2777851cb..91caea00b 100644 --- a/rust/src/manager/send_flow_manager/error.rs +++ b/rust/src/manager/send_flow_manager/error.rs @@ -46,6 +46,9 @@ pub enum SendFlowError { #[error("unable to get fee details: {0}")] UnableToGetFeeDetails(String), + + #[error("sending to silent payment addresses is not yet supported")] + SilentPaymentNotSupported, } impl SendFlowError { @@ -57,6 +60,7 @@ impl SendFlowError { Self::WrongNetwork { address, valid_for, current } } + AddressError::SilentPaymentNotSupported => Self::SilentPaymentNotSupported, _ => Self::InvalidAddress(address), } } diff --git a/rust/src/multi_format.rs b/rust/src/multi_format.rs index dd73248ed..0151e6d32 100644 --- a/rust/src/multi_format.rs +++ b/rust/src/multi_format.rs @@ -53,6 +53,9 @@ pub enum MultiFormatError { #[error("Address is not supported for any network")] UnsupportedNetworkAddress, + #[error("silent payment addresses (sp1...) are not yet supported for sending")] + SilentPaymentNotSupported, + #[error( "Not a valid format, we only support addresses, SeedQr, mnemonic, descriptors, XPUBs and PSBTs" )] @@ -117,6 +120,10 @@ impl MultiFormat { return Err(MultiFormatError::UnsupportedNetworkAddress); } + Err(AddressError::SilentPaymentNotSupported) => { + return Err(MultiFormatError::SilentPaymentNotSupported); + } + _ => {} } diff --git a/rust/src/qr_scanner.rs b/rust/src/qr_scanner.rs index 091780b37..afde24d1c 100644 --- a/rust/src/qr_scanner.rs +++ b/rust/src/qr_scanner.rs @@ -60,6 +60,9 @@ pub enum MultiQrError { #[error("{0}")] ParseError(String), + #[error("silent payment addresses (sp1...) are not yet supported for sending")] + SilentPaymentNotSupported, + #[error("Invalid UTF-8")] InvalidUtf8, @@ -214,8 +217,7 @@ fn parse_ur(qr: &str) -> Result<(QrScanner, ScanResult), MultiQrError> { match ur.to_foundation_ur()? { foundation_ur::UR::SinglePart { .. } | foundation_ur::UR::SinglePartDeserialized { .. } => { debug!("Single-part UR, converting to MultiFormat"); - let multi_format = - MultiFormat::try_from_string(qr).map_err_str(MultiQrError::ParseError)?; + let multi_format = MultiFormat::try_from_string(qr)?; let result = ScanResult::Complete { data: multi_format, haptic: HapticFeedback::Success }; @@ -273,7 +275,17 @@ fn parse_bbqr_data( FileType::UnicodeText | FileType::Json => { let data_string = String::from_utf8(data).map_err(|_| MultiQrError::InvalidUtf8)?; - MultiFormat::try_from_string(&data_string).map_err_str(MultiQrError::ParseError) + MultiFormat::try_from_string(&data_string).map_err(MultiQrError::from) + } + } +} + +impl From for MultiQrError { + fn from(err: crate::multi_format::MultiFormatError) -> Self { + use crate::multi_format::MultiFormatError; + match err { + MultiFormatError::SilentPaymentNotSupported => Self::SilentPaymentNotSupported, + other => Self::ParseError(other.to_string()), } } } @@ -395,8 +407,7 @@ impl QrScanner { } // plain string - address, xpub, etc. - let multi_format = - MultiFormat::try_from_string(&qr).map_err_str(MultiQrError::ParseError)?; + let multi_format = MultiFormat::try_from_string(&qr)?; let result = ScanResult::Complete { data: multi_format, haptic: HapticFeedback::Success }; Ok((Self::Complete(result.clone()), result)) }