fix: address CodeRabbit review on Nostr key persistence

This commit is contained in:
Forte11Cuba
2026-03-29 23:30:56 -06:00
parent 27a2baefcc
commit c44a17efbd
3 changed files with 52 additions and 18 deletions
+43 -13
View File
@@ -10,6 +10,7 @@ import '../src/rust/api/keys.dart';
import '../src/rust/api/payment_request.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:uuid/uuid.dart';
import '../data/transaction_meta_storage.dart';
import '../data/pending_token.dart';
@@ -1253,13 +1254,23 @@ class WalletProvider extends ChangeNotifier {
/// TTL for pending Nostr requests (24 hours).
static const _pendingNostrRequestTtl = Duration(hours: 24);
StreamSubscription<NostrPaymentEvent>? _pendingNostrSubscription;
bool _pendingNostrResumeAttempted = false;
static const _pendingNostrSecretKey = 'pending_nostr_secret';
static const _secureStorage = FlutterSecureStorage();
/// Save a pending Nostr payment request for recovery after app restart.
/// Secret key goes to FlutterSecureStorage, metadata to SharedPreferences.
Future<void> savePendingNostrRequest(PersistedRequestData data) async {
try {
// Secret key in encrypted storage
await _secureStorage.write(
key: _pendingNostrSecretKey,
value: data.secretHex,
);
// Metadata in SharedPreferences (no secrets)
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_pendingNostrRequestKey, jsonEncode({
'secretHex': data.secretHex,
'pubkeyHex': data.pubkeyHex,
'relays': data.relays,
'amount': data.amount?.toString(),
@@ -1275,6 +1286,7 @@ class WalletProvider extends ChangeNotifier {
/// Remove the pending Nostr payment request (payment received or cancelled).
Future<void> removePendingNostrRequest() async {
try {
await _secureStorage.delete(key: _pendingNostrSecretKey);
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_pendingNostrRequestKey);
} catch (e) {
@@ -1283,8 +1295,12 @@ class WalletProvider extends ChangeNotifier {
}
/// Check for a pending Nostr payment request and resume listening.
/// Called automatically during app startup.
/// Called automatically during app startup. Guarded against duplicate calls.
Future<void> resumePendingNostrRequest() async {
if (_pendingNostrResumeAttempted) return;
_pendingNostrResumeAttempted = true;
await _pendingNostrSubscription?.cancel();
_pendingNostrSubscription = null;
try {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString(_pendingNostrRequestKey);
@@ -1296,14 +1312,22 @@ class WalletProvider extends ChangeNotifier {
final createdAt = DateTime.tryParse(map['createdAt'] ?? '');
if (createdAt == null ||
DateTime.now().difference(createdAt) > _pendingNostrRequestTtl) {
await prefs.remove(_pendingNostrRequestKey);
await removePendingNostrRequest();
debugPrint('Pending Nostr request expired, removed');
return;
}
// Read secret from secure storage
final secretHex = await _secureStorage.read(key: _pendingNostrSecretKey);
if (secretHex == null) {
await prefs.remove(_pendingNostrRequestKey);
debugPrint('Pending Nostr request has no secret key, removed');
return;
}
// Reconstruct handle
final data = PersistedRequestData(
secretHex: map['secretHex'] as String,
secretHex: secretHex,
pubkeyHex: map['pubkeyHex'] as String,
relays: List<String>.from(map['relays']),
amount: map['amount'] != null
@@ -1315,27 +1339,33 @@ class WalletProvider extends ChangeNotifier {
final handle = NostrListenerHandle.fromPersisted(data: data);
// Find the wallet for this mint+unit (key format: "mintUrl:unit")
final walletKey = '${data.mintUrl}:${data.unit}';
final wallet = _wallets[walletKey];
if (wallet == null) {
debugPrint('No wallet found for pending Nostr request, removing');
await prefs.remove(_pendingNostrRequestKey);
// Get or create the wallet for this mint+unit (lazy instantiation)
Wallet wallet;
try {
wallet = await getWallet(data.mintUrl, data.unit);
} catch (e) {
debugPrint('No wallet for pending Nostr request: $e');
await removePendingNostrRequest();
return;
}
// Resume listening in background
debugPrint('Resuming pending Nostr payment request...');
wallet.waitForNostrPayment(handle: handle).listen(
_pendingNostrSubscription = wallet.waitForNostrPayment(handle: handle).listen(
(event) {
if (event.state == NostrPaymentState.received) {
debugPrint('Pending Nostr payment received: ${event.amount}');
removePendingNostrRequest();
confettiController.fire();
_pendingNostrSubscription?.cancel();
_pendingNostrSubscription = null;
}
},
onError: (_) => removePendingNostrRequest(),
onError: (error) {
debugPrint('Pending Nostr listener error (kept for retry): $error');
_pendingNostrSubscription = null;
_pendingNostrResumeAttempted = false; // Allow retry on next startup
},
);
} catch (e) {
debugPrint('Error resuming pending Nostr request: $e');
+7 -3
View File
@@ -72,6 +72,10 @@ class _RequestScreenState extends State<RequestScreen> {
_nostrSubscription?.cancel();
_mintSubscription?.cancel();
if (_nfcEmulating) NfcService.stopEmulating();
// Clear persisted request if user abandoned without receiving payment
if (!_paymentHandled && _creqB != null) {
context.read<WalletProvider>().removePendingNostrRequest();
}
super.dispose();
}
@@ -757,7 +761,7 @@ class _RequestScreenState extends State<RequestScreen> {
_creqB = request.creqB;
// Persist handle for recovery if app is killed
walletProvider.savePendingNostrRequest(
await walletProvider.savePendingNostrRequest(
request.listenerHandle.toPersisted(),
);
@@ -816,7 +820,7 @@ class _RequestScreenState extends State<RequestScreen> {
}
}
void _onPaymentSuccess(BigInt amount) {
Future<void> _onPaymentSuccess(BigInt amount) async {
if (_nfcEmulating) NfcService.stopEmulating();
setState(() {
_status = RequestStatus.received;
@@ -824,7 +828,7 @@ class _RequestScreenState extends State<RequestScreen> {
});
final walletProvider = context.read<WalletProvider>();
walletProvider.confettiController.fire();
walletProvider.removePendingNostrRequest();
await walletProvider.removePendingNostrRequest();
}
// ─── QR Content ───
+2 -2
View File
@@ -251,8 +251,8 @@ impl NostrListenerHandle {
let secret_key = nostr_sdk::SecretKey::from_hex(&data.secret_hex)
.map_err(|e| Error::Cdk(format!("Invalid persisted secret key: {e}")))?;
let keys = NostrKeys::new(secret_key);
let pubkey = PublicKey::from_hex(&data.pubkey_hex)
.map_err(|e| Error::Cdk(format!("Invalid persisted public key: {e}")))?;
// Derive pubkey from secret — don't trust the persisted copy
let pubkey = keys.public_key;
let unit = CurrencyUnit::from_str(&data.unit)
.unwrap_or(CurrencyUnit::Custom(data.unit));
let mint_url = MintUrl::from_str(&data.mint_url)