fix: observe-only reconcile + settled state for offline sends
This commit is contained in:
@@ -1,4 +1,30 @@
|
||||
/// Modelo para rastrear envíos offline pendientes.
|
||||
/// Estado del envío dentro del storage propio.
|
||||
///
|
||||
/// - [active] : el receptor aún no reclamó; los proofs están PendingSpent
|
||||
/// localmente. Mostrar con acción de cancelar/reclamar.
|
||||
/// - [settled] : la reconciliación con el mint confirmó que los proofs
|
||||
/// fueron gastados (receptor reclamó) o que una reconciliación
|
||||
/// previa ya los procesó. Mostrar como tx saliente histórica,
|
||||
/// sin acción de cancel.
|
||||
enum PendingSendStatus {
|
||||
active,
|
||||
settled;
|
||||
|
||||
String get wireName => name;
|
||||
|
||||
static PendingSendStatus fromWire(String? raw) {
|
||||
switch (raw) {
|
||||
case 'settled':
|
||||
return PendingSendStatus.settled;
|
||||
case 'active':
|
||||
case null:
|
||||
default:
|
||||
return PendingSendStatus.active;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modelo para rastrear envíos offline.
|
||||
///
|
||||
/// Los envíos offline no crean una Transaction en CDK (porque se hacen con
|
||||
/// selección manual de proofs + createOfflineToken), por lo que necesitamos
|
||||
@@ -32,6 +58,13 @@ class PendingSend {
|
||||
/// Memo opcional
|
||||
final String? memo;
|
||||
|
||||
/// Estado del envío. Ver [PendingSendStatus].
|
||||
final PendingSendStatus status;
|
||||
|
||||
/// Timestamp de cuando el envío pasó a [PendingSendStatus.settled].
|
||||
/// Null mientras esté activo. Se usa para ordenar en el historial.
|
||||
final DateTime? settledAt;
|
||||
|
||||
PendingSend({
|
||||
required this.id,
|
||||
required this.encoded,
|
||||
@@ -41,8 +74,17 @@ class PendingSend {
|
||||
required List<String> proofYs,
|
||||
required this.createdAt,
|
||||
this.memo,
|
||||
this.status = PendingSendStatus.active,
|
||||
this.settledAt,
|
||||
}) : proofYs = List.unmodifiable(proofYs);
|
||||
|
||||
bool get isActive => status == PendingSendStatus.active;
|
||||
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;
|
||||
|
||||
PendingSend copyWith({
|
||||
String? id,
|
||||
String? encoded,
|
||||
@@ -52,6 +94,8 @@ class PendingSend {
|
||||
List<String>? proofYs,
|
||||
DateTime? createdAt,
|
||||
String? memo,
|
||||
PendingSendStatus? status,
|
||||
DateTime? settledAt,
|
||||
}) {
|
||||
return PendingSend(
|
||||
id: id ?? this.id,
|
||||
@@ -62,6 +106,8 @@ class PendingSend {
|
||||
proofYs: proofYs ?? this.proofYs,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
memo: memo ?? this.memo,
|
||||
status: status ?? this.status,
|
||||
settledAt: settledAt ?? this.settledAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,6 +121,8 @@ class PendingSend {
|
||||
'proof_ys': proofYs.join(','),
|
||||
'created_at': createdAt.millisecondsSinceEpoch,
|
||||
'memo': memo,
|
||||
'status': status.wireName,
|
||||
'settled_at': settledAt?.millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,6 +131,7 @@ class PendingSend {
|
||||
final ys = ysRaw.isEmpty
|
||||
? <String>[]
|
||||
: ysRaw.split(',').where((s) => s.isNotEmpty).toList();
|
||||
final settledAtRaw = map['settled_at'] as int?;
|
||||
return PendingSend(
|
||||
id: map['id'] as String,
|
||||
encoded: map['encoded'] as String,
|
||||
@@ -92,10 +141,14 @@ class PendingSend {
|
||||
proofYs: ys,
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(map['created_at'] as int),
|
||||
memo: map['memo'] as String?,
|
||||
status: PendingSendStatus.fromWire(map['status'] as String?),
|
||||
settledAt: settledAtRaw == null
|
||||
? null
|
||||
: DateTime.fromMillisecondsSinceEpoch(settledAtRaw),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'PendingSend(id: $id, amount: $amount $unit, mintUrl: $mintUrl, ys: ${proofYs.length})';
|
||||
'PendingSend(id: $id, amount: $amount $unit, mintUrl: $mintUrl, ys: ${proofYs.length}, status: ${status.name})';
|
||||
}
|
||||
|
||||
@@ -5,17 +5,19 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'pending_send.dart';
|
||||
|
||||
/// Storage persistente para envíos offline pendientes.
|
||||
/// Storage persistente para envíos offline.
|
||||
///
|
||||
/// Los envíos offline no crean una Transaction en CDK, así que necesitamos
|
||||
/// rastrearlos nosotros para poder:
|
||||
/// - Mostrarlos en el historial como "pendientes de reclamo por el receptor".
|
||||
/// - Mostrarlos en el historial como activos (cancelables) o como
|
||||
/// liquidados (settled, display-only).
|
||||
/// - Reclamar las proofs por transacción si el receptor no reclamó.
|
||||
///
|
||||
/// Mirrors PendingTokenStorage (singleton + cache + SQLite + change stream).
|
||||
class PendingSendStorage {
|
||||
static const _dbName = 'pending_sends.db';
|
||||
static const _tableName = 'pending_sends';
|
||||
static const _schemaVersion = 2;
|
||||
|
||||
Database? _db;
|
||||
final Map<String, PendingSend> _cache = {};
|
||||
@@ -33,6 +35,9 @@ class PendingSendStorage {
|
||||
bool get isInitialized => _isInitialized;
|
||||
int get count => _cache.length;
|
||||
bool get hasPendingSends => _cache.isNotEmpty;
|
||||
int get activeCount =>
|
||||
_cache.values.where((s) => s.isActive).length;
|
||||
bool get hasActivePendingSends => activeCount > 0;
|
||||
|
||||
Future<void> init() async {
|
||||
if (_isInitialized) return;
|
||||
@@ -47,7 +52,7 @@ class PendingSendStorage {
|
||||
|
||||
_db = await openDatabase(
|
||||
dbPath,
|
||||
version: 1,
|
||||
version: _schemaVersion,
|
||||
onCreate: (db, version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE $_tableName (
|
||||
@@ -58,7 +63,9 @@ class PendingSendStorage {
|
||||
unit TEXT NOT NULL,
|
||||
proof_ys TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
memo TEXT
|
||||
memo TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
settled_at INTEGER
|
||||
)
|
||||
''');
|
||||
await db.execute(
|
||||
@@ -67,12 +74,32 @@ class PendingSendStorage {
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_ps_created_at ON $_tableName(created_at)',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_ps_status ON $_tableName(status)',
|
||||
);
|
||||
},
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
if (oldVersion < 2) {
|
||||
// v2: status + settled_at. Los records existentes quedan 'active'.
|
||||
await db.execute(
|
||||
"ALTER TABLE $_tableName ADD COLUMN status TEXT NOT NULL DEFAULT 'active'",
|
||||
);
|
||||
await db.execute(
|
||||
'ALTER TABLE $_tableName ADD COLUMN settled_at INTEGER',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX IF NOT EXISTS idx_ps_status ON $_tableName(status)',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await _loadFromDb();
|
||||
_isInitialized = true;
|
||||
debugPrint('PendingSendStorage inicializado con ${_cache.length} envíos');
|
||||
debugPrint(
|
||||
'PendingSendStorage inicializado: ${_cache.length} envíos '
|
||||
'($activeCount activos)',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadFromDb() async {
|
||||
@@ -103,6 +130,7 @@ class PendingSendStorage {
|
||||
proofYs: proofYs,
|
||||
createdAt: DateTime.now(),
|
||||
memo: memo,
|
||||
// status default = active
|
||||
);
|
||||
_cache[send.id] = send;
|
||||
await _db?.insert(_tableName, send.toMap());
|
||||
@@ -110,6 +138,30 @@ class PendingSendStorage {
|
||||
return send;
|
||||
}
|
||||
|
||||
/// Marca un envío como liquidado (settled). Se usa cuando la reconciliación
|
||||
/// confirmó que el receptor reclamó o el envío ya no es actionable.
|
||||
/// Si no existe el record, no hace nada.
|
||||
Future<void> markSettled(String id) async {
|
||||
final current = _cache[id];
|
||||
if (current == null) return;
|
||||
if (current.isSettled) return; // idempotente
|
||||
final updated = current.copyWith(
|
||||
status: PendingSendStatus.settled,
|
||||
settledAt: DateTime.now(),
|
||||
);
|
||||
_cache[id] = updated;
|
||||
await _db?.update(
|
||||
_tableName,
|
||||
updated.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
_changesController.add(null);
|
||||
}
|
||||
|
||||
/// Borrado permanente. Reservado para "dismiss manual" (no implementado
|
||||
/// 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);
|
||||
await _db?.delete(_tableName, where: 'id = ?', whereArgs: [id]);
|
||||
@@ -122,6 +174,20 @@ class PendingSendStorage {
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Envíos activos (el receptor aún no reclamó). Ordenados por creación
|
||||
/// descendente. Renderizables en el historial con botón cancel.
|
||||
List<PendingSend> listActive() {
|
||||
return _cache.values.where((s) => s.isActive).toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
|
||||
/// Envíos liquidados (el receptor ya reclamó). Ordenados por `settledAt`
|
||||
/// descendente — representa la línea temporal de finalización.
|
||||
List<PendingSend> listSettled() {
|
||||
return _cache.values.where((s) => s.isSettled).toList()
|
||||
..sort((a, b) => b.effectiveTimestamp.compareTo(a.effectiveTimestamp));
|
||||
}
|
||||
|
||||
PendingSend? get(String id) => _cache[id];
|
||||
|
||||
/// Filtra por mint+unit. Útil para mostrar pendientes específicos del
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
"pendingStatus": "Ausstehend",
|
||||
"receivedStatus": "Empfangen",
|
||||
"sentStatus": "Gesendet",
|
||||
"receiving": "Wird empfangen",
|
||||
"sending": "Wird gesendet",
|
||||
"now": "Jetzt",
|
||||
"agoMinutes": "Vor {minutes} Min",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -209,6 +209,8 @@
|
||||
"pendingStatus": "Pending",
|
||||
"receivedStatus": "Received",
|
||||
"sentStatus": "Sent",
|
||||
"receiving": "Receiving",
|
||||
"sending": "Sending",
|
||||
"now": "Now",
|
||||
"agoMinutes": "{minutes} min ago",
|
||||
"agoHours": "{hours} h ago",
|
||||
|
||||
@@ -251,6 +251,8 @@
|
||||
"pendingStatus": "Pendiente",
|
||||
"receivedStatus": "Recibido",
|
||||
"sentStatus": "Enviado",
|
||||
"receiving": "Recibiendo",
|
||||
"sending": "Enviando",
|
||||
"now": "Ahora",
|
||||
"agoMinutes": "Hace {minutes} min",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
"pendingStatus": "En attente",
|
||||
"receivedStatus": "Reçu",
|
||||
"sentStatus": "Envoyé",
|
||||
"receiving": "En réception",
|
||||
"sending": "En envoi",
|
||||
"now": "Maintenant",
|
||||
"agoMinutes": "Il y a {minutes} min",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
"pendingStatus": "In attesa",
|
||||
"receivedStatus": "Ricevuto",
|
||||
"sentStatus": "Inviato",
|
||||
"receiving": "In ricezione",
|
||||
"sending": "In invio",
|
||||
"now": "Adesso",
|
||||
"agoMinutes": "{minutes} min fa",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
"pendingStatus": "保留中",
|
||||
"receivedStatus": "受取済み",
|
||||
"sentStatus": "送金済み",
|
||||
"receiving": "受取中",
|
||||
"sending": "送金中",
|
||||
"now": "たった今",
|
||||
"agoMinutes": "{minutes}分前",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
"pendingStatus": "대기 중",
|
||||
"receivedStatus": "받음",
|
||||
"sentStatus": "보냄",
|
||||
"receiving": "받는 중",
|
||||
"sending": "보내는 중",
|
||||
"now": "방금",
|
||||
"agoMinutes": "{minutes}분 전",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -1135,6 +1135,18 @@ abstract class L10n {
|
||||
/// **'Enviado'**
|
||||
String get sentStatus;
|
||||
|
||||
/// No description provided for @receiving.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Recibiendo'**
|
||||
String get receiving;
|
||||
|
||||
/// No description provided for @sending.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Enviando'**
|
||||
String get sending;
|
||||
|
||||
/// No description provided for @now.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
|
||||
@@ -558,6 +558,12 @@ class L10nDe extends L10n {
|
||||
@override
|
||||
String get sentStatus => 'Gesendet';
|
||||
|
||||
@override
|
||||
String get receiving => 'Wird empfangen';
|
||||
|
||||
@override
|
||||
String get sending => 'Wird gesendet';
|
||||
|
||||
@override
|
||||
String get now => 'Jetzt';
|
||||
|
||||
|
||||
@@ -551,6 +551,12 @@ class L10nEn extends L10n {
|
||||
@override
|
||||
String get sentStatus => 'Sent';
|
||||
|
||||
@override
|
||||
String get receiving => 'Receiving';
|
||||
|
||||
@override
|
||||
String get sending => 'Sending';
|
||||
|
||||
@override
|
||||
String get now => 'Now';
|
||||
|
||||
|
||||
@@ -551,6 +551,12 @@ class L10nEs extends L10n {
|
||||
@override
|
||||
String get sentStatus => 'Enviado';
|
||||
|
||||
@override
|
||||
String get receiving => 'Recibiendo';
|
||||
|
||||
@override
|
||||
String get sending => 'Enviando';
|
||||
|
||||
@override
|
||||
String get now => 'Ahora';
|
||||
|
||||
|
||||
@@ -558,6 +558,12 @@ class L10nFr extends L10n {
|
||||
@override
|
||||
String get sentStatus => 'Envoyé';
|
||||
|
||||
@override
|
||||
String get receiving => 'En réception';
|
||||
|
||||
@override
|
||||
String get sending => 'En envoi';
|
||||
|
||||
@override
|
||||
String get now => 'Maintenant';
|
||||
|
||||
|
||||
@@ -553,6 +553,12 @@ class L10nIt extends L10n {
|
||||
@override
|
||||
String get sentStatus => 'Inviato';
|
||||
|
||||
@override
|
||||
String get receiving => 'In ricezione';
|
||||
|
||||
@override
|
||||
String get sending => 'In invio';
|
||||
|
||||
@override
|
||||
String get now => 'Adesso';
|
||||
|
||||
|
||||
@@ -544,6 +544,12 @@ class L10nJa extends L10n {
|
||||
@override
|
||||
String get sentStatus => '送金済み';
|
||||
|
||||
@override
|
||||
String get receiving => '受取中';
|
||||
|
||||
@override
|
||||
String get sending => '送金中';
|
||||
|
||||
@override
|
||||
String get now => 'たった今';
|
||||
|
||||
|
||||
@@ -546,6 +546,12 @@ class L10nKo extends L10n {
|
||||
@override
|
||||
String get sentStatus => '보냄';
|
||||
|
||||
@override
|
||||
String get receiving => '받는 중';
|
||||
|
||||
@override
|
||||
String get sending => '보내는 중';
|
||||
|
||||
@override
|
||||
String get now => '방금';
|
||||
|
||||
|
||||
@@ -554,6 +554,12 @@ class L10nPt extends L10n {
|
||||
@override
|
||||
String get sentStatus => 'Enviado';
|
||||
|
||||
@override
|
||||
String get receiving => 'Recebendo';
|
||||
|
||||
@override
|
||||
String get sending => 'Enviando';
|
||||
|
||||
@override
|
||||
String get now => 'Agora';
|
||||
|
||||
|
||||
@@ -552,6 +552,12 @@ class L10nRu extends L10n {
|
||||
@override
|
||||
String get sentStatus => 'Отправлено';
|
||||
|
||||
@override
|
||||
String get receiving => 'Получение';
|
||||
|
||||
@override
|
||||
String get sending => 'Отправка';
|
||||
|
||||
@override
|
||||
String get now => 'Сейчас';
|
||||
|
||||
|
||||
@@ -551,6 +551,12 @@ class L10nSw extends L10n {
|
||||
@override
|
||||
String get sentStatus => 'Imetumwa';
|
||||
|
||||
@override
|
||||
String get receiving => 'Inapokea';
|
||||
|
||||
@override
|
||||
String get sending => 'Inatumwa';
|
||||
|
||||
@override
|
||||
String get now => 'Sasa';
|
||||
|
||||
|
||||
@@ -542,6 +542,12 @@ class L10nZh extends L10n {
|
||||
@override
|
||||
String get sentStatus => '已发送';
|
||||
|
||||
@override
|
||||
String get receiving => '接收中';
|
||||
|
||||
@override
|
||||
String get sending => '发送中';
|
||||
|
||||
@override
|
||||
String get now => '刚刚';
|
||||
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
"pendingStatus": "Pendente",
|
||||
"receivedStatus": "Recebido",
|
||||
"sentStatus": "Enviado",
|
||||
"receiving": "Recebendo",
|
||||
"sending": "Enviando",
|
||||
"now": "Agora",
|
||||
"agoMinutes": "Há {minutes} min",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
"pendingStatus": "Ожидание",
|
||||
"receivedStatus": "Получено",
|
||||
"sentStatus": "Отправлено",
|
||||
"receiving": "Получение",
|
||||
"sending": "Отправка",
|
||||
"now": "Сейчас",
|
||||
"agoMinutes": "{minutes} мин назад",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
"pendingStatus": "Inasubiri",
|
||||
"receivedStatus": "Imepokelewa",
|
||||
"sentStatus": "Imetumwa",
|
||||
"receiving": "Inapokea",
|
||||
"sending": "Inatumwa",
|
||||
"now": "Sasa",
|
||||
"agoMinutes": "Dakika {minutes} zilizopita",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -243,6 +243,8 @@
|
||||
"pendingStatus": "待处理",
|
||||
"receivedStatus": "已收到",
|
||||
"sentStatus": "已发送",
|
||||
"receiving": "接收中",
|
||||
"sending": "发送中",
|
||||
"now": "刚刚",
|
||||
"agoMinutes": "{minutes} 分钟前",
|
||||
"@agoMinutes": {
|
||||
|
||||
@@ -135,11 +135,23 @@ class WalletProvider extends ChangeNotifier {
|
||||
// ============================================================
|
||||
|
||||
int get pendingSendCount => _pendingSendStorage.count;
|
||||
int get activePendingSendCount => _pendingSendStorage.activeCount;
|
||||
bool get hasPendingSends => _pendingSendStorage.hasPendingSends;
|
||||
bool get hasActivePendingSends => _pendingSendStorage.hasActivePendingSends;
|
||||
Stream<void> get pendingSendChanges => _pendingSendStorage.changes;
|
||||
|
||||
List<PendingSend> listPendingSends() => _pendingSendStorage.listAll();
|
||||
|
||||
/// Envíos activos (receptor aún no reclamó). Render: tile warning con
|
||||
/// botón cancel.
|
||||
List<PendingSend> listActivePendingSends() =>
|
||||
_pendingSendStorage.listActive();
|
||||
|
||||
/// Envíos liquidados (receptor reclamó). Render: tile outgoing settled,
|
||||
/// sin botón cancel.
|
||||
List<PendingSend> listSettledPendingSends() =>
|
||||
_pendingSendStorage.listSettled();
|
||||
|
||||
List<PendingSend> listPendingSendsByMintUnit(String mintUrl, String unit) =>
|
||||
_pendingSendStorage.listByMintUnit(mintUrl, unit);
|
||||
|
||||
@@ -176,27 +188,88 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Reclama las proofs de un envío offline pendiente. Verifica con el mint:
|
||||
/// - Si están UNSPENT → revierte a Unspent local y borra el PendingSend.
|
||||
/// - Si están SPENT (receptor ya reclamó) → borra el PendingSend igual
|
||||
/// (el envío se completó).
|
||||
/// - Si algunas PENDING en el mint → deja el PendingSend para reintento.
|
||||
/// - Si algunas están UNSPENT → revierte localmente y borra el PendingSend.
|
||||
/// - Si ninguna UNSPENT y ninguna PENDING (todas SPENT o ya reconciliadas)
|
||||
/// → el receptor reclamó: borra el PendingSend.
|
||||
/// - Si hay proofs PENDING en el mint → conserva el PendingSend para retry.
|
||||
Future<ReclaimResult> reclaimPendingSend(PendingSend send) async {
|
||||
final wallet = await getWallet(send.mintUrl, send.unit);
|
||||
final result = await wallet.reclaimProofsByYs(ys: send.proofYs);
|
||||
// Solo removemos el registro si el mint confirmó que había proofs
|
||||
// UNSPENT y las revertimos. Un count=0 es ambiguo: puede significar
|
||||
// "receptor ya reclamó (SPENT)" o "receptor en medio del swap
|
||||
// (PENDING)". En el caso PENDING, necesitamos conservar el record
|
||||
// para poder reintentar después — borrar perdería el retry handle.
|
||||
// Esto puede dejar records "fantasma" tras un SPENT real; se puede
|
||||
// agregar dismissal manual como mejora futura.
|
||||
if (result.count > BigInt.zero) {
|
||||
await _pendingSendStorage.remove(send.id);
|
||||
}
|
||||
await _settlePendingSendOutcome(send, result);
|
||||
notifyListeners();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Reconcilia todos los PendingSend contra el mint. Análogo a
|
||||
/// `checkPendingTransactions` pero para el storage propio de envíos
|
||||
/// offline (que CDK no conoce). Pensado para correr en pull-to-refresh,
|
||||
/// history mount y startup.
|
||||
///
|
||||
/// **Importante:** usa `checkProofsByYs` (observe-only), no
|
||||
/// `reclaimProofsByYs`. La reconciliación automática NO debe revertir
|
||||
/// proofs Unspent — eso cancelaría envíos legítimos que el receptor
|
||||
/// todavía no reclamó, abriendo una ventana de doble-gasto.
|
||||
///
|
||||
/// Sólo marca `settled` cuando el mint confirma que todos los proofs
|
||||
/// están gastados. Cualquier otro caso (pending mid-swap o unspent
|
||||
/// esperando al receptor) mantiene el record activo.
|
||||
///
|
||||
/// Tolera errores por-record (un mint offline no rompe el resto).
|
||||
/// Devuelve cuántos records pasaron a settled.
|
||||
Future<int> reconcilePendingSends() async {
|
||||
// Sólo activos: los settled ya están resueltos.
|
||||
final sends = _pendingSendStorage.listActive();
|
||||
if (sends.isEmpty) return 0;
|
||||
|
||||
var settled = 0;
|
||||
for (final send in sends) {
|
||||
try {
|
||||
final wallet = await getWallet(send.mintUrl, send.unit);
|
||||
final result = await wallet.checkProofsByYs(ys: send.proofYs);
|
||||
final total = BigInt.from(send.proofYs.length);
|
||||
final allSpent = result.pendingCount == BigInt.zero &&
|
||||
result.spentCount >= total;
|
||||
if (allSpent) {
|
||||
await _pendingSendStorage.markSettled(send.id);
|
||||
settled++;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('reconcilePendingSends: ${send.id} failed: $e');
|
||||
}
|
||||
}
|
||||
if (settled > 0) notifyListeners();
|
||||
return settled;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
Future<bool> _settlePendingSendOutcome(
|
||||
PendingSend send,
|
||||
ReclaimResult result,
|
||||
) async {
|
||||
if (result.count > BigInt.zero) {
|
||||
// Recuperamos las proofs; el envío se canceló desde nuestro lado.
|
||||
await _pendingSendStorage.remove(send.id);
|
||||
return true;
|
||||
}
|
||||
if (result.pendingCount == BigInt.zero) {
|
||||
// Ninguna unspent, ninguna pending → todas consumidas por el receptor.
|
||||
// Pasa a histórico como "outgoing liquidado".
|
||||
await _pendingSendStorage.markSettled(send.id);
|
||||
return true;
|
||||
}
|
||||
// pending > 0 → todavía hay proofs mid-swap; conservamos activo.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> removePendingSend(String id) async {
|
||||
await _pendingSendStorage.remove(id);
|
||||
notifyListeners();
|
||||
@@ -1611,6 +1684,10 @@ class WalletProvider extends ChangeNotifier {
|
||||
perUnit[unit] = ReclaimResult(
|
||||
count: (prev?.count ?? BigInt.zero) + result.count,
|
||||
amount: (prev?.amount ?? BigInt.zero) + result.amount,
|
||||
pendingCount:
|
||||
(prev?.pendingCount ?? BigInt.zero) + result.pendingCount,
|
||||
spentCount:
|
||||
(prev?.spentCount ?? BigInt.zero) + result.spentCount,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -166,7 +166,12 @@ class _SplashScreenState extends State<SplashScreen>
|
||||
/// Se ejecuta sin bloquear la navegación.
|
||||
Future<void> _checkPendingTransactions(WalletProvider wallet) async {
|
||||
try {
|
||||
await wallet.checkPendingTransactions();
|
||||
// CDK reconcilia online; reconcilePendingSends reconcilia los envíos
|
||||
// offline (storage propio).
|
||||
await Future.wait([
|
||||
wallet.checkPendingTransactions(),
|
||||
wallet.reconcilePendingSends(),
|
||||
]);
|
||||
debugPrint('Verificación de proofs completada');
|
||||
} catch (e) {
|
||||
debugPrint('Error verificando proofs: $e');
|
||||
|
||||
@@ -44,6 +44,17 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
HistoryFilter _currentFilter = HistoryFilter.all;
|
||||
bool _isRefreshing = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Reconciliar al entrar para capturar envíos que el receptor reclamó
|
||||
// desde la última vez. Evita que el usuario tenga que pull-to-refresh
|
||||
// manualmente para que un pending offline pase a histórico.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _refreshTransactions();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GradientBackground(
|
||||
@@ -161,12 +172,21 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
return _buildPendingTokensList(walletProvider);
|
||||
}
|
||||
|
||||
// Envíos offline pendientes (no tienen Transaction en CDK).
|
||||
// Solo mostrar en filtros "all" y "pending".
|
||||
final pendingSends =
|
||||
// Envíos offline activos (receptor aún no reclamó) → tile warning arriba
|
||||
// con botón de cancel. Solo visibles en filtros "all" y "pending".
|
||||
final activeSends =
|
||||
(_currentFilter == HistoryFilter.all ||
|
||||
_currentFilter == HistoryFilter.pending)
|
||||
? walletProvider.listPendingSends()
|
||||
? walletProvider.listActivePendingSends()
|
||||
: <PendingSend>[];
|
||||
|
||||
// Envíos offline liquidados (receptor reclamó) → se mezclan con las
|
||||
// transactions de CDK por timestamp, renderizados como outgoing settled.
|
||||
// Visibles en "all" y "cashu" (son cashu por definición).
|
||||
final settledSends =
|
||||
(_currentFilter == HistoryFilter.all ||
|
||||
_currentFilter == HistoryFilter.cashu)
|
||||
? walletProvider.listSettledPendingSends()
|
||||
: <PendingSend>[];
|
||||
|
||||
return FutureBuilder<List<Transaction>>(
|
||||
@@ -184,11 +204,32 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
final filteredTransactions =
|
||||
_applyFilter(allTransactions, walletProvider);
|
||||
|
||||
if (filteredTransactions.isEmpty && pendingSends.isEmpty) {
|
||||
if (filteredTransactions.isEmpty &&
|
||||
activeSends.isEmpty &&
|
||||
settledSends.isEmpty) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
final totalCount = pendingSends.length + filteredTransactions.length;
|
||||
// Merge settled sends + transactions en una sola lista ordenada por
|
||||
// timestamp descendente. Cada item es Transaction o PendingSend.
|
||||
final merged = <Object>[
|
||||
...filteredTransactions,
|
||||
...settledSends,
|
||||
]..sort((a, b) {
|
||||
final ta = a is Transaction
|
||||
? a.timestamp.toInt() * 1000
|
||||
: (a as PendingSend)
|
||||
.effectiveTimestamp
|
||||
.millisecondsSinceEpoch;
|
||||
final tb = b is Transaction
|
||||
? b.timestamp.toInt() * 1000
|
||||
: (b as PendingSend)
|
||||
.effectiveTimestamp
|
||||
.millisecondsSinceEpoch;
|
||||
return tb.compareTo(ta);
|
||||
});
|
||||
|
||||
final totalCount = activeSends.length + merged.length;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _refreshTransactions,
|
||||
@@ -197,17 +238,22 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
itemCount: totalCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < pendingSends.length) {
|
||||
final send = pendingSends[index];
|
||||
// Activos primero (pin al tope).
|
||||
if (index < activeSends.length) {
|
||||
return _PendingSendTile(
|
||||
send: send,
|
||||
send: activeSends[index],
|
||||
walletProvider: walletProvider,
|
||||
);
|
||||
}
|
||||
final tx = filteredTransactions[index - pendingSends.length];
|
||||
return _HistoryTransactionTile(
|
||||
transaction: tx,
|
||||
walletProvider: walletProvider,
|
||||
final item = merged[index - activeSends.length];
|
||||
if (item is Transaction) {
|
||||
return _HistoryTransactionTile(
|
||||
transaction: item,
|
||||
walletProvider: walletProvider,
|
||||
);
|
||||
}
|
||||
return _SettledPendingSendTile(
|
||||
send: item as PendingSend,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -386,7 +432,13 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||
|
||||
try {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
await walletProvider.checkPendingTransactions();
|
||||
// CDK reconcilia sus Transaction pendientes (online); reconcilePendingSends
|
||||
// hace lo mismo para el storage de envíos offline. Paralelos e
|
||||
// independientes.
|
||||
await Future.wait([
|
||||
walletProvider.checkPendingTransactions(),
|
||||
walletProvider.reconcilePendingSends(),
|
||||
]);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isRefreshing = false);
|
||||
@@ -575,7 +627,13 @@ class _HistoryTransactionTile extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isIncoming ? L10n.of(context)!.receivedStatus : L10n.of(context)!.sentStatus,
|
||||
isPending
|
||||
? (isIncoming
|
||||
? L10n.of(context)!.receiving
|
||||
: L10n.of(context)!.sending)
|
||||
: (isIncoming
|
||||
? L10n.of(context)!.receivedStatus
|
||||
: L10n.of(context)!.sentStatus),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
@@ -1675,6 +1733,10 @@ class _MinimalDetailRow extends StatelessWidget {
|
||||
|
||||
/// Tile para mostrar un envío offline pendiente en el historial.
|
||||
/// Se renderiza arriba de las Transactions de CDK.
|
||||
/// Tile para un envío offline activo (receptor aún no reclamó).
|
||||
/// Visualmente idéntico a una Transaction saliente pending (ecash online):
|
||||
/// fondo blanco 5%, ícono coins, título "Ecash", badge Pendiente y fila
|
||||
/// "↗ Enviando • timestamp". Al tap abre el detail screen con QR + cancel.
|
||||
class _PendingSendTile extends StatelessWidget {
|
||||
final PendingSend send;
|
||||
final WalletProvider walletProvider;
|
||||
@@ -1689,7 +1751,7 @@ class _PendingSendTile extends StatelessWidget {
|
||||
final l10n = L10n.of(context)!;
|
||||
final formattedAmount = UnitFormatter.formatBalance(send.amount, send.unit);
|
||||
final unitLabel = UnitFormatter.getUnitLabel(send.unit);
|
||||
final mintDisplay = UnitFormatter.getMintDisplayName(send.mintUrl);
|
||||
final dateStr = _formatRelativeDate(send.createdAt, context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@@ -1704,10 +1766,10 @@ class _PendingSendTile extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: AppDimensions.paddingSmall),
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning.withValues(alpha: 0.08),
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColors.warning.withValues(alpha: 0.3),
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -1717,12 +1779,12 @@ class _PendingSendTile extends StatelessWidget {
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.warning.withValues(alpha: 0.2),
|
||||
color: AppColors.primaryAction.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.clock,
|
||||
color: AppColors.warning,
|
||||
child: const Icon(
|
||||
LucideIcons.coins,
|
||||
color: AppColors.primaryAction,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
@@ -1733,11 +1795,11 @@ class _PendingSendTile extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
l10n.pendingOfflineSend,
|
||||
style: const TextStyle(
|
||||
const Text(
|
||||
'Ecash',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 15,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
@@ -1765,14 +1827,47 @@ class _PendingSendTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
mintDisplay,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.8),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.arrowUpRight,
|
||||
size: 12,
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
l10n.sending,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'• $dateStr',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (send.memo != null && send.memo!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
send.memo!,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1784,20 +1879,26 @@ class _PendingSendTile extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
unitLabel,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 11,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1805,8 +1906,170 @@ class _PendingSendTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pantalla de detalle para un envío offline pendiente.
|
||||
/// Muestra QR del token, monto, y botón para cancelar/reclamar.
|
||||
/// Tile para un envío offline liquidado (receptor ya reclamó). Visualmente
|
||||
/// equivalente a una Transaction saliente Cashu settled, para que el
|
||||
/// historial sea uniforme entre online y offline.
|
||||
class _SettledPendingSendTile extends StatelessWidget {
|
||||
final PendingSend send;
|
||||
|
||||
const _SettledPendingSendTile({required this.send});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context)!;
|
||||
final formattedAmount =
|
||||
UnitFormatter.formatBalance(send.amount, send.unit);
|
||||
final unitLabel = UnitFormatter.getUnitLabel(send.unit);
|
||||
final dateStr = _formatRelativeDate(
|
||||
send.effectiveTimestamp,
|
||||
context,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => _PendingSendDetailScreen(send: send),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: AppDimensions.paddingSmall),
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryAction.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.coins,
|
||||
color: AppColors.primaryAction,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimensions.paddingMedium),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Ecash',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.arrowUpRight,
|
||||
size: 12,
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
l10n.sentStatus,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'• $dateStr',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (send.memo != null && send.memo!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
send.memo!,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'-$formattedAmount',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
unitLabel,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Formateo relativo de fecha. Compartido por los tiles del historial.
|
||||
String _formatRelativeDate(DateTime date, BuildContext context) {
|
||||
final l10n = L10n.of(context)!;
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(date);
|
||||
if (diff.inMinutes < 1) return l10n.now;
|
||||
if (diff.inHours < 1) return l10n.agoMinutes(diff.inMinutes);
|
||||
if (diff.inDays < 1) return l10n.agoHours(diff.inHours);
|
||||
if (diff.inDays < 7) return l10n.agoDays(diff.inDays);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
return DateFormat.yMd(locale).format(date);
|
||||
}
|
||||
|
||||
/// Pantalla de detalle para un envío offline. Si el envío está activo,
|
||||
/// muestra QR + botón cancel. Si está liquidado (receptor reclamó), muestra
|
||||
/// solo el audit trail.
|
||||
class _PendingSendDetailScreen extends StatefulWidget {
|
||||
final PendingSend send;
|
||||
|
||||
@@ -1833,7 +2096,9 @@ class _PendingSendDetailScreenState extends State<_PendingSendDetailScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_encodeTokenToUR();
|
||||
// Para settled el QR ya no sirve (receptor reclamó), no desperdiciamos
|
||||
// ciclos codificándolo.
|
||||
if (widget.send.isActive) _encodeTokenToUR();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2058,6 +2323,7 @@ class _PendingSendDetailScreenState extends State<_PendingSendDetailScreen> {
|
||||
final unitLabel = UnitFormatter.getUnitLabel(widget.send.unit);
|
||||
final mintDisplay =
|
||||
UnitFormatter.getMintDisplayName(widget.send.mintUrl);
|
||||
final isActive = widget.send.isActive;
|
||||
final qrData = _urFragments.isNotEmpty
|
||||
? _urFragments[_currentFragment]
|
||||
: widget.send.encoded;
|
||||
@@ -2090,26 +2356,29 @@ class _PendingSendDetailScreenState extends State<_PendingSendDetailScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// 2. QR
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
// 2. QR — solo cuando el envío está activo (receptor aún no
|
||||
// reclamó). Cuando está settled, el token ya es obsoleto.
|
||||
if (isActive) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: QrImageView(
|
||||
data: qrData,
|
||||
version: QrVersions.auto,
|
||||
size: 240,
|
||||
backgroundColor: Colors.white,
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
child: QrImageView(
|
||||
data: qrData,
|
||||
version: QrVersions.auto,
|
||||
size: 240,
|
||||
backgroundColor: Colors.white,
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
// 3. Controles del QR (si es animado)
|
||||
if (_urFragments.length > 1) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildQRControls(),
|
||||
// 3. Controles del QR (si es animado)
|
||||
if (_urFragments.length > 1) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildQRControls(),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
// 4. Monto grande
|
||||
@@ -2137,10 +2406,11 @@ class _PendingSendDetailScreenState extends State<_PendingSendDetailScreen> {
|
||||
value: mintDisplay,
|
||||
),
|
||||
_MinimalDetailRow(
|
||||
icon: LucideIcons.clock,
|
||||
icon: isActive ? LucideIcons.clock : LucideIcons.check,
|
||||
label: l10n.status,
|
||||
value: l10n.pending,
|
||||
valueColor: AppColors.warning,
|
||||
value: isActive ? l10n.pending : l10n.sentStatus,
|
||||
valueColor:
|
||||
isActive ? AppColors.warning : AppColors.success,
|
||||
),
|
||||
if (widget.send.memo != null &&
|
||||
widget.send.memo!.isNotEmpty)
|
||||
@@ -2186,6 +2456,8 @@ class _PendingSendDetailScreenState extends State<_PendingSendDetailScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Botón cancel sólo cuando el envío está activo.
|
||||
if (isActive) ...[
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -2223,6 +2495,7 @@ class _PendingSendDetailScreenState extends State<_PendingSendDetailScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -10,7 +10,8 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
import 'payment_request.dart';
|
||||
import 'token.dart';
|
||||
|
||||
// These functions are ignored because they are not marked as `pub`: `mint_url`, `unit`, `update_balance_streams`
|
||||
// These functions are ignored because they are not marked as `pub`: `empty`, `from_states`, `into_result`, `mint_url`, `unit`, `update_balance_streams`
|
||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `StateBuckets`
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `clone`, `clone`, `clone`, `clone`, `cmp`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `into`, `partial_cmp`, `try_into`, `try_into`
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PreparedMelt>>
|
||||
@@ -71,6 +72,24 @@ abstract class Wallet implements RustOpaqueInterface {
|
||||
|
||||
Future<void> checkPendingTransactions();
|
||||
|
||||
/// Observe-only counterpart of `reclaim_proofs_by_ys`. Queries the mint
|
||||
/// for the state of the target Ys but **does NOT** revert Unspent proofs
|
||||
/// to the local Unspent state.
|
||||
///
|
||||
/// Use this for periodic reconciliation where we want to auto-settle
|
||||
/// sends the receiver already claimed, without accidentally cancelling
|
||||
/// sends the receiver has not claimed yet (which `reclaim_proofs_by_ys`
|
||||
/// would do by calling `unreserve_proofs`).
|
||||
///
|
||||
/// Return semantics:
|
||||
/// - `count` / `amount` always 0 (nothing is recovered).
|
||||
/// - `pending_count` / `spent_count` reflect the mint's report.
|
||||
/// - `spent_count` also includes target Ys that are no longer present
|
||||
/// in local PendingSpent (already reconciled or externally cleaned).
|
||||
/// The caller can infer "all resolved as spent" from
|
||||
/// `spent_count == target_ys.len() && pending_count == 0`.
|
||||
Future<ReclaimResult> checkProofsByYs({required List<String> ys});
|
||||
|
||||
Future<BigInt> confirmMelt({required PreparedMelt melt});
|
||||
|
||||
/// Create a NUT-18 payment request with Nostr transport.
|
||||
@@ -125,13 +144,19 @@ abstract class Wallet implements RustOpaqueInterface {
|
||||
Future<BigInt> receive({required Token token, ReceiveOptions? opts});
|
||||
|
||||
/// Check pending-spent proofs with the mint and revert unspent ones.
|
||||
/// Returns count and total amount of proofs recovered.
|
||||
/// Returns per-state buckets (unspent reverted, still pending, spent).
|
||||
Future<ReclaimResult> reclaimPendingProofs();
|
||||
|
||||
/// Reclaim proofs belonging to a specific transaction identified by its Y values.
|
||||
/// Works like reclaim_pending_proofs but scoped: only proofs whose Y matches
|
||||
/// Reclaim proofs belonging to a specific send identified by its Y values.
|
||||
/// Scoped variant of `reclaim_pending_proofs`: only proofs whose Y matches
|
||||
/// the input list are checked with the mint and reverted if Unspent.
|
||||
/// Use tx.ys from a pending outgoing transaction to cancel that specific send.
|
||||
///
|
||||
/// Empty returns:
|
||||
/// - Input `ys` empty or all unparseable → `ReclaimResult::empty()`.
|
||||
/// - None of the target Ys are in local pending anymore (a previous
|
||||
/// reconciliation already processed them) → `spent_count` is set to
|
||||
/// the number of targets, signalling to callers that the send is
|
||||
/// definitively finished and any local record can be removed.
|
||||
Future<ReclaimResult> reclaimProofsByYs({required List<String> ys});
|
||||
|
||||
Future<void> recoverIncompleteSagas();
|
||||
@@ -280,13 +305,36 @@ class ReceiveOptions {
|
||||
}
|
||||
|
||||
class ReclaimResult {
|
||||
/// Proofs reverted from PendingSpent to Unspent. Also the number of
|
||||
/// recovered proofs reflected in `amount`.
|
||||
final BigInt count;
|
||||
|
||||
/// Total value of the recovered proofs.
|
||||
final BigInt amount;
|
||||
|
||||
const ReclaimResult({required this.count, required this.amount});
|
||||
/// Proofs the mint reports as still Pending (receiver mid-swap). The
|
||||
/// caller should keep the record for retry — a later reconciliation
|
||||
/// will resolve them.
|
||||
final BigInt pendingCount;
|
||||
|
||||
/// Proofs the mint reports as Spent (receiver already claimed). When
|
||||
/// `count == 0 && pending_count == 0`, the send is definitively
|
||||
/// finished and the caller can delete its local record.
|
||||
final BigInt spentCount;
|
||||
|
||||
const ReclaimResult({
|
||||
required this.count,
|
||||
required this.amount,
|
||||
required this.pendingCount,
|
||||
required this.spentCount,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => count.hashCode ^ amount.hashCode;
|
||||
int get hashCode =>
|
||||
count.hashCode ^
|
||||
amount.hashCode ^
|
||||
pendingCount.hashCode ^
|
||||
spentCount.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -294,7 +342,9 @@ class ReclaimResult {
|
||||
other is ReclaimResult &&
|
||||
runtimeType == other.runtimeType &&
|
||||
count == other.count &&
|
||||
amount == other.amount;
|
||||
amount == other.amount &&
|
||||
pendingCount == other.pendingCount &&
|
||||
spentCount == other.spentCount;
|
||||
}
|
||||
|
||||
class SendOptions {
|
||||
|
||||
+123
-42
@@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
String get codegenVersion => '2.11.1';
|
||||
|
||||
@override
|
||||
int get rustContentHash => -740347571;
|
||||
int get rustContentHash => 1593022460;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
@@ -250,6 +250,11 @@ abstract class RustLibApi extends BaseApi {
|
||||
required Wallet that,
|
||||
});
|
||||
|
||||
Future<ReclaimResult> crateApiWalletWalletCheckProofsByYs({
|
||||
required Wallet that,
|
||||
required List<String> ys,
|
||||
});
|
||||
|
||||
Future<BigInt> crateApiWalletWalletConfirmMelt({
|
||||
required Wallet that,
|
||||
required PreparedMelt melt,
|
||||
@@ -1803,6 +1808,44 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
argNames: ["that"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<ReclaimResult> crateApiWalletWalletCheckProofsByYs({
|
||||
required Wallet that,
|
||||
required List<String> ys,
|
||||
}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerWallet(
|
||||
that,
|
||||
serializer,
|
||||
);
|
||||
sse_encode_list_String(ys, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 42,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_reclaim_result,
|
||||
decodeErrorData: sse_decode_error,
|
||||
),
|
||||
constMeta: kCrateApiWalletWalletCheckProofsByYsConstMeta,
|
||||
argValues: [that, ys],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiWalletWalletCheckProofsByYsConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "Wallet_check_proofs_by_ys",
|
||||
argNames: ["that", "ys"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<BigInt> crateApiWalletWalletConfirmMelt({
|
||||
required Wallet that,
|
||||
@@ -1823,7 +1866,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 42,
|
||||
funcId: 43,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1861,7 +1904,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 43,
|
||||
funcId: 44,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1898,7 +1941,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 44,
|
||||
funcId: 45,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1932,7 +1975,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 45,
|
||||
funcId: 46,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -1967,7 +2010,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 46,
|
||||
funcId: 47,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2008,7 +2051,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 47,
|
||||
funcId: 48,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2046,7 +2089,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 48,
|
||||
funcId: 49,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2089,7 +2132,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 49,
|
||||
funcId: 50,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2131,7 +2174,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
db,
|
||||
serializer,
|
||||
);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 50)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 51)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData:
|
||||
@@ -2169,7 +2212,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 51,
|
||||
funcId: 52,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2207,7 +2250,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 52,
|
||||
funcId: 53,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2248,7 +2291,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 53,
|
||||
funcId: 54,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2289,7 +2332,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 54,
|
||||
funcId: 55,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2325,7 +2368,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 55,
|
||||
funcId: 56,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2363,7 +2406,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 56,
|
||||
funcId: 57,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2399,7 +2442,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 57,
|
||||
funcId: 58,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2433,7 +2476,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 58,
|
||||
funcId: 59,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2467,7 +2510,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 59,
|
||||
funcId: 60,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2509,7 +2552,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 60,
|
||||
funcId: 61,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2545,7 +2588,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 61,
|
||||
funcId: 62,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2591,7 +2634,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 62,
|
||||
funcId: 63,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2629,7 +2672,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_String(proofsJson, serializer);
|
||||
sse_encode_opt_String(memo, serializer);
|
||||
sse_encode_opt_String(unit, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 63)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 64)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_token,
|
||||
@@ -2659,7 +2702,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_box_autoadd_token(token, serializer);
|
||||
sse_encode_opt_box_autoadd_usize(maxFragmentLength, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 64)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 65)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_list_String,
|
||||
@@ -2689,7 +2732,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 65,
|
||||
funcId: 66,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2713,7 +2756,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
SyncTask(
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 66)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 67)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_String,
|
||||
@@ -2739,7 +2782,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 67,
|
||||
funcId: 68,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2764,7 +2807,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_String(secret, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 68)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 69)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_String,
|
||||
@@ -2787,7 +2830,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_String(mnemonic, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 69)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 70)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_list_prim_u_8_strict,
|
||||
@@ -2814,7 +2857,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_String(encoded, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 70)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 71)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_payment_request_info,
|
||||
@@ -2843,7 +2886,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 71,
|
||||
funcId: 72,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2870,7 +2913,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 72,
|
||||
funcId: 73,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2897,7 +2940,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 73,
|
||||
funcId: 74,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -2922,7 +2965,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_list_prim_u_8_loose(raw, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 74)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 75)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_token,
|
||||
@@ -2945,7 +2988,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_String(encoded, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 75)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 76)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_token,
|
||||
@@ -3797,11 +3840,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
ReclaimResult dco_decode_reclaim_result(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 2)
|
||||
throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
|
||||
if (arr.length != 4)
|
||||
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||
return ReclaimResult(
|
||||
count: dco_decode_u_64(arr[0]),
|
||||
amount: dco_decode_u_64(arr[1]),
|
||||
pendingCount: dco_decode_u_64(arr[2]),
|
||||
spentCount: dco_decode_u_64(arr[3]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4966,7 +5011,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
var var_count = sse_decode_u_64(deserializer);
|
||||
var var_amount = sse_decode_u_64(deserializer);
|
||||
return ReclaimResult(count: var_count, amount: var_amount);
|
||||
var var_pendingCount = sse_decode_u_64(deserializer);
|
||||
var var_spentCount = sse_decode_u_64(deserializer);
|
||||
return ReclaimResult(
|
||||
count: var_count,
|
||||
amount: var_amount,
|
||||
pendingCount: var_pendingCount,
|
||||
spentCount: var_spentCount,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -6122,6 +6174,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_u_64(self.count, serializer);
|
||||
sse_encode_u_64(self.amount, serializer);
|
||||
sse_encode_u_64(self.pendingCount, serializer);
|
||||
sse_encode_u_64(self.spentCount, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -6541,6 +6595,27 @@ class WalletImpl extends RustOpaque implements Wallet {
|
||||
Future<void> checkPendingTransactions() => RustLib.instance.api
|
||||
.crateApiWalletWalletCheckPendingTransactions(that: this);
|
||||
|
||||
/// Observe-only counterpart of `reclaim_proofs_by_ys`. Queries the mint
|
||||
/// for the state of the target Ys but **does NOT** revert Unspent proofs
|
||||
/// to the local Unspent state.
|
||||
///
|
||||
/// Use this for periodic reconciliation where we want to auto-settle
|
||||
/// sends the receiver already claimed, without accidentally cancelling
|
||||
/// sends the receiver has not claimed yet (which `reclaim_proofs_by_ys`
|
||||
/// would do by calling `unreserve_proofs`).
|
||||
///
|
||||
/// Return semantics:
|
||||
/// - `count` / `amount` always 0 (nothing is recovered).
|
||||
/// - `pending_count` / `spent_count` reflect the mint's report.
|
||||
/// - `spent_count` also includes target Ys that are no longer present
|
||||
/// in local PendingSpent (already reconciled or externally cleaned).
|
||||
/// The caller can infer "all resolved as spent" from
|
||||
/// `spent_count == target_ys.len() && pending_count == 0`.
|
||||
Future<ReclaimResult> checkProofsByYs({required List<String> ys}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateApiWalletWalletCheckProofsByYs(that: this, ys: ys);
|
||||
|
||||
Future<BigInt> confirmMelt({required PreparedMelt melt}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
@@ -6620,14 +6695,20 @@ class WalletImpl extends RustOpaque implements Wallet {
|
||||
);
|
||||
|
||||
/// Check pending-spent proofs with the mint and revert unspent ones.
|
||||
/// Returns count and total amount of proofs recovered.
|
||||
/// Returns per-state buckets (unspent reverted, still pending, spent).
|
||||
Future<ReclaimResult> reclaimPendingProofs() =>
|
||||
RustLib.instance.api.crateApiWalletWalletReclaimPendingProofs(that: this);
|
||||
|
||||
/// Reclaim proofs belonging to a specific transaction identified by its Y values.
|
||||
/// Works like reclaim_pending_proofs but scoped: only proofs whose Y matches
|
||||
/// Reclaim proofs belonging to a specific send identified by its Y values.
|
||||
/// Scoped variant of `reclaim_pending_proofs`: only proofs whose Y matches
|
||||
/// the input list are checked with the mint and reverted if Unspent.
|
||||
/// Use tx.ys from a pending outgoing transaction to cancel that specific send.
|
||||
///
|
||||
/// Empty returns:
|
||||
/// - Input `ys` empty or all unparseable → `ReclaimResult::empty()`.
|
||||
/// - None of the target Ys are in local pending anymore (a previous
|
||||
/// reconciliation already processed them) → `spent_count` is set to
|
||||
/// the number of targets, signalling to callers that the send is
|
||||
/// definitively finished and any local record can be removed.
|
||||
Future<ReclaimResult> reclaimProofsByYs({required List<String> ys}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
|
||||
+175
-40
@@ -531,11 +531,11 @@ impl Wallet {
|
||||
// === Reclaim orphaned proofs ===
|
||||
|
||||
/// Check pending-spent proofs with the mint and revert unspent ones.
|
||||
/// Returns count and total amount of proofs recovered.
|
||||
/// Returns per-state buckets (unspent reverted, still pending, spent).
|
||||
pub async fn reclaim_pending_proofs(&self) -> Result<ReclaimResult, Error> {
|
||||
let pending = self.inner.get_pending_spent_proofs().await?;
|
||||
if pending.is_empty() {
|
||||
return Ok(ReclaimResult { count: 0, amount: 0 });
|
||||
return Ok(ReclaimResult::empty());
|
||||
}
|
||||
|
||||
// Build a map of Y → amount for later lookup
|
||||
@@ -548,56 +548,52 @@ impl Wallet {
|
||||
|
||||
// check_proofs_spent: queries mint AND removes spent proofs from local DB
|
||||
let states = self.inner.check_proofs_spent(pending).await?;
|
||||
let buckets = StateBuckets::from_states(states, &amount_by_y);
|
||||
|
||||
// Collect Y values of proofs the mint says are NOT spent
|
||||
// Only reclaim proofs the mint explicitly reports as Unspent.
|
||||
// Pending proofs (still being processed) must not be unreserved.
|
||||
let unspent_ys: Vec<PublicKey> = states
|
||||
.into_iter()
|
||||
.filter(|s| s.state == ProofState::Unspent)
|
||||
.map(|s| s.y)
|
||||
.collect();
|
||||
|
||||
let count = unspent_ys.len() as u64;
|
||||
let amount: u64 = unspent_ys.iter()
|
||||
.filter_map(|y| amount_by_y.get(y))
|
||||
.sum();
|
||||
|
||||
if count > 0 {
|
||||
if !buckets.unspent_ys.is_empty() {
|
||||
// Revert from PendingSpent to Unspent
|
||||
self.inner.unreserve_proofs(unspent_ys).await?;
|
||||
self.inner.unreserve_proofs(buckets.unspent_ys.clone()).await?;
|
||||
}
|
||||
|
||||
// Always refresh: check_proofs_spent may have removed spent proofs
|
||||
self.update_balance_streams().await;
|
||||
Ok(ReclaimResult { count, amount })
|
||||
Ok(buckets.into_result())
|
||||
}
|
||||
|
||||
/// Reclaim proofs belonging to a specific transaction identified by its Y values.
|
||||
/// Works like reclaim_pending_proofs but scoped: only proofs whose Y matches
|
||||
/// Reclaim proofs belonging to a specific send identified by its Y values.
|
||||
/// Scoped variant of `reclaim_pending_proofs`: only proofs whose Y matches
|
||||
/// the input list are checked with the mint and reverted if Unspent.
|
||||
/// Use tx.ys from a pending outgoing transaction to cancel that specific send.
|
||||
///
|
||||
/// Empty returns:
|
||||
/// - Input `ys` empty or all unparseable → `ReclaimResult::empty()`.
|
||||
/// - None of the target Ys are in local pending anymore (a previous
|
||||
/// reconciliation already processed them) → `spent_count` is set to
|
||||
/// the number of targets, signalling to callers that the send is
|
||||
/// definitively finished and any local record can be removed.
|
||||
pub async fn reclaim_proofs_by_ys(&self, ys: Vec<String>) -> Result<ReclaimResult, Error> {
|
||||
let target_ys: std::collections::HashSet<PublicKey> = ys
|
||||
.iter()
|
||||
.filter_map(|y| PublicKey::from_hex(y).ok())
|
||||
.collect();
|
||||
if target_ys.is_empty() {
|
||||
return Ok(ReclaimResult { count: 0, amount: 0 });
|
||||
return Ok(ReclaimResult::empty());
|
||||
}
|
||||
|
||||
let pending = self.inner.get_pending_spent_proofs().await?;
|
||||
if pending.is_empty() {
|
||||
return Ok(ReclaimResult { count: 0, amount: 0 });
|
||||
}
|
||||
|
||||
// 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();
|
||||
if relevant.is_empty() {
|
||||
return Ok(ReclaimResult { count: 0, amount: 0 });
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
|
||||
let mut amount_by_y: HashMap<PublicKey, u64> = HashMap::new();
|
||||
@@ -608,22 +604,83 @@ impl Wallet {
|
||||
}
|
||||
|
||||
let states = self.inner.check_proofs_spent(relevant).await?;
|
||||
let buckets = StateBuckets::from_states(states, &amount_by_y);
|
||||
|
||||
let unspent_ys: Vec<PublicKey> = states
|
||||
.into_iter()
|
||||
.filter(|s| s.state == ProofState::Unspent)
|
||||
.map(|s| s.y)
|
||||
.collect();
|
||||
|
||||
let count = unspent_ys.len() as u64;
|
||||
let amount: u64 = unspent_ys.iter().filter_map(|y| amount_by_y.get(y)).sum();
|
||||
|
||||
if count > 0 {
|
||||
self.inner.unreserve_proofs(unspent_ys).await?;
|
||||
if !buckets.unspent_ys.is_empty() {
|
||||
self.inner.unreserve_proofs(buckets.unspent_ys.clone()).await?;
|
||||
}
|
||||
|
||||
self.update_balance_streams().await;
|
||||
Ok(ReclaimResult { count, amount })
|
||||
Ok(buckets.into_result())
|
||||
}
|
||||
|
||||
/// Observe-only counterpart of `reclaim_proofs_by_ys`. Queries the mint
|
||||
/// for the state of the target Ys but **does NOT** revert Unspent proofs
|
||||
/// to the local Unspent state.
|
||||
///
|
||||
/// Use this for periodic reconciliation where we want to auto-settle
|
||||
/// sends the receiver already claimed, without accidentally cancelling
|
||||
/// sends the receiver has not claimed yet (which `reclaim_proofs_by_ys`
|
||||
/// would do by calling `unreserve_proofs`).
|
||||
///
|
||||
/// Return semantics:
|
||||
/// - `count` / `amount` always 0 (nothing is recovered).
|
||||
/// - `pending_count` / `spent_count` reflect the mint's report.
|
||||
/// - `spent_count` also includes target Ys that are no longer present
|
||||
/// in local PendingSpent (already reconciled or externally cleaned).
|
||||
/// The caller can infer "all resolved as spent" from
|
||||
/// `spent_count == target_ys.len() && pending_count == 0`.
|
||||
pub async fn check_proofs_by_ys(&self, ys: Vec<String>) -> Result<ReclaimResult, Error> {
|
||||
let target_ys: std::collections::HashSet<PublicKey> = ys
|
||||
.iter()
|
||||
.filter_map(|y| PublicKey::from_hex(y).ok())
|
||||
.collect();
|
||||
if target_ys.is_empty() {
|
||||
return Ok(ReclaimResult::empty());
|
||||
}
|
||||
let total = target_ys.len() as u64;
|
||||
|
||||
let pending = self.inner.get_pending_spent_proofs().await?;
|
||||
let relevant: Vec<_> = pending
|
||||
.into_iter()
|
||||
.filter(|proof| proof.y().map(|y| target_ys.contains(&y)).unwrap_or(false))
|
||||
.collect();
|
||||
|
||||
// Ys we expected to be in local PendingSpent but aren't anymore.
|
||||
// Safe assumption: they were reconciled out earlier because the
|
||||
// mint confirmed them spent (check_proofs_spent removed them).
|
||||
let resolved_out_of_pending = total.saturating_sub(relevant.len() as u64);
|
||||
|
||||
if relevant.is_empty() {
|
||||
return Ok(ReclaimResult {
|
||||
count: 0,
|
||||
amount: 0,
|
||||
pending_count: 0,
|
||||
spent_count: total,
|
||||
});
|
||||
}
|
||||
|
||||
let mut amount_by_y: HashMap<PublicKey, u64> = HashMap::new();
|
||||
for proof in &relevant {
|
||||
if let Ok(y) = proof.y() {
|
||||
amount_by_y.insert(y, u64::from(proof.amount));
|
||||
}
|
||||
}
|
||||
|
||||
// check_proofs_spent queries the mint AND removes locally-spent
|
||||
// proofs from the DB. That cleanup is fine — it doesn't cancel
|
||||
// an active send, it just advances state once the mint confirms
|
||||
// SPENT. The part we deliberately skip is `unreserve_proofs`.
|
||||
let states = self.inner.check_proofs_spent(relevant).await?;
|
||||
let buckets = StateBuckets::from_states(states, &amount_by_y);
|
||||
|
||||
self.update_balance_streams().await;
|
||||
Ok(ReclaimResult {
|
||||
count: 0,
|
||||
amount: 0,
|
||||
pending_count: buckets.pending_count,
|
||||
spent_count: buckets.spent_count + resolved_out_of_pending,
|
||||
})
|
||||
}
|
||||
|
||||
// === Utility ===
|
||||
@@ -787,8 +844,86 @@ impl<'a> From<CdkPreparedSend<'a>> for PreparedSend {
|
||||
}
|
||||
|
||||
pub struct ReclaimResult {
|
||||
/// Proofs reverted from PendingSpent to Unspent. Also the number of
|
||||
/// recovered proofs reflected in `amount`.
|
||||
pub count: u64,
|
||||
/// Total value of the recovered proofs.
|
||||
pub amount: u64,
|
||||
/// Proofs the mint reports as still Pending (receiver mid-swap). The
|
||||
/// caller should keep the record for retry — a later reconciliation
|
||||
/// will resolve them.
|
||||
pub pending_count: u64,
|
||||
/// Proofs the mint reports as Spent (receiver already claimed). When
|
||||
/// `count == 0 && pending_count == 0`, the send is definitively
|
||||
/// finished and the caller can delete its local record.
|
||||
pub spent_count: u64,
|
||||
}
|
||||
|
||||
impl ReclaimResult {
|
||||
#[frb(ignore)]
|
||||
fn empty() -> Self {
|
||||
Self {
|
||||
count: 0,
|
||||
amount: 0,
|
||||
pending_count: 0,
|
||||
spent_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper: partition the response of `check_proofs_spent` into
|
||||
/// the three buckets we care about (Unspent / Pending / Spent) and carry
|
||||
/// the Ys/amount needed to build a `ReclaimResult`.
|
||||
#[frb(ignore)]
|
||||
struct StateBuckets {
|
||||
unspent_ys: Vec<PublicKey>,
|
||||
unspent_amount: u64,
|
||||
pending_count: u64,
|
||||
spent_count: u64,
|
||||
}
|
||||
|
||||
impl StateBuckets {
|
||||
fn from_states(
|
||||
states: Vec<cdk::nuts::ProofState>,
|
||||
amount_by_y: &HashMap<PublicKey, u64>,
|
||||
) -> Self {
|
||||
let mut unspent_ys = Vec::new();
|
||||
let mut unspent_amount: u64 = 0;
|
||||
let mut pending_count: u64 = 0;
|
||||
let mut spent_count: u64 = 0;
|
||||
|
||||
for s in states {
|
||||
match s.state {
|
||||
ProofState::Unspent => {
|
||||
if let Some(&a) = amount_by_y.get(&s.y) {
|
||||
unspent_amount = unspent_amount.saturating_add(a);
|
||||
}
|
||||
unspent_ys.push(s.y);
|
||||
}
|
||||
ProofState::Pending => pending_count += 1,
|
||||
ProofState::Spent => spent_count += 1,
|
||||
// Reserved / other states: not expected for pending-spent
|
||||
// queries; ignore so we don't miscount.
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
unspent_ys,
|
||||
unspent_amount,
|
||||
pending_count,
|
||||
spent_count,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_result(self) -> ReclaimResult {
|
||||
ReclaimResult {
|
||||
count: self.unspent_ys.len() as u64,
|
||||
amount: self.unspent_amount,
|
||||
pending_count: self.pending_count,
|
||||
spent_count: self.spent_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PreparedMelt {
|
||||
|
||||
+109
-35
@@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -740347571;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1593022460;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -2043,6 +2043,66 @@ fn wire__crate__api__wallet__Wallet_check_pending_transactions_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__wallet__Wallet_check_proofs_by_ys_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "Wallet_check_proofs_by_ys",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
let api_that = <RustOpaqueMoi<
|
||||
flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Wallet>,
|
||||
>>::sse_decode(&mut deserializer);
|
||||
let api_ys = <Vec<String>>::sse_decode(&mut deserializer);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, crate::api::error::Error>(
|
||||
(move || async move {
|
||||
let mut api_that_guard = None;
|
||||
let decode_indices_ =
|
||||
flutter_rust_bridge::for_generated::lockable_compute_decode_order(
|
||||
vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new(
|
||||
&api_that, 0, false,
|
||||
)],
|
||||
);
|
||||
for i in decode_indices_ {
|
||||
match i {
|
||||
0 => {
|
||||
api_that_guard =
|
||||
Some(api_that.lockable_decode_async_ref().await)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
let api_that_guard = api_that_guard.unwrap();
|
||||
let output_ok = crate::api::wallet::Wallet::check_proofs_by_ys(
|
||||
&*api_that_guard,
|
||||
api_ys,
|
||||
)
|
||||
.await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__wallet__Wallet_confirm_melt_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -4579,9 +4639,13 @@ impl SseDecode for crate::api::wallet::ReclaimResult {
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut var_count = <u64>::sse_decode(deserializer);
|
||||
let mut var_amount = <u64>::sse_decode(deserializer);
|
||||
let mut var_pendingCount = <u64>::sse_decode(deserializer);
|
||||
let mut var_spentCount = <u64>::sse_decode(deserializer);
|
||||
return crate::api::wallet::ReclaimResult {
|
||||
count: var_count,
|
||||
amount: var_amount,
|
||||
pending_count: var_pendingCount,
|
||||
spent_count: var_spentCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4775,82 +4839,88 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
42 => wire__crate__api__wallet__Wallet_confirm_melt_impl(port, ptr, rust_vec_len, data_len),
|
||||
43 => wire__crate__api__wallet__Wallet_create_payment_request_impl(
|
||||
42 => wire__crate__api__wallet__Wallet_check_proofs_by_ys_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
44 => wire__crate__api__wallet__Wallet_finalize_pending_melts_impl(
|
||||
43 => wire__crate__api__wallet__Wallet_confirm_melt_impl(port, ptr, rust_vec_len, data_len),
|
||||
44 => wire__crate__api__wallet__Wallet_create_payment_request_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
45 => wire__crate__api__wallet__Wallet_get_mint_impl(port, ptr, rust_vec_len, data_len),
|
||||
46 => {
|
||||
45 => wire__crate__api__wallet__Wallet_finalize_pending_melts_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
46 => wire__crate__api__wallet__Wallet_get_mint_impl(port, ptr, rust_vec_len, data_len),
|
||||
47 => {
|
||||
wire__crate__api__wallet__Wallet_is_token_spent_impl(port, ptr, rust_vec_len, data_len)
|
||||
}
|
||||
47 => wire__crate__api__wallet__Wallet_list_transactions_impl(
|
||||
48 => wire__crate__api__wallet__Wallet_list_transactions_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
48 => wire__crate__api__wallet__Wallet_melt_quote_impl(port, ptr, rust_vec_len, data_len),
|
||||
49 => wire__crate__api__wallet__Wallet_mint_impl(port, ptr, rust_vec_len, data_len),
|
||||
51 => wire__crate__api__wallet__Wallet_pay_payment_request_impl(
|
||||
49 => wire__crate__api__wallet__Wallet_melt_quote_impl(port, ptr, rust_vec_len, data_len),
|
||||
50 => wire__crate__api__wallet__Wallet_mint_impl(port, ptr, rust_vec_len, data_len),
|
||||
52 => wire__crate__api__wallet__Wallet_pay_payment_request_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
52 => wire__crate__api__wallet__Wallet_prepare_melt_impl(port, ptr, rust_vec_len, data_len),
|
||||
53 => wire__crate__api__wallet__Wallet_prepare_send_impl(port, ptr, rust_vec_len, data_len),
|
||||
54 => wire__crate__api__wallet__Wallet_receive_impl(port, ptr, rust_vec_len, data_len),
|
||||
55 => wire__crate__api__wallet__Wallet_reclaim_pending_proofs_impl(
|
||||
53 => wire__crate__api__wallet__Wallet_prepare_melt_impl(port, ptr, rust_vec_len, data_len),
|
||||
54 => wire__crate__api__wallet__Wallet_prepare_send_impl(port, ptr, rust_vec_len, data_len),
|
||||
55 => wire__crate__api__wallet__Wallet_receive_impl(port, ptr, rust_vec_len, data_len),
|
||||
56 => wire__crate__api__wallet__Wallet_reclaim_pending_proofs_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
56 => wire__crate__api__wallet__Wallet_reclaim_proofs_by_ys_impl(
|
||||
57 => wire__crate__api__wallet__Wallet_reclaim_proofs_by_ys_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
57 => wire__crate__api__wallet__Wallet_recover_incomplete_sagas_impl(
|
||||
58 => wire__crate__api__wallet__Wallet_recover_incomplete_sagas_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
58 => {
|
||||
59 => {
|
||||
wire__crate__api__wallet__Wallet_refresh_balance_impl(port, ptr, rust_vec_len, data_len)
|
||||
}
|
||||
59 => wire__crate__api__wallet__Wallet_restore_impl(port, ptr, rust_vec_len, data_len),
|
||||
60 => wire__crate__api__wallet__Wallet_send_impl(port, ptr, rust_vec_len, data_len),
|
||||
61 => {
|
||||
60 => wire__crate__api__wallet__Wallet_restore_impl(port, ptr, rust_vec_len, data_len),
|
||||
61 => wire__crate__api__wallet__Wallet_send_impl(port, ptr, rust_vec_len, data_len),
|
||||
62 => {
|
||||
wire__crate__api__wallet__Wallet_stream_balance_impl(port, ptr, rust_vec_len, data_len)
|
||||
}
|
||||
62 => wire__crate__api__wallet__Wallet_wait_for_nostr_payment_impl(
|
||||
63 => wire__crate__api__wallet__Wallet_wait_for_nostr_payment_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
65 => wire__crate__api__mint_info__fetch_keysets_impl(port, ptr, rust_vec_len, data_len),
|
||||
67 => wire__crate__api__mint_info__get_mint_info_impl(port, ptr, rust_vec_len, data_len),
|
||||
71 => wire__crate__api__mint_info__ping_mint_impl(port, ptr, rust_vec_len, data_len),
|
||||
72 => wire__crate__api__wallet__receive_options_default_impl(
|
||||
66 => wire__crate__api__mint_info__fetch_keysets_impl(port, ptr, rust_vec_len, data_len),
|
||||
68 => wire__crate__api__mint_info__get_mint_info_impl(port, ptr, rust_vec_len, data_len),
|
||||
72 => wire__crate__api__mint_info__ping_mint_impl(port, ptr, rust_vec_len, data_len),
|
||||
73 => wire__crate__api__wallet__receive_options_default_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
73 => {
|
||||
74 => {
|
||||
wire__crate__api__wallet__send_options_default_impl(port, ptr, rust_vec_len, data_len)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@@ -4899,15 +4969,15 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||
34 => wire__crate__api__wallet__Wallet_auto_accessor_get_unit_impl(ptr, rust_vec_len, data_len),
|
||||
35 => wire__crate__api__wallet__Wallet_auto_accessor_set_mint_url_impl(ptr, rust_vec_len, data_len),
|
||||
36 => wire__crate__api__wallet__Wallet_auto_accessor_set_unit_impl(ptr, rust_vec_len, data_len),
|
||||
50 => wire__crate__api__wallet__Wallet_new_impl(ptr, rust_vec_len, data_len),
|
||||
63 => wire__crate__api__token__create_offline_token_impl(ptr, rust_vec_len, data_len),
|
||||
64 => wire__crate__api__token__encode_qr_token_impl(ptr, rust_vec_len, data_len),
|
||||
66 => wire__crate__api__keys__generate_mnemonic_impl(ptr, rust_vec_len, data_len),
|
||||
68 => wire__crate__api__keys__get_pub_key_impl(ptr, rust_vec_len, data_len),
|
||||
69 => wire__crate__api__keys__mnemonic_to_seed_impl(ptr, rust_vec_len, data_len),
|
||||
70 => wire__crate__api__payment_request__payment_request_info_parse_impl(ptr, rust_vec_len, data_len),
|
||||
74 => wire__crate__api__token__token_from_raw_bytes_impl(ptr, rust_vec_len, data_len),
|
||||
75 => wire__crate__api__token__token_parse_impl(ptr, rust_vec_len, data_len),
|
||||
51 => wire__crate__api__wallet__Wallet_new_impl(ptr, rust_vec_len, data_len),
|
||||
64 => wire__crate__api__token__create_offline_token_impl(ptr, rust_vec_len, data_len),
|
||||
65 => wire__crate__api__token__encode_qr_token_impl(ptr, rust_vec_len, data_len),
|
||||
67 => wire__crate__api__keys__generate_mnemonic_impl(ptr, rust_vec_len, data_len),
|
||||
69 => wire__crate__api__keys__get_pub_key_impl(ptr, rust_vec_len, data_len),
|
||||
70 => wire__crate__api__keys__mnemonic_to_seed_impl(ptr, rust_vec_len, data_len),
|
||||
71 => wire__crate__api__payment_request__payment_request_info_parse_impl(ptr, rust_vec_len, data_len),
|
||||
75 => wire__crate__api__token__token_from_raw_bytes_impl(ptr, rust_vec_len, data_len),
|
||||
76 => wire__crate__api__token__token_parse_impl(ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -5480,6 +5550,8 @@ impl flutter_rust_bridge::IntoDart for crate::api::wallet::ReclaimResult {
|
||||
[
|
||||
self.count.into_into_dart().into_dart(),
|
||||
self.amount.into_into_dart().into_dart(),
|
||||
self.pending_count.into_into_dart().into_dart(),
|
||||
self.spent_count.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
@@ -6324,6 +6396,8 @@ impl SseEncode for crate::api::wallet::ReclaimResult {
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<u64>::sse_encode(self.count, serializer);
|
||||
<u64>::sse_encode(self.amount, serializer);
|
||||
<u64>::sse_encode(self.pending_count, serializer);
|
||||
<u64>::sse_encode(self.spent_count, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user