Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'services/wallet_service.dart';
import 'services/ln_address_service.dart';
import 'services/app_info_service.dart';
import 'services/deep_link_service.dart';
import 'services/cleared_invoice_store.dart';
import 'screens/auth_checker.dart';
import 'screens/10send_screen.dart';
import 'l10n/generated/app_localizations.dart';
Expand All @@ -25,6 +26,9 @@ void main() async {
// Initialize deep link service
await DeepLinkService().initialize();

// Load persisted cleared invoice hashes
await ClearedInvoiceStore.instance.load();

runApp(const LaChispaApp());
}

Expand Down
13 changes: 10 additions & 3 deletions lib/screens/7history_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import '../services/wallet_service.dart';
import '../models/transaction_info.dart';
import '../l10n/generated/app_localizations.dart';
import '../theme/app_tokens.dart';
import '../services/cleared_invoice_store.dart';

class HistoryScreen extends StatefulWidget {
const HistoryScreen({super.key});
Expand Down Expand Up @@ -165,14 +166,20 @@ class _HistoryScreenState extends State<HistoryScreen> with TickerProviderStateM
}

List<TransactionInfo> get _filteredTransactions {
var filtered = _transactions.where((tx) =>
!(tx.isPending &&
tx.paymentHash != null &&
ClearedInvoiceStore.instance.contains(tx.paymentHash))
).toList();

switch (_currentFilter) {
case TransactionFilter.incoming:
return _transactions.where((tx) => tx.isIncoming).toList();
return filtered.where((tx) => tx.isIncoming).toList();
case TransactionFilter.outgoing:
return _transactions.where((tx) => tx.isOutgoing).toList();
return filtered.where((tx) => tx.isOutgoing).toList();
case TransactionFilter.all:
default:
return _transactions;
return filtered;
}
}

Expand Down
48 changes: 45 additions & 3 deletions lib/screens/9receive_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import '../services/invoice_service.dart';
import '../services/yadio_service.dart';
import '../services/transaction_detector.dart';
import '../services/nfc_charge_service.dart';
import '../services/cleared_invoice_store.dart';
import '../models/lightning_invoice.dart';
import '../models/wallet_info.dart';
import '../l10n/generated/app_localizations.dart';
Expand All @@ -28,6 +29,7 @@ class ReceiveScreen extends StatefulWidget {
}

class _ReceiveScreenState extends State<ReceiveScreen> {

final _amountController = TextEditingController();
final _noteController = TextEditingController();
String _selectedCurrency = 'sats';
Expand Down Expand Up @@ -125,6 +127,11 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
_yadioService.dispose();
_invoicePaymentTimer?.cancel();
_invoicePaymentTimeoutTimer?.cancel();
if (_generatedInvoice != null) {
final hash = _generatedInvoice!.paymentHash;
ClearedInvoiceStore.instance.add(hash);
unawaited(_tryCancelInvoiceOnServer(hash));
}
Comment on lines +130 to +134
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm ClearedInvoiceStore.add signature
fd -t f 'cleared_invoice_store.dart' | xargs cat

Repository: lachispame/lachispa

Length of output: 1204


🏁 Script executed:

#!/bin/bash
# Find the receive screen file
fd -t f 'receive_screen.dart' -o '.*receive.*\.dart'

Repository: lachispame/lachispa

Length of output: 217


🏁 Script executed:

#!/bin/bash
# Find the receive screen file correctly
fd 'receive' --type f

Repository: lachispame/lachispa

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Check line count and then examine the specific lines
wc -l lib/screens/9receive_screen.dart
echo "---"
# Extract the three relevant sections
echo "Lines 128-136:"
sed -n '128,136p' lib/screens/9receive_screen.dart
echo "---"
echo "Lines 791-799:"
sed -n '791,799p' lib/screens/9receive_screen.dart
echo "---"
echo "Lines 810-818:"
sed -n '810,818p' lib/screens/9receive_screen.dart

Repository: lachispame/lachispa

Length of output: 1042


Wrap ClearedInvoiceStore.instance.add(hash) with unawaited() at all three call sites.

ClearedInvoiceStore.add() returns Future<void> and performs async SharedPreferences I/O. The current code at lines 132, 796, and 814 drops the returned Future without unawaited(), which violates the unawaited_futures lint and silently discards persistence errors. Wrap each call in unawaited(...) for consistency with the adjacent _tryCancelInvoiceOnServer call.

♻️ Proposed change (apply at all three sites)
-      ClearedInvoiceStore.instance.add(hash);
+      unawaited(ClearedInvoiceStore.instance.add(hash));
       unawaited(_tryCancelInvoiceOnServer(hash));

Also applies to: 796, 814

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/screens/9receive_screen.dart` around lines 130 - 134, The call to
ClearedInvoiceStore.instance.add(hash) returns a Future and must be wrapped with
unawaited(...) to avoid dropping the Future and to match the surrounding pattern
(_tryCancelInvoiceOnServer is already unawaited); replace direct calls to
ClearedInvoiceStore.instance.add(hash) with
unawaited(ClearedInvoiceStore.instance.add(hash)) at all three call sites in
this file (the branch guarded by _generatedInvoice != null and the other two
occurrences referenced in the review) so persistence I/O errors are not silently
discarded.

super.dispose();
}

Expand Down Expand Up @@ -783,6 +790,13 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
void _clearInvoice() {
_invoicePaymentTimer?.cancel();
_invoicePaymentTimeoutTimer?.cancel();

if (_generatedInvoice != null) {
final hash = _generatedInvoice!.paymentHash;
ClearedInvoiceStore.instance.add(hash);
unawaited(_tryCancelInvoiceOnServer(hash));
}

setState(() {
_generatedInvoice = null;
});
Expand All @@ -792,6 +806,36 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
);
}

void _discardInvoice() {
_invoicePaymentTimer?.cancel();
_invoicePaymentTimeoutTimer?.cancel();
if (_generatedInvoice != null) {
final hash = _generatedInvoice!.paymentHash;
ClearedInvoiceStore.instance.add(hash);
unawaited(_tryCancelInvoiceOnServer(hash));
}
setState(() {
_generatedInvoice = null;
});
}

Future<void> _tryCancelInvoiceOnServer(String paymentHash) async {
try {
final walletProvider = context.read<WalletProvider>();
final authProvider = context.read<AuthProvider>();
final serverUrl = authProvider.sessionData?.serverUrl;
final wallet = walletProvider.primaryWallet;

if (serverUrl == null || wallet == null) return;

await _invoiceService.cancelInvoice(
serverUrl: serverUrl,
adminKey: wallet.inKey,
paymentHash: paymentHash,
);
} catch (_) {}
}
Comment on lines +822 to +837
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect InvoiceService.cancelInvoice to confirm which key the endpoint expects
fd -t f 'invoice_service.dart' | xargs cat

Repository: lachispame/lachispa

Length of output: 50375


🏁 Script executed:

rg -A 20 'Future.*cancelInvoice' lib/services/

Repository: lachispame/lachispa

Length of output: 1376


🏁 Script executed:

rg -A 40 'Future<bool> cancelInvoice' lib/services/invoice_service.dart

Repository: lachispame/lachispa

Length of output: 1355


🏁 Script executed:

rg -B 5 -A 15 '_tryCancelInvoiceOnServer' lib/screens/9receive_screen.dart

Repository: lachispame/lachispa

Length of output: 2176


🏁 Script executed:

fd -t f 'wallet' lib/models/ | xargs grep -l 'class.*Wallet' | head -1 | xargs grep -A 30 'class.*Wallet' | grep -E '(inKey|adminKey)'

Repository: lachispame/lachispa

Length of output: 313


Pass wallet.adminKey instead of wallet.inKey to cancelInvoice.

_tryCancelInvoiceOnServer at lines 822–837 passes wallet.inKey as adminKey to InvoiceService.cancelInvoice, but the LNBits DELETE endpoint for cancelling invoices requires the wallet's admin key, not the read-only invoice key. Using inKey causes a 401/403 response, which the empty catch (_) {} silently swallows. The invoice then remains live on the server while appearing cancelled locally.

Change line 833 to:

adminKey: wallet.adminKey,

The wallet model already provides both keys separately (see line 144: setAuthHeaders(wallet.inKey, wallet.adminKey)).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/screens/9receive_screen.dart` around lines 822 - 837, In
_tryCancelInvoiceOnServer replace the read-only invoice key with the wallet's
admin key when calling InvoiceService.cancelInvoice: pass wallet.adminKey (not
wallet.inKey) as the adminKey argument to ensure the LNBits DELETE endpoint is
authorized; locate the call to cancelInvoice inside _tryCancelInvoiceOnServer
and update the adminKey parameter to use wallet.adminKey.


void _showCopySheet(LNAddress? defaultAddress) {
final hasInvoice = _generatedInvoice != null;
final lnurl = defaultAddress?.lnurl;
Expand Down Expand Up @@ -1444,9 +1488,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
_invoicePaymentTimeoutTimer = Timer(const Duration(minutes: 10), () {
_invoicePaymentTimer?.cancel();
if (!mounted) return;
setState(() {
_generatedInvoice = null;
});
_discardInvoice();
_showInfoSnackBar(
AppLocalizations.of(context)!.invoice_monitoring_timeout_message,
);
Expand Down
40 changes: 40 additions & 0 deletions lib/services/cleared_invoice_store.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

class ClearedInvoiceStore {
static final ClearedInvoiceStore _instance = ClearedInvoiceStore._();
static ClearedInvoiceStore get instance => _instance;
ClearedInvoiceStore._();

static const String _storageKey = 'cleared_invoice_hashes';
Set<String> _hashes = {};

Future<void> load() async {
try {
final prefs = await SharedPreferences.getInstance();
final stored = prefs.getString(_storageKey);
if (stored != null && stored.isNotEmpty) {
final List<dynamic> decoded = jsonDecode(stored);
_hashes = decoded.cast<String>().toSet();
}
} catch (_) {
_hashes = {};
}
}

Future<void> add(String hash) async {
_hashes.add(hash);
await _persist();
}

bool contains(String? hash) => hash != null && _hashes.contains(hash);

Set<String> get all => Set.unmodifiable(_hashes);

Future<void> _persist() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_storageKey, jsonEncode(_hashes.toList()));
} catch (_) {}
}
}
43 changes: 43 additions & 0 deletions lib/services/invoice_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,49 @@ class InvoiceService {
};
}

Future<bool> cancelInvoice({
required String serverUrl,
required String adminKey,
required String paymentHash,
}) async {
try {
String baseUrl = serverUrl;
if (!baseUrl.startsWith('http')) {
baseUrl = 'https://$baseUrl';
}

final headers = {'X-API-KEY': adminKey};

final endpoints = [
'$baseUrl/api/v1/payments/$paymentHash',
'$baseUrl/api/v1/wallet/payment/$paymentHash',
];

for (final endpoint in endpoints) {
try {
_debugLog('[INVOICE_SERVICE] Attempting to cancel invoice: $paymentHash');
final response = await _dio.delete(
endpoint,
options: Options(headers: headers),
);
if (response.statusCode == 200 || response.statusCode == 204) {
_debugLog('[INVOICE_SERVICE] Invoice cancelled successfully: $paymentHash');
return true;
}
} catch (e) {
_debugLog('[INVOICE_SERVICE] Cancel failed at $endpoint: $e');
continue;
}
}

_debugLog('[INVOICE_SERVICE] No endpoint succeeded for invoice cancellation: $paymentHash');
return false;
} catch (e) {
_debugLog('[INVOICE_SERVICE] Error cancelling invoice: $e');
return false;
}
}

void dispose() {
_dio.close();
}
Expand Down