fix: address CodeRabbit review
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user