fix: address CodeRabbit review

This commit is contained in:
Forte11Cuba
2026-04-18 03:29:51 -06:00
parent a0f9ca738b
commit d5d4b3a6ea
5 changed files with 50 additions and 21 deletions
+4 -2
View File
@@ -82,8 +82,10 @@ class PendingSend {
bool get isSettled => status == PendingSendStatus.settled;
/// Para ordenar en el historial: si está settled, usar settledAt; si no,
/// usar createdAt.
DateTime get effectiveTimestamp => settledAt ?? createdAt;
/// usar createdAt. Condicionado en `isSettled` (no sólo en `settledAt != null`)
/// para que una corrupción futura del campo no reubique un record activo.
DateTime get effectiveTimestamp =>
isSettled ? (settledAt ?? createdAt) : createdAt;
PendingSend copyWith({
String? id,
+10 -3
View File
@@ -132,8 +132,10 @@ class PendingSendStorage {
memo: memo,
// status default = active
);
_cache[send.id] = send;
// Persistencia primero: si SQLite falla, la cache no queda contaminada
// con un record que el reinicio no va a encontrar.
await _db?.insert(_tableName, send.toMap());
_cache[send.id] = send;
_changesController.add(null);
return send;
}
@@ -149,13 +151,16 @@ class PendingSendStorage {
status: PendingSendStatus.settled,
settledAt: DateTime.now(),
);
_cache[id] = updated;
// Persistencia primero: si SQLite falla, la cache queda activa y un
// próximo reconcile reintenta. Con el orden inverso, la idempotencia
// (`isSettled → early return`) impediría el retry tras una falla.
await _db?.update(
_tableName,
updated.toMap(),
where: 'id = ?',
whereArgs: [id],
);
_cache[id] = updated;
_changesController.add(null);
}
@@ -163,8 +168,10 @@ class PendingSendStorage {
/// hoy en UI) o cleanup explícito del wallet (`clear`). Para el caso
/// "receptor reclamó", usar `markSettled` en vez de este.
Future<void> remove(String id) async {
_cache.remove(id);
// Persistencia primero: igual que add/markSettled, la cache no se
// adelanta al storage.
await _db?.delete(_tableName, where: 'id = ?', whereArgs: [id]);
_cache.remove(id);
_changesController.add(null);
}
+18 -10
View File
@@ -244,13 +244,12 @@ class WalletProvider extends ChangeNotifier {
/// Decisión central sobre qué hacer con un PendingSend tras consultar
/// el mint. Devuelve `true` si el record dejó de estar activo.
///
/// - `count > 0` → recuperamos nosotros: el envío se
/// abortó, no hay ledger que mostrar. REMOVE el record.
/// - `count == 0 && pending == 0` → receptor reclamó (o reconciliación
/// previa ya lo procesó): el envío se completó. SETTLE el record
/// para que aparezca en histórico como outgoing liquidado.
/// - `pending_count > 0` → receptor mid-swap: conservar activo
/// para retry.
/// - `count > 0` → recuperamos nosotros:
/// el envío se abortó, no hay ledger que mostrar. REMOVE.
/// - `pending == 0 && spent >= proofYs.length` → mint confirmó todos
/// spent: receptor reclamó. SETTLE para histórico.
/// - cualquier otro caso (pending > 0, o cobertura parcial de spent)
/// → conservar activo; próxima reconciliación reintenta.
Future<bool> _settlePendingSendOutcome(
PendingSend send,
ReclaimResult result,
@@ -260,13 +259,22 @@ class WalletProvider extends ChangeNotifier {
await _pendingSendStorage.remove(send.id);
return true;
}
if (result.pendingCount == BigInt.zero) {
// Ninguna unspent, ninguna pending → todas consumidas por el receptor.
// Defensa en profundidad: aunque `pending_count == 0` suele bastar para
// decir "todas consumidas", exigimos además que `spent_count` cubra
// todos los proofs del send. Un resultado parcial o malformado podría
// dejar `pending == 0` sin haber observado spent para todos los ys.
final expected = BigInt.from(send.proofYs.length);
final allAccountedSpent = expected > BigInt.zero &&
result.pendingCount == BigInt.zero &&
result.spentCount >= expected;
if (allAccountedSpent) {
// Ninguna unspent, ninguna pending, todas spent → receptor reclamó.
// Pasa a histórico como "outgoing liquidado".
await _pendingSendStorage.markSettled(send.id);
return true;
}
// pending > 0 → todavía hay proofs mid-swap; conservamos activo.
// pending > 0 (receptor mid-swap) o cobertura parcial → conservamos
// activo para que la próxima reconciliación lo resuelva.
return false;
}
+4 -2
View File
@@ -173,10 +173,12 @@ class _HistoryScreenState extends State<HistoryScreen> {
}
// Envíos offline activos (receptor aún no reclamó) → tile warning arriba
// con botón de cancel. Solo visibles en filtros "all" y "pending".
// con botón de cancel. Son Cashu por definición, así que aparecen en
// "all", "pending" y "cashu" (consistente con los settled).
final activeSends =
(_currentFilter == HistoryFilter.all ||
_currentFilter == HistoryFilter.pending)
_currentFilter == HistoryFilter.pending ||
_currentFilter == HistoryFilter.cashu)
? walletProvider.listActivePendingSends()
: <PendingSend>[];
+14 -4
View File
@@ -579,20 +579,28 @@ impl Wallet {
return Ok(ReclaimResult::empty());
}
let total = target_ys.len() as u64;
let pending = self.inner.get_pending_spent_proofs().await?;
// Filter to only proofs with a Y in our target set.
let relevant: Vec<_> = pending
.into_iter()
.filter(|proof| proof.y().map(|y| target_ys.contains(&y)).unwrap_or(false))
.collect();
// Target Ys that aren't in local PendingSpent anymore are treated as
// spent — a previous `check_proofs_spent` call removed them from the
// local DB because the mint confirmed SPENT. This matches
// `check_proofs_by_ys` and keeps `ReclaimResult`'s bucket invariant
// (`unspent + pending + spent == total`) across both paths.
let resolved_out_of_pending = total.saturating_sub(relevant.len() as u64);
if relevant.is_empty() {
// Nothing left locally for this send → already reconciled.
// Caller can safely drop the record.
return Ok(ReclaimResult {
count: 0,
amount: 0,
pending_count: 0,
spent_count: target_ys.len() as u64,
spent_count: total,
});
}
@@ -611,7 +619,9 @@ impl Wallet {
}
self.update_balance_streams().await;
Ok(buckets.into_result())
let mut result = buckets.into_result();
result.spent_count += resolved_out_of_pending;
Ok(result)
}
/// Observe-only counterpart of `reclaim_proofs_by_ys`. Queries the mint