Handle network errors, improve Lightning setup, and add deposit events (#3127)
What changed, and why it matters
This commit is a routine bug-fix and feature update for Cake Wallet's Bitcoin and Lightning support. It improves how the app handles network errors when checking Lightning usernames, fixes 'send all' behavior for Lightning payments, normalizes amount inputs that use commas instead of dots, and adds event handling for deposit transactions. There is no clear security vulnerability being fixed; the changes mostly improve robustness and user experience.
Treat as a normal functional update. Reviewers may want to verify that silently swallowing SdkError_NetworkError and DNS Spark errors does not mask persistent connectivity issues or allow stale/cached Lightning addresses to be presented as valid. No immediate security action is indicated by the diff alone.
Security signals we found
Network error handling added to external service calls (Lightning SDK / LNURL username lookup)
Amount parsing normalization (comma vs dot decimal separators)
New deposit transaction event callbacks in Bitcoin/Lightning wallet
Exception handler updated to ignore a specific DNS/unavailable RPC error string
Evidence from the diff
The patch touches Lightning wallet integration, amount parsing/validation, and deposit event handling. Key changes include: catching SdkError_NetworkError and DNS-related SdkError_SparkError during Lightning address setup and invoice generation; adding a fallback to cached Lightning address; passing a feePolicy flag into Lightning payment preparation; normalizing comma-to-dot decimal input; adding deposit create/update event callbacks; and updating exception-handler ignore patterns. No cryptographic, authentication, or authorization flaws are evident in the diff.
Changed components
cw_bitcoin/lib/bitcoin_wallet.dartcw_bitcoin/lib/electrum_wallet_addresses.dartcw_bitcoin/lib/lightning/lightning_wallet.dartlib/core/amount_validator.dartlib/entities/lnurlpay_record.dartlib/new-ui/pages/send_page.dartlib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dartlib/new-ui/widgets/send_page/send_amount_input.dartlib/utils/exception_handler.dartInspect captured patch +108 / −43
diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart
index 24618844..a75de4d4 100644
--- a/cw_bitcoin/lib/bitcoin_wallet.dart
+++ b/cw_bitcoin/lib/bitcoin_wallet.dart
@@ -112,10 +112,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
);
} catch (e) {
printV(e);
- lightningWallet = null;
}
- } else {
- lightningWallet = null;
}
payjoinManager = PayjoinManager(PayjoinStorage(payjoinBox), this);
@@ -153,6 +150,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
lnurlDomain: "cake.cash",
cachedAddress: cachedLightningAddress,
);
+ walletAddresses.setLightningAddress(walletInfo.name);
}
} else {
lightningWallet = null;
@@ -367,6 +365,18 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
await fetchBalances();
}
},
+ onCreateDepositTransactionEvent: (txs) async {
+ if (txs.isNotEmpty) {
+ transactionHistory.addMany(txs);
+ await transactionHistory.save();
+ }
+ },
+ onUpdateDepositTransactionEvent: (txs) async {
+ if (txs.isNotEmpty) {
+ txs.forEach((tx) => transactionHistory.transactions.remove(tx.id));
+ await transactionHistory.save();
+ }
+ },
onBalanceChangedEvent: fetchBalances,
);
}
@@ -390,7 +400,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
return super.fetchTransactions();
}
- late LightningWallet? lightningWallet;
+ LightningWallet? lightningWallet;
late final PayjoinManager payjoinManager;
@@ -479,10 +489,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final isLNCompatible = await lightningWallet?.isCompatible(lnAddr);
if ((credentials.coinTypeToSpendFrom == UnspentCoinType.lightning && lightningWallet != null) ||
isLNCompatible == true) {
-
BigInt amount;
if (credentials.outputs.first.sendAll) {
- amount = (await lightningWallet!.getBalance()) - BigInt.from(10);
+ amount = await lightningWallet!.getBalance();
} else {
amount = parseFixed(
credentials.outputs.first.cryptoAmount?.isNotEmpty == true
@@ -491,8 +500,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
8);
}
- return lightningWallet!.createTransaction(lnAddr,
- amount > BigInt.zero ? amount : null, credentials.priority);
+ return lightningWallet!.createTransaction(lnAddr, amount > BigInt.zero ? amount : null,
+ credentials.priority, credentials.outputs.first.sendAll);
}
final tx = (await super.createTransaction(credentials)) as PendingBitcoinTransaction;
diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart
index 15e0e02d..0c334385 100644
--- a/cw_bitcoin/lib/electrum_wallet_addresses.dart
+++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart
@@ -3,6 +3,7 @@ import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
+import 'package:breez_sdk_spark_flutter/breez_sdk_spark.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/lightning/lightning_addres_type.dart';
@@ -807,10 +808,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
printV(username);
rethrow;
}
- } catch (e) {
- // make sure lightningAddress gets initialized anyway
+ } on SdkError_NetworkError catch (_) {
+ } on SdkError_SparkError catch (e) {
+ if (!e.field0.contains("dns")) rethrow;
+ } finally {
lightningAddress ??= lightningWallet!.cachedAddress;
- rethrow;
}
}
}
diff --git a/cw_bitcoin/lib/lightning/lightning_wallet.dart b/cw_bitcoin/lib/lightning/lightning_wallet.dart
index 8f713b21..12a28776 100644
--- a/cw_bitcoin/lib/lightning/lightning_wallet.dart
+++ b/cw_bitcoin/lib/lightning/lightning_wallet.dart
@@ -137,17 +137,24 @@ class LightningWallet {
.lightningAddress;
}
- Future<String> getBolt11Invoice(BigInt? amount, String description) async {
- final response = await sdk.receivePayment(
- request: ReceivePaymentRequest(
- paymentMethod: ReceivePaymentMethod.bolt11Invoice(
- description: description,
- amountSats: amount,
+ Future<String?> getBolt11Invoice(BigInt? amount, String description) async {
+ try {
+ final response = await sdk.receivePayment(
+ request: ReceivePaymentRequest(
+ paymentMethod: ReceivePaymentMethod.bolt11Invoice(
+ description: description,
+ amountSats: amount,
+ ),
),
- ),
- );
+ );
- return response.paymentRequest;
+ return response.paymentRequest;
+ } on SdkError_NetworkError catch (_) {
+ return null;
+ } on SdkError_SparkError catch (e) {
+ if (!e.field0.contains("dns")) rethrow;
+ return null;
+ }
}
Future<bool> isCompatible(String input) async {
@@ -162,12 +169,14 @@ class LightningWallet {
}
Future<PendingLightningTransaction> createTransaction(
- String address, BigInt? amountSats, BitcoinTransactionPriority? priority) async {
+ String address, BigInt? amountSats, BitcoinTransactionPriority? priority, bool feesIncluded) async {
final inputType = await sdk.parse(input: address);
+ final feePolicy = feesIncluded ? FeePolicy.feesIncluded : FeePolicy.feesExcluded;
+
if (inputType is InputType_Bolt11Invoice) {
final request = PrepareSendPaymentRequest(
- paymentRequest: inputType.field0.invoice.bolt11, amount: amountSats);
+ paymentRequest: inputType.field0.invoice.bolt11, amount: amountSats, feePolicy: feePolicy);
final prepareResponse = await sdk.prepareSendPayment(request: request);
final paymentMethod = prepareResponse.paymentMethod;
@@ -177,7 +186,7 @@ class LightningWallet {
return PendingLightningTransaction(
id: paymentMethod.invoiceDetails.paymentHash,
- amount: amountSats?.toInt() ??
+ amount: request.amount?.toInt() ?? amountSats?.toInt() ??
((paymentMethod.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(),
fee: lightningFeeSats.toInt() + (sparkTransferFeeSats?.toInt() ?? 0),
commitOverride: () async {
@@ -203,12 +212,14 @@ class LightningWallet {
amountSats: amountSats!,
payRequest: inputType.field0.payRequest,
validateSuccessActionUrl: optionalValidateSuccessActionUrl,
+ feePolicy: feePolicy,
);
} else {
request = PrepareLnurlPayRequest(
amountSats: amountSats!,
payRequest: (inputType as InputType_LnurlPay).field0,
validateSuccessActionUrl: optionalValidateSuccessActionUrl,
+ feePolicy: feePolicy,
);
}
@@ -218,7 +229,7 @@ class LightningWallet {
return PendingLightningTransaction(
id: prepareResponse.invoiceDetails.paymentHash,
- amount: ((prepareResponse.invoiceDetails.amountMsat?.toInt() ?? 0) / 1000).round(),
+ amount: prepareResponse.amountSats.toInt(),
fee: feeSats.toInt(),
commitOverride: () async {
final res =
@@ -227,8 +238,11 @@ class LightningWallet {
},
);
} else if (inputType is InputType_BitcoinAddress) {
- final request =
- PrepareSendPaymentRequest(paymentRequest: inputType.field0.address, amount: amountSats);
+ final request = PrepareSendPaymentRequest(
+ paymentRequest: inputType.field0.address,
+ amount: amountSats,
+ feePolicy: feePolicy,
+ );
final prepareResponse = await sdk.prepareSendPayment(request: request);
final paymentMethod = prepareResponse.paymentMethod;
@@ -355,8 +369,12 @@ class LightningWallet {
return response.txHex;
}
- void setEventListener(
- {required Function(ElectrumTransactionInfo) onTransactionEvent, required Function onBalanceChangedEvent}) {
+ void setEventListener({
+ required Function(ElectrumTransactionInfo) onTransactionEvent,
+ required Function onBalanceChangedEvent,
+ required Function(Map<String, ElectrumTransactionInfo>) onCreateDepositTransactionEvent,
+ required Function(List<ElectrumTransactionInfo>) onUpdateDepositTransactionEvent,
+ }) {
_eventSubscription = _eventStream?.listen((sdkEvent) {
if (sdkEvent is SdkEvent_PaymentSucceeded) {
onTransactionEvent(_getElectrumTransactionInfoFromPayment(sdkEvent.payment));
@@ -364,6 +382,16 @@ class LightningWallet {
onTransactionEvent(_getElectrumTransactionInfoFromPayment(sdkEvent.payment));
} else if (sdkEvent is SdkEvent_ClaimedDeposits) {
onBalanceChangedEvent();
+ onUpdateDepositTransactionEvent(
+ sdkEvent.claimedDeposits.map(_getElectrumTransactionInfoFromDepositInfo).toList());
+ } else if (sdkEvent is SdkEvent_UnclaimedDeposits) {
+ final unclaimedDeposits = <String, ElectrumTransactionInfo>{};
+
+ for (final deposit in sdkEvent.unclaimedDeposits) {
+ unclaimedDeposits[deposit.txid] = _getElectrumTransactionInfoFromDepositInfo(deposit);
+ }
+
+ onCreateDepositTransactionEvent(unclaimedDeposits);
}
});
}
@@ -390,4 +418,18 @@ class LightningWallet {
additionalInfo: {"isLightning": true},
);
}
+
+ ElectrumTransactionInfo _getElectrumTransactionInfoFromDepositInfo(DepositInfo deposit) {
+ return ElectrumTransactionInfo(
+ WalletType.bitcoin,
+ id: deposit.txid,
+ amount: deposit.amountSats.toInt(),
+ direction: TransactionDirection.incoming,
+ isPending: true,
+ fee: 0,
+ date: DateTime.now(),
+ confirmations: 0,
+ additionalInfo: {"isLightning": true, "isSparkDeposit": true},
+ );
+ }
}
diff --git a/lib/core/amount_validator.dart b/lib/core/amount_validator.dart
index d056ad0c..dfa99993 100644
--- a/lib/core/amount_validator.dart
+++ b/lib/core/amount_validator.dart
@@ -65,7 +65,7 @@ class SymbolsAmountValidator extends TextValidator {
maxLength: 0,
);
- static String _pattern() => '^([0-9]+([.\,][0-9]+)?|[.\,][0-9]+)\$';
+ static String _pattern() => r'^(?:\d+(?:[.\,]\d+)?|[.\,]\d+)$';
}
class DecimalAmountValidator extends TextValidator {
diff --git a/lib/entities/lnurlpay_record.dart b/lib/entities/lnurlpay_record.dart
index c6e9788a..35fec0d5 100644
--- a/lib/entities/lnurlpay_record.dart
+++ b/lib/entities/lnurlpay_record.dart
@@ -12,7 +12,8 @@ class LNUrlPayRecord {
final String name;
final String address;
- static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency) async {
+ static Future<String?> checkWellKnownUsername(String username, CryptoCurrency currency,
+ {bool allowRethrow = false}) async {
if (![CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return null;
// split the string by the @ symbol:
@@ -40,6 +41,7 @@ class LNUrlPayRecord {
return LNURL.encode(expectedUrl);
}
} catch (e) {
+ if (allowRethrow) rethrow;
printV("error checking well-known username: $e");
}
return null;
diff --git a/lib/new-ui/pages/send_page.dart b/lib/new-ui/pages/send_page.dart
index 90ec3279..b27bf829 100644
--- a/lib/new-ui/pages/send_page.dart
+++ b/lib/new-ui/pages/send_page.dart
@@ -691,12 +691,15 @@ class _NewSendPageState extends State<NewSendPage> {
void _handleSend() async {
//TODO refactor this action. code was copied over from old ui. i don't like it.
- for(int i=0; i<widget.sendViewModel.outputs.length; i++) {
- if(i < _amountControllers.length) {
- if(_fiatInputMode) {
+ for (var i = 0; i < widget.sendViewModel.outputs.length; i++) {
+ if (i < _amountControllers.length && !widget.sendViewModel.outputs[i].sendAll) {
+ if (_fiatInputMode) {
widget.sendViewModel.outputs[i].setFiatAmount(_amountControllers[i].text);
} else {
- widget.sendViewModel.outputs[i].cryptoAmount = _amountControllers[i].text;
+ final amount = widget.sendViewModel.amountParsingProxy.getCanonicalCryptoAmount(
+ _amountControllers[i].text.replaceAll(",", "."),
+ widget.sendViewModel.selectedCryptoCurrency);
+ widget.sendViewModel.outputs[i].setCryptoAmount(amount);
}
}
}
diff --git a/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart b/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart
index 4cf78de6..435154aa 100644
--- a/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart
+++ b/lib/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart
@@ -81,10 +81,15 @@ class LightningUsernameBloc extends Bloc<LightningUsernameEvent, LightningUserna
return UsernameError(S.current.already_your_username, isInfo: true);
}
- if ((await LNUrlPayRecord.checkWellKnownUsername(
- "${username}${usernameSuffix}", CryptoCurrency.btc)) !=
- null) {
- return UsernameError(S.current.username_not_available);
+ try {
+ if ((await LNUrlPayRecord.checkWellKnownUsername(
+ "${username}${usernameSuffix}", CryptoCurrency.btc,
+ allowRethrow: true)) !=
+ null) {
+ return UsernameError(S.current.username_not_available);
+ }
+ } catch (e) {
+ return UsernameError(S.current.username_check_your_network);
}
return null;
diff --git a/lib/new-ui/widgets/send_page/send_amount_input.dart b/lib/new-ui/widgets/send_page/send_amount_input.dart
index 178b316d..99b25c39 100644
--- a/lib/new-ui/widgets/send_page/send_amount_input.dart
+++ b/lib/new-ui/widgets/send_page/send_amount_input.dart
@@ -3,7 +3,6 @@ import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
-import 'package:flutter_svg/flutter_svg.dart';
class NewSendAmountInput extends StatefulWidget {
const NewSendAmountInput(
@@ -66,7 +65,7 @@ class _NewSendAmountInputState extends State<NewSendAmountInput> {
keyboardType:
TextInputType.numberWithOptions(signed: false, decimal: true),
inputFormatters: <TextInputFormatter>[
- FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$'))
+ FilteringTextInputFormatter.allow(RegExp(r'^\d*[.,]?\d*$'))
],
onChanged: state.didChange,
controller: widget.amountController,
diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart
index 9711fdf1..9bee1704 100644
--- a/lib/utils/exception_handler.dart
+++ b/lib/utils/exception_handler.dart
@@ -301,7 +301,8 @@ class ExceptionHandler {
"_QueuedFuture.execute (package:universal_ble/src/queue.dart:65)",
"reown_core/relay_client/websocket/websocket_handler.dart",
"Image upload failed due to loss of GPU access",
- 'transport error',
+ "transport error",
+ "SdkError.sparkError(field0: Operator RPC error: Connection error: status: Unavailable, message: \"dns error\", details: []"
];
static Future<void> _addDeviceInfo(File file) async {
diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb
index 4737c1d5..f01448c0 100644
--- a/res/values/strings_de.arb
+++ b/res/values/strings_de.arb
@@ -1273,6 +1273,7 @@
"username_length_error": "Benutzernamen müssen 6–32 alphanumerische Zeichen enthalten.",
"username_length_error_1_64": "Benutzernamen müssen 1–64 alphanumerische Zeichen enthalten.",
"username_not_available": "Dieser Benutzername ist nicht verfügbar.",
+ "username_check_your_network": "Benutzername konnte nicht geprüft werden. Bitte überprüfen Sie Ihre Netzwerkverbindung.",
"value": "Wert",
"value_type": "Werttyp",
"variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt",
@@ -1373,4 +1374,4 @@
"zcash_card_missing_funds": "Fehlende Gelder?",
"zcash_card_scan": "Scannen",
"zcash_card_warning": "Schließen Sie die App nicht, bis der Vorgang abgeschlossen ist. Andernfalls muss der Vorgang von Grund auf neu gestartet werden."
-}
\ No newline at end of file
+}
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index f3f732c2..832ea23a 100644
--- a/res/values/strings_en.arb
+++ b/res/values/strings_en.arb
@@ -1272,6 +1272,7 @@
"username_length_error": "Usernames must contain 6-32 alphanumeric characters.",
"username_length_error_1_64": "Usernames must contain 1-64 alphanumeric characters.",
"username_not_available": "This username is not available.",
+ "username_check_your_network": "Unable to check username. Please check your network connection.",
"value": "Value",
"value_type": "Value Type",
"variable_pair_not_supported": "This variable pair is not supported with the selected exchanges",
@@ -1371,4 +1372,4 @@
"zcash_card_missing_funds": "Missing funds?",
"zcash_card_scan": "Scan",
"zcash_card_warning": "Do not close the app until the procedure completes, if you do so this process will need to restart from scratch."
-}
\ No newline at end of file
+}
Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.