Handle network errors, improve Lightning setup, and add deposit events (#3127)

* fix: send all for lightning * fix: adjust crypto amount parsing logic for send page outputs * feat: add event handling for deposit transactions in Bitcoin and Lightning wallets - Introduced `onCreateDepositTransactionEvent` and `onUpdateDepositTransactionEvent` callbacks to manage deposit transaction updates. - Updated event listener logic to handle unclaimed and claimed deposit events. - Added support for processing deposit transaction info with `ElectrumTransactionInfo`. * fix: handle network errors on username validation and improve Lightning address setup - Added network error handling for username validation with a fallback error. - Enhanced Lightning address setup logic with error handling and a fail-safe mechanism to disable Lightning usage if the setup fails. * fix: enhance error handling for Lightning and update validation patterns * fix: remove redundant raw string mark from amount validation pattern * fix: normalize crypto amount input by replacing commas with dots

Konstantin Ullrich committed Mar 25, 2026 at 20:45 UTC e7b8229429b90881ebd6ab9e4e9f66a065e85c42
11 files changed +108 -43
cw_bitcoin/lib/bitcoin_wallet.dart
+17 -8
@@ -112,10 +112,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
112 );
113 } catch (e) {
114 printV(e);
115 - lightningWallet = null;
115 }
117 - } else {
118 - lightningWallet = null;
116 }
117
118 payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
@@ -153,6 +150,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
150 lnurlDomain: "cake.cash",
151 cachedAddress: cachedLightningAddress,
152 );
153 + walletAddresses.setLightningAddress(walletInfo.name);
154 }
155 } else {
156 lightningWallet = null;
@@ -367,6 +365,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
365 await fetchBalances();
366 }
367 },
368 + onCreateDepositTransactionEvent: (txs) async {
369 + if (txs.isNotEmpty) {
370 + transactionHistory.addMany(txs);
371 + await transactionHistory.save();
372 + }
373 + },
374 + onUpdateDepositTransactionEvent: (txs) async {
375 + if (txs.isNotEmpty) {
376 + txs.forEach((tx) => transactionHistory.transactions.remove(tx.id));
377 + await transactionHistory.save();
378 + }
379 + },
380 onBalanceChangedEvent: fetchBalances,
381 );
382 }
@@ -390,7 +400,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
400 return super.fetchTransactions();
401 }
402
393 - late LightningWallet? lightningWallet;
403 + LightningWallet? lightningWallet;
404
405 late final PayjoinManager payjoinManager;
406
@@ -479,10 +489,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
489 final isLNCompatible = await lightningWallet?.isCompatible(lnAddr);
490 if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) ||
491 isLNCompatible == true) {
482 -
492 BigInt amount;
493 if (credentials.outputs.first.sendAll) {
485 - amount = (await lightningWallet!.getBalance()) - BigInt.from(10);
494 + amount = await lightningWallet!.getBalance();
495 } else {
496 amount = parseFixed(
497 credentials.outputs.first.cryptoAmount?.isNotEmpty == true
@@ -491,8 +500,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
500 8);
501 }
502
494 - return lightningWallet!.createTransaction(lnAddr,
495 - amount > BigInt.zero ? amount : null, credentials.priority);
503 + return lightningWallet!.createTransaction(lnAddr, amount > BigInt.zero ? amount : null,
504 + credentials.priority, credentials.outputs.first.sendAll);
505 }
506
507 final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction;
cw_bitcoin/lib/electrum_wallet_addresses.dart
+5 -3
@@ -3,6 +3,7 @@ import 'dart:math';
3
4 import 'package:bitcoin_base/bitcoin_base.dart';
5 import 'package:blockchain_utils/blockchain_utils.dart';
6 +import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart';
7 import 'package:cw_bitcoin/bitcoin_address_record.dart';
8 import 'package:cw_bitcoin/bitcoin_unspent.dart';
9 import 'package:cw_bitcoin/lightning/lightning_addres_type.dart';
@@ -807,10 +808,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
808 printV(username);
809 rethrow;
810 }
810 - } catch (e) {
811 - // make sure lightningAddress gets initialized anyway
811 + } on SdkError_NetworkError catch (_) {
812 + } on SdkError_SparkError catch (e) {
813 + if (!e.field0.contains("dns")) rethrow;
814 + } finally {
815 lightningAddress ??= lightningWallet!.cachedAddress;
813 - rethrow;
816 }
817 }
818 }
cw_bitcoin/lib/lightning/lightning_wallet.dart
+59 -17
@@ -137,17 +137,24 @@ class LightningWallet {
137 .lightningAddress;
138 }
139
140 - Future<String> getBolt11Invoice(BigInt? amount, String description) async {
141 - final response = await sdk.receivePayment(
142 - request: ReceivePaymentRequest(
143 - paymentMethod: ReceivePaymentMethod.bolt11Invoice(
144 - description: description,
145 - amountSats: amount,
140 + Future<String?> getBolt11Invoice(BigInt? amount, String description) async {
141 + try {
142 + final response = await sdk.receivePayment(
143 + request: ReceivePaymentRequest(
144 + paymentMethod: ReceivePaymentMethod.bolt11Invoice(
145 + description: description,
146 + amountSats: amount,
147 + ),
148 ),
147 - ),
148 - );
149 + );
150
150 - return response.paymentRequest;
151 + return response.paymentRequest;
152 + } on SdkError_NetworkError catch (_) {
153 + return null;
154 + } on SdkError_SparkError catch (e) {
155 + if (!e.field0.contains("dns")) rethrow;
156 + return null;
157 + }
158 }
159
160 Future<bool> isCompatible(String input) async {
@@ -162,12 +169,14 @@ class LightningWallet {
169 }
170
171 Future<PendingLightningTransaction> createTransaction(
165 - String address, BigInt? amountSats, BitcoinTransactionPriority? priority) async {
172 + String address, BigInt? amountSats, BitcoinTransactionPriority? priority, bool feesIncluded) async {
173 final inputType = await sdk.parse(input: address);
174
175 + final feePolicy = feesIncluded ? FeePolicy.feesIncluded : FeePolicy.feesExcluded;
176 +
177 if (inputType is InputType_Bolt11Invoice) {
178 final request = PrepareSendPaymentRequest(
170 - paymentRequest: inputType.field0.invoice.bolt11, amount: amountSats);
179 + paymentRequest: inputType.field0.invoice.bolt11, amount: amountSats, feePolicy: feePolicy);
180 final prepareResponse = await sdk.prepareSendPayment(request: request);
181
182 final paymentMethod = prepareResponse.paymentMethod;
@@ -177,7 +186,7 @@ class LightningWallet {
186
187 return PendingLightningTransaction(
188 id: paymentMethod.invoiceDetails.paymentHash,
180 - amount: amountSats?.toInt() ??
189 + amount: request.amount?.toInt() ?? amountSats?.toInt() ??
190 ((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(),
191 fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0),
192 commitOverride: () async {
@@ -203,12 +212,14 @@ class LightningWallet {
212 amountSats: amountSats!,
213 payRequest: inputType.field0.payRequest,
214 validateSuccessActionUrl: optionalValidateSuccessActionUrl,
215 + feePolicy: feePolicy,
216 );
217 } else {
218 request = PrepareLnurlPayRequest(
219 amountSats: amountSats!,
220 payRequest: (inputType as InputType_LnurlPay).field0,
221 validateSuccessActionUrl: optionalValidateSuccessActionUrl,
222 + feePolicy: feePolicy,
223 );
224 }
225
@@ -218,7 +229,7 @@ class LightningWallet {
229
230 return PendingLightningTransaction(
231 id: prepareResponse.invoiceDetails.paymentHash,
221 - amount: ((prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(),
232 + amount: prepareResponse.amountSats.toInt(),
233 fee: feeSats.toInt(),
234 commitOverride: () async {
235 final res =
@@ -227,8 +238,11 @@ class LightningWallet {
238 },
239 );
240 } else if (inputType is InputType_BitcoinAddress) {
230 - final request =
231 - PrepareSendPaymentRequest(paymentRequest: inputType.field0.address, amount: amountSats);
241 + final request = PrepareSendPaymentRequest(
242 + paymentRequest: inputType.field0.address,
243 + amount: amountSats,
244 + feePolicy: feePolicy,
245 + );
246 final prepareResponse = await sdk.prepareSendPayment(request: request);
247
248 final paymentMethod = prepareResponse.paymentMethod;
@@ -355,8 +369,12 @@ class LightningWallet {
369 return response.txHex;
370 }
371
358 - void setEventListener(
359 - {required Function(ElectrumTransactionInfo) onTransactionEvent, required Function onBalanceChangedEvent}) {
372 + void setEventListener({
373 + required Function(ElectrumTransactionInfo) onTransactionEvent,
374 + required Function onBalanceChangedEvent,
375 + required Function(Map<String, ElectrumTransactionInfo>) onCreateDepositTransactionEvent,
376 + required Function(List<ElectrumTransactionInfo>) onUpdateDepositTransactionEvent,
377 + }) {
378 _eventSubscription = _eventStream?.listen((sdkEvent) {
379 if (sdkEvent is SdkEvent_PaymentSucceeded) {
380 onTransactionEvent(_getElectrumTransactionInfoFromPayment(sdkEvent.payment));
@@ -364,6 +382,16 @@ class LightningWallet {
382 onTransactionEvent(_getElectrumTransactionInfoFromPayment(sdkEvent.payment));
383 } else if (sdkEvent is SdkEvent_ClaimedDeposits) {
384 onBalanceChangedEvent();
385 + onUpdateDepositTransactionEvent(
386 + sdkEvent.claimedDeposits.map(_getElectrumTransactionInfoFromDepositInfo).toList());
387 + } else if (sdkEvent is SdkEvent_UnclaimedDeposits) {
388 + final unclaimedDeposits = <String, ElectrumTransactionInfo>{};
389 +
390 + for (final deposit in sdkEvent.unclaimedDeposits) {
391 + unclaimedDeposits[deposit.txid] = _getElectrumTransactionInfoFromDepositInfo(deposit);
392 + }
393 +
394 + onCreateDepositTransactionEvent(unclaimedDeposits);
395 }
396 });
397 }
@@ -390,4 +418,18 @@ class LightningWallet {
418 additionalInfo: {"isLightning": true},
419 );
420 }
421 +
422 + ElectrumTransactionInfo _getElectrumTransactionInfoFromDepositInfo(DepositInfo deposit) {
423 + return ElectrumTransactionInfo(
424 + WalletType.bitcoin,
425 + id: deposit.txid,
426 + amount: deposit.amountSats.toInt(),
427 + direction: TransactionDirection.incoming,
428 + isPending: true,
429 + fee: 0,
430 + date: DateTime.now(),
431 + confirmations: 0,
432 + additionalInfo: {"isLightning": true, "isSparkDeposit": true},
433 + );
434 + }
435 }
lib/core/amount_validator.dart
+1 -1
@@ -65,7 +65,7 @@ class SymbolsAmountValidator extends TextValidator {
65 maxLength: 0,
66 );
67
68 - static String _pattern() => '^([0-9]+([.\,][0-9]+)?|[.\,][0-9]+)\$';
68 + static String _pattern() => r'^(?:\d+(?:[.\,]\d+)?|[.\,]\d+)$';
69 }
70
71 class DecimalAmountValidator extends TextValidator {
lib/entities/lnurlpay_record.dart
+3 -1
@@ -12,7 +12,8 @@ class LNUrlPayRecord {
12 final String name;
13 final String address;
14
15 - static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency) async {
15 + static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency,
16 + {bool allowRethrow = false}) async {
17 if (![CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return null;
18
19 // split the string by the @ symbol:
@@ -40,6 +41,7 @@ class LNUrlPayRecord {
41 return LNURL.encode(expectedUrl);
42 }
43 } catch (e) {
44 + if (allowRethrow) rethrow;
45 printV("error checking well-known username: $e");
46 }
47 return null;
lib/new-ui/pages/send_page.dart
+7 -4
@@ -691,12 +691,15 @@ class _NewSendPageState extends State<NewSendPage> {
691 void _handleSend() async {
692 //TODO refactor this action. code was copied over from old ui. i don't like it.
693
694 - for(int i=0; i<widget.sendViewModel.outputs.length; i++) {
695 - if(i < _amountControllers.length) {
696 - if(_fiatInputMode) {
694 + for (var i = 0; i < widget.sendViewModel.outputs.length; i++) {
695 + if (i < _amountControllers.length && !widget.sendViewModel.outputs[i].sendAll) {
696 + if (_fiatInputMode) {
697 widget.sendViewModel.outputs[i].setFiatAmount(_amountControllers[i].text);
698 } else {
699 - widget.sendViewModel.outputs[i].cryptoAmount = _amountControllers[i].text;
699 + final amount = widget.sendViewModel.amountParsingProxy.getCanonicalCryptoAmount(
700 + _amountControllers[i].text.replaceAll(",", "."),
701 + widget.sendViewModel.selectedCryptoCurrency);
702 + widget.sendViewModel.outputs[i].setCryptoAmount(amount);
703 }
704 }
705 }
lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart
+9 -4
@@ -81,10 +81,15 @@ class LightningUsernameBloc extends Bloc<LightningUsernameEvent, LightningUserna
81 return UsernameError(S.current.already_your_username, isInfo: true);
82 }
83
84 - if ((await LNUrlPayRecord.checkWellKnownUsername(
85 - "${username}${usernameSuffix}", CryptoCurrency.btc)) !=
86 - null) {
87 - return UsernameError(S.current.username_not_available);
84 + try {
85 + if ((await LNUrlPayRecord.checkWellKnownUsername(
86 + "${username}${usernameSuffix}", CryptoCurrency.btc,
87 + allowRethrow: true)) !=
88 + null) {
89 + return UsernameError(S.current.username_not_available);
90 + }
91 + } catch (e) {
92 + return UsernameError(S.current.username_check_your_network);
93 }
94
95 return null;
lib/new-ui/widgets/send_page/send_amount_input.dart
+1 -2
@@ -3,7 +3,6 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 import 'package:flutter/material.dart';
4 import 'package:flutter/services.dart';
5 import 'package:flutter_mobx/flutter_mobx.dart';
6 -import 'package:flutter_svg/flutter_svg.dart';
6
7 class NewSendAmountInput extends StatefulWidget {
8 const NewSendAmountInput(
@@ -66,7 +65,7 @@ class _NewSendAmountInputState extends State<NewSendAmountInput> {
65 keyboardType:
66 TextInputType.numberWithOptions(signed: false, decimal: true),
67 inputFormatters: <TextInputFormatter>[
69 - FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$'))
68 + FilteringTextInputFormatter.allow(RegExp(r'^\d*[.,]?\d*$'))
69 ],
70 onChanged: state.didChange,
71 controller: widget.amountController,
lib/utils/exception_handler.dart
+2 -1
@@ -301,7 +301,8 @@ class ExceptionHandler {
301 "_QueuedFuture.execute (package:universal_ble/src/queue.dart:65)",
302 "reown_core/relay_client/websocket/websocket_handler.dart",
303 "Image upload failed due to loss of GPU access",
304 - 'transport error',
304 + "transport error",
305 + "SdkError.sparkError(field0: Operator RPC error: Connection error: status: Unavailable, message: \"dns error\", details: []"
306 ];
307
308 static Future<void> _addDeviceInfo(File file) async {
res/values/strings_de.arb
+2 -1
@@ -1273,6 +1273,7 @@
1273 "username_length_error": "Benutzernamen müssen 6–32 alphanumerische Zeichen enthalten.",
1274 "username_length_error_1_64": "Benutzernamen müssen 1–64 alphanumerische Zeichen enthalten.",
1275 "username_not_available": "Dieser Benutzername ist nicht verfügbar.",
1276 + "username_check_your_network": "Benutzername konnte nicht geprüft werden. Bitte überprüfen Sie Ihre Netzwerkverbindung.",
1277 "value": "Wert",
1278 "value_type": "Werttyp",
1279 "variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt",
@@ -1373,4 +1374,4 @@
1374 "zcash_card_missing_funds": "Fehlende Gelder?",
1375 "zcash_card_scan": "Scannen",
1376 "zcash_card_warning": "Schließen Sie die App nicht, bis der Vorgang abgeschlossen ist. Andernfalls muss der Vorgang von Grund auf neu gestartet werden."
1376 -}
\ No newline at end of file
1377 +}
res/values/strings_en.arb
+2 -1
@@ -1272,6 +1272,7 @@
1272 "username_length_error": "Usernames must contain 6-32 alphanumeric characters.",
1273 "username_length_error_1_64": "Usernames must contain 1-64 alphanumeric characters.",
1274 "username_not_available": "This username is not available.",
1275 + "username_check_your_network": "Unable to check username. Please check your network connection.",
1276 "value": "Value",
1277 "value_type": "Value Type",
1278 "variable_pair_not_supported": "This variable pair is not supported with the selected exchanges",
@@ -1371,4 +1372,4 @@
1372 "zcash_card_missing_funds": "Missing funds?",
1373 "zcash_card_scan": "Scan",
1374 "zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch."
1374 -}
\ No newline at end of file
1375 +}