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
This commit is contained in:
Konstantin Ullrich
2026-03-25 20:45:23 +01:00
committed by GitHub
parent d5ee2abe50
commit e7b8229429
11 changed files with 108 additions and 43 deletions
+17 -8
View File
@@ -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;
@@ -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;
}
}
}
+59 -17
View File
@@ -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},
);
}
}
+1 -1
View File
@@ -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 {
+3 -1
View File
@@ -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;
+7 -4
View File
@@ -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);
}
}
}
@@ -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;
@@ -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,
+2 -1
View File
@@ -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 {
+2 -1
View File
@@ -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."
}
}
+2 -1
View File
@@ -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."
}
}