fix: observe-only reconcile + settled state for offline sends

This commit is contained in:
Forte11Cuba
2026-04-18 02:09:50 -06:00
parent ca63c56ecf
commit a0f9ca738b
32 changed files with 1123 additions and 209 deletions
+55 -2
View File
@@ -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})';
}
+71 -5
View File
@@ -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
+2
View File
@@ -243,6 +243,8 @@
"pendingStatus": "Ausstehend",
"receivedStatus": "Empfangen",
"sentStatus": "Gesendet",
"receiving": "Wird empfangen",
"sending": "Wird gesendet",
"now": "Jetzt",
"agoMinutes": "Vor {minutes} Min",
"@agoMinutes": {
+2
View File
@@ -209,6 +209,8 @@
"pendingStatus": "Pending",
"receivedStatus": "Received",
"sentStatus": "Sent",
"receiving": "Receiving",
"sending": "Sending",
"now": "Now",
"agoMinutes": "{minutes} min ago",
"agoHours": "{hours} h ago",
+2
View File
@@ -251,6 +251,8 @@
"pendingStatus": "Pendiente",
"receivedStatus": "Recibido",
"sentStatus": "Enviado",
"receiving": "Recibiendo",
"sending": "Enviando",
"now": "Ahora",
"agoMinutes": "Hace {minutes} min",
"@agoMinutes": {
+2
View File
@@ -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": {
+2
View File
@@ -243,6 +243,8 @@
"pendingStatus": "In attesa",
"receivedStatus": "Ricevuto",
"sentStatus": "Inviato",
"receiving": "In ricezione",
"sending": "In invio",
"now": "Adesso",
"agoMinutes": "{minutes} min fa",
"@agoMinutes": {
+2
View File
@@ -243,6 +243,8 @@
"pendingStatus": "保留中",
"receivedStatus": "受取済み",
"sentStatus": "送金済み",
"receiving": "受取中",
"sending": "送金中",
"now": "たった今",
"agoMinutes": "{minutes}分前",
"@agoMinutes": {
+2
View File
@@ -243,6 +243,8 @@
"pendingStatus": "대기 중",
"receivedStatus": "받음",
"sentStatus": "보냄",
"receiving": "받는 중",
"sending": "보내는 중",
"now": "방금",
"agoMinutes": "{minutes}분 전",
"@agoMinutes": {
+12
View File
@@ -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:
+6
View File
@@ -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';
+6
View File
@@ -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';
+6
View File
@@ -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';
+6
View File
@@ -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';
+6
View File
@@ -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';
+6
View File
@@ -544,6 +544,12 @@ class L10nJa extends L10n {
@override
String get sentStatus => '送金済み';
@override
String get receiving => '受取中';
@override
String get sending => '送金中';
@override
String get now => 'たった今';
+6
View File
@@ -546,6 +546,12 @@ class L10nKo extends L10n {
@override
String get sentStatus => '보냄';
@override
String get receiving => '받는 중';
@override
String get sending => '보내는 중';
@override
String get now => '방금';
+6
View File
@@ -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';
+6
View File
@@ -552,6 +552,12 @@ class L10nRu extends L10n {
@override
String get sentStatus => 'Отправлено';
@override
String get receiving => 'Получение';
@override
String get sending => 'Отправка';
@override
String get now => 'Сейчас';
+6
View File
@@ -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';
+6
View File
@@ -542,6 +542,12 @@ class L10nZh extends L10n {
@override
String get sentStatus => '已发送';
@override
String get receiving => '接收中';
@override
String get sending => '发送中';
@override
String get now => '刚刚';
+2
View File
@@ -243,6 +243,8 @@
"pendingStatus": "Pendente",
"receivedStatus": "Recebido",
"sentStatus": "Enviado",
"receiving": "Recebendo",
"sending": "Enviando",
"now": "Agora",
"agoMinutes": "Há {minutes} min",
"@agoMinutes": {
+2
View File
@@ -243,6 +243,8 @@
"pendingStatus": "Ожидание",
"receivedStatus": "Получено",
"sentStatus": "Отправлено",
"receiving": "Получение",
"sending": "Отправка",
"now": "Сейчас",
"agoMinutes": "{minutes} мин назад",
"@agoMinutes": {
+2
View File
@@ -243,6 +243,8 @@
"pendingStatus": "Inasubiri",
"receivedStatus": "Imepokelewa",
"sentStatus": "Imetumwa",
"receiving": "Inapokea",
"sending": "Inatumwa",
"now": "Sasa",
"agoMinutes": "Dakika {minutes} zilizopita",
"@agoMinutes": {
+2
View File
@@ -243,6 +243,8 @@
"pendingStatus": "待处理",
"receivedStatus": "已收到",
"sentStatus": "已发送",
"receiving": "接收中",
"sending": "发送中",
"now": "刚刚",
"agoMinutes": "{minutes} 分钟前",
"@agoMinutes": {
+91 -14
View File
@@ -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) {
+6 -1
View File
@@ -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');
+335 -62
View File
@@ -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),
],
),
+58 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}