Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bc9639e89 | ||
|
|
e11778ff0a | ||
|
|
d5d4b3a6ea | ||
|
|
a0f9ca738b | ||
|
|
ca63c56ecf | ||
|
|
5cdce99154 | ||
|
|
f734b35670 | ||
|
|
5eef9ce130 | ||
|
|
f2951d2bef | ||
|
|
8015b4817b | ||
|
|
cc1531f3d5 | ||
|
|
aaef4b1cdc | ||
|
|
dacec9ea22 | ||
|
|
4b8edf9669 | ||
|
|
f458cfad34 | ||
|
|
428f2c1f45 | ||
|
|
9a14af33a2 | ||
|
|
770868a773 | ||
|
|
dc78f8958c | ||
|
|
03ba0e532f | ||
|
|
a6a4365a0d | ||
|
|
ac23574ae2 | ||
|
|
87f8e52036 | ||
|
|
eb9319fca0 | ||
|
|
a9e4272762 | ||
|
|
f66668f0ec | ||
|
|
100a4943eb | ||
|
|
5b1317c3e4 | ||
|
|
d85b6562dc | ||
|
|
56585319cc | ||
|
|
79ca30c5ed | ||
|
|
0bd988a7a6 | ||
|
|
218d798d2a | ||
|
|
e8d8789413 | ||
|
|
a2866d035a | ||
|
|
1a8e3e601b | ||
|
|
b3dde35141 | ||
|
|
8541222da6 | ||
|
|
d6085f7754 | ||
|
|
d2035ba416 | ||
|
|
42bab5d18a | ||
|
|
6c05785391 | ||
|
|
65224314e1 | ||
|
|
2534cf5d1e | ||
|
|
262b049c20 | ||
|
|
697f6fa555 | ||
|
|
02fec140e1 | ||
|
|
7cc92d6732 | ||
|
|
013560d25b | ||
|
|
d19a58df84 | ||
|
|
fd64d2f269 |
@@ -214,7 +214,7 @@ flutter build apk --release --target-platform android-arm64
|
||||
│ flutter_rust_bridge v2.11.1 (FFI) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ elcaju_core (Rust interno) │
|
||||
│ CDK 0.15.1 + SQLite + BIP39 + Tokio │
|
||||
│ CDK 0.16.0 + SQLite + BIP39 + Tokio │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -256,7 +256,7 @@ elcaju/
|
||||
│ │ └── scanner/ # QR scanner widget
|
||||
│ └── l10n/ # 11 languages (354 translation keys)
|
||||
├── rust/
|
||||
│ ├── Cargo.toml # elcaju_core: CDK 0.15.1, flutter_rust_bridge 2.11.1
|
||||
│ ├── Cargo.toml # elcaju_core: CDK 0.16.0, flutter_rust_bridge 2.11.1
|
||||
│ └── src/api/ # wallet, token, keys, mint_info, error
|
||||
├── rust_builder/ # Cargokit integration (auto Rust compilation)
|
||||
├── android/ # Android config (NDK 27.0, NFC HCE service)
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -384,7 +384,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -401,7 +401,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -416,7 +416,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -547,7 +547,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -569,7 +569,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// Servicio para obtener precios de Bitcoin usando Yadio API
|
||||
@@ -86,4 +87,59 @@ class PriceService {
|
||||
// Retornar en centavos
|
||||
return BigInt.from((fiatAmount * 100).round());
|
||||
}
|
||||
|
||||
// --- Blink API (precios históricos) ---
|
||||
|
||||
static const _blinkUrl = 'https://api.blink.sv/graphql';
|
||||
|
||||
/// Obtiene precios históricos de BTC desde Blink API.
|
||||
/// range: ONE_DAY, ONE_WEEK, ONE_MONTH, ONE_YEAR, FIVE_YEARS
|
||||
static Future<List<PricePoint>> getHistoricalPrices({
|
||||
String range = 'ONE_DAY',
|
||||
}) async {
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse(_blinkUrl),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'query':
|
||||
'query { btcPriceList(range: $range) { price { base offset } timestamp } }',
|
||||
}),
|
||||
).timeout(const Duration(seconds: 15));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Error HTTP: ${response.statusCode}');
|
||||
}
|
||||
|
||||
final json = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final data = json['data'] as Map<String, dynamic>?;
|
||||
if (data == null) throw Exception('No data in response');
|
||||
|
||||
final list = data['btcPriceList'] as List?;
|
||||
if (list == null || list.isEmpty) {
|
||||
throw Exception('No historical prices in response');
|
||||
}
|
||||
|
||||
return list.map((point) {
|
||||
final price = point['price'] as Map<String, dynamic>;
|
||||
final base = (price['base'] as num).toDouble();
|
||||
final offset = (price['offset'] as num).toInt();
|
||||
// base / 10^offset = USD cents → /100 = USD
|
||||
final priceUsd = base / pow(10, offset) / 100;
|
||||
final timestamp = (point['timestamp'] as num).toInt();
|
||||
|
||||
return PricePoint(timestamp: timestamp, priceUsd: priceUsd);
|
||||
}).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Error obteniendo precios históricos: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Punto de precio histórico de BTC
|
||||
class PricePoint {
|
||||
final int timestamp;
|
||||
final double priceUsd;
|
||||
|
||||
PricePoint({required this.timestamp, required this.priceUsd});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/// 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
|
||||
/// un storage propio para poder mostrarlos en el historial y permitir
|
||||
/// cancelar/reclamar por transacción.
|
||||
///
|
||||
/// Los proofs se identifican por sus Y values (hex) — mismos que acepta
|
||||
/// Wallet.reclaimProofsByYs() en el bridge Rust.
|
||||
class PendingSend {
|
||||
/// UUID único
|
||||
final String id;
|
||||
|
||||
/// Token codificado completo (cashuA... o cashuB...)
|
||||
final String encoded;
|
||||
|
||||
/// Monto total del envío (suma de los proofs)
|
||||
final BigInt amount;
|
||||
|
||||
/// URL del mint
|
||||
final String mintUrl;
|
||||
|
||||
/// Unidad (sat, usd, eur, etc.)
|
||||
final String unit;
|
||||
|
||||
/// Y values de los proofs incluidos (hex). Se usan para reclamar.
|
||||
final List<String> proofYs;
|
||||
|
||||
/// Fecha de creación del envío
|
||||
final DateTime createdAt;
|
||||
|
||||
/// 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,
|
||||
required this.amount,
|
||||
required this.mintUrl,
|
||||
required this.unit,
|
||||
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. Condicionado en `isSettled` (no sólo en `settledAt != null`)
|
||||
/// para que una corrupción futura del campo no reubique un record activo.
|
||||
DateTime get effectiveTimestamp =>
|
||||
isSettled ? (settledAt ?? createdAt) : createdAt;
|
||||
|
||||
PendingSend copyWith({
|
||||
String? id,
|
||||
String? encoded,
|
||||
BigInt? amount,
|
||||
String? mintUrl,
|
||||
String? unit,
|
||||
List<String>? proofYs,
|
||||
DateTime? createdAt,
|
||||
String? memo,
|
||||
PendingSendStatus? status,
|
||||
DateTime? settledAt,
|
||||
}) {
|
||||
return PendingSend(
|
||||
id: id ?? this.id,
|
||||
encoded: encoded ?? this.encoded,
|
||||
amount: amount ?? this.amount,
|
||||
mintUrl: mintUrl ?? this.mintUrl,
|
||||
unit: unit ?? this.unit,
|
||||
proofYs: proofYs ?? this.proofYs,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
memo: memo ?? this.memo,
|
||||
status: status ?? this.status,
|
||||
settledAt: settledAt ?? this.settledAt,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'encoded': encoded,
|
||||
'amount': amount.toString(),
|
||||
'mint_url': mintUrl,
|
||||
'unit': unit,
|
||||
'proof_ys': proofYs.join(','),
|
||||
'created_at': createdAt.millisecondsSinceEpoch,
|
||||
'memo': memo,
|
||||
'status': status.wireName,
|
||||
'settled_at': settledAt?.millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
factory PendingSend.fromMap(Map<String, dynamic> map) {
|
||||
final ysRaw = map['proof_ys'] as String? ?? '';
|
||||
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,
|
||||
amount: BigInt.parse(map['amount'] as String),
|
||||
mintUrl: map['mint_url'] as String,
|
||||
unit: map['unit'] as String,
|
||||
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}, status: ${status.name})';
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
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.
|
||||
///
|
||||
/// Los envíos offline no crean una Transaction en CDK, así que necesitamos
|
||||
/// rastrearlos nosotros para poder:
|
||||
/// - 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 = {};
|
||||
bool _isInitialized = false;
|
||||
|
||||
final StreamController<void> _changesController =
|
||||
StreamController<void>.broadcast();
|
||||
|
||||
Stream<void> get changes => _changesController.stream;
|
||||
|
||||
static final PendingSendStorage _instance = PendingSendStorage._internal();
|
||||
factory PendingSendStorage() => _instance;
|
||||
PendingSendStorage._internal();
|
||||
|
||||
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;
|
||||
|
||||
if (Platform.isLinux || Platform.isWindows) {
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
}
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dbPath = '${dir.path}/$_dbName';
|
||||
|
||||
_db = await openDatabase(
|
||||
dbPath,
|
||||
version: _schemaVersion,
|
||||
onCreate: (db, version) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE $_tableName (
|
||||
id TEXT PRIMARY KEY,
|
||||
encoded TEXT NOT NULL,
|
||||
amount TEXT NOT NULL,
|
||||
mint_url TEXT NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
proof_ys TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
memo TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
settled_at INTEGER
|
||||
)
|
||||
''');
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_ps_mint_unit ON $_tableName(mint_url, unit)',
|
||||
);
|
||||
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: ${_cache.length} envíos '
|
||||
'($activeCount activos)',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadFromDb() async {
|
||||
if (_db == null) return;
|
||||
final results = await _db!.query(_tableName, orderBy: 'created_at DESC');
|
||||
_cache.clear();
|
||||
for (final row in results) {
|
||||
final send = PendingSend.fromMap(row);
|
||||
_cache[send.id] = send;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PendingSend> add({
|
||||
required String id,
|
||||
required String encoded,
|
||||
required BigInt amount,
|
||||
required String mintUrl,
|
||||
required String unit,
|
||||
required List<String> proofYs,
|
||||
String? memo,
|
||||
}) async {
|
||||
final send = PendingSend(
|
||||
id: id,
|
||||
encoded: encoded,
|
||||
amount: amount,
|
||||
mintUrl: mintUrl,
|
||||
unit: unit,
|
||||
proofYs: proofYs,
|
||||
createdAt: DateTime.now(),
|
||||
memo: memo,
|
||||
// status default = active
|
||||
);
|
||||
// Persistencia primero: si SQLite falla, la cache no queda contaminada
|
||||
// con un record que el reinicio no va a encontrar.
|
||||
await _db?.insert(_tableName, send.toMap());
|
||||
_cache[send.id] = send;
|
||||
_changesController.add(null);
|
||||
return send;
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
);
|
||||
// Persistencia primero: si SQLite falla, la cache queda activa y un
|
||||
// próximo reconcile reintenta. Con el orden inverso, la idempotencia
|
||||
// (`isSettled → early return`) impediría el retry tras una falla.
|
||||
await _db?.update(
|
||||
_tableName,
|
||||
updated.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
_cache[id] = updated;
|
||||
_changesController.add(null);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// Persistencia primero: igual que add/markSettled, la cache no se
|
||||
// adelanta al storage.
|
||||
await _db?.delete(_tableName, where: 'id = ?', whereArgs: [id]);
|
||||
_cache.remove(id);
|
||||
_changesController.add(null);
|
||||
}
|
||||
|
||||
List<PendingSend> listAll() {
|
||||
final list = _cache.values.toList();
|
||||
list.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
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
|
||||
/// wallet activo.
|
||||
List<PendingSend> listByMintUnit(String mintUrl, String unit) {
|
||||
return _cache.values
|
||||
.where((s) => s.mintUrl == mintUrl && s.unit == unit)
|
||||
.toList()
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
_cache.clear();
|
||||
await _db?.delete(_tableName);
|
||||
_changesController.add(null);
|
||||
}
|
||||
}
|
||||
+43
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "Start",
|
||||
"receive": "Empfangen",
|
||||
"send": "Senden",
|
||||
"sendAction": "Senden ↗",
|
||||
"receiveAction": "↘ Empfangen",
|
||||
"sendAction": "Senden",
|
||||
"receiveAction": "Empfangen",
|
||||
"deposit": "Einzahlen",
|
||||
"withdraw": "Abheben",
|
||||
"lightning": "Lightning",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "Cashu empfangen",
|
||||
"pasteTheCashuToken": "Füge den Cashu Token ein:",
|
||||
"pasteFromClipboard": "Aus Zwischenablage einfügen",
|
||||
"emptyClipboard": "Zwischenablage leer",
|
||||
"cancelSend": "Senden abbrechen",
|
||||
"cancelSendConfirmTitle": "Diesen Versand abbrechen?",
|
||||
"cancelSendConfirmBody": "Funktioniert nur, wenn der Empfänger das Token noch nicht eingelöst hat. Falls doch, wird der Versand als abgeschlossen markiert.",
|
||||
"cancelSendSuccess": "Versand abgebrochen. {amount} {unit} zurückerhalten",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "Der Empfänger hat das Token bereits eingelöst",
|
||||
"pendingOfflineSend": "Ausstehender Offline-Versand",
|
||||
"pendingOfflineSendsHeader": "Nicht eingelöste Offline-Versendungen",
|
||||
"pendingSendDetailTitle": "Ausstehender Versand",
|
||||
"validToken": "Token gültig",
|
||||
"invalidToken": "Ungültiger oder fehlerhafter Token",
|
||||
"amount": "Betrag:",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "Bestätigen",
|
||||
"cancel": "Abbrechen",
|
||||
"insufficientBalance": "Unzureichendes Guthaben",
|
||||
"feeExceedsAmount": "Die Gebühr übersteigt den Sendebetrag",
|
||||
"tokenCreationError": "Fehler beim Erstellen des Tokens: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -227,6 +243,8 @@
|
||||
"pendingStatus": "Ausstehend",
|
||||
"receivedStatus": "Empfangen",
|
||||
"sentStatus": "Gesendet",
|
||||
"receiving": "Wird empfangen",
|
||||
"sending": "Wird gesendet",
|
||||
"now": "Jetzt",
|
||||
"agoMinutes": "Vor {minutes} Min",
|
||||
"@agoMinutes": {
|
||||
@@ -559,5 +577,27 @@
|
||||
"requestPaymentReceived": "Zahlung erhalten",
|
||||
"requestDescriptionHint": "Beschreibung (optional)",
|
||||
"universal": "Universal",
|
||||
"copiedToClipboard": "In die Zwischenablage kopiert"
|
||||
"copiedToClipboard": "In die Zwischenablage kopiert",
|
||||
|
||||
"swap": "Tauschen",
|
||||
"swapDescription": "Zwischen Sats und USD wechseln",
|
||||
"swapFrom": "Von",
|
||||
"swapTo": "Nach",
|
||||
"swapAction": "Tauschen",
|
||||
"swapEstimatedFee": "Geschätzte Gebühr",
|
||||
"swapUseAll": "Alles verwenden",
|
||||
"swapMinimum": "Minimum: {amount}",
|
||||
"swapProcessing": "Swap wird verarbeitet...",
|
||||
"swapSuccess": "Swap abgeschlossen",
|
||||
"swapErrorInsufficient": "Unzureichendes Guthaben",
|
||||
"swapErrorExpired": "Angebot abgelaufen",
|
||||
"swapErrorGeneric": "Swap-Fehler: {error}",
|
||||
"swapChartUnavailable": "Preis nicht verfügbar · Tippen zum Wiederholen",
|
||||
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
|
||||
"privacyPolicy": "Datenschutz",
|
||||
"privacyTitle": "WIR SAMMELN NICHTS",
|
||||
"privacyGoodbye": "TSCHÜSS",
|
||||
"privacyKeepReading": "(lies weiter, wenn du willst…)",
|
||||
"privacyBody": "Wir wissen nicht, wer du bist\nWir wissen nicht, wie viel du hast\nWir wissen nicht, was du tust",
|
||||
"privacyConclusion": "Der beste Weg, deine Daten zu schützen,\nist sie nicht zu haben"
|
||||
}
|
||||
|
||||
+43
-3
@@ -53,8 +53,8 @@
|
||||
"homeTitle": "Home",
|
||||
"receive": "Receive",
|
||||
"send": "Send",
|
||||
"sendAction": "Send ↗",
|
||||
"receiveAction": "↘ Receive",
|
||||
"sendAction": "Send",
|
||||
"receiveAction": "Receive",
|
||||
"deposit": "Deposit",
|
||||
"withdraw": "Withdraw",
|
||||
"lightning": "Lightning",
|
||||
@@ -76,6 +76,21 @@
|
||||
"receiveCashu": "Receive Cashu",
|
||||
"pasteTheCashuToken": "Paste the Cashu token:",
|
||||
"pasteFromClipboard": "Paste from clipboard",
|
||||
"emptyClipboard": "Clipboard is empty",
|
||||
"cancelSend": "Cancel send",
|
||||
"cancelSendConfirmTitle": "Cancel this send?",
|
||||
"cancelSendConfirmBody": "Only works if the recipient has not claimed the token yet. If they already did, the send will be marked as completed.",
|
||||
"cancelSendSuccess": "Send canceled. Recovered {amount} {unit}",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "The recipient already claimed the token",
|
||||
"pendingOfflineSend": "Pending offline send",
|
||||
"pendingOfflineSendsHeader": "Unclaimed offline sends",
|
||||
"pendingSendDetailTitle": "Pending send",
|
||||
"validToken": "Valid token",
|
||||
"invalidToken": "Invalid or malformed token",
|
||||
"amount": "Amount:",
|
||||
@@ -103,6 +118,7 @@
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"insufficientBalance": "Insufficient balance",
|
||||
"feeExceedsAmount": "Fee exceeds the amount to send",
|
||||
"tokenCreationError": "Error creating token: {error}",
|
||||
|
||||
"tokenCreated": "Token created",
|
||||
@@ -193,6 +209,8 @@
|
||||
"pendingStatus": "Pending",
|
||||
"receivedStatus": "Received",
|
||||
"sentStatus": "Sent",
|
||||
"receiving": "Receiving",
|
||||
"sending": "Sending",
|
||||
"now": "Now",
|
||||
"agoMinutes": "{minutes} min ago",
|
||||
"agoHours": "{hours} h ago",
|
||||
@@ -452,5 +470,27 @@
|
||||
"requestPaymentReceived": "Payment received",
|
||||
"requestDescriptionHint": "Description (optional)",
|
||||
"universal": "Universal",
|
||||
"copiedToClipboard": "Copied to clipboard"
|
||||
"copiedToClipboard": "Copied to clipboard",
|
||||
|
||||
"swap": "Swap",
|
||||
"swapDescription": "Convert between sats and USD",
|
||||
"swapFrom": "From",
|
||||
"swapTo": "To",
|
||||
"swapAction": "Swap",
|
||||
"swapEstimatedFee": "Estimated fee",
|
||||
"swapUseAll": "Use all",
|
||||
"swapMinimum": "Minimum: {amount}",
|
||||
"swapProcessing": "Processing swap...",
|
||||
"swapSuccess": "Swap completed",
|
||||
"swapErrorInsufficient": "Insufficient balance",
|
||||
"swapErrorExpired": "Quote has expired",
|
||||
"swapErrorGeneric": "Swap error: {error}",
|
||||
"swapChartUnavailable": "Price unavailable · Tap to retry",
|
||||
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
|
||||
"privacyPolicy": "Privacy policy",
|
||||
"privacyTitle": "WE COLLECT NOTHING",
|
||||
"privacyGoodbye": "BYE",
|
||||
"privacyKeepReading": "(keep reading if you want…)",
|
||||
"privacyBody": "We don't know who you are\nWe don't know how much you have\nWe don't know what you do",
|
||||
"privacyConclusion": "The best way to protect your data\nis not to have it"
|
||||
}
|
||||
|
||||
+59
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "Inicio",
|
||||
"receive": "Recibir",
|
||||
"send": "Enviar",
|
||||
"sendAction": "Enviar ↗",
|
||||
"receiveAction": "↘ Recibir",
|
||||
"sendAction": "Enviar",
|
||||
"receiveAction": "Recibir",
|
||||
"deposit": "Depositar",
|
||||
"withdraw": "Retirar",
|
||||
"lightning": "Lightning",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "Recibir Cashu",
|
||||
"pasteTheCashuToken": "Pega el token Cashu:",
|
||||
"pasteFromClipboard": "Pegar del portapapeles",
|
||||
"emptyClipboard": "Portapapeles vacío",
|
||||
"cancelSend": "Cancelar envío",
|
||||
"cancelSendConfirmTitle": "¿Cancelar envío?",
|
||||
"cancelSendConfirmBody": "Solo funciona si el destinatario aún no reclamó el token. Si ya lo hizo, el envío se marcará como completado.",
|
||||
"cancelSendSuccess": "Envío cancelado. Recuperaste {amount} {unit}",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "El destinatario ya reclamó el token",
|
||||
"pendingOfflineSend": "Envío offline pendiente",
|
||||
"pendingOfflineSendsHeader": "Envíos offline no reclamados",
|
||||
"pendingSendDetailTitle": "Envío pendiente",
|
||||
"validToken": "Token válido",
|
||||
"invalidToken": "Token inválido o malformado",
|
||||
"amount": "Monto:",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "Confirmar",
|
||||
"cancel": "Cancelar",
|
||||
"insufficientBalance": "Balance insuficiente",
|
||||
"feeExceedsAmount": "La comisión supera el monto a enviar",
|
||||
"tokenCreationError": "Error al crear token: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -235,6 +251,8 @@
|
||||
"pendingStatus": "Pendiente",
|
||||
"receivedStatus": "Recibido",
|
||||
"sentStatus": "Enviado",
|
||||
"receiving": "Recibiendo",
|
||||
"sending": "Enviando",
|
||||
"now": "Ahora",
|
||||
"agoMinutes": "Hace {minutes} min",
|
||||
"@agoMinutes": {
|
||||
@@ -572,5 +590,43 @@
|
||||
"requestPaymentReceived": "Pago recibido",
|
||||
"requestDescriptionHint": "Descripción (opcional)",
|
||||
"universal": "Universal",
|
||||
"copiedToClipboard": "Copiado al portapapeles"
|
||||
"copiedToClipboard": "Copiado al portapapeles",
|
||||
|
||||
"swap": "Cambiar",
|
||||
"swapDescription": "Convertir entre sats y USD",
|
||||
"swapFrom": "De",
|
||||
"swapTo": "A",
|
||||
"swapAction": "Cambiar",
|
||||
"swapEstimatedFee": "Fee estimado",
|
||||
"swapUseAll": "Usar todo",
|
||||
"swapMinimum": "Mínimo: {amount}",
|
||||
"@swapMinimum": {
|
||||
"placeholders": {
|
||||
"amount": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"swapProcessing": "Procesando swap...",
|
||||
"swapSuccess": "Swap completado",
|
||||
"swapErrorInsufficient": "Saldo insuficiente",
|
||||
"swapErrorExpired": "La cotización ha expirado",
|
||||
"swapErrorGeneric": "Error en el swap: {error}",
|
||||
"@swapErrorGeneric": {
|
||||
"placeholders": {
|
||||
"error": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"swapChartUnavailable": "Precio no disponible · Toca para reintentar",
|
||||
"swapChartMinMax": "24h Mín: {minPrice} — Máx: {maxPrice}",
|
||||
"@swapChartMinMax": {
|
||||
"placeholders": {
|
||||
"minPrice": { "type": "String" },
|
||||
"maxPrice": { "type": "String" }
|
||||
}
|
||||
},
|
||||
"privacyPolicy": "Política de privacidad",
|
||||
"privacyTitle": "NO RECOPILAMOS NADA",
|
||||
"privacyGoodbye": "ADIÓS",
|
||||
"privacyKeepReading": "(sigue leyendo si quieres…)",
|
||||
"privacyBody": "No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces",
|
||||
"privacyConclusion": "La mejor forma de proteger tus datos\nes no tenerlos"
|
||||
}
|
||||
|
||||
+43
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "Accueil",
|
||||
"receive": "Recevoir",
|
||||
"send": "Envoyer",
|
||||
"sendAction": "Envoyer ↗",
|
||||
"receiveAction": "↘ Recevoir",
|
||||
"sendAction": "Envoyer",
|
||||
"receiveAction": "Recevoir",
|
||||
"deposit": "Déposer",
|
||||
"withdraw": "Retirer",
|
||||
"lightning": "Lightning",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "Recevoir Cashu",
|
||||
"pasteTheCashuToken": "Collez le token Cashu :",
|
||||
"pasteFromClipboard": "Coller depuis le presse-papiers",
|
||||
"emptyClipboard": "Presse-papiers vide",
|
||||
"cancelSend": "Annuler l'envoi",
|
||||
"cancelSendConfirmTitle": "Annuler cet envoi ?",
|
||||
"cancelSendConfirmBody": "Ne fonctionne que si le destinataire n'a pas encore réclamé le token. S'il l'a déjà fait, l'envoi sera marqué comme terminé.",
|
||||
"cancelSendSuccess": "Envoi annulé. Récupéré {amount} {unit}",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "Le destinataire a déjà réclamé le token",
|
||||
"pendingOfflineSend": "Envoi hors ligne en attente",
|
||||
"pendingOfflineSendsHeader": "Envois hors ligne non réclamés",
|
||||
"pendingSendDetailTitle": "Envoi en attente",
|
||||
"validToken": "Token valide",
|
||||
"invalidToken": "Token invalide ou malformé",
|
||||
"amount": "Montant :",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "Confirmer",
|
||||
"cancel": "Annuler",
|
||||
"insufficientBalance": "Solde insuffisant",
|
||||
"feeExceedsAmount": "Les frais dépassent le montant à envoyer",
|
||||
"tokenCreationError": "Erreur de création du token : {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -227,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": {
|
||||
@@ -559,5 +577,27 @@
|
||||
"requestPaymentReceived": "Paiement reçu",
|
||||
"requestDescriptionHint": "Description (facultatif)",
|
||||
"universal": "Universel",
|
||||
"copiedToClipboard": "Copié dans le presse-papiers"
|
||||
"copiedToClipboard": "Copié dans le presse-papiers",
|
||||
|
||||
"swap": "Échanger",
|
||||
"swapDescription": "Convertir entre sats et USD",
|
||||
"swapFrom": "De",
|
||||
"swapTo": "Vers",
|
||||
"swapAction": "Échanger",
|
||||
"swapEstimatedFee": "Frais estimés",
|
||||
"swapUseAll": "Tout utiliser",
|
||||
"swapMinimum": "Minimum : {amount}",
|
||||
"swapProcessing": "Swap en cours...",
|
||||
"swapSuccess": "Swap terminé",
|
||||
"swapErrorInsufficient": "Solde insuffisant",
|
||||
"swapErrorExpired": "Le devis a expiré",
|
||||
"swapErrorGeneric": "Erreur de swap : {error}",
|
||||
"swapChartUnavailable": "Prix indisponible · Appuyez pour réessayer",
|
||||
"swapChartMinMax": "24h Min : {minPrice} — Max : {maxPrice}",
|
||||
"privacyPolicy": "Politique de confidentialité",
|
||||
"privacyTitle": "NOUS NE COLLECTONS RIEN",
|
||||
"privacyGoodbye": "AU REVOIR",
|
||||
"privacyKeepReading": "(continue à lire si tu veux…)",
|
||||
"privacyBody": "Nous ne savons pas qui tu es\nNous ne savons pas combien tu as\nNous ne savons pas ce que tu fais",
|
||||
"privacyConclusion": "La meilleure façon de protéger tes données,\nc'est de ne pas les avoir"
|
||||
}
|
||||
|
||||
+43
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "Home",
|
||||
"receive": "Ricevi",
|
||||
"send": "Invia",
|
||||
"sendAction": "Invia ↗",
|
||||
"receiveAction": "↘ Ricevi",
|
||||
"sendAction": "Invia",
|
||||
"receiveAction": "Ricevi",
|
||||
"deposit": "Deposita",
|
||||
"withdraw": "Preleva",
|
||||
"lightning": "Lightning",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "Ricevi Cashu",
|
||||
"pasteTheCashuToken": "Incolla il token Cashu:",
|
||||
"pasteFromClipboard": "Incolla dagli appunti",
|
||||
"emptyClipboard": "Appunti vuoti",
|
||||
"cancelSend": "Annulla invio",
|
||||
"cancelSendConfirmTitle": "Annullare questo invio?",
|
||||
"cancelSendConfirmBody": "Funziona solo se il destinatario non ha ancora riscattato il token. Se l'ha già fatto, l'invio sarà contrassegnato come completato.",
|
||||
"cancelSendSuccess": "Invio annullato. Recuperati {amount} {unit}",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "Il destinatario ha già riscattato il token",
|
||||
"pendingOfflineSend": "Invio offline in sospeso",
|
||||
"pendingOfflineSendsHeader": "Invii offline non riscattati",
|
||||
"pendingSendDetailTitle": "Invio in sospeso",
|
||||
"validToken": "Token valido",
|
||||
"invalidToken": "Token non valido o malformato",
|
||||
"amount": "Importo:",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "Conferma",
|
||||
"cancel": "Annulla",
|
||||
"insufficientBalance": "Saldo insufficiente",
|
||||
"feeExceedsAmount": "La commissione supera l'importo da inviare",
|
||||
"tokenCreationError": "Errore creazione token: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -227,6 +243,8 @@
|
||||
"pendingStatus": "In attesa",
|
||||
"receivedStatus": "Ricevuto",
|
||||
"sentStatus": "Inviato",
|
||||
"receiving": "In ricezione",
|
||||
"sending": "In invio",
|
||||
"now": "Adesso",
|
||||
"agoMinutes": "{minutes} min fa",
|
||||
"@agoMinutes": {
|
||||
@@ -559,5 +577,27 @@
|
||||
"requestPaymentReceived": "Pagamento ricevuto",
|
||||
"requestDescriptionHint": "Descrizione (opzionale)",
|
||||
"universal": "Universale",
|
||||
"copiedToClipboard": "Copiato negli appunti"
|
||||
"copiedToClipboard": "Copiato negli appunti",
|
||||
|
||||
"swap": "Scambia",
|
||||
"swapDescription": "Converti tra sats e USD",
|
||||
"swapFrom": "Da",
|
||||
"swapTo": "A",
|
||||
"swapAction": "Scambia",
|
||||
"swapEstimatedFee": "Commissione stimata",
|
||||
"swapUseAll": "Usa tutto",
|
||||
"swapMinimum": "Minimo: {amount}",
|
||||
"swapProcessing": "Swap in corso...",
|
||||
"swapSuccess": "Swap completato",
|
||||
"swapErrorInsufficient": "Saldo insufficiente",
|
||||
"swapErrorExpired": "Il preventivo è scaduto",
|
||||
"swapErrorGeneric": "Errore swap: {error}",
|
||||
"swapChartUnavailable": "Prezzo non disponibile · Tocca per riprovare",
|
||||
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
|
||||
"privacyPolicy": "Informativa sulla privacy",
|
||||
"privacyTitle": "NON RACCOGLIAMO NULLA",
|
||||
"privacyGoodbye": "CIAO",
|
||||
"privacyKeepReading": "(continua a leggere se vuoi…)",
|
||||
"privacyBody": "Non sappiamo chi sei\nNon sappiamo quanto hai\nNon sappiamo cosa fai",
|
||||
"privacyConclusion": "Il modo migliore per proteggere i tuoi dati\nè non averli"
|
||||
}
|
||||
|
||||
+43
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "ホーム",
|
||||
"receive": "受取",
|
||||
"send": "送金",
|
||||
"sendAction": "送金 ↗",
|
||||
"receiveAction": "↘ 受取",
|
||||
"sendAction": "送金",
|
||||
"receiveAction": "受取",
|
||||
"deposit": "入金",
|
||||
"withdraw": "出金",
|
||||
"lightning": "Lightning",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "Cashuを受け取る",
|
||||
"pasteTheCashuToken": "Cashuトークンを貼り付け:",
|
||||
"pasteFromClipboard": "クリップボードから貼り付け",
|
||||
"emptyClipboard": "クリップボードは空です",
|
||||
"cancelSend": "送金をキャンセル",
|
||||
"cancelSendConfirmTitle": "この送金をキャンセルしますか?",
|
||||
"cancelSendConfirmBody": "受取人がまだトークンを引き換えていない場合のみ機能します。既に引き換え済みの場合、送金は完了として記録されます。",
|
||||
"cancelSendSuccess": "送金をキャンセルしました。{amount} {unit}を回復しました",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "受取人は既にトークンを引き換えました",
|
||||
"pendingOfflineSend": "保留中のオフライン送金",
|
||||
"pendingOfflineSendsHeader": "未請求のオフライン送金",
|
||||
"pendingSendDetailTitle": "保留中の送金",
|
||||
"validToken": "有効なトークン",
|
||||
"invalidToken": "無効または不正なトークン",
|
||||
"amount": "金額:",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "確認",
|
||||
"cancel": "キャンセル",
|
||||
"insufficientBalance": "残高不足",
|
||||
"feeExceedsAmount": "手数料が送金額を超えています",
|
||||
"tokenCreationError": "トークン作成エラー:{error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -227,6 +243,8 @@
|
||||
"pendingStatus": "保留中",
|
||||
"receivedStatus": "受取済み",
|
||||
"sentStatus": "送金済み",
|
||||
"receiving": "受取中",
|
||||
"sending": "送金中",
|
||||
"now": "たった今",
|
||||
"agoMinutes": "{minutes}分前",
|
||||
"@agoMinutes": {
|
||||
@@ -559,5 +577,27 @@
|
||||
"requestPaymentReceived": "支払いを受け取りました",
|
||||
"requestDescriptionHint": "説明(任意)",
|
||||
"universal": "ユニバーサル",
|
||||
"copiedToClipboard": "クリップボードにコピーしました"
|
||||
"copiedToClipboard": "クリップボードにコピーしました",
|
||||
|
||||
"swap": "交換",
|
||||
"swapDescription": "SatsとUSDを変換",
|
||||
"swapFrom": "送信元",
|
||||
"swapTo": "送信先",
|
||||
"swapAction": "交換",
|
||||
"swapEstimatedFee": "推定手数料",
|
||||
"swapUseAll": "全額使用",
|
||||
"swapMinimum": "最小: {amount}",
|
||||
"swapProcessing": "スワップ処理中...",
|
||||
"swapSuccess": "スワップ完了",
|
||||
"swapErrorInsufficient": "残高不足",
|
||||
"swapErrorExpired": "見積もりの有効期限切れ",
|
||||
"swapErrorGeneric": "スワップエラー: {error}",
|
||||
"swapChartUnavailable": "価格を取得できません・タップで再試行",
|
||||
"swapChartMinMax": "24h 安値: {minPrice} — 高値: {maxPrice}",
|
||||
"privacyPolicy": "プライバシーポリシー",
|
||||
"privacyTitle": "何も収集しません",
|
||||
"privacyGoodbye": "さようなら",
|
||||
"privacyKeepReading": "(読み続けたければどうぞ…)",
|
||||
"privacyBody": "あなたが誰か知りません\nいくら持っているか知りません\n何をしているか知りません",
|
||||
"privacyConclusion": "データを守る最善の方法は\nデータを持たないことです"
|
||||
}
|
||||
|
||||
+43
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "홈",
|
||||
"receive": "받기",
|
||||
"send": "보내기",
|
||||
"sendAction": "보내기 ↗",
|
||||
"receiveAction": "↘ 받기",
|
||||
"sendAction": "보내기",
|
||||
"receiveAction": "받기",
|
||||
"deposit": "입금",
|
||||
"withdraw": "출금",
|
||||
"lightning": "Lightning",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "Cashu 받기",
|
||||
"pasteTheCashuToken": "Cashu 토큰을 붙여넣으세요:",
|
||||
"pasteFromClipboard": "클립보드에서 붙여넣기",
|
||||
"emptyClipboard": "클립보드가 비어 있음",
|
||||
"cancelSend": "전송 취소",
|
||||
"cancelSendConfirmTitle": "이 전송을 취소하시겠습니까?",
|
||||
"cancelSendConfirmBody": "수신자가 아직 토큰을 받지 않은 경우에만 작동합니다. 이미 받았다면 전송이 완료로 표시됩니다.",
|
||||
"cancelSendSuccess": "전송이 취소되었습니다. {amount} {unit}를 복구했습니다",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "수신자가 이미 토큰을 받았습니다",
|
||||
"pendingOfflineSend": "대기 중인 오프라인 전송",
|
||||
"pendingOfflineSendsHeader": "청구되지 않은 오프라인 전송",
|
||||
"pendingSendDetailTitle": "대기 중인 전송",
|
||||
"validToken": "유효한 토큰",
|
||||
"invalidToken": "유효하지 않거나 손상된 토큰",
|
||||
"amount": "금액:",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "확인",
|
||||
"cancel": "취소",
|
||||
"insufficientBalance": "잔액 부족",
|
||||
"feeExceedsAmount": "수수료가 송금액을 초과합니다",
|
||||
"tokenCreationError": "토큰 생성 오류: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -227,6 +243,8 @@
|
||||
"pendingStatus": "대기 중",
|
||||
"receivedStatus": "받음",
|
||||
"sentStatus": "보냄",
|
||||
"receiving": "받는 중",
|
||||
"sending": "보내는 중",
|
||||
"now": "방금",
|
||||
"agoMinutes": "{minutes}분 전",
|
||||
"@agoMinutes": {
|
||||
@@ -559,5 +577,27 @@
|
||||
"requestPaymentReceived": "결제가 수신되었습니다",
|
||||
"requestDescriptionHint": "설명 (선택사항)",
|
||||
"universal": "유니버설",
|
||||
"copiedToClipboard": "클립보드에 복사되었습니다"
|
||||
"copiedToClipboard": "클립보드에 복사되었습니다",
|
||||
|
||||
"swap": "교환",
|
||||
"swapDescription": "Sats와 USD 간 변환",
|
||||
"swapFrom": "에서",
|
||||
"swapTo": "으로",
|
||||
"swapAction": "교환",
|
||||
"swapEstimatedFee": "예상 수수료",
|
||||
"swapUseAll": "전액 사용",
|
||||
"swapMinimum": "최소: {amount}",
|
||||
"swapProcessing": "교환 처리 중...",
|
||||
"swapSuccess": "교환 완료",
|
||||
"swapErrorInsufficient": "잔액 부족",
|
||||
"swapErrorExpired": "견적이 만료되었습니다",
|
||||
"swapErrorGeneric": "교환 오류: {error}",
|
||||
"swapChartUnavailable": "가격 불러오기 실패 · 탭하여 재시도",
|
||||
"swapChartMinMax": "24h 최저: {minPrice} — 최고: {maxPrice}",
|
||||
"privacyPolicy": "개인정보 처리방침",
|
||||
"privacyTitle": "우리는 아무것도 수집하지 않습니다",
|
||||
"privacyGoodbye": "안녕",
|
||||
"privacyKeepReading": "(계속 읽고 싶으면…)",
|
||||
"privacyBody": "당신이 누구인지 모릅니다\n얼마를 가지고 있는지 모릅니다\n무엇을 하는지 모릅니다",
|
||||
"privacyConclusion": "데이터를 보호하는 가장 좋은 방법은\n데이터를 갖지 않는 것입니다"
|
||||
}
|
||||
|
||||
@@ -388,13 +388,13 @@ abstract class L10n {
|
||||
/// No description provided for @sendAction.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Enviar ↗'**
|
||||
/// **'Enviar'**
|
||||
String get sendAction;
|
||||
|
||||
/// No description provided for @receiveAction.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'↘ Recibir'**
|
||||
/// **'Recibir'**
|
||||
String get receiveAction;
|
||||
|
||||
/// No description provided for @deposit.
|
||||
@@ -505,6 +505,60 @@ abstract class L10n {
|
||||
/// **'Pegar del portapapeles'**
|
||||
String get pasteFromClipboard;
|
||||
|
||||
/// No description provided for @emptyClipboard.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Portapapeles vacío'**
|
||||
String get emptyClipboard;
|
||||
|
||||
/// No description provided for @cancelSend.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Cancelar envío'**
|
||||
String get cancelSend;
|
||||
|
||||
/// No description provided for @cancelSendConfirmTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'¿Cancelar envío?'**
|
||||
String get cancelSendConfirmTitle;
|
||||
|
||||
/// No description provided for @cancelSendConfirmBody.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Solo funciona si el destinatario aún no reclamó el token. Si ya lo hizo, el envío se marcará como completado.'**
|
||||
String get cancelSendConfirmBody;
|
||||
|
||||
/// No description provided for @cancelSendSuccess.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Envío cancelado. Recuperaste {amount} {unit}'**
|
||||
String cancelSendSuccess(String amount, String unit);
|
||||
|
||||
/// No description provided for @cancelSendAlreadyClaimed.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'El destinatario ya reclamó el token'**
|
||||
String get cancelSendAlreadyClaimed;
|
||||
|
||||
/// No description provided for @pendingOfflineSend.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Envío offline pendiente'**
|
||||
String get pendingOfflineSend;
|
||||
|
||||
/// No description provided for @pendingOfflineSendsHeader.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Envíos offline no reclamados'**
|
||||
String get pendingOfflineSendsHeader;
|
||||
|
||||
/// No description provided for @pendingSendDetailTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Envío pendiente'**
|
||||
String get pendingSendDetailTitle;
|
||||
|
||||
/// No description provided for @validToken.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
@@ -661,6 +715,12 @@ abstract class L10n {
|
||||
/// **'Balance insuficiente'**
|
||||
String get insufficientBalance;
|
||||
|
||||
/// No description provided for @feeExceedsAmount.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'La comisión supera el monto a enviar'**
|
||||
String get feeExceedsAmount;
|
||||
|
||||
/// No description provided for @tokenCreationError.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
@@ -1075,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:
|
||||
@@ -2352,6 +2424,132 @@ abstract class L10n {
|
||||
/// In es, this message translates to:
|
||||
/// **'Copiado al portapapeles'**
|
||||
String get copiedToClipboard;
|
||||
|
||||
/// No description provided for @swap.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Cambiar'**
|
||||
String get swap;
|
||||
|
||||
/// No description provided for @swapDescription.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Convertir entre sats y USD'**
|
||||
String get swapDescription;
|
||||
|
||||
/// No description provided for @swapFrom.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'De'**
|
||||
String get swapFrom;
|
||||
|
||||
/// No description provided for @swapTo.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'A'**
|
||||
String get swapTo;
|
||||
|
||||
/// No description provided for @swapAction.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Cambiar'**
|
||||
String get swapAction;
|
||||
|
||||
/// No description provided for @swapEstimatedFee.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Fee estimado'**
|
||||
String get swapEstimatedFee;
|
||||
|
||||
/// No description provided for @swapUseAll.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Usar todo'**
|
||||
String get swapUseAll;
|
||||
|
||||
/// No description provided for @swapMinimum.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Mínimo: {amount}'**
|
||||
String swapMinimum(String amount);
|
||||
|
||||
/// No description provided for @swapProcessing.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Procesando swap...'**
|
||||
String get swapProcessing;
|
||||
|
||||
/// No description provided for @swapSuccess.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Swap completado'**
|
||||
String get swapSuccess;
|
||||
|
||||
/// No description provided for @swapErrorInsufficient.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Saldo insuficiente'**
|
||||
String get swapErrorInsufficient;
|
||||
|
||||
/// No description provided for @swapErrorExpired.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'La cotización ha expirado'**
|
||||
String get swapErrorExpired;
|
||||
|
||||
/// No description provided for @swapErrorGeneric.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Error en el swap: {error}'**
|
||||
String swapErrorGeneric(String error);
|
||||
|
||||
/// No description provided for @swapChartUnavailable.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Precio no disponible · Toca para reintentar'**
|
||||
String get swapChartUnavailable;
|
||||
|
||||
/// No description provided for @swapChartMinMax.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'24h Mín: {minPrice} — Máx: {maxPrice}'**
|
||||
String swapChartMinMax(String minPrice, String maxPrice);
|
||||
|
||||
/// No description provided for @privacyPolicy.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Política de privacidad'**
|
||||
String get privacyPolicy;
|
||||
|
||||
/// No description provided for @privacyTitle.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'NO RECOPILAMOS NADA'**
|
||||
String get privacyTitle;
|
||||
|
||||
/// No description provided for @privacyGoodbye.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'ADIÓS'**
|
||||
String get privacyGoodbye;
|
||||
|
||||
/// No description provided for @privacyKeepReading.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'(sigue leyendo si quieres…)'**
|
||||
String get privacyKeepReading;
|
||||
|
||||
/// No description provided for @privacyBody.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces'**
|
||||
String get privacyBody;
|
||||
|
||||
/// No description provided for @privacyConclusion.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'La mejor forma de proteger tus datos\nes no tenerlos'**
|
||||
String get privacyConclusion;
|
||||
}
|
||||
|
||||
class _L10nDelegate extends LocalizationsDelegate<L10n> {
|
||||
|
||||
@@ -156,10 +156,10 @@ class L10nDe extends L10n {
|
||||
String get send => 'Senden';
|
||||
|
||||
@override
|
||||
String get sendAction => 'Senden ↗';
|
||||
String get sendAction => 'Senden';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ Empfangen';
|
||||
String get receiveAction => 'Empfangen';
|
||||
|
||||
@override
|
||||
String get deposit => 'Einzahlen';
|
||||
@@ -215,6 +215,38 @@ class L10nDe extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => 'Aus Zwischenablage einfügen';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => 'Zwischenablage leer';
|
||||
|
||||
@override
|
||||
String get cancelSend => 'Senden abbrechen';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => 'Diesen Versand abbrechen?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'Funktioniert nur, wenn der Empfänger das Token noch nicht eingelöst hat. Falls doch, wird der Versand als abgeschlossen markiert.';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return 'Versand abgebrochen. $amount $unit zurückerhalten';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed =>
|
||||
'Der Empfänger hat das Token bereits eingelöst';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => 'Ausstehender Offline-Versand';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader =>
|
||||
'Nicht eingelöste Offline-Versendungen';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => 'Ausstehender Versand';
|
||||
|
||||
@override
|
||||
String get validToken => 'Token gültig';
|
||||
|
||||
@@ -295,6 +327,9 @@ class L10nDe extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => 'Unzureichendes Guthaben';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => 'Die Gebühr übersteigt den Sendebetrag';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'Fehler beim Erstellen des Tokens: $error';
|
||||
@@ -523,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';
|
||||
|
||||
@@ -1241,4 +1282,76 @@ class L10nDe extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => 'In die Zwischenablage kopiert';
|
||||
|
||||
@override
|
||||
String get swap => 'Tauschen';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'Zwischen Sats und USD wechseln';
|
||||
|
||||
@override
|
||||
String get swapFrom => 'Von';
|
||||
|
||||
@override
|
||||
String get swapTo => 'Nach';
|
||||
|
||||
@override
|
||||
String get swapAction => 'Tauschen';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => 'Geschätzte Gebühr';
|
||||
|
||||
@override
|
||||
String get swapUseAll => 'Alles verwenden';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return 'Minimum: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => 'Swap wird verarbeitet...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => 'Swap abgeschlossen';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => 'Unzureichendes Guthaben';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => 'Angebot abgelaufen';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Swap-Fehler: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Preis nicht verfügbar · Tippen zum Wiederholen';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h Min: $minPrice — Max: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => 'Datenschutz';
|
||||
|
||||
@override
|
||||
String get privacyTitle => 'WIR SAMMELN NICHTS';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => 'TSCHÜSS';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(lies weiter, wenn du willst…)';
|
||||
|
||||
@override
|
||||
String get privacyBody =>
|
||||
'Wir wissen nicht, wer du bist\nWir wissen nicht, wie viel du hast\nWir wissen nicht, was du tust';
|
||||
|
||||
@override
|
||||
String get privacyConclusion =>
|
||||
'Der beste Weg, deine Daten zu schützen,\nist sie nicht zu haben';
|
||||
}
|
||||
|
||||
@@ -153,10 +153,10 @@ class L10nEn extends L10n {
|
||||
String get send => 'Send';
|
||||
|
||||
@override
|
||||
String get sendAction => 'Send ↗';
|
||||
String get sendAction => 'Send';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ Receive';
|
||||
String get receiveAction => 'Receive';
|
||||
|
||||
@override
|
||||
String get deposit => 'Deposit';
|
||||
@@ -212,6 +212,37 @@ class L10nEn extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => 'Paste from clipboard';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => 'Clipboard is empty';
|
||||
|
||||
@override
|
||||
String get cancelSend => 'Cancel send';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => 'Cancel this send?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'Only works if the recipient has not claimed the token yet. If they already did, the send will be marked as completed.';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return 'Send canceled. Recovered $amount $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed =>
|
||||
'The recipient already claimed the token';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => 'Pending offline send';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => 'Unclaimed offline sends';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => 'Pending send';
|
||||
|
||||
@override
|
||||
String get validToken => 'Valid token';
|
||||
|
||||
@@ -292,6 +323,9 @@ class L10nEn extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => 'Insufficient balance';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => 'Fee exceeds the amount to send';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'Error creating token: $error';
|
||||
@@ -517,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';
|
||||
|
||||
@@ -1222,4 +1262,75 @@ class L10nEn extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => 'Copied to clipboard';
|
||||
|
||||
@override
|
||||
String get swap => 'Swap';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'Convert between sats and USD';
|
||||
|
||||
@override
|
||||
String get swapFrom => 'From';
|
||||
|
||||
@override
|
||||
String get swapTo => 'To';
|
||||
|
||||
@override
|
||||
String get swapAction => 'Swap';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => 'Estimated fee';
|
||||
|
||||
@override
|
||||
String get swapUseAll => 'Use all';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return 'Minimum: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => 'Processing swap...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => 'Swap completed';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => 'Insufficient balance';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => 'Quote has expired';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Swap error: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => 'Price unavailable · Tap to retry';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h Min: $minPrice — Max: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => 'Privacy policy';
|
||||
|
||||
@override
|
||||
String get privacyTitle => 'WE COLLECT NOTHING';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => 'BYE';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(keep reading if you want…)';
|
||||
|
||||
@override
|
||||
String get privacyBody =>
|
||||
'We don\'t know who you are\nWe don\'t know how much you have\nWe don\'t know what you do';
|
||||
|
||||
@override
|
||||
String get privacyConclusion =>
|
||||
'The best way to protect your data\nis not to have it';
|
||||
}
|
||||
|
||||
@@ -153,10 +153,10 @@ class L10nEs extends L10n {
|
||||
String get send => 'Enviar';
|
||||
|
||||
@override
|
||||
String get sendAction => 'Enviar ↗';
|
||||
String get sendAction => 'Enviar';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ Recibir';
|
||||
String get receiveAction => 'Recibir';
|
||||
|
||||
@override
|
||||
String get deposit => 'Depositar';
|
||||
@@ -212,6 +212,36 @@ class L10nEs extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => 'Pegar del portapapeles';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => 'Portapapeles vacío';
|
||||
|
||||
@override
|
||||
String get cancelSend => 'Cancelar envío';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => '¿Cancelar envío?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'Solo funciona si el destinatario aún no reclamó el token. Si ya lo hizo, el envío se marcará como completado.';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return 'Envío cancelado. Recuperaste $amount $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed => 'El destinatario ya reclamó el token';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => 'Envío offline pendiente';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => 'Envíos offline no reclamados';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => 'Envío pendiente';
|
||||
|
||||
@override
|
||||
String get validToken => 'Token válido';
|
||||
|
||||
@@ -292,6 +322,9 @@ class L10nEs extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => 'Balance insuficiente';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => 'La comisión supera el monto a enviar';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'Error al crear token: $error';
|
||||
@@ -518,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';
|
||||
|
||||
@@ -1230,4 +1269,76 @@ class L10nEs extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => 'Copiado al portapapeles';
|
||||
|
||||
@override
|
||||
String get swap => 'Cambiar';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'Convertir entre sats y USD';
|
||||
|
||||
@override
|
||||
String get swapFrom => 'De';
|
||||
|
||||
@override
|
||||
String get swapTo => 'A';
|
||||
|
||||
@override
|
||||
String get swapAction => 'Cambiar';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => 'Fee estimado';
|
||||
|
||||
@override
|
||||
String get swapUseAll => 'Usar todo';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return 'Mínimo: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => 'Procesando swap...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => 'Swap completado';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => 'Saldo insuficiente';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => 'La cotización ha expirado';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Error en el swap: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Precio no disponible · Toca para reintentar';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h Mín: $minPrice — Máx: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => 'Política de privacidad';
|
||||
|
||||
@override
|
||||
String get privacyTitle => 'NO RECOPILAMOS NADA';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => 'ADIÓS';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(sigue leyendo si quieres…)';
|
||||
|
||||
@override
|
||||
String get privacyBody =>
|
||||
'No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces';
|
||||
|
||||
@override
|
||||
String get privacyConclusion =>
|
||||
'La mejor forma de proteger tus datos\nes no tenerlos';
|
||||
}
|
||||
|
||||
@@ -157,10 +157,10 @@ class L10nFr extends L10n {
|
||||
String get send => 'Envoyer';
|
||||
|
||||
@override
|
||||
String get sendAction => 'Envoyer ↗';
|
||||
String get sendAction => 'Envoyer';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ Recevoir';
|
||||
String get receiveAction => 'Recevoir';
|
||||
|
||||
@override
|
||||
String get deposit => 'Déposer';
|
||||
@@ -216,6 +216,37 @@ class L10nFr extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => 'Coller depuis le presse-papiers';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => 'Presse-papiers vide';
|
||||
|
||||
@override
|
||||
String get cancelSend => 'Annuler l\'envoi';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => 'Annuler cet envoi ?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'Ne fonctionne que si le destinataire n\'a pas encore réclamé le token. S\'il l\'a déjà fait, l\'envoi sera marqué comme terminé.';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return 'Envoi annulé. Récupéré $amount $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed =>
|
||||
'Le destinataire a déjà réclamé le token';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => 'Envoi hors ligne en attente';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => 'Envois hors ligne non réclamés';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => 'Envoi en attente';
|
||||
|
||||
@override
|
||||
String get validToken => 'Token valide';
|
||||
|
||||
@@ -296,6 +327,9 @@ class L10nFr extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => 'Solde insuffisant';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => 'Les frais dépassent le montant à envoyer';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'Erreur de création du token : $error';
|
||||
@@ -524,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';
|
||||
|
||||
@@ -1246,4 +1286,76 @@ class L10nFr extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => 'Copié dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get swap => 'Échanger';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'Convertir entre sats et USD';
|
||||
|
||||
@override
|
||||
String get swapFrom => 'De';
|
||||
|
||||
@override
|
||||
String get swapTo => 'Vers';
|
||||
|
||||
@override
|
||||
String get swapAction => 'Échanger';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => 'Frais estimés';
|
||||
|
||||
@override
|
||||
String get swapUseAll => 'Tout utiliser';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return 'Minimum : $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => 'Swap en cours...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => 'Swap terminé';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => 'Solde insuffisant';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => 'Le devis a expiré';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Erreur de swap : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Prix indisponible · Appuyez pour réessayer';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h Min : $minPrice — Max : $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => 'Politique de confidentialité';
|
||||
|
||||
@override
|
||||
String get privacyTitle => 'NOUS NE COLLECTONS RIEN';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => 'AU REVOIR';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(continue à lire si tu veux…)';
|
||||
|
||||
@override
|
||||
String get privacyBody =>
|
||||
'Nous ne savons pas qui tu es\nNous ne savons pas combien tu as\nNous ne savons pas ce que tu fais';
|
||||
|
||||
@override
|
||||
String get privacyConclusion =>
|
||||
'La meilleure façon de protéger tes données,\nc\'est de ne pas les avoir';
|
||||
}
|
||||
|
||||
@@ -154,10 +154,10 @@ class L10nIt extends L10n {
|
||||
String get send => 'Invia';
|
||||
|
||||
@override
|
||||
String get sendAction => 'Invia ↗';
|
||||
String get sendAction => 'Invia';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ Ricevi';
|
||||
String get receiveAction => 'Ricevi';
|
||||
|
||||
@override
|
||||
String get deposit => 'Deposita';
|
||||
@@ -213,6 +213,37 @@ class L10nIt extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => 'Incolla dagli appunti';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => 'Appunti vuoti';
|
||||
|
||||
@override
|
||||
String get cancelSend => 'Annulla invio';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => 'Annullare questo invio?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'Funziona solo se il destinatario non ha ancora riscattato il token. Se l\'ha già fatto, l\'invio sarà contrassegnato come completato.';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return 'Invio annullato. Recuperati $amount $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed =>
|
||||
'Il destinatario ha già riscattato il token';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => 'Invio offline in sospeso';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => 'Invii offline non riscattati';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => 'Invio in sospeso';
|
||||
|
||||
@override
|
||||
String get validToken => 'Token valido';
|
||||
|
||||
@@ -293,6 +324,9 @@ class L10nIt extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => 'Saldo insufficiente';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => 'La commissione supera l\'importo da inviare';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'Errore creazione token: $error';
|
||||
@@ -519,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';
|
||||
|
||||
@@ -1234,4 +1274,76 @@ class L10nIt extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => 'Copiato negli appunti';
|
||||
|
||||
@override
|
||||
String get swap => 'Scambia';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'Converti tra sats e USD';
|
||||
|
||||
@override
|
||||
String get swapFrom => 'Da';
|
||||
|
||||
@override
|
||||
String get swapTo => 'A';
|
||||
|
||||
@override
|
||||
String get swapAction => 'Scambia';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => 'Commissione stimata';
|
||||
|
||||
@override
|
||||
String get swapUseAll => 'Usa tutto';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return 'Minimo: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => 'Swap in corso...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => 'Swap completato';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => 'Saldo insufficiente';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => 'Il preventivo è scaduto';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Errore swap: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Prezzo non disponibile · Tocca per riprovare';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h Min: $minPrice — Max: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => 'Informativa sulla privacy';
|
||||
|
||||
@override
|
||||
String get privacyTitle => 'NON RACCOGLIAMO NULLA';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => 'CIAO';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(continua a leggere se vuoi…)';
|
||||
|
||||
@override
|
||||
String get privacyBody =>
|
||||
'Non sappiamo chi sei\nNon sappiamo quanto hai\nNon sappiamo cosa fai';
|
||||
|
||||
@override
|
||||
String get privacyConclusion =>
|
||||
'Il modo migliore per proteggere i tuoi dati\nè non averli';
|
||||
}
|
||||
|
||||
@@ -149,10 +149,10 @@ class L10nJa extends L10n {
|
||||
String get send => '送金';
|
||||
|
||||
@override
|
||||
String get sendAction => '送金 ↗';
|
||||
String get sendAction => '送金';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ 受取';
|
||||
String get receiveAction => '受取';
|
||||
|
||||
@override
|
||||
String get deposit => '入金';
|
||||
@@ -208,6 +208,36 @@ class L10nJa extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => 'クリップボードから貼り付け';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => 'クリップボードは空です';
|
||||
|
||||
@override
|
||||
String get cancelSend => '送金をキャンセル';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => 'この送金をキャンセルしますか?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'受取人がまだトークンを引き換えていない場合のみ機能します。既に引き換え済みの場合、送金は完了として記録されます。';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return '送金をキャンセルしました。$amount $unitを回復しました';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed => '受取人は既にトークンを引き換えました';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => '保留中のオフライン送金';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => '未請求のオフライン送金';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => '保留中の送金';
|
||||
|
||||
@override
|
||||
String get validToken => '有効なトークン';
|
||||
|
||||
@@ -288,6 +318,9 @@ class L10nJa extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => '残高不足';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => '手数料が送金額を超えています';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'トークン作成エラー:$error';
|
||||
@@ -511,6 +544,12 @@ class L10nJa extends L10n {
|
||||
@override
|
||||
String get sentStatus => '送金済み';
|
||||
|
||||
@override
|
||||
String get receiving => '受取中';
|
||||
|
||||
@override
|
||||
String get sending => '送金中';
|
||||
|
||||
@override
|
||||
String get now => 'たった今';
|
||||
|
||||
@@ -1207,4 +1246,73 @@ class L10nJa extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => 'クリップボードにコピーしました';
|
||||
|
||||
@override
|
||||
String get swap => '交換';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'SatsとUSDを変換';
|
||||
|
||||
@override
|
||||
String get swapFrom => '送信元';
|
||||
|
||||
@override
|
||||
String get swapTo => '送信先';
|
||||
|
||||
@override
|
||||
String get swapAction => '交換';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => '推定手数料';
|
||||
|
||||
@override
|
||||
String get swapUseAll => '全額使用';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return '最小: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => 'スワップ処理中...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => 'スワップ完了';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => '残高不足';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => '見積もりの有効期限切れ';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'スワップエラー: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => '価格を取得できません・タップで再試行';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h 安値: $minPrice — 高値: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => 'プライバシーポリシー';
|
||||
|
||||
@override
|
||||
String get privacyTitle => '何も収集しません';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => 'さようなら';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(読み続けたければどうぞ…)';
|
||||
|
||||
@override
|
||||
String get privacyBody => 'あなたが誰か知りません\nいくら持っているか知りません\n何をしているか知りません';
|
||||
|
||||
@override
|
||||
String get privacyConclusion => 'データを守る最善の方法は\nデータを持たないことです';
|
||||
}
|
||||
|
||||
@@ -151,10 +151,10 @@ class L10nKo extends L10n {
|
||||
String get send => '보내기';
|
||||
|
||||
@override
|
||||
String get sendAction => '보내기 ↗';
|
||||
String get sendAction => '보내기';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ 받기';
|
||||
String get receiveAction => '받기';
|
||||
|
||||
@override
|
||||
String get deposit => '입금';
|
||||
@@ -210,6 +210,36 @@ class L10nKo extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => '클립보드에서 붙여넣기';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => '클립보드가 비어 있음';
|
||||
|
||||
@override
|
||||
String get cancelSend => '전송 취소';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => '이 전송을 취소하시겠습니까?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'수신자가 아직 토큰을 받지 않은 경우에만 작동합니다. 이미 받았다면 전송이 완료로 표시됩니다.';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return '전송이 취소되었습니다. $amount $unit를 복구했습니다';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed => '수신자가 이미 토큰을 받았습니다';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => '대기 중인 오프라인 전송';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => '청구되지 않은 오프라인 전송';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => '대기 중인 전송';
|
||||
|
||||
@override
|
||||
String get validToken => '유효한 토큰';
|
||||
|
||||
@@ -290,6 +320,9 @@ class L10nKo extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => '잔액 부족';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => '수수료가 송금액을 초과합니다';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return '토큰 생성 오류: $error';
|
||||
@@ -513,6 +546,12 @@ class L10nKo extends L10n {
|
||||
@override
|
||||
String get sentStatus => '보냄';
|
||||
|
||||
@override
|
||||
String get receiving => '받는 중';
|
||||
|
||||
@override
|
||||
String get sending => '보내는 중';
|
||||
|
||||
@override
|
||||
String get now => '방금';
|
||||
|
||||
@@ -1209,4 +1248,73 @@ class L10nKo extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => '클립보드에 복사되었습니다';
|
||||
|
||||
@override
|
||||
String get swap => '교환';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'Sats와 USD 간 변환';
|
||||
|
||||
@override
|
||||
String get swapFrom => '에서';
|
||||
|
||||
@override
|
||||
String get swapTo => '으로';
|
||||
|
||||
@override
|
||||
String get swapAction => '교환';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => '예상 수수료';
|
||||
|
||||
@override
|
||||
String get swapUseAll => '전액 사용';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return '최소: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => '교환 처리 중...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => '교환 완료';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => '잔액 부족';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => '견적이 만료되었습니다';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return '교환 오류: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => '가격 불러오기 실패 · 탭하여 재시도';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h 최저: $minPrice — 최고: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => '개인정보 처리방침';
|
||||
|
||||
@override
|
||||
String get privacyTitle => '우리는 아무것도 수집하지 않습니다';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => '안녕';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(계속 읽고 싶으면…)';
|
||||
|
||||
@override
|
||||
String get privacyBody => '당신이 누구인지 모릅니다\n얼마를 가지고 있는지 모릅니다\n무엇을 하는지 모릅니다';
|
||||
|
||||
@override
|
||||
String get privacyConclusion => '데이터를 보호하는 가장 좋은 방법은\n데이터를 갖지 않는 것입니다';
|
||||
}
|
||||
|
||||
@@ -154,10 +154,10 @@ class L10nPt extends L10n {
|
||||
String get send => 'Enviar';
|
||||
|
||||
@override
|
||||
String get sendAction => 'Enviar ↗';
|
||||
String get sendAction => 'Enviar';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ Receber';
|
||||
String get receiveAction => 'Receber';
|
||||
|
||||
@override
|
||||
String get deposit => 'Depositar';
|
||||
@@ -213,6 +213,36 @@ class L10nPt extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => 'Colar da área de transferência';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => 'Área de transferência vazia';
|
||||
|
||||
@override
|
||||
String get cancelSend => 'Cancelar envio';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => 'Cancelar este envio?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'Só funciona se o destinatário ainda não resgatou o token. Se já resgatou, o envio será marcado como concluído.';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return 'Envio cancelado. Recuperou $amount $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed => 'O destinatário já resgatou o token';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => 'Envio offline pendente';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => 'Envios offline não resgatados';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => 'Envio pendente';
|
||||
|
||||
@override
|
||||
String get validToken => 'Token válido';
|
||||
|
||||
@@ -293,6 +323,9 @@ class L10nPt extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => 'Saldo insuficiente';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => 'A taxa excede o valor a enviar';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'Erro ao criar token: $error';
|
||||
@@ -521,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';
|
||||
|
||||
@@ -1234,4 +1273,76 @@ class L10nPt extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => 'Copiado para a área de transferência';
|
||||
|
||||
@override
|
||||
String get swap => 'Trocar';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'Converter entre sats e USD';
|
||||
|
||||
@override
|
||||
String get swapFrom => 'De';
|
||||
|
||||
@override
|
||||
String get swapTo => 'Para';
|
||||
|
||||
@override
|
||||
String get swapAction => 'Trocar';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => 'Taxa estimada';
|
||||
|
||||
@override
|
||||
String get swapUseAll => 'Usar tudo';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return 'Mínimo: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => 'Processando swap...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => 'Swap concluído';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => 'Saldo insuficiente';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => 'A cotação expirou';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Erro no swap: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Preço indisponível · Toque para tentar novamente';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h Mín: $minPrice — Máx: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => 'Política de privacidade';
|
||||
|
||||
@override
|
||||
String get privacyTitle => 'NÃO RECOLHEMOS NADA';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => 'ADEUS';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(continue lendo se quiser…)';
|
||||
|
||||
@override
|
||||
String get privacyBody =>
|
||||
'Não sabemos quem você é\nNão sabemos quanto você tem\nNão sabemos o que você faz';
|
||||
|
||||
@override
|
||||
String get privacyConclusion =>
|
||||
'A melhor forma de proteger seus dados\né não tê-los';
|
||||
}
|
||||
|
||||
@@ -153,10 +153,10 @@ class L10nRu extends L10n {
|
||||
String get send => 'Отправить';
|
||||
|
||||
@override
|
||||
String get sendAction => 'Отправить ↗';
|
||||
String get sendAction => 'Отправить';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ Получить';
|
||||
String get receiveAction => 'Получить';
|
||||
|
||||
@override
|
||||
String get deposit => 'Пополнить';
|
||||
@@ -212,6 +212,36 @@ class L10nRu extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => 'Вставить из буфера обмена';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => 'Буфер обмена пуст';
|
||||
|
||||
@override
|
||||
String get cancelSend => 'Отменить отправку';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => 'Отменить эту отправку?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'Работает только если получатель еще не получил токен. Если уже получил, отправка будет помечена как завершенная.';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return 'Отправка отменена. Восстановлено $amount $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed => 'Получатель уже получил токен';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => 'Ожидающая оффлайн-отправка';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => 'Неполученные оффлайн-отправки';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => 'Ожидающая отправка';
|
||||
|
||||
@override
|
||||
String get validToken => 'Токен действителен';
|
||||
|
||||
@@ -292,6 +322,9 @@ class L10nRu extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => 'Недостаточный баланс';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => 'Комиссия превышает сумму отправки';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'Ошибка создания токена: $error';
|
||||
@@ -519,6 +552,12 @@ class L10nRu extends L10n {
|
||||
@override
|
||||
String get sentStatus => 'Отправлено';
|
||||
|
||||
@override
|
||||
String get receiving => 'Получение';
|
||||
|
||||
@override
|
||||
String get sending => 'Отправка';
|
||||
|
||||
@override
|
||||
String get now => 'Сейчас';
|
||||
|
||||
@@ -1230,4 +1269,75 @@ class L10nRu extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => 'Скопировано в буфер обмена';
|
||||
|
||||
@override
|
||||
String get swap => 'Обменять';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'Конвертировать между sats и USD';
|
||||
|
||||
@override
|
||||
String get swapFrom => 'Из';
|
||||
|
||||
@override
|
||||
String get swapTo => 'В';
|
||||
|
||||
@override
|
||||
String get swapAction => 'Обменять';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => 'Ориентировочная комиссия';
|
||||
|
||||
@override
|
||||
String get swapUseAll => 'Использовать всё';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return 'Минимум: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => 'Обработка обмена...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => 'Обмен завершён';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => 'Недостаточный баланс';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => 'Котировка истекла';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Ошибка обмена: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => 'Цена недоступна · Нажмите для повтора';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h Мин: $minPrice — Макс: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => 'Политика конфиденциальности';
|
||||
|
||||
@override
|
||||
String get privacyTitle => 'МЫ НЕ СОБИРАЕМ НИЧЕГО';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => 'ПОКА';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(читай дальше, если хочешь…)';
|
||||
|
||||
@override
|
||||
String get privacyBody =>
|
||||
'Мы не знаем, кто ты\nМы не знаем, сколько у тебя\nМы не знаем, что ты делаешь';
|
||||
|
||||
@override
|
||||
String get privacyConclusion =>
|
||||
'Лучший способ защитить твои данные —\nне иметь их';
|
||||
}
|
||||
|
||||
@@ -153,10 +153,10 @@ class L10nSw extends L10n {
|
||||
String get send => 'Tuma';
|
||||
|
||||
@override
|
||||
String get sendAction => 'Tuma ↗';
|
||||
String get sendAction => 'Tuma';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ Pokea';
|
||||
String get receiveAction => 'Pokea';
|
||||
|
||||
@override
|
||||
String get deposit => 'Weka';
|
||||
@@ -212,6 +212,36 @@ class L10nSw extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => 'Bandika kutoka ubao';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => 'Ubao wa kunakili ni tupu';
|
||||
|
||||
@override
|
||||
String get cancelSend => 'Ghairi kutuma';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => 'Ghairi kutuma huku?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody =>
|
||||
'Hufanya kazi tu ikiwa mpokeaji hajatumia tokeni. Ikiwa tayari alitumia, kutuma kutaonyeshwa kama kumekamilika.';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return 'Kutuma kumeghairiwa. Umerudisha $amount $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed => 'Mpokeaji tayari alitumia tokeni';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => 'Kutuma nje ya mtandao kunasubiri';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => 'Kutuma nje ya mtandao bila kudaiwa';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => 'Kutuma kunakusubiri';
|
||||
|
||||
@override
|
||||
String get validToken => 'Tokeni halali';
|
||||
|
||||
@@ -293,6 +323,9 @@ class L10nSw extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => 'Salio halitoshi';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => 'Ada inazidi kiasi cha kutuma';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'Hitilafu ya kuunda tokeni: $error';
|
||||
@@ -518,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';
|
||||
|
||||
@@ -1231,4 +1270,75 @@ class L10nSw extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => 'Imenakiliwa kwenye ubao wa kunakili';
|
||||
|
||||
@override
|
||||
String get swap => 'Badilisha';
|
||||
|
||||
@override
|
||||
String get swapDescription => 'Badilisha kati ya sats na USD';
|
||||
|
||||
@override
|
||||
String get swapFrom => 'Kutoka';
|
||||
|
||||
@override
|
||||
String get swapTo => 'Kwenda';
|
||||
|
||||
@override
|
||||
String get swapAction => 'Badilisha';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => 'Ada inayokadiriwa';
|
||||
|
||||
@override
|
||||
String get swapUseAll => 'Tumia yote';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return 'Kiwango cha chini: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => 'Inashughulikia ubadilishaji...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => 'Ubadilishaji umekamilika';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => 'Salio haitoshi';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => 'Bei imeisha muda';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Kosa la ubadilishaji: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => 'Bei haipatikani · Gusa kurudia';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h Chini: $minPrice — Juu: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => 'Sera ya faragha';
|
||||
|
||||
@override
|
||||
String get privacyTitle => 'HATUKUSANYI CHOCHOTE';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => 'KWAHERI';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(endelea kusoma ukitaka…)';
|
||||
|
||||
@override
|
||||
String get privacyBody =>
|
||||
'Hatujui wewe ni nani\nHatujui una kiasi gani\nHatujui unafanya nini';
|
||||
|
||||
@override
|
||||
String get privacyConclusion =>
|
||||
'Njia bora ya kulinda data yako\nni kutokuwa nayo';
|
||||
}
|
||||
|
||||
@@ -148,10 +148,10 @@ class L10nZh extends L10n {
|
||||
String get send => '付款';
|
||||
|
||||
@override
|
||||
String get sendAction => '付款 ↗';
|
||||
String get sendAction => '付款';
|
||||
|
||||
@override
|
||||
String get receiveAction => '↘ 收款';
|
||||
String get receiveAction => '收款';
|
||||
|
||||
@override
|
||||
String get deposit => '存入';
|
||||
@@ -207,6 +207,35 @@ class L10nZh extends L10n {
|
||||
@override
|
||||
String get pasteFromClipboard => '从剪贴板粘贴';
|
||||
|
||||
@override
|
||||
String get emptyClipboard => '剪贴板为空';
|
||||
|
||||
@override
|
||||
String get cancelSend => '取消发送';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmTitle => '取消此次发送?';
|
||||
|
||||
@override
|
||||
String get cancelSendConfirmBody => '仅当接收方尚未领取代币时有效。如已领取,发送将标记为已完成。';
|
||||
|
||||
@override
|
||||
String cancelSendSuccess(String amount, String unit) {
|
||||
return '发送已取消。已恢复 $amount $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancelSendAlreadyClaimed => '接收方已领取代币';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSend => '待处理的离线发送';
|
||||
|
||||
@override
|
||||
String get pendingOfflineSendsHeader => '未领取的离线发送';
|
||||
|
||||
@override
|
||||
String get pendingSendDetailTitle => '待处理发送';
|
||||
|
||||
@override
|
||||
String get validToken => '有效代币';
|
||||
|
||||
@@ -287,6 +316,9 @@ class L10nZh extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => '余额不足';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => '手续费超过发送金额';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return '创建代币错误:$error';
|
||||
@@ -510,6 +542,12 @@ class L10nZh extends L10n {
|
||||
@override
|
||||
String get sentStatus => '已发送';
|
||||
|
||||
@override
|
||||
String get receiving => '接收中';
|
||||
|
||||
@override
|
||||
String get sending => '发送中';
|
||||
|
||||
@override
|
||||
String get now => '刚刚';
|
||||
|
||||
@@ -1202,4 +1240,73 @@ class L10nZh extends L10n {
|
||||
|
||||
@override
|
||||
String get copiedToClipboard => '已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get swap => '兑换';
|
||||
|
||||
@override
|
||||
String get swapDescription => '在 Sats 和 USD 之间转换';
|
||||
|
||||
@override
|
||||
String get swapFrom => '从';
|
||||
|
||||
@override
|
||||
String get swapTo => '到';
|
||||
|
||||
@override
|
||||
String get swapAction => '兑换';
|
||||
|
||||
@override
|
||||
String get swapEstimatedFee => '预估手续费';
|
||||
|
||||
@override
|
||||
String get swapUseAll => '全部使用';
|
||||
|
||||
@override
|
||||
String swapMinimum(String amount) {
|
||||
return '最低: $amount';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapProcessing => '兑换处理中...';
|
||||
|
||||
@override
|
||||
String get swapSuccess => '兑换完成';
|
||||
|
||||
@override
|
||||
String get swapErrorInsufficient => '余额不足';
|
||||
|
||||
@override
|
||||
String get swapErrorExpired => '报价已过期';
|
||||
|
||||
@override
|
||||
String swapErrorGeneric(String error) {
|
||||
return '兑换错误: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => '价格不可用 · 点击重试';
|
||||
|
||||
@override
|
||||
String swapChartMinMax(String minPrice, String maxPrice) {
|
||||
return '24h 最低: $minPrice — 最高: $maxPrice';
|
||||
}
|
||||
|
||||
@override
|
||||
String get privacyPolicy => '隐私政策';
|
||||
|
||||
@override
|
||||
String get privacyTitle => '我们什么都不收集';
|
||||
|
||||
@override
|
||||
String get privacyGoodbye => '再见';
|
||||
|
||||
@override
|
||||
String get privacyKeepReading => '(想继续看就看吧…)';
|
||||
|
||||
@override
|
||||
String get privacyBody => '我们不知道你是谁\n我们不知道你有多少\n我们不知道你在做什么';
|
||||
|
||||
@override
|
||||
String get privacyConclusion => '保护数据的最好方式\n就是不拥有它';
|
||||
}
|
||||
|
||||
+43
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "Início",
|
||||
"receive": "Receber",
|
||||
"send": "Enviar",
|
||||
"sendAction": "Enviar ↗",
|
||||
"receiveAction": "↘ Receber",
|
||||
"sendAction": "Enviar",
|
||||
"receiveAction": "Receber",
|
||||
"deposit": "Depositar",
|
||||
"withdraw": "Sacar",
|
||||
"lightning": "Lightning",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "Receber Cashu",
|
||||
"pasteTheCashuToken": "Cole o token Cashu:",
|
||||
"pasteFromClipboard": "Colar da área de transferência",
|
||||
"emptyClipboard": "Área de transferência vazia",
|
||||
"cancelSend": "Cancelar envio",
|
||||
"cancelSendConfirmTitle": "Cancelar este envio?",
|
||||
"cancelSendConfirmBody": "Só funciona se o destinatário ainda não resgatou o token. Se já resgatou, o envio será marcado como concluído.",
|
||||
"cancelSendSuccess": "Envio cancelado. Recuperou {amount} {unit}",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "O destinatário já resgatou o token",
|
||||
"pendingOfflineSend": "Envio offline pendente",
|
||||
"pendingOfflineSendsHeader": "Envios offline não resgatados",
|
||||
"pendingSendDetailTitle": "Envio pendente",
|
||||
"validToken": "Token válido",
|
||||
"invalidToken": "Token inválido ou malformado",
|
||||
"amount": "Valor:",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "Confirmar",
|
||||
"cancel": "Cancelar",
|
||||
"insufficientBalance": "Saldo insuficiente",
|
||||
"feeExceedsAmount": "A taxa excede o valor a enviar",
|
||||
"tokenCreationError": "Erro ao criar token: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -227,6 +243,8 @@
|
||||
"pendingStatus": "Pendente",
|
||||
"receivedStatus": "Recebido",
|
||||
"sentStatus": "Enviado",
|
||||
"receiving": "Recebendo",
|
||||
"sending": "Enviando",
|
||||
"now": "Agora",
|
||||
"agoMinutes": "Há {minutes} min",
|
||||
"@agoMinutes": {
|
||||
@@ -559,5 +577,27 @@
|
||||
"requestPaymentReceived": "Pagamento recebido",
|
||||
"requestDescriptionHint": "Descrição (opcional)",
|
||||
"universal": "Universal",
|
||||
"copiedToClipboard": "Copiado para a área de transferência"
|
||||
"copiedToClipboard": "Copiado para a área de transferência",
|
||||
|
||||
"swap": "Trocar",
|
||||
"swapDescription": "Converter entre sats e USD",
|
||||
"swapFrom": "De",
|
||||
"swapTo": "Para",
|
||||
"swapAction": "Trocar",
|
||||
"swapEstimatedFee": "Taxa estimada",
|
||||
"swapUseAll": "Usar tudo",
|
||||
"swapMinimum": "Mínimo: {amount}",
|
||||
"swapProcessing": "Processando swap...",
|
||||
"swapSuccess": "Swap concluído",
|
||||
"swapErrorInsufficient": "Saldo insuficiente",
|
||||
"swapErrorExpired": "A cotação expirou",
|
||||
"swapErrorGeneric": "Erro no swap: {error}",
|
||||
"swapChartUnavailable": "Preço indisponível · Toque para tentar novamente",
|
||||
"swapChartMinMax": "24h Mín: {minPrice} — Máx: {maxPrice}",
|
||||
"privacyPolicy": "Política de privacidade",
|
||||
"privacyTitle": "NÃO RECOLHEMOS NADA",
|
||||
"privacyGoodbye": "ADEUS",
|
||||
"privacyKeepReading": "(continue lendo se quiser…)",
|
||||
"privacyBody": "Não sabemos quem você é\nNão sabemos quanto você tem\nNão sabemos o que você faz",
|
||||
"privacyConclusion": "A melhor forma de proteger seus dados\né não tê-los"
|
||||
}
|
||||
|
||||
+43
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "Главная",
|
||||
"receive": "Получить",
|
||||
"send": "Отправить",
|
||||
"sendAction": "Отправить ↗",
|
||||
"receiveAction": "↘ Получить",
|
||||
"sendAction": "Отправить",
|
||||
"receiveAction": "Получить",
|
||||
"deposit": "Пополнить",
|
||||
"withdraw": "Вывести",
|
||||
"lightning": "Lightning",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "Получить Cashu",
|
||||
"pasteTheCashuToken": "Вставьте Cashu токен:",
|
||||
"pasteFromClipboard": "Вставить из буфера обмена",
|
||||
"emptyClipboard": "Буфер обмена пуст",
|
||||
"cancelSend": "Отменить отправку",
|
||||
"cancelSendConfirmTitle": "Отменить эту отправку?",
|
||||
"cancelSendConfirmBody": "Работает только если получатель еще не получил токен. Если уже получил, отправка будет помечена как завершенная.",
|
||||
"cancelSendSuccess": "Отправка отменена. Восстановлено {amount} {unit}",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "Получатель уже получил токен",
|
||||
"pendingOfflineSend": "Ожидающая оффлайн-отправка",
|
||||
"pendingOfflineSendsHeader": "Неполученные оффлайн-отправки",
|
||||
"pendingSendDetailTitle": "Ожидающая отправка",
|
||||
"validToken": "Токен действителен",
|
||||
"invalidToken": "Недействительный или повреждённый токен",
|
||||
"amount": "Сумма:",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "Подтвердить",
|
||||
"cancel": "Отмена",
|
||||
"insufficientBalance": "Недостаточный баланс",
|
||||
"feeExceedsAmount": "Комиссия превышает сумму отправки",
|
||||
"tokenCreationError": "Ошибка создания токена: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -227,6 +243,8 @@
|
||||
"pendingStatus": "Ожидание",
|
||||
"receivedStatus": "Получено",
|
||||
"sentStatus": "Отправлено",
|
||||
"receiving": "Получение",
|
||||
"sending": "Отправка",
|
||||
"now": "Сейчас",
|
||||
"agoMinutes": "{minutes} мин назад",
|
||||
"@agoMinutes": {
|
||||
@@ -559,5 +577,27 @@
|
||||
"requestPaymentReceived": "Платёж получен",
|
||||
"requestDescriptionHint": "Описание (необязательно)",
|
||||
"universal": "Универсальный",
|
||||
"copiedToClipboard": "Скопировано в буфер обмена"
|
||||
"copiedToClipboard": "Скопировано в буфер обмена",
|
||||
|
||||
"swap": "Обменять",
|
||||
"swapDescription": "Конвертировать между sats и USD",
|
||||
"swapFrom": "Из",
|
||||
"swapTo": "В",
|
||||
"swapAction": "Обменять",
|
||||
"swapEstimatedFee": "Ориентировочная комиссия",
|
||||
"swapUseAll": "Использовать всё",
|
||||
"swapMinimum": "Минимум: {amount}",
|
||||
"swapProcessing": "Обработка обмена...",
|
||||
"swapSuccess": "Обмен завершён",
|
||||
"swapErrorInsufficient": "Недостаточный баланс",
|
||||
"swapErrorExpired": "Котировка истекла",
|
||||
"swapErrorGeneric": "Ошибка обмена: {error}",
|
||||
"swapChartUnavailable": "Цена недоступна · Нажмите для повтора",
|
||||
"swapChartMinMax": "24h Мин: {minPrice} — Макс: {maxPrice}",
|
||||
"privacyPolicy": "Политика конфиденциальности",
|
||||
"privacyTitle": "МЫ НЕ СОБИРАЕМ НИЧЕГО",
|
||||
"privacyGoodbye": "ПОКА",
|
||||
"privacyKeepReading": "(читай дальше, если хочешь…)",
|
||||
"privacyBody": "Мы не знаем, кто ты\nМы не знаем, сколько у тебя\nМы не знаем, что ты делаешь",
|
||||
"privacyConclusion": "Лучший способ защитить твои данные —\nне иметь их"
|
||||
}
|
||||
|
||||
+43
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "Nyumbani",
|
||||
"receive": "Pokea",
|
||||
"send": "Tuma",
|
||||
"sendAction": "Tuma ↗",
|
||||
"receiveAction": "↘ Pokea",
|
||||
"sendAction": "Tuma",
|
||||
"receiveAction": "Pokea",
|
||||
"deposit": "Weka",
|
||||
"withdraw": "Toa",
|
||||
"lightning": "Lightning",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "Pokea Cashu",
|
||||
"pasteTheCashuToken": "Bandika tokeni ya Cashu:",
|
||||
"pasteFromClipboard": "Bandika kutoka ubao",
|
||||
"emptyClipboard": "Ubao wa kunakili ni tupu",
|
||||
"cancelSend": "Ghairi kutuma",
|
||||
"cancelSendConfirmTitle": "Ghairi kutuma huku?",
|
||||
"cancelSendConfirmBody": "Hufanya kazi tu ikiwa mpokeaji hajatumia tokeni. Ikiwa tayari alitumia, kutuma kutaonyeshwa kama kumekamilika.",
|
||||
"cancelSendSuccess": "Kutuma kumeghairiwa. Umerudisha {amount} {unit}",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "Mpokeaji tayari alitumia tokeni",
|
||||
"pendingOfflineSend": "Kutuma nje ya mtandao kunasubiri",
|
||||
"pendingOfflineSendsHeader": "Kutuma nje ya mtandao bila kudaiwa",
|
||||
"pendingSendDetailTitle": "Kutuma kunakusubiri",
|
||||
"validToken": "Tokeni halali",
|
||||
"invalidToken": "Tokeni batili au imeharibika",
|
||||
"amount": "Kiasi:",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "Thibitisha",
|
||||
"cancel": "Ghairi",
|
||||
"insufficientBalance": "Salio halitoshi",
|
||||
"feeExceedsAmount": "Ada inazidi kiasi cha kutuma",
|
||||
"tokenCreationError": "Hitilafu ya kuunda tokeni: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -227,6 +243,8 @@
|
||||
"pendingStatus": "Inasubiri",
|
||||
"receivedStatus": "Imepokelewa",
|
||||
"sentStatus": "Imetumwa",
|
||||
"receiving": "Inapokea",
|
||||
"sending": "Inatumwa",
|
||||
"now": "Sasa",
|
||||
"agoMinutes": "Dakika {minutes} zilizopita",
|
||||
"@agoMinutes": {
|
||||
@@ -559,5 +577,27 @@
|
||||
"requestPaymentReceived": "Malipo limepokelewa",
|
||||
"requestDescriptionHint": "Maelezo (si lazima)",
|
||||
"universal": "Universal",
|
||||
"copiedToClipboard": "Imenakiliwa kwenye ubao wa kunakili"
|
||||
"copiedToClipboard": "Imenakiliwa kwenye ubao wa kunakili",
|
||||
|
||||
"swap": "Badilisha",
|
||||
"swapDescription": "Badilisha kati ya sats na USD",
|
||||
"swapFrom": "Kutoka",
|
||||
"swapTo": "Kwenda",
|
||||
"swapAction": "Badilisha",
|
||||
"swapEstimatedFee": "Ada inayokadiriwa",
|
||||
"swapUseAll": "Tumia yote",
|
||||
"swapMinimum": "Kiwango cha chini: {amount}",
|
||||
"swapProcessing": "Inashughulikia ubadilishaji...",
|
||||
"swapSuccess": "Ubadilishaji umekamilika",
|
||||
"swapErrorInsufficient": "Salio haitoshi",
|
||||
"swapErrorExpired": "Bei imeisha muda",
|
||||
"swapErrorGeneric": "Kosa la ubadilishaji: {error}",
|
||||
"swapChartUnavailable": "Bei haipatikani · Gusa kurudia",
|
||||
"swapChartMinMax": "24h Chini: {minPrice} — Juu: {maxPrice}",
|
||||
"privacyPolicy": "Sera ya faragha",
|
||||
"privacyTitle": "HATUKUSANYI CHOCHOTE",
|
||||
"privacyGoodbye": "KWAHERI",
|
||||
"privacyKeepReading": "(endelea kusoma ukitaka…)",
|
||||
"privacyBody": "Hatujui wewe ni nani\nHatujui una kiasi gani\nHatujui unafanya nini",
|
||||
"privacyConclusion": "Njia bora ya kulinda data yako\nni kutokuwa nayo"
|
||||
}
|
||||
|
||||
+43
-3
@@ -63,8 +63,8 @@
|
||||
"homeTitle": "首页",
|
||||
"receive": "收款",
|
||||
"send": "付款",
|
||||
"sendAction": "付款 ↗",
|
||||
"receiveAction": "↘ 收款",
|
||||
"sendAction": "付款",
|
||||
"receiveAction": "收款",
|
||||
"deposit": "存入",
|
||||
"withdraw": "提取",
|
||||
"lightning": "闪电网络",
|
||||
@@ -86,6 +86,21 @@
|
||||
"receiveCashu": "接收 Cashu",
|
||||
"pasteTheCashuToken": "粘贴 Cashu 代币:",
|
||||
"pasteFromClipboard": "从剪贴板粘贴",
|
||||
"emptyClipboard": "剪贴板为空",
|
||||
"cancelSend": "取消发送",
|
||||
"cancelSendConfirmTitle": "取消此次发送?",
|
||||
"cancelSendConfirmBody": "仅当接收方尚未领取代币时有效。如已领取,发送将标记为已完成。",
|
||||
"cancelSendSuccess": "发送已取消。已恢复 {amount} {unit}",
|
||||
"@cancelSendSuccess": {
|
||||
"placeholders": {
|
||||
"amount": {"type": "String"},
|
||||
"unit": {"type": "String"}
|
||||
}
|
||||
},
|
||||
"cancelSendAlreadyClaimed": "接收方已领取代币",
|
||||
"pendingOfflineSend": "待处理的离线发送",
|
||||
"pendingOfflineSendsHeader": "未领取的离线发送",
|
||||
"pendingSendDetailTitle": "待处理发送",
|
||||
"validToken": "有效代币",
|
||||
"invalidToken": "无效或格式错误的代币",
|
||||
"amount": "金额:",
|
||||
@@ -118,6 +133,7 @@
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"insufficientBalance": "余额不足",
|
||||
"feeExceedsAmount": "手续费超过发送金额",
|
||||
"tokenCreationError": "创建代币错误:{error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -227,6 +243,8 @@
|
||||
"pendingStatus": "待处理",
|
||||
"receivedStatus": "已收到",
|
||||
"sentStatus": "已发送",
|
||||
"receiving": "接收中",
|
||||
"sending": "发送中",
|
||||
"now": "刚刚",
|
||||
"agoMinutes": "{minutes} 分钟前",
|
||||
"@agoMinutes": {
|
||||
@@ -559,5 +577,27 @@
|
||||
"requestPaymentReceived": "已收到付款",
|
||||
"requestDescriptionHint": "描述(可选)",
|
||||
"universal": "通用",
|
||||
"copiedToClipboard": "已复制到剪贴板"
|
||||
"copiedToClipboard": "已复制到剪贴板",
|
||||
|
||||
"swap": "兑换",
|
||||
"swapDescription": "在 Sats 和 USD 之间转换",
|
||||
"swapFrom": "从",
|
||||
"swapTo": "到",
|
||||
"swapAction": "兑换",
|
||||
"swapEstimatedFee": "预估手续费",
|
||||
"swapUseAll": "全部使用",
|
||||
"swapMinimum": "最低: {amount}",
|
||||
"swapProcessing": "兑换处理中...",
|
||||
"swapSuccess": "兑换完成",
|
||||
"swapErrorInsufficient": "余额不足",
|
||||
"swapErrorExpired": "报价已过期",
|
||||
"swapErrorGeneric": "兑换错误: {error}",
|
||||
"swapChartUnavailable": "价格不可用 · 点击重试",
|
||||
"swapChartMinMax": "24h 最低: {minPrice} — 最高: {maxPrice}",
|
||||
"privacyPolicy": "隐私政策",
|
||||
"privacyTitle": "我们什么都不收集",
|
||||
"privacyGoodbye": "再见",
|
||||
"privacyKeepReading": "(想继续看就看吧…)",
|
||||
"privacyBody": "我们不知道你是谁\n我们不知道你有多少\n我们不知道你在做什么",
|
||||
"privacyConclusion": "保护数据的最好方式\n就是不拥有它"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import 'package:uuid/uuid.dart';
|
||||
import '../data/transaction_meta_storage.dart';
|
||||
import '../data/pending_token.dart';
|
||||
import '../data/pending_token_storage.dart';
|
||||
import '../data/pending_send.dart';
|
||||
import '../data/pending_send_storage.dart';
|
||||
import '../core/utils/keyset_debug.dart';
|
||||
import '../core/utils/p2pk_utils.dart';
|
||||
import '../widgets/effects/cashu_confetti.dart';
|
||||
@@ -47,6 +49,9 @@ class WalletProvider extends ChangeNotifier {
|
||||
/// Storage para tokens pendientes de reclamar (Receive Later)
|
||||
final PendingTokenStorage _pendingTokenStorage = PendingTokenStorage();
|
||||
|
||||
/// Storage para envíos offline pendientes de ser reclamados por el receptor
|
||||
final PendingSendStorage _pendingSendStorage = PendingSendStorage();
|
||||
|
||||
/// Controller global de confetti para celebrar recepciones
|
||||
final CashuConfettiController confettiController = CashuConfettiController();
|
||||
|
||||
@@ -125,6 +130,159 @@ class WalletProvider extends ChangeNotifier {
|
||||
/// Stream de cambios en pending tokens
|
||||
Stream<void> get pendingTokenChanges => _pendingTokenStorage.changes;
|
||||
|
||||
// ============================================================
|
||||
// PENDING SENDS GETTERS (envíos offline no reclamados)
|
||||
// ============================================================
|
||||
|
||||
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);
|
||||
|
||||
/// Persiste un envío offline pendiente para poder reclamarlo después.
|
||||
/// Llamar desde el flujo de offline_send_screen después de crear el token.
|
||||
Future<PendingSend> addPendingSend({
|
||||
required String encoded,
|
||||
required BigInt amount,
|
||||
required String mintUrl,
|
||||
required String unit,
|
||||
required List<String> proofYs,
|
||||
String? memo,
|
||||
}) async {
|
||||
final send = await _pendingSendStorage.add(
|
||||
id: _uuid.v4(),
|
||||
encoded: encoded,
|
||||
amount: amount,
|
||||
mintUrl: mintUrl,
|
||||
unit: unit,
|
||||
proofYs: proofYs,
|
||||
memo: memo,
|
||||
);
|
||||
// El flujo offline modifica el DB directamente (markProofsPendingSpent),
|
||||
// así que CDK no sabe que cambió. Forzamos refresh del stream de balance
|
||||
// para que el home se actualice al instante.
|
||||
try {
|
||||
final wallet = await getWallet(mintUrl, unit);
|
||||
await wallet.refreshBalance();
|
||||
} catch (e) {
|
||||
debugPrint('refreshBalance tras addPendingSend falló: $e');
|
||||
}
|
||||
notifyListeners();
|
||||
return send;
|
||||
}
|
||||
|
||||
/// Reclama las proofs de un envío offline pendiente. Verifica con el mint:
|
||||
/// - 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);
|
||||
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.
|
||||
/// - `pending == 0 && spent >= proofYs.length` → mint confirmó todos
|
||||
/// spent: receptor reclamó. SETTLE para histórico.
|
||||
/// - cualquier otro caso (pending > 0, o cobertura parcial de spent)
|
||||
/// → conservar activo; próxima reconciliación reintenta.
|
||||
Future<bool> _settlePendingSendOutcome(
|
||||
PendingSend send,
|
||||
ReclaimResult result,
|
||||
) async {
|
||||
if (result.count > BigInt.zero) {
|
||||
// Recuperamos las proofs; el envío se canceló desde nuestro lado.
|
||||
await _pendingSendStorage.remove(send.id);
|
||||
return true;
|
||||
}
|
||||
// Defensa en profundidad: aunque `pending_count == 0` suele bastar para
|
||||
// decir "todas consumidas", exigimos además que `spent_count` cubra
|
||||
// todos los proofs del send. Un resultado parcial o malformado podría
|
||||
// dejar `pending == 0` sin haber observado spent para todos los ys.
|
||||
final expected = BigInt.from(send.proofYs.length);
|
||||
final allAccountedSpent = expected > BigInt.zero &&
|
||||
result.pendingCount == BigInt.zero &&
|
||||
result.spentCount >= expected;
|
||||
if (allAccountedSpent) {
|
||||
// Ninguna unspent, ninguna pending, todas spent → receptor reclamó.
|
||||
// Pasa a histórico como "outgoing liquidado".
|
||||
await _pendingSendStorage.markSettled(send.id);
|
||||
return true;
|
||||
}
|
||||
// pending > 0 (receptor mid-swap) o cobertura parcial → conservamos
|
||||
// activo para que la próxima reconciliación lo resuelva.
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> removePendingSend(String id) async {
|
||||
await _pendingSendStorage.remove(id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Lista de mints ordenados por balance.
|
||||
/// Cuba Bitcoin siempre primero, luego ordenados por: sats → usd → eur → otros.
|
||||
Future<List<String>> getSortedMintUrls() async {
|
||||
@@ -333,6 +491,9 @@ class WalletProvider extends ChangeNotifier {
|
||||
// Inicializar storage de tokens pendientes
|
||||
await _pendingTokenStorage.init();
|
||||
|
||||
// Inicializar storage de envíos offline pendientes
|
||||
await _pendingSendStorage.init();
|
||||
|
||||
// Obtener directorio de documentos (path absoluto requerido)
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dbPath = '${dir.path}/elcaju_wallet.sqlite';
|
||||
@@ -1092,43 +1253,27 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Método de conveniencia: prepara y confirma en un solo paso.
|
||||
/// Si confirmSend falla, libera proofs reservados con cancelSend.
|
||||
/// Si confirmSend falla, los proofs quedan en PendingSpent.
|
||||
/// No llamamos reclaimPendingProofs aquí porque podría revocar
|
||||
/// tokens de otros envíos no reclamados. El usuario puede recuperar
|
||||
/// manualmente desde Settings → Recover Tokens.
|
||||
Future<String> sendTokens(BigInt amount, String? memo) async {
|
||||
final prepared = await prepareSend(amount);
|
||||
debugPrint('[SEND] prepareSend OK - fee=${prepared.fee}');
|
||||
try {
|
||||
final token = await confirmSend(prepared, memo);
|
||||
debugPrint('[SEND] Send completed');
|
||||
return token;
|
||||
} catch (e) {
|
||||
try {
|
||||
await cancelSend(prepared);
|
||||
} catch (cancelErr) {
|
||||
debugPrint('[SEND] cancelSend failed: $cancelErr');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
final token = await confirmSend(prepared, memo);
|
||||
debugPrint('[SEND] Send completed');
|
||||
return token;
|
||||
}
|
||||
|
||||
/// Envía tokens P2PK (bloqueados a una clave pública).
|
||||
/// CDK 0.15+ con includeFee: true maneja correctamente mints con ppk>0.
|
||||
/// Si confirmSend falla, libera proofs reservados con cancelSend.
|
||||
Future<String> sendTokensP2pk(BigInt amount, String pubkey, String? memo) async {
|
||||
debugPrint('[P2PK] Sending $amount to ${pubkey.length > 16 ? pubkey.substring(0, 16) : pubkey}...');
|
||||
final prepared = await prepareSendP2pk(amount, pubkey);
|
||||
debugPrint('[P2PK] prepareSend OK - fee=${prepared.fee}');
|
||||
try {
|
||||
final token = await confirmSend(prepared, memo);
|
||||
debugPrint('[P2PK] Send completed');
|
||||
return token;
|
||||
} catch (e) {
|
||||
try {
|
||||
await cancelSend(prepared);
|
||||
} catch (cancelErr) {
|
||||
debugPrint('[P2PK] cancelSend failed: $cancelErr');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
final token = await confirmSend(prepared, memo);
|
||||
debugPrint('[P2PK] Send completed');
|
||||
return token;
|
||||
}
|
||||
|
||||
/// Verifica si hay transacciones salientes pendientes en el wallet activo.
|
||||
@@ -1498,19 +1643,82 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Ejecuta el pago del invoice.
|
||||
/// Guarda metadata type=lightning para identificar en historial.
|
||||
/// FRB consume PreparedMelt por valor, así que cancelMelt no es posible.
|
||||
/// Si falla, usamos recoverIncompleteSagas para reanudar solo esta operación
|
||||
/// sin afectar otros proofs pendientes (como tokens enviados no reclamados).
|
||||
Future<BigInt> melt(MeltQuote quote) async {
|
||||
final wallet = await getActiveWallet();
|
||||
final totalPaid = await wallet.melt(quote: quote);
|
||||
final prepared = await wallet.prepareMelt(quote: quote);
|
||||
try {
|
||||
final totalPaid = await wallet.confirmMelt(melt: prepared);
|
||||
|
||||
// Guardar metadata con el invoice
|
||||
if (_pendingMeltInvoice != null) {
|
||||
await _saveMeltMetadata(wallet, _pendingMeltInvoice!);
|
||||
_pendingMeltInvoice = null;
|
||||
// Guardar metadata con el invoice
|
||||
if (_pendingMeltInvoice != null) {
|
||||
await _saveMeltMetadata(wallet, _pendingMeltInvoice!);
|
||||
_pendingMeltInvoice = null;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return totalPaid;
|
||||
} catch (e) {
|
||||
// PreparedMelt ya fue consumido por FRB — cancelMelt es imposible.
|
||||
// recoverIncompleteSagas reanuda solo la saga interrumpida,
|
||||
// sin afectar otros proofs en PendingSpent (sends no reclamados).
|
||||
try {
|
||||
await wallet.recoverIncompleteSagas();
|
||||
} catch (recoverErr) {
|
||||
debugPrint('[MELT] recoverIncompleteSagas failed: $recoverErr');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return totalPaid;
|
||||
/// Recupera proofs huérfanas en PendingSpent consultando el mint.
|
||||
/// Solo revierte proofs que el mint confirma como no gastadas.
|
||||
/// Si [mintUrl] es null, escanea todos los mints.
|
||||
/// Retorna Map<unit, ReclaimResult> con resultados agrupados por unidad.
|
||||
Future<Map<String, ReclaimResult>> reclaimPendingProofs({String? mintUrl}) async {
|
||||
final perUnit = <String, ReclaimResult>{};
|
||||
final mints = mintUrl != null
|
||||
? {mintUrl: _mintUnits[mintUrl] ?? ['sat']}
|
||||
: _mintUnits;
|
||||
for (final entry in mints.entries) {
|
||||
for (final unit in entry.value) {
|
||||
try {
|
||||
final wallet = await getWallet(entry.key, unit);
|
||||
final result = await wallet.reclaimPendingProofs();
|
||||
if (result.count > BigInt.zero) {
|
||||
final prev = perUnit[unit];
|
||||
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) {
|
||||
debugPrint('Reclaim pending proofs failed for ${entry.key}:$unit: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (perUnit.isNotEmpty) {
|
||||
notifyListeners();
|
||||
}
|
||||
return perUnit;
|
||||
}
|
||||
|
||||
/// Cancela un envío pendiente reclamando sus proofs.
|
||||
/// Usa tx.ys para identificar las proofs exactas de esa transacción.
|
||||
/// Solo revierte las que el mint confirma como no gastadas.
|
||||
Future<ReclaimResult> reclaimTransaction(Transaction tx) async {
|
||||
final wallet = await getWallet(tx.mintUrl, tx.unit);
|
||||
final result = await wallet.reclaimProofsByYs(ys: tx.ys);
|
||||
if (result.count > BigInt.zero) {
|
||||
notifyListeners();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Guarda metadata para una transacción de melt (Lightning withdrawal).
|
||||
@@ -1537,6 +1745,58 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Guarda metadata para el lado melt (enviado) de un swap.
|
||||
/// Busca la tx outgoing más reciente que coincida con el monto.
|
||||
Future<void> saveSwapMeltMetadata(Wallet wallet, String invoice, BigInt amount) async {
|
||||
try {
|
||||
final txs = await wallet.listTransactions(
|
||||
direction: TransactionDirection.outgoing,
|
||||
);
|
||||
final tx = txs.cast<Transaction?>().firstWhere(
|
||||
(t) => t!.amount == amount && !_txMetaStorage.has(t.id),
|
||||
orElse: () => null,
|
||||
);
|
||||
if (tx != null) {
|
||||
await _txMetaStorage.save(
|
||||
tx.id,
|
||||
TransactionMeta(type: TransactionType.lightning, invoice: invoice),
|
||||
);
|
||||
debugPrint('Swap melt metadata guardada para tx ${tx.id}');
|
||||
} else {
|
||||
debugPrint('No matching untagged outgoing tx found for amount $amount');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error guardando swap melt metadata: $e');
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Guarda metadata para el lado mint (recibido) de un swap.
|
||||
/// Busca la tx incoming más reciente que coincida con el monto.
|
||||
Future<void> saveSwapMintMetadata(Wallet wallet, String invoice, BigInt amount) async {
|
||||
try {
|
||||
final txs = await wallet.listTransactions(
|
||||
direction: TransactionDirection.incoming,
|
||||
);
|
||||
final tx = txs.cast<Transaction?>().firstWhere(
|
||||
(t) => t!.amount == amount && !_txMetaStorage.has(t.id),
|
||||
orElse: () => null,
|
||||
);
|
||||
if (tx != null) {
|
||||
await _txMetaStorage.save(
|
||||
tx.id,
|
||||
TransactionMeta(type: TransactionType.lightning, invoice: invoice),
|
||||
);
|
||||
debugPrint('Swap mint metadata guardada para tx ${tx.id}');
|
||||
} else {
|
||||
debugPrint('No matching untagged incoming tx found for amount $amount');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error guardando swap mint metadata: $e');
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HISTORIAL
|
||||
// ============================================================
|
||||
@@ -1642,6 +1902,7 @@ class WalletProvider extends ChangeNotifier {
|
||||
// Silencioso - puede fallar offline
|
||||
debugPrint('Check pending failed: $e');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('Error getting wallet ${entry.key}:$unit: $e');
|
||||
}
|
||||
@@ -1859,6 +2120,10 @@ class WalletProvider extends ChangeNotifier {
|
||||
// Limpiar metadata de transacciones
|
||||
await _txMetaStorage.clear();
|
||||
|
||||
// Limpiar envíos offline pendientes (storage separado, sobrevive al
|
||||
// borrado del wallet.sqlite si no se limpia acá).
|
||||
await _pendingSendStorage.clear();
|
||||
|
||||
// Borrar archivo
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dbPath = '${dir.path}/elcaju_wallet.sqlite';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
@@ -59,38 +60,81 @@ class _ScanScreenState extends State<ScanScreen> {
|
||||
children: [
|
||||
// Scanner
|
||||
QrScannerWidget(
|
||||
onDetect: _onCodeDetected,
|
||||
showFlashControl: true,
|
||||
showCameraSwitch: false,
|
||||
),
|
||||
|
||||
// Instrucciones en la parte inferior
|
||||
Positioned(
|
||||
bottom: 100,
|
||||
left: 24,
|
||||
right: 24,
|
||||
child: Text(
|
||||
_getInstructionForMode(l10n),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
onDetect: _onCodeDetected,
|
||||
showFlashControl: true,
|
||||
showCameraSwitch: false,
|
||||
),
|
||||
),
|
||||
|
||||
// Indicador de procesamiento
|
||||
if (_isProcessing)
|
||||
Container(
|
||||
color: Colors.black54,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryAction,
|
||||
// Instrucciones en la parte inferior
|
||||
Positioned(
|
||||
bottom: 170,
|
||||
left: 24,
|
||||
right: 24,
|
||||
child: Text(
|
||||
_getInstructionForMode(l10n),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
// Botón pegar del portapapeles (encima del flash)
|
||||
Positioned(
|
||||
bottom: 100,
|
||||
left: 24,
|
||||
right: 24,
|
||||
child: Center(
|
||||
child: Material(
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: InkWell(
|
||||
onTap: _isProcessing ? null : _pasteFromClipboard,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.clipboard,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.pasteFromClipboard,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Indicador de procesamiento
|
||||
if (_isProcessing)
|
||||
Container(
|
||||
color: Colors.black54,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -118,6 +162,24 @@ class _ScanScreenState extends State<ScanScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pasteFromClipboard() async {
|
||||
final l10n = L10n.of(context)!;
|
||||
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final text = clipboardData?.text?.trim();
|
||||
if (!mounted) return;
|
||||
if (text == null || text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.emptyClipboard),
|
||||
backgroundColor: AppColors.warning,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_onCodeDetected(text);
|
||||
}
|
||||
|
||||
void _onCodeDetected(String rawData) async {
|
||||
if (_isProcessing) return;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../../core/utils/incoming_data_parser.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/animated_action_button.dart';
|
||||
import '../../widgets/common/bottom_sheet_container.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
import '../../providers/p2pk_provider.dart';
|
||||
@@ -356,6 +357,9 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
Expanded(
|
||||
child: AnimatedActionButton(
|
||||
label: l10n.sendAction,
|
||||
icon: LucideIcons.arrowUpRight,
|
||||
showIcon: true,
|
||||
iconTrailing: true,
|
||||
type: ButtonType.criticalAction,
|
||||
onTap: _showSendOptions,
|
||||
),
|
||||
@@ -368,6 +372,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
Expanded(
|
||||
child: AnimatedActionButton(
|
||||
label: l10n.receiveAction,
|
||||
icon: LucideIcons.arrowDownRight,
|
||||
showIcon: true,
|
||||
type: ButtonType.primaryAction,
|
||||
onTap: _showReceiveOptions,
|
||||
),
|
||||
@@ -573,29 +579,11 @@ class _MethodSelectorModal extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.deepVoidPurple,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
return BottomSheetContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const BottomSheetHandle(),
|
||||
|
||||
// Título
|
||||
Text(
|
||||
@@ -618,9 +606,6 @@ class _MethodSelectorModal extends StatelessWidget {
|
||||
child: _MethodOptionTile(option: option),
|
||||
),
|
||||
),
|
||||
|
||||
// Espacio para la barra de navegación del sistema
|
||||
SizedBox(height: MediaQuery.of(context).padding.bottom + AppDimensions.paddingSmall),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -635,67 +620,76 @@ class _MethodOptionTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: option.onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: option.label,
|
||||
hint: option.description,
|
||||
child: Material(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: option.onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icono
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: AppColors.buttonGradient,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icono
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: AppColors.buttonGradient,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(option.icon, color: Colors.white, size: 24),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(option.icon, color: Colors.white, size: 24),
|
||||
),
|
||||
const SizedBox(width: AppDimensions.paddingMedium),
|
||||
// Texto
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
option.label,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: AppDimensions.paddingMedium),
|
||||
// Texto
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
option.label,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
option.description,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
option.description,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Flecha
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
// Flecha
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
color: Colors.white.withValues(alpha: 0.5),
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/models/proof.dart';
|
||||
@@ -11,6 +13,7 @@ import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
import '../../widgets/proof/proof_selector.dart';
|
||||
import '../../core/utils/keyset_debug.dart';
|
||||
import 'share_token_screen.dart';
|
||||
|
||||
/// Pantalla para enviar tokens de forma offline seleccionando proofs manualmente.
|
||||
@@ -32,6 +35,13 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
|
||||
final TextEditingController _memoController = TextEditingController();
|
||||
final ProofService _proofService = ProofService();
|
||||
|
||||
// Capturado en initState para poder persistir el PendingSend incluso si
|
||||
// el widget se desmonta durante el await de markProofsPendingSpent.
|
||||
// Usar `context.read` después del await es inseguro (widget puede no
|
||||
// existir), y gatear con `mounted` perdería el record — los proofs
|
||||
// quedan marcados PENDING_SPENT pero sin handle para reclamar.
|
||||
late final WalletProvider _walletProvider;
|
||||
|
||||
List<LocalProof> _availableProofs = [];
|
||||
Set<String> _selectedIds = {};
|
||||
bool _isLoading = true;
|
||||
@@ -41,6 +51,7 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_walletProvider = context.read<WalletProvider>();
|
||||
_loadProofs();
|
||||
}
|
||||
|
||||
@@ -325,6 +336,21 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
|
||||
Future<void> _createOfflineToken() async {
|
||||
final selectedProofs = _selectedProofs;
|
||||
final selectedTotal = _proofService.calculateTotal(selectedProofs);
|
||||
|
||||
// Validar monto mínimo viable para el receptor
|
||||
// Leer ppk del keyset local, fallback a 100 (estándar)
|
||||
var ppk = await KeysetDebug.getInputFeePpk(widget.mintUrl, widget.unit);
|
||||
if (ppk < 0) ppk = 100; // -1 = error leyendo DB, asumir fee estándar
|
||||
final minReceiveFee = ppk > 0 ? BigInt.from((ppk + 999) ~/ 1000) : BigInt.zero;
|
||||
if (selectedTotal <= minReceiveFee) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = L10n.of(context)!.feeExceedsAmount;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var proofsMarkedPending = false;
|
||||
|
||||
setState(() {
|
||||
@@ -349,6 +375,20 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
|
||||
);
|
||||
final token = cdkToken.encoded;
|
||||
|
||||
// Rastrear el envío offline como pendiente para poder cancelarlo
|
||||
// desde el historial. Los offline sends no crean CDK Transaction.
|
||||
// Usamos el provider capturado en initState (no context.read) para
|
||||
// que el record se persista aunque el widget se haya desmontado
|
||||
// mientras esperábamos markProofsPendingSpent.
|
||||
await _walletProvider.addPendingSend(
|
||||
encoded: token,
|
||||
amount: selectedTotal,
|
||||
mintUrl: widget.mintUrl,
|
||||
unit: widget.unit,
|
||||
proofYs: selectedProofs.map((p) => p.yHex).toList(),
|
||||
memo: memo,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
_isCreating = false;
|
||||
Navigator.pushReplacement(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
@@ -7,9 +8,12 @@ import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../core/utils/keyset_debug.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
import '../../widgets/common/secondary_button.dart';
|
||||
import '../../widgets/common/bottom_sheet_container.dart';
|
||||
import '../../widgets/common/numpad_widget.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../core/utils/nostr_utils.dart';
|
||||
@@ -572,11 +576,32 @@ class _SendScreenState extends State<SendScreen> {
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Offline: verificar monto mínimo con ppk del keyset local (fallback 100)
|
||||
var ppk = await KeysetDebug.getInputFeePpk(mintUrl, _activeUnit);
|
||||
if (ppk < 0) ppk = 100;
|
||||
final minReceiveFee = ppk > 0 ? BigInt.from((ppk + 999) ~/ 1000) : BigInt.zero;
|
||||
if (!mounted) return;
|
||||
if (_amount <= minReceiveFee) {
|
||||
setState(() {
|
||||
_errorMessage = L10n.of(context)!.feeExceedsAmount;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Offline: ir directo a selección de monedas
|
||||
_goToOfflineModeWithMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
// Online: verificar monto mínimo con ppk real del mint
|
||||
final minReceiveFee = await _getMinReceiveFee(mintUrl);
|
||||
if (!mounted) return;
|
||||
if (_amount <= minReceiveFee) {
|
||||
setState(() {
|
||||
_errorMessage = L10n.of(context)!.feeExceedsAmount;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Online: mostrar modal de confirmación normal
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -595,6 +620,32 @@ class _SendScreenState extends State<SendScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Calcula el fee mínimo que el receptor pagará al reclamar el token.
|
||||
/// Consulta /v1/keysets del mint para obtener el ppk real.
|
||||
/// Fallback: ppk=100 (estándar) → fee mínimo = 1 sat.
|
||||
Future<BigInt> _getMinReceiveFee(String mintUrl) async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('$mintUrl/v1/keysets'),
|
||||
).timeout(const Duration(seconds: 3));
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body) as Map<String, dynamic>;
|
||||
final keysets = data['keysets'] as List<dynamic>? ?? [];
|
||||
for (final ks in keysets) {
|
||||
if (ks['active'] == true && ks['unit'] == _activeUnit) {
|
||||
final ppk = (ks['input_fee_ppk'] as num?)?.toInt() ?? 0;
|
||||
if (ppk > 0) {
|
||||
return BigInt.from((ppk + 999) ~/ 1000); // ceil(ppk / 1000)
|
||||
}
|
||||
return BigInt.zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
// Fallback: ppk=100 estándar
|
||||
return BigInt.one;
|
||||
}
|
||||
|
||||
/// Verifica conectividad haciendo petición HTTP real al mint.
|
||||
Future<bool> _checkConnectivity(String mintUrl) async {
|
||||
try {
|
||||
@@ -748,29 +799,12 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.deepVoidPurple,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
final l10n = L10n.of(context)!;
|
||||
return BottomSheetContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const BottomSheetHandle(),
|
||||
|
||||
// Icono
|
||||
Container(
|
||||
@@ -790,7 +824,7 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
|
||||
// Titulo
|
||||
Text(
|
||||
L10n.of(context)!.confirmSend,
|
||||
l10n.confirmSend,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 20,
|
||||
@@ -831,40 +865,22 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onCancel,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
L10n.of(context)!.cancel,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SecondaryButton(
|
||||
text: l10n.cancel,
|
||||
onPressed: onCancel,
|
||||
height: 52,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimensions.paddingMedium),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
text: L10n.of(context)!.confirm,
|
||||
text: l10n.confirm,
|
||||
onPressed: onConfirm,
|
||||
height: 52,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -9,6 +9,8 @@ import '../../core/services/lnurl_service.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
import '../../widgets/common/secondary_button.dart';
|
||||
import '../../widgets/common/bottom_sheet_container.dart';
|
||||
import '../../widgets/common/numpad_widget.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../providers/price_provider.dart';
|
||||
@@ -404,6 +406,8 @@ class _AmountScreenState extends State<AmountScreen> {
|
||||
Future<void> _processPayment() async {
|
||||
if (!_canPay) return;
|
||||
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
|
||||
setState(() {
|
||||
_isProcessing = true;
|
||||
_errorMessage = null;
|
||||
@@ -431,14 +435,22 @@ class _AmountScreenState extends State<AmountScreen> {
|
||||
if (!mounted) return;
|
||||
|
||||
// 3. Obtener quote del mint (en la unidad del mint)
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final quote = await walletProvider.getMeltQuote(invoiceResult.invoice);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final total = quote.amount + quote.feeReserve;
|
||||
|
||||
// 4. Verificar balance suficiente
|
||||
// 4a. Verificar que el fee no supere el monto (operación inviable)
|
||||
if (quote.amount <= quote.feeReserve) {
|
||||
setState(() {
|
||||
_isProcessing = false;
|
||||
_errorMessage = L10n.of(context)!.feeExceedsAmount;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 4b. Verificar balance suficiente
|
||||
if (total > _availableBalance) {
|
||||
setState(() {
|
||||
_isProcessing = false;
|
||||
@@ -506,32 +518,16 @@ class _AmountScreenState extends State<AmountScreen> {
|
||||
|
||||
Future<bool> _showConfirmation(BigInt amount, BigInt fee, BigInt total) async {
|
||||
final unitLabel = UnitFormatter.getUnitLabel(_activeUnit);
|
||||
final l10n = L10n.of(context)!;
|
||||
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.deepVoidPurple,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
builder: (context) => BottomSheetContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const BottomSheetHandle(),
|
||||
|
||||
// Icono
|
||||
Container(
|
||||
@@ -551,7 +547,7 @@ class _AmountScreenState extends State<AmountScreen> {
|
||||
|
||||
// Título
|
||||
Text(
|
||||
L10n.of(context)!.confirmPayment,
|
||||
l10n.confirmPayment,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 20,
|
||||
@@ -596,40 +592,22 @@ class _AmountScreenState extends State<AmountScreen> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.pop(context, false),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
L10n.of(context)!.cancel,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SecondaryButton(
|
||||
text: l10n.cancel,
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
height: 52,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimensions.paddingMedium),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
text: L10n.of(context)!.pay,
|
||||
text: l10n.pay,
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
height: 52,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -12,6 +12,8 @@ import '../../core/services/lnurl_service.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
import '../../widgets/common/secondary_button.dart';
|
||||
import '../../widgets/common/bottom_sheet_container.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../10_scanner/scan_screen.dart';
|
||||
import 'amount_screen.dart';
|
||||
@@ -858,29 +860,12 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.deepVoidPurple,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
final l10n = L10n.of(context)!;
|
||||
return BottomSheetContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const BottomSheetHandle(),
|
||||
|
||||
// Icono
|
||||
Container(
|
||||
@@ -900,7 +885,7 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
|
||||
// Título
|
||||
Text(
|
||||
L10n.of(context)!.confirmPayment,
|
||||
l10n.confirmPayment,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 20,
|
||||
@@ -945,40 +930,22 @@ class _ConfirmationModal extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onCancel,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
L10n.of(context)!.cancel,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SecondaryButton(
|
||||
text: l10n.cancel,
|
||||
onPressed: onCancel,
|
||||
height: 52,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimensions.paddingMedium),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
text: L10n.of(context)!.pay,
|
||||
text: l10n.pay,
|
||||
onPressed: onConfirm,
|
||||
height: 52,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
|
||||
/// Muestra el diálogo "Acerca de" de ElCaju
|
||||
void showElCajuAboutDialog(BuildContext context, String appVersion) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppColors.deepVoidPurple,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Logo
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Image.asset(
|
||||
'assets/img/elcajucubano.png',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'El Caju',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'v$appVersion',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
L10n.of(context)!.aboutTagline,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildAboutDescription(context),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
L10n.of(context)!.close,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAboutDescription(BuildContext context) {
|
||||
final description = L10n.of(context)!.aboutDescription;
|
||||
const keyword = 'LaChispa';
|
||||
final index = description.indexOf(keyword);
|
||||
if (index == -1) {
|
||||
return Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
final before = description.substring(0, index);
|
||||
final after = description.substring(index + keyword.length);
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: before),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: GestureDetector(
|
||||
onTap: () => _openLaChispa(context),
|
||||
child: Text(
|
||||
keyword,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.secondaryAction,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: AppColors.secondaryAction,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(text: after),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openLaChispa(BuildContext context) async {
|
||||
final url = Uri.parse('https://app.lachispa.me');
|
||||
try {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.couldNotOpenLink),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../widgets/common/bottom_sheet_container.dart';
|
||||
import '../../widgets/common/secondary_button.dart';
|
||||
|
||||
/// Modal para confirmar borrado de wallet
|
||||
class DeleteWalletModal extends StatefulWidget {
|
||||
final VoidCallback onConfirm;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const DeleteWalletModal({
|
||||
super.key,
|
||||
required this.onConfirm,
|
||||
required this.onCancel,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DeleteWalletModal> createState() => _DeleteWalletModalState();
|
||||
}
|
||||
|
||||
class _DeleteWalletModalState extends State<DeleteWalletModal> {
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
bool _canDelete = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context)!;
|
||||
final confirmWord = l10n.deleteConfirmWord;
|
||||
return BottomSheetContainer(
|
||||
respectKeyboard: true,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const BottomSheetHandle(),
|
||||
|
||||
// Icono advertencia
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.error.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.alertTriangle,
|
||||
color: AppColors.error,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
|
||||
// Título
|
||||
Text(
|
||||
l10n.deleteWalletQuestion,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
|
||||
// Advertencia
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.error.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.alertCircle,
|
||||
color: AppColors.error,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.actionIrreversible,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.deleteWalletWarning,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.error.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
|
||||
// Input de confirmación
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.typeDeleteToConfirm,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: confirmWord,
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_canDelete = value == confirmWord;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingLarge),
|
||||
|
||||
// Botones
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
text: l10n.cancel,
|
||||
onPressed: widget.onCancel,
|
||||
height: 52,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimensions.paddingMedium),
|
||||
Expanded(
|
||||
child: _DestructiveButton(
|
||||
text: l10n.deleteWallet,
|
||||
enabled: _canDelete,
|
||||
onPressed: widget.onConfirm,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Botón destructivo con semantics accesibles.
|
||||
/// Separado de los botones estándar porque la acción es irreversible.
|
||||
class _DestructiveButton extends StatelessWidget {
|
||||
final String text;
|
||||
final bool enabled;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _DestructiveButton({
|
||||
required this.text,
|
||||
required this.enabled,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: enabled,
|
||||
label: text,
|
||||
child: SizedBox(
|
||||
height: 52,
|
||||
child: Material(
|
||||
color: enabled
|
||||
? AppColors.error
|
||||
: AppColors.error.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: InkWell(
|
||||
onTap: enabled ? onPressed : null,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Center(
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: enabled
|
||||
? Colors.white
|
||||
: Colors.white.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -275,52 +275,53 @@ class _MintsScreenState extends State<MintsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Badges de balance compactos (estilo cashu.me)
|
||||
/// Badges de balance compactos (estilo cashu.me).
|
||||
/// Muestra todas las unidades soportadas por el mint: las de saldo > 0
|
||||
/// con estilo activo y las de saldo 0 atenuadas, para reflejar las
|
||||
/// capacidades del mint aunque aún no se hayan usado.
|
||||
Widget _buildBalanceBadges(List<String> units, Map<String, BigInt> balances) {
|
||||
final nonZeroBalances = balances.entries
|
||||
.where((e) => e.value > BigInt.zero)
|
||||
.toList();
|
||||
final entries = balances.isNotEmpty
|
||||
? balances.entries.toList()
|
||||
: units.map((u) => MapEntry(u, BigInt.zero)).toList();
|
||||
|
||||
if (nonZeroBalances.isEmpty) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'0 ${units.isNotEmpty ? units.first : "sat"}',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: Colors.white.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (entries.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
entries.sort((a, b) {
|
||||
final aZero = a.value == BigInt.zero;
|
||||
final bZero = b.value == BigInt.zero;
|
||||
if (aZero == bZero) return 0;
|
||||
return aZero ? 1 : -1;
|
||||
});
|
||||
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: nonZeroBalances.map((entry) {
|
||||
children: entries.map((entry) {
|
||||
final unit = entry.key;
|
||||
final balance = entry.value;
|
||||
final isZero = balance == BigInt.zero;
|
||||
final formatted = UnitFormatter.formatBalance(balance, unit);
|
||||
final label = UnitFormatter.getUnitLabel(unit);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryAction.withValues(alpha: 0.15),
|
||||
color: isZero
|
||||
? Colors.white.withValues(alpha: 0.08)
|
||||
: AppColors.primaryAction.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'$formatted $label',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primaryAction,
|
||||
fontWeight: isZero ? FontWeight.w400 : FontWeight.w500,
|
||||
color: isZero
|
||||
? Colors.white.withValues(alpha: 0.5)
|
||||
: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
|
||||
/// Diálogo para ingresar PIN de 4 dígitos
|
||||
class PinDialog extends StatefulWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
|
||||
const PinDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PinDialog> createState() => _PinDialogState();
|
||||
}
|
||||
|
||||
class _PinDialogState extends State<PinDialog> {
|
||||
final List<String> _pin = [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: AppColors.deepVoidPurple,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
title: Text(
|
||||
widget.title,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.subtitle,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Indicadores de PIN
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(4, (index) {
|
||||
final filled = index < _pin.length;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: filled
|
||||
? AppColors.primaryAction
|
||||
: Colors.white.withValues(alpha: 0.2),
|
||||
border: Border.all(
|
||||
color: filled
|
||||
? AppColors.primaryAction
|
||||
: Colors.white.withValues(alpha: 0.3),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Teclado numérico
|
||||
_buildNumPad(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNumPad() {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: ['1', '2', '3'].map((n) => _buildNumKey(n)).toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: ['4', '5', '6'].map((n) => _buildNumKey(n)).toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: ['7', '8', '9'].map((n) => _buildNumKey(n)).toList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Cancelar
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.x,
|
||||
color: AppColors.textSecondary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildNumKey('0'),
|
||||
// Borrar
|
||||
GestureDetector(
|
||||
onTap: _deleteDigit,
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.delete,
|
||||
color: AppColors.textSecondary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNumKey(String number) {
|
||||
return GestureDetector(
|
||||
onTap: () => _addDigit(number),
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
number,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addDigit(String digit) {
|
||||
if (_pin.length < 4) {
|
||||
setState(() {
|
||||
_pin.add(digit);
|
||||
});
|
||||
|
||||
if (_pin.length == 4) {
|
||||
// PIN completo, cerrar con resultado
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
if (mounted) {
|
||||
Navigator.pop(context, _pin.join());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteDigit() {
|
||||
if (_pin.isNotEmpty) {
|
||||
setState(() {
|
||||
_pin.removeLast();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
|
||||
class PrivacyScreen extends StatelessWidget {
|
||||
const PrivacyScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = L10n.of(context)!;
|
||||
final conclusionLines = l10n.privacyConclusion
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
return GradientBackground(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
l10n.privacyPolicy,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
l10n.privacyTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.privacyGoodbye,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
l10n.privacyKeepReading,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 13,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
...l10n.privacyBody.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.map(
|
||||
(line) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Text(
|
||||
line,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.85),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
...conclusionLines.asMap().entries.map((entry) {
|
||||
final isLast = entry.key == conclusionLines.length - 1;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: isLast ? 0 : 8),
|
||||
child: Text(
|
||||
entry.value,
|
||||
style: isLast
|
||||
? const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
)
|
||||
: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../widgets/common/bottom_sheet_container.dart';
|
||||
|
||||
/// Modal para recuperar tokens usando NUT-13
|
||||
class RecoverTokensModal extends StatefulWidget {
|
||||
const RecoverTokensModal({super.key});
|
||||
|
||||
@override
|
||||
State<RecoverTokensModal> createState() => _RecoverTokensModalState();
|
||||
}
|
||||
|
||||
class _RecoverTokensModalState extends State<RecoverTokensModal> {
|
||||
final TextEditingController _mnemonicController = TextEditingController();
|
||||
bool _useCurrentMnemonic = true;
|
||||
bool _scanAllMints = true;
|
||||
String? _selectedMintUrl;
|
||||
List<String> _availableMints = [];
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingMints = true;
|
||||
String? _result;
|
||||
bool _isSuccess = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _loadMints());
|
||||
}
|
||||
|
||||
void _loadMints() {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
setState(() {
|
||||
_availableMints = walletProvider.mintUrls;
|
||||
_isLoadingMints = false;
|
||||
if (_availableMints.isNotEmpty) {
|
||||
_selectedMintUrl = _availableMints.first;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mnemonicController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BottomSheetContainer(
|
||||
respectKeyboard: true,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const BottomSheetHandle(),
|
||||
|
||||
// Icono
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryAction.withValues(alpha: 0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.refreshCw,
|
||||
color: AppColors.primaryAction,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
|
||||
// Título
|
||||
Text(
|
||||
L10n.of(context)!.recoverTokensTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
|
||||
// Descripción
|
||||
Text(
|
||||
L10n.of(context)!.recoverTokensDescription,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingLarge),
|
||||
|
||||
// Opciones de mnemonic
|
||||
_buildMnemonicOptions(),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
|
||||
// Selector de mint (solo si usa mnemonic actual)
|
||||
if (_useCurrentMnemonic) _buildMintSelector(),
|
||||
|
||||
// Input para mnemonic personalizado
|
||||
if (!_useCurrentMnemonic) _buildMnemonicInput(),
|
||||
|
||||
// Resultado
|
||||
if (_result != null) _buildResult(),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
|
||||
// Botón de acción
|
||||
_buildActionButton(),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMnemonicOptions() {
|
||||
final l10n = L10n.of(context)!;
|
||||
return Column(
|
||||
children: [
|
||||
// Opción: Usar mnemonic actual
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _useCurrentMnemonic = true),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _useCurrentMnemonic
|
||||
? AppColors.primaryAction.withValues(alpha: 0.2)
|
||||
: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _useCurrentMnemonic
|
||||
? AppColors.primaryAction.withValues(alpha: 0.5)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_useCurrentMnemonic
|
||||
? LucideIcons.checkCircle
|
||||
: LucideIcons.circle,
|
||||
color: _useCurrentMnemonic
|
||||
? AppColors.primaryAction
|
||||
: AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.useCurrentSeedPhrase,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
l10n.scanWithSavedWords,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Opción: Usar otro mnemonic
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _useCurrentMnemonic = false),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: !_useCurrentMnemonic
|
||||
? AppColors.primaryAction.withValues(alpha: 0.2)
|
||||
: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: !_useCurrentMnemonic
|
||||
? AppColors.primaryAction.withValues(alpha: 0.5)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
!_useCurrentMnemonic
|
||||
? LucideIcons.checkCircle
|
||||
: LucideIcons.circle,
|
||||
color: !_useCurrentMnemonic
|
||||
? AppColors.primaryAction
|
||||
: AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.useOtherSeedPhrase,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
l10n.recoverFromOtherWords,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMintSelector() {
|
||||
final l10n = L10n.of(context)!;
|
||||
if (_isLoadingMints) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColors.primaryAction,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.mintsToScan,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Opción: Escanear todos
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _scanAllMints = true),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: _scanAllMints
|
||||
? AppColors.primaryAction.withValues(alpha: 0.2)
|
||||
: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: _scanAllMints
|
||||
? AppColors.primaryAction.withValues(alpha: 0.5)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_scanAllMints ? LucideIcons.checkCircle : LucideIcons.circle,
|
||||
color: _scanAllMints
|
||||
? AppColors.primaryAction
|
||||
: AppColors.textSecondary,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
l10n.allMints(_availableMints.length),
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
fontWeight: _scanAllMints ? FontWeight.w500 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Opción: Mint específico
|
||||
GestureDetector(
|
||||
onTap: () => setState(() => _scanAllMints = false),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: !_scanAllMints
|
||||
? AppColors.primaryAction.withValues(alpha: 0.2)
|
||||
: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: !_scanAllMints
|
||||
? AppColors.primaryAction.withValues(alpha: 0.5)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
!_scanAllMints ? LucideIcons.checkCircle : LucideIcons.circle,
|
||||
color: !_scanAllMints
|
||||
? AppColors.primaryAction
|
||||
: AppColors.textSecondary,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
l10n.specificMint,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Dropdown de mints (solo si selecciona específico)
|
||||
if (!_scanAllMints && _availableMints.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
child: DropdownButton<String>(
|
||||
value: _selectedMintUrl,
|
||||
isExpanded: true,
|
||||
dropdownColor: AppColors.deepVoidPurple,
|
||||
underline: const SizedBox(),
|
||||
icon: Icon(
|
||||
LucideIcons.chevronDown,
|
||||
color: AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
items: _availableMints.map((url) {
|
||||
final host = Uri.parse(url).host;
|
||||
return DropdownMenuItem(
|
||||
value: url,
|
||||
child: Text(
|
||||
host,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() => _selectedMintUrl = value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMnemonicInput() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: AppDimensions.paddingMedium),
|
||||
child: TextField(
|
||||
controller: _mnemonicController,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
textCapitalization: TextCapitalization.none,
|
||||
maxLines: 3,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!.enterMnemonicWords,
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white.withValues(alpha: 0.05),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResult() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: AppDimensions.paddingMedium),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: _isSuccess
|
||||
? AppColors.success.withValues(alpha: 0.1)
|
||||
: AppColors.error.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: _isSuccess
|
||||
? AppColors.success.withValues(alpha: 0.3)
|
||||
: AppColors.error.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_isSuccess ? LucideIcons.checkCircle : LucideIcons.alertCircle,
|
||||
color: _isSuccess ? AppColors.success : AppColors.error,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_result!,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: _isSuccess ? AppColors.success : AppColors.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: GestureDetector(
|
||||
onTap: _isLoading ? null : _startRecover,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: _isLoading
|
||||
? null
|
||||
: const LinearGradient(
|
||||
colors: [AppColors.primaryAction, Color(0xFFFF9100)],
|
||||
),
|
||||
color: _isLoading ? Colors.grey : null,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Center(
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
L10n.of(context)!.scanMints,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startRecover() async {
|
||||
if (!mounted) return;
|
||||
final l10n = L10n.of(context)!;
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_result = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
|
||||
if (_useCurrentMnemonic) {
|
||||
// Usar mnemonic actual
|
||||
if (_scanAllMints) {
|
||||
// Escanear todos los mints (retorna Map<String, Map<String, BigInt>>)
|
||||
final results = await walletProvider.restoreAllMints();
|
||||
|
||||
int mintsRecovered = 0;
|
||||
int mintsWithError = 0;
|
||||
final recoveredDetails = <String>[];
|
||||
|
||||
for (final mintEntry in results.entries) {
|
||||
final unitBalances = mintEntry.value;
|
||||
bool hasError = false;
|
||||
bool hasRecovered = false;
|
||||
for (final unitEntry in unitBalances.entries) {
|
||||
final unit = unitEntry.key;
|
||||
final balance = unitEntry.value;
|
||||
if (balance < BigInt.zero) {
|
||||
hasError = true;
|
||||
} else if (balance > BigInt.zero) {
|
||||
final formatted = UnitFormatter.formatBalance(balance, unit);
|
||||
final label = UnitFormatter.getUnitLabel(unit);
|
||||
recoveredDetails.add('$formatted $label');
|
||||
hasRecovered = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) mintsWithError++;
|
||||
if (hasRecovered) mintsRecovered++;
|
||||
}
|
||||
|
||||
// Recuperar proofs pendientes (sends no reclamados, melts fallidos)
|
||||
final reclaimMap = await walletProvider.reclaimPendingProofs();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Agregar cada unidad reclamada por separado
|
||||
if (reclaimMap.isNotEmpty) {
|
||||
for (final entry in reclaimMap.entries) {
|
||||
final unit = entry.key;
|
||||
final reclaim = entry.value;
|
||||
final formatted = UnitFormatter.formatBalance(reclaim.amount, unit);
|
||||
final label = UnitFormatter.getUnitLabel(unit);
|
||||
recoveredDetails.add('$formatted $label (${reclaim.count} proofs)');
|
||||
}
|
||||
mintsRecovered++;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
if (recoveredDetails.isNotEmpty) {
|
||||
_result = l10n.recoveredTokens(recoveredDetails.join(", "), mintsRecovered);
|
||||
} else {
|
||||
_result = l10n.scanCompleteNoTokens;
|
||||
}
|
||||
if (mintsWithError > 0) {
|
||||
_result = '$_result ${l10n.mintsWithError(mintsWithError)}';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Escanear mint específico (retorna Map<String, BigInt>)
|
||||
if (_selectedMintUrl == null) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSuccess = false;
|
||||
_result = l10n.selectMintToScan;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final unitBalances = await walletProvider.restoreFromMint(_selectedMintUrl!);
|
||||
final mintHost = UnitFormatter.getMintDisplayName(_selectedMintUrl!);
|
||||
|
||||
final recoveredDetails = <String>[];
|
||||
for (final entry in unitBalances.entries) {
|
||||
final unit = entry.key;
|
||||
final balance = entry.value;
|
||||
if (balance > BigInt.zero) {
|
||||
final formatted = UnitFormatter.formatBalance(balance, unit);
|
||||
final label = UnitFormatter.getUnitLabel(unit);
|
||||
recoveredDetails.add('$formatted $label');
|
||||
}
|
||||
}
|
||||
|
||||
// Recuperar proofs pendientes de este mint
|
||||
final reclaimMap = await walletProvider.reclaimPendingProofs(mintUrl: _selectedMintUrl);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Agregar cada unidad reclamada por separado
|
||||
for (final entry in reclaimMap.entries) {
|
||||
final unit = entry.key;
|
||||
final reclaim = entry.value;
|
||||
final formatted = UnitFormatter.formatBalance(reclaim.amount, unit);
|
||||
final label = UnitFormatter.getUnitLabel(unit);
|
||||
recoveredDetails.add('$formatted $label (${reclaim.count} proofs)');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
if (recoveredDetails.isNotEmpty) {
|
||||
_result = l10n.recoveredFromMint(recoveredDetails.join(", "), mintHost);
|
||||
} else {
|
||||
_result = l10n.noTokensFoundInMint(mintHost);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Usar otro mnemonic
|
||||
final mnemonic = _mnemonicController.text.trim().toLowerCase();
|
||||
final words = mnemonic.split(RegExp(r'\s+'));
|
||||
|
||||
if (words.length != 12 && words.length != 24) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSuccess = false;
|
||||
_result = l10n.mnemonicMustHaveWords;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener lista de mints actuales para escanear
|
||||
final mintUrls = walletProvider.mintUrls;
|
||||
|
||||
if (mintUrls.isEmpty) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSuccess = false;
|
||||
_result = l10n.noConnectedMintsToScan;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final recovered = await walletProvider.restoreWithMnemonic(
|
||||
mnemonic,
|
||||
mintUrls,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSuccess = true;
|
||||
if (recovered > BigInt.zero) {
|
||||
// Usamos la unidad activa como aproximación para el formato
|
||||
final activeUnit = walletProvider.activeUnit;
|
||||
final formatted = UnitFormatter.formatBalance(recovered, activeUnit);
|
||||
final label = UnitFormatter.getUnitLabel(activeUnit);
|
||||
_result = l10n.recoveredAndTransferred(formatted, label);
|
||||
} else {
|
||||
_result = l10n.noTokensForMnemonic;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSuccess = false;
|
||||
_result = '${l10n.error}: $e';
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,9 +10,29 @@ 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>>
|
||||
abstract class PreparedMelt implements RustOpaqueInterface {
|
||||
BigInt get amount;
|
||||
|
||||
BigInt get feeReserve;
|
||||
|
||||
BigInt get inputFee;
|
||||
|
||||
BigInt get swapFee;
|
||||
|
||||
set amount(BigInt amount);
|
||||
|
||||
set feeReserve(BigInt feeReserve);
|
||||
|
||||
set inputFee(BigInt inputFee);
|
||||
|
||||
set swapFee(BigInt swapFee);
|
||||
}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PreparedSend>>
|
||||
abstract class PreparedSend implements RustOpaqueInterface {
|
||||
BigInt get amount;
|
||||
@@ -44,12 +64,34 @@ abstract class Wallet implements RustOpaqueInterface {
|
||||
|
||||
Future<BigInt> balance();
|
||||
|
||||
Future<void> cancelMelt({required PreparedMelt melt});
|
||||
|
||||
Future<void> cancelSend({required PreparedSend send});
|
||||
|
||||
Future<void> checkAllMintQuotes();
|
||||
|
||||
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.
|
||||
///
|
||||
/// Builds a PaymentRequest with the wallet's mint URL and unit,
|
||||
@@ -67,8 +109,6 @@ abstract class Wallet implements RustOpaqueInterface {
|
||||
|
||||
Future<List<Transaction>> listTransactions({TransactionDirection? direction});
|
||||
|
||||
Future<BigInt> melt({required MeltQuote quote});
|
||||
|
||||
Future<MeltQuote> meltQuote({required String request});
|
||||
|
||||
Stream<MintQuote> mint({required BigInt amount, String? description});
|
||||
@@ -97,16 +137,35 @@ abstract class Wallet implements RustOpaqueInterface {
|
||||
BigInt? customAmount,
|
||||
});
|
||||
|
||||
Future<PreparedMelt> prepareMelt({required MeltQuote quote});
|
||||
|
||||
Future<PreparedSend> prepareSend({required BigInt amount, SendOptions? opts});
|
||||
|
||||
Future<BigInt> receive({required Token token, ReceiveOptions? opts});
|
||||
|
||||
/// Check pending-spent proofs with the mint and revert unspent ones.
|
||||
/// Returns the number of proofs recovered.
|
||||
Future<BigInt> reclaimPendingProofs();
|
||||
/// Returns per-state buckets (unspent reverted, still pending, spent).
|
||||
Future<ReclaimResult> reclaimPendingProofs();
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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();
|
||||
|
||||
/// Recalcula el balance desde la DB y lo empuja al stream.
|
||||
/// Llamar después de manipulaciones directas al DB (ej: offline send
|
||||
/// marking proofs as PendingSpent fuera de la API de CDK).
|
||||
Future<void> refreshBalance();
|
||||
|
||||
Future<void> restore();
|
||||
|
||||
Future<SendResult> send({
|
||||
@@ -182,7 +241,7 @@ class MintQuote {
|
||||
final Token? token;
|
||||
final String? error;
|
||||
|
||||
/// Deterministic transaction ID (set when state == Issued)
|
||||
/// Deterministic transaction ID (set when proofs are available, typically on Issued)
|
||||
final String? transactionId;
|
||||
|
||||
const MintQuote({
|
||||
@@ -245,6 +304,49 @@ class ReceiveOptions {
|
||||
preimages == other.preimages;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/// 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 ^
|
||||
pendingCount.hashCode ^
|
||||
spentCount.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ReclaimResult &&
|
||||
runtimeType == other.runtimeType &&
|
||||
count == other.count &&
|
||||
amount == other.amount &&
|
||||
pendingCount == other.pendingCount &&
|
||||
spentCount == other.spentCount;
|
||||
}
|
||||
|
||||
class SendOptions {
|
||||
final String? pubkey;
|
||||
final bool? includeFee;
|
||||
|
||||
+919
-105
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
get rust_arc_decrement_strong_count_NostrListenerHandlePtr => wire
|
||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr;
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_PreparedMeltPtr => wire
|
||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr;
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_PreparedSendPtr => wire
|
||||
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSendPtr;
|
||||
@@ -62,6 +66,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -92,6 +102,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -122,6 +138,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -161,6 +183,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -372,6 +400,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
ReceiveOptions dco_decode_receive_options(dynamic raw);
|
||||
|
||||
@protected
|
||||
ReclaimResult dco_decode_reclaim_result(dynamic raw);
|
||||
|
||||
@protected
|
||||
(String, String) dco_decode_record_string_string(dynamic raw);
|
||||
|
||||
@@ -426,6 +457,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -456,6 +493,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -486,6 +529,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -527,6 +576,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -776,6 +831,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
ReceiveOptions sse_decode_receive_options(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ReclaimResult sse_decode_reclaim_result(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
(String, String) sse_decode_record_string_string(
|
||||
SseDeserializer deserializer,
|
||||
@@ -839,6 +897,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
PreparedMelt self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -874,6 +939,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
PreparedMelt self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -909,6 +981,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
PreparedMelt self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -957,6 +1036,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
PreparedMelt self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -1270,6 +1356,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_reclaim_result(ReclaimResult self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_record_string_string(
|
||||
(String, String) self,
|
||||
@@ -1404,6 +1493,40 @@ class RustLibWire implements BaseWire {
|
||||
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'frbgen_elcaju_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt',
|
||||
);
|
||||
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt =
|
||||
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
) {
|
||||
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
ptr,
|
||||
);
|
||||
}
|
||||
|
||||
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr =
|
||||
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
|
||||
'frbgen_elcaju_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt',
|
||||
);
|
||||
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt =
|
||||
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr
|
||||
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
ffi.Pointer<ffi.Void> ptr,
|
||||
|
||||
@@ -33,6 +33,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
get rust_arc_decrement_strong_count_NostrListenerHandlePtr => wire
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle;
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_PreparedMeltPtr => wire
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt;
|
||||
|
||||
CrossPlatformFinalizerArg
|
||||
get rust_arc_decrement_strong_count_PreparedSendPtr => wire
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend;
|
||||
@@ -64,6 +68,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -94,6 +104,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -124,6 +140,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -163,6 +185,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
dynamic raw,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -374,6 +402,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
ReceiveOptions dco_decode_receive_options(dynamic raw);
|
||||
|
||||
@protected
|
||||
ReclaimResult dco_decode_reclaim_result(dynamic raw);
|
||||
|
||||
@protected
|
||||
(String, String) dco_decode_record_string_string(dynamic raw);
|
||||
|
||||
@@ -428,6 +459,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -458,6 +495,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -488,6 +531,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -529,6 +578,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedMelt
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
PreparedSend
|
||||
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -778,6 +833,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
ReceiveOptions sse_decode_receive_options(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
ReclaimResult sse_decode_reclaim_result(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
(String, String) sse_decode_record_string_string(
|
||||
SseDeserializer deserializer,
|
||||
@@ -841,6 +899,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
PreparedMelt self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -876,6 +941,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
PreparedMelt self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -911,6 +983,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
PreparedMelt self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -959,6 +1038,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
PreparedMelt self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void
|
||||
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
@@ -1272,6 +1358,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_reclaim_result(ReclaimResult self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_record_string_string(
|
||||
(String, String) self,
|
||||
@@ -1361,6 +1450,22 @@ class RustLibWire implements BaseWire {
|
||||
ptr,
|
||||
);
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
int ptr,
|
||||
) => wasmModule
|
||||
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
ptr,
|
||||
);
|
||||
|
||||
void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
int ptr,
|
||||
) => wasmModule
|
||||
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
ptr,
|
||||
);
|
||||
|
||||
void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
int ptr,
|
||||
@@ -1452,6 +1557,16 @@ extension type RustLibWasmModule._(JSObject _) implements JSObject {
|
||||
int ptr,
|
||||
);
|
||||
|
||||
external void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
int ptr,
|
||||
);
|
||||
|
||||
external void
|
||||
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
|
||||
int ptr,
|
||||
);
|
||||
|
||||
external void
|
||||
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
|
||||
int ptr,
|
||||
|
||||
@@ -58,6 +58,9 @@ class AnimatedActionButton extends StatefulWidget {
|
||||
/// Si mostrar el icono (default: false)
|
||||
final bool showIcon;
|
||||
|
||||
/// Si el icono va después del texto (default: false = antes del texto)
|
||||
final bool iconTrailing;
|
||||
|
||||
/// Ancho del botón (default: expandir al padre)
|
||||
final double? width;
|
||||
|
||||
@@ -73,6 +76,7 @@ class AnimatedActionButton extends StatefulWidget {
|
||||
this.backgroundColor,
|
||||
this.icon,
|
||||
this.showIcon = false,
|
||||
this.iconTrailing = false,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
@@ -300,8 +304,8 @@ class _AnimatedActionButtonState extends State<AnimatedActionButton>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Icono opcional
|
||||
if (widget.showIcon && widget.icon != null) ...[
|
||||
// Icono leading (antes del texto)
|
||||
if (widget.showIcon && widget.icon != null && !widget.iconTrailing) ...[
|
||||
Icon(
|
||||
widget.icon,
|
||||
color: Colors.white,
|
||||
@@ -319,6 +323,15 @@ class _AnimatedActionButtonState extends State<AnimatedActionButton>
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
// Icono trailing (después del texto)
|
||||
if (widget.showIcon && widget.icon != null && widget.iconTrailing) ...[
|
||||
const SizedBox(width: AppDimensions.paddingSmall),
|
||||
Icon(
|
||||
widget.icon,
|
||||
color: Colors.white,
|
||||
size: widget.type == ButtonType.navigation ? 28 : 20,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
|
||||
/// Frame común para modales tipo bottom sheet.
|
||||
///
|
||||
/// Aplica SafeArea(top: false) para que los botones internos no queden
|
||||
/// tapados por la barra de navegación del sistema (gestos o 3 botones).
|
||||
///
|
||||
/// Usá [respectKeyboard]: true cuando el modal contenga TextField, para
|
||||
/// combinar el inset de la barra con el del teclado (viewInsets.bottom).
|
||||
class BottomSheetContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final bool respectKeyboard;
|
||||
|
||||
const BottomSheetContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding = const EdgeInsets.all(AppDimensions.paddingMedium),
|
||||
this.respectKeyboard = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mq = MediaQuery.of(context);
|
||||
// viewPadding.bottom es el inset crudo de la nav bar del sistema
|
||||
// (no se consume aunque un SafeArea padre ya lo haya absorbido).
|
||||
final navBarInset = mq.viewPadding.bottom;
|
||||
|
||||
// Combinar el padding base con el extra del navBar abajo.
|
||||
final resolvedPadding = padding.resolve(Directionality.of(context));
|
||||
final effectivePadding = EdgeInsets.only(
|
||||
left: resolvedPadding.left,
|
||||
top: resolvedPadding.top,
|
||||
right: resolvedPadding.right,
|
||||
bottom: resolvedPadding.bottom + navBarInset,
|
||||
);
|
||||
|
||||
final frame = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.deepVoidPurple,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppDimensions.radiusXLarge),
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Padding(padding: effectivePadding, child: child),
|
||||
);
|
||||
|
||||
if (respectKeyboard) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: mq.viewInsets.bottom),
|
||||
child: frame,
|
||||
);
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle visual en la parte superior del modal (la barrita).
|
||||
class BottomSheetHandle extends StatelessWidget {
|
||||
const BottomSheetHandle({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
|
||||
set(BINARY_NAME "elcaju")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.elcaju")
|
||||
set(APPLICATION_ID "me.elcaju")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/elcaju.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/elcaju";
|
||||
@@ -399,7 +399,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/elcaju.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/elcaju";
|
||||
@@ -413,7 +413,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/elcaju.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/elcaju";
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
PRODUCT_NAME = elcaju
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved.
|
||||
PRODUCT_COPYRIGHT = Copyright © 2026 ElCaju. All rights reserved.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: elcaju
|
||||
description: "ElCaju - Tu wallet de ecash privado"
|
||||
publish_to: 'none'
|
||||
version: 0.1.0+2
|
||||
version: 0.2.0+3
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0
|
||||
|
||||
Generated
+39
-15
@@ -367,6 +367,17 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-payment-instructions"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c300c948b2ff78c965ea3a613372352125448a22f1acf49e95e3878149824091"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"lightning",
|
||||
"lightning-invoice",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-private"
|
||||
version = "0.1.0"
|
||||
@@ -489,9 +500,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cashu"
|
||||
version = "0.15.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d612261d61069314bb0e39adbd63d0fd3bd22084f332e85c98e615c15f24e5a"
|
||||
checksum = "26b4ad04070fb67914feb2641e7b54001000f1f7f67a7c39a08f32edf01fd3ae"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"cbor-diag",
|
||||
@@ -507,6 +518,7 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"unicode-normalization",
|
||||
"url",
|
||||
"uuid",
|
||||
"web-time",
|
||||
@@ -555,14 +567,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cdk"
|
||||
version = "0.15.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8955437beea95ee6849c6eab59012760a7589e6eeff4d6e36ebf324c641388d"
|
||||
checksum = "c6536eb1df7a497cb334edf6bb9eea1fa8e4a9b08c79fe45c40d43413ecb88c8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
"bitcoin",
|
||||
"bitcoin-payment-instructions",
|
||||
"cbor-diag",
|
||||
"cdk-common",
|
||||
"cdk-signatory",
|
||||
@@ -582,7 +595,6 @@ dependencies = [
|
||||
"serde_with",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -593,9 +605,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cdk-common"
|
||||
version = "0.15.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f5bf771d3780d2321211eb575e1f8341cec50f8a16c4f50b2787807b5090a38"
|
||||
checksum = "a371d218ffe83dcabd202278251af53744b8fa3e846124ddcd34247c4e38bdc3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -606,6 +618,7 @@ dependencies = [
|
||||
"ciborium",
|
||||
"futures",
|
||||
"getrandom 0.2.17",
|
||||
"jsonwebtoken",
|
||||
"lightning",
|
||||
"lightning-invoice",
|
||||
"parking_lot",
|
||||
@@ -626,23 +639,31 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cdk-http-client"
|
||||
version = "0.15.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b58f47c4af4a4c862d145863211f5454c3ccfbacaaef9e4913276519a1ac498e"
|
||||
checksum = "bf194792d45975b360911417d3713e4d3f0142e18b082e2a8c2c88a91630b43d"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"futures-channel",
|
||||
"js-sys",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cdk-signatory"
|
||||
version = "0.15.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc1dca3b84999ac0e6fd15330d566aff6df6082a349134fd884deef8c9eeb570"
|
||||
checksum = "4f40745c9402049bbba47ae52e1fc7552d4af8e99572fd73697448f51bbb3a02"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -662,9 +683,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cdk-sql-common"
|
||||
version = "0.15.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6625a8248a43fbebb92cc57f1b09c453d5de94d62a512b62e9a62b25d0dd5506"
|
||||
checksum = "36e920cffaa36f396d16727649b05d6dc8d42aecc4108fa955876515149bdb0e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bitcoin",
|
||||
@@ -681,9 +702,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cdk-sqlite"
|
||||
version = "0.15.1"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9334877fe3f9fb25f6819c0876af1b0690f47aa2b1e548a20ac904740cbd574c"
|
||||
checksum = "c706b86b76d89bbbfad7e106328c14f1d99440ee5829840510e50922f48fdc51"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bitcoin",
|
||||
@@ -1054,6 +1075,9 @@ name = "dnssec-prover"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec4f825369fc7134da70ca4040fddc8e03b80a46d249ae38d9c1c39b7b4476bf"
|
||||
dependencies = [
|
||||
"bitcoin_hashes 0.14.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
|
||||
+4
-4
@@ -8,9 +8,9 @@ crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
# Cashu — acceso directo, sin intermediarios
|
||||
cdk = { version = "0.15.1", default-features = false, features = ["wallet", "nostr"] }
|
||||
cdk-common = { version = "0.15.1", default-features = false, features = ["nostr"] }
|
||||
cdk-sqlite = { version = "0.15.1", default-features = false, features = ["wallet"] }
|
||||
cdk = { version = "0.16.0", default-features = false, features = ["wallet", "nostr"] }
|
||||
cdk-common = { version = "0.16.0", default-features = false, features = ["nostr"] }
|
||||
cdk-sqlite = { version = "0.16.0", default-features = false, features = ["wallet"] }
|
||||
|
||||
# Bridge Flutter <-> Rust
|
||||
flutter_rust_bridge = { version = "=2.11.1", default-features = false, features = [
|
||||
@@ -31,7 +31,7 @@ bip39 = { version = "2.1", default-features = false, features = ["std"] }
|
||||
# Entropy
|
||||
getrandom = { version = "0.3", default-features = false, features = ["std"] }
|
||||
|
||||
# Nostr keys (same version as CDK 0.15.1 — CDK does not re-export Keys)
|
||||
# Nostr keys (same version as CDK 0.16.0 — CDK does not re-export Keys)
|
||||
nostr-sdk = { version = "0.44.1", default-features = false, features = ["nip04", "nip44", "nip59"] }
|
||||
|
||||
# Async runtime
|
||||
|
||||
@@ -69,16 +69,7 @@ fn parse_payment_request_inner(encoded: String) -> Result<PaymentRequestInfo, Er
|
||||
let pr = CdkPaymentRequest::from_str(&creq_str)
|
||||
.map_err(|e| Error::Cdk(format!("Invalid payment request: {e}")))?;
|
||||
|
||||
let mints: Vec<String> = match &pr.mints {
|
||||
Some(list) => {
|
||||
let mut v = Vec::new();
|
||||
for m in list {
|
||||
v.push(format!("{}", m));
|
||||
}
|
||||
v
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
let mints: Vec<String> = pr.mints.iter().map(|m| m.to_string()).collect();
|
||||
let mut transports = Vec::new();
|
||||
for t in &pr.transports {
|
||||
transports.push(TransportInfo {
|
||||
@@ -320,7 +311,7 @@ impl Wallet {
|
||||
let nostr_transport = Transport {
|
||||
_type: TransportType::Nostr,
|
||||
target: nprofile_bech32,
|
||||
tags: Some(vec![vec!["n".to_string(), "17".to_string()]]),
|
||||
tags: vec![vec!["n".to_string(), "17".to_string()]],
|
||||
};
|
||||
|
||||
// Build the PaymentRequest with this wallet's mint and unit
|
||||
@@ -336,7 +327,7 @@ impl Wallet {
|
||||
amount: params.amount.map(Amount::from),
|
||||
unit: Some(unit.clone()),
|
||||
single_use: Some(true),
|
||||
mints: Some(vec![mint_url.clone()]),
|
||||
mints: vec![mint_url.clone()],
|
||||
description: params.description,
|
||||
transports: vec![nostr_transport],
|
||||
nut10: None,
|
||||
|
||||
+296
-23
@@ -155,7 +155,7 @@ impl Wallet {
|
||||
// Compute the deterministic transaction ID from the token's proofs
|
||||
// (SHA-256 of sorted Y values — same as CDK uses internally)
|
||||
// Best-effort: send is already committed, don't fail on ID computation
|
||||
let tx_id = match self.inner.get_mint_keysets().await {
|
||||
let tx_id = match self.inner.get_mint_keysets(cdk::wallet::KeysetFilter::Active).await {
|
||||
Ok(keysets) => match cdk_token.proofs(&keysets) {
|
||||
Ok(proofs) => TransactionId::try_from(proofs)
|
||||
.map(|id| id.to_string())
|
||||
@@ -421,15 +421,52 @@ impl Wallet {
|
||||
.into())
|
||||
}
|
||||
|
||||
pub async fn melt(&self, quote: MeltQuote) -> Result<u64, Error> {
|
||||
let melted = self
|
||||
pub async fn prepare_melt(&self, quote: MeltQuote) -> Result<PreparedMelt, Error> {
|
||||
let prepared = self
|
||||
.inner
|
||||
.prepare_melt("e.id, HashMap::new())
|
||||
.await?
|
||||
.confirm()
|
||||
.await?;
|
||||
Ok(PreparedMelt {
|
||||
amount: prepared.amount().into(),
|
||||
fee_reserve: prepared.quote().fee_reserve.into(),
|
||||
swap_fee: prepared.swap_fee().into(),
|
||||
input_fee: prepared.input_fee().into(),
|
||||
operation_id: prepared.operation_id(),
|
||||
cdk_quote: prepared.quote().clone(),
|
||||
proofs: prepared.proofs().clone(),
|
||||
proofs_to_swap: prepared.proofs_to_swap().clone(),
|
||||
cdk_input_fee: prepared.input_fee(),
|
||||
cdk_input_fee_without_swap: prepared.input_fee_without_swap(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn confirm_melt(&self, melt: PreparedMelt) -> Result<u64, Error> {
|
||||
let finalized = self
|
||||
.inner
|
||||
.confirm_prepared_melt(
|
||||
melt.operation_id,
|
||||
melt.cdk_quote,
|
||||
melt.proofs,
|
||||
melt.proofs_to_swap,
|
||||
melt.cdk_input_fee,
|
||||
melt.cdk_input_fee_without_swap,
|
||||
HashMap::new(),
|
||||
)
|
||||
.await?;
|
||||
self.update_balance_streams().await;
|
||||
Ok(melted.total_amount().into())
|
||||
Ok(finalized.total_amount().into())
|
||||
}
|
||||
|
||||
pub async fn cancel_melt(&self, melt: PreparedMelt) -> Result<(), Error> {
|
||||
self.inner
|
||||
.cancel_prepared_melt(
|
||||
melt.operation_id,
|
||||
melt.proofs,
|
||||
melt.proofs_to_swap,
|
||||
)
|
||||
.await?;
|
||||
self.update_balance_streams().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// === Transactions ===
|
||||
@@ -494,41 +531,173 @@ impl Wallet {
|
||||
// === Reclaim orphaned proofs ===
|
||||
|
||||
/// Check pending-spent proofs with the mint and revert unspent ones.
|
||||
/// Returns the number of proofs recovered.
|
||||
pub async fn reclaim_pending_proofs(&self) -> Result<u64, Error> {
|
||||
/// 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(0);
|
||||
return Ok(ReclaimResult::empty());
|
||||
}
|
||||
|
||||
// Build a map of Y → amount for later lookup
|
||||
let mut amount_by_y: HashMap<PublicKey, u64> = HashMap::new();
|
||||
for proof in &pending {
|
||||
if let Ok(y) = proof.y() {
|
||||
amount_by_y.insert(y, u64::from(proof.amount));
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
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(count)
|
||||
Ok(buckets.into_result())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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::empty());
|
||||
}
|
||||
|
||||
let total = target_ys.len() as u64;
|
||||
|
||||
let pending = self.inner.get_pending_spent_proofs().await?;
|
||||
// Filter to only proofs with a Y in our target set.
|
||||
let relevant: Vec<_> = pending
|
||||
.into_iter()
|
||||
.filter(|proof| proof.y().map(|y| target_ys.contains(&y)).unwrap_or(false))
|
||||
.collect();
|
||||
|
||||
// Target Ys that aren't in local PendingSpent anymore are treated as
|
||||
// spent — a previous `check_proofs_spent` call removed them from the
|
||||
// local DB because the mint confirmed SPENT. This matches
|
||||
// `check_proofs_by_ys` and keeps `ReclaimResult`'s bucket invariant
|
||||
// (`unspent + pending + spent == total`) across both paths.
|
||||
let resolved_out_of_pending = total.saturating_sub(relevant.len() as u64);
|
||||
|
||||
if relevant.is_empty() {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
let states = self.inner.check_proofs_spent(relevant).await?;
|
||||
let buckets = StateBuckets::from_states(states, &amount_by_y);
|
||||
|
||||
if !buckets.unspent_ys.is_empty() {
|
||||
self.inner.unreserve_proofs(buckets.unspent_ys.clone()).await?;
|
||||
}
|
||||
|
||||
self.update_balance_streams().await;
|
||||
let mut result = buckets.into_result();
|
||||
result.spent_count += resolved_out_of_pending;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Observe-only counterpart of `reclaim_proofs_by_ys`. Queries the mint
|
||||
/// 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 ===
|
||||
|
||||
pub async fn is_token_spent(&self, token: Token) -> Result<bool, Error> {
|
||||
let token: CdkToken = token.try_into()?;
|
||||
let mint_keysets = self.inner.get_mint_keysets().await?;
|
||||
let mint_keysets = self.inner.get_mint_keysets(cdk::wallet::KeysetFilter::All).await?;
|
||||
let proof_states = self
|
||||
.inner
|
||||
.check_proofs_spent(token.proofs(&mint_keysets)?)
|
||||
@@ -552,6 +721,13 @@ impl Wallet {
|
||||
CurrencyUnit::from_str(&self.unit).unwrap_or(CurrencyUnit::Custom(self.unit.clone()))
|
||||
}
|
||||
|
||||
/// Recalcula el balance desde la DB y lo empuja al stream.
|
||||
/// Llamar después de manipulaciones directas al DB (ej: offline send
|
||||
/// marking proofs as PendingSpent fuera de la API de CDK).
|
||||
pub async fn refresh_balance(&self) {
|
||||
self.update_balance_streams().await;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_balance_streams(&self) {
|
||||
let balance = self
|
||||
.inner
|
||||
@@ -677,6 +853,103 @@ 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 {
|
||||
pub amount: u64,
|
||||
pub fee_reserve: u64,
|
||||
pub swap_fee: u64,
|
||||
pub input_fee: u64,
|
||||
|
||||
operation_id: Uuid,
|
||||
cdk_quote: CdkMeltQuote,
|
||||
proofs: cdk::nuts::Proofs,
|
||||
proofs_to_swap: cdk::nuts::Proofs,
|
||||
cdk_input_fee: Amount,
|
||||
cdk_input_fee_without_swap: Amount,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ReceiveOptions {
|
||||
pub signing_keys: Option<Vec<String>>,
|
||||
|
||||
+953
-113
File diff suppressed because it is too large
Load Diff
@@ -89,11 +89,11 @@ BEGIN
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "com.example" "\0"
|
||||
VALUE "CompanyName", "ElCaju" "\0"
|
||||
VALUE "FileDescription", "elcaju" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "elcaju" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2026 ElCaju. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "elcaju.exe" "\0"
|
||||
VALUE "ProductName", "elcaju" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
|
||||
Reference in New Issue
Block a user