Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f01fc66dcf | ||
|
|
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 | ||
|
|
426b63456d | ||
|
|
4d2ad39323 | ||
|
|
060c6acd4e | ||
|
|
14da243b89 | ||
|
|
4f91a2be1c | ||
|
|
ab13e8f6c2 | ||
|
|
f431f825c5 | ||
|
|
b56aa5ce62 | ||
|
|
c69de2b41b | ||
|
|
e5285058a8 | ||
|
|
b01b658b93 | ||
|
|
6596aa963a | ||
|
|
ed309d18e8 | ||
|
|
1b30d9836b | ||
|
|
588a127f9a | ||
|
|
362e45e40f | ||
|
|
fae57cc46e | ||
|
|
80ab0fd9e1 | ||
|
|
29895f4079 | ||
|
|
41fa32d1b4 | ||
|
|
63ce12d745 | ||
|
|
7245dbb669 | ||
|
|
e124e0839a | ||
|
|
1a31b47c6b | ||
|
|
a5cd50af13 | ||
|
|
66119f6b56 | ||
|
|
484f73a33b | ||
|
|
67204a12f8 | ||
|
|
b4a62fe9e5 | ||
|
|
638b288b7e | ||
|
|
357cc6827f | ||
|
|
682ec4ba7d | ||
|
|
03cf1d2119 | ||
|
|
2309a2edda | ||
|
|
ac1ac11075 | ||
|
|
aafa9f97eb | ||
|
|
7d3fe5ac58 | ||
|
|
b9c01ce865 | ||
|
|
679aa6be5f |
@@ -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});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class ProofService {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dbPath = '${dir.path}/elcaju_wallet.sqlite';
|
||||
|
||||
_db = await openDatabase(dbPath);
|
||||
_db = await openDatabase(dbPath, singleInstance: false);
|
||||
return _db!;
|
||||
}
|
||||
|
||||
|
||||
+24
-1
@@ -118,6 +118,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": {
|
||||
@@ -559,5 +560,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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -103,6 +103,7 @@
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"insufficientBalance": "Insufficient balance",
|
||||
"feeExceedsAmount": "Fee exceeds the amount to send",
|
||||
"tokenCreationError": "Error creating token: {error}",
|
||||
|
||||
"tokenCreated": "Token created",
|
||||
@@ -452,5 +453,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"
|
||||
}
|
||||
|
||||
+40
-1
@@ -118,6 +118,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": {
|
||||
@@ -572,5 +573,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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -118,6 +118,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": {
|
||||
@@ -559,5 +560,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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -118,6 +118,7 @@
|
||||
"confirm": "Conferma",
|
||||
"cancel": "Annulla",
|
||||
"insufficientBalance": "Saldo insufficiente",
|
||||
"feeExceedsAmount": "La commissione supera l'importo da inviare",
|
||||
"tokenCreationError": "Errore creazione token: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -559,5 +560,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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -118,6 +118,7 @@
|
||||
"confirm": "確認",
|
||||
"cancel": "キャンセル",
|
||||
"insufficientBalance": "残高不足",
|
||||
"feeExceedsAmount": "手数料が送金額を超えています",
|
||||
"tokenCreationError": "トークン作成エラー:{error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -559,5 +560,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データを持たないことです"
|
||||
}
|
||||
|
||||
+24
-1
@@ -118,6 +118,7 @@
|
||||
"confirm": "확인",
|
||||
"cancel": "취소",
|
||||
"insufficientBalance": "잔액 부족",
|
||||
"feeExceedsAmount": "수수료가 송금액을 초과합니다",
|
||||
"tokenCreationError": "토큰 생성 오류: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -559,5 +560,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데이터를 갖지 않는 것입니다"
|
||||
}
|
||||
|
||||
@@ -661,6 +661,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:
|
||||
@@ -2352,6 +2358,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> {
|
||||
|
||||
@@ -295,6 +295,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';
|
||||
@@ -1241,4 +1244,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';
|
||||
}
|
||||
|
||||
@@ -292,6 +292,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';
|
||||
@@ -1222,4 +1225,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';
|
||||
}
|
||||
|
||||
@@ -292,6 +292,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';
|
||||
@@ -1230,4 +1233,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';
|
||||
}
|
||||
|
||||
@@ -296,6 +296,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';
|
||||
@@ -1246,4 +1249,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';
|
||||
}
|
||||
|
||||
@@ -293,6 +293,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';
|
||||
@@ -1234,4 +1237,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';
|
||||
}
|
||||
|
||||
@@ -288,6 +288,9 @@ class L10nJa extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => '残高不足';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => '手数料が送金額を超えています';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'トークン作成エラー:$error';
|
||||
@@ -1207,4 +1210,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データを持たないことです';
|
||||
}
|
||||
|
||||
@@ -290,6 +290,9 @@ class L10nKo extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => '잔액 부족';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => '수수료가 송금액을 초과합니다';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return '토큰 생성 오류: $error';
|
||||
@@ -1209,4 +1212,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데이터를 갖지 않는 것입니다';
|
||||
}
|
||||
|
||||
@@ -293,6 +293,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';
|
||||
@@ -1234,4 +1237,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';
|
||||
}
|
||||
|
||||
@@ -292,6 +292,9 @@ class L10nRu extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => 'Недостаточный баланс';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => 'Комиссия превышает сумму отправки';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return 'Ошибка создания токена: $error';
|
||||
@@ -1230,4 +1233,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не иметь их';
|
||||
}
|
||||
|
||||
@@ -293,6 +293,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';
|
||||
@@ -1231,4 +1234,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';
|
||||
}
|
||||
|
||||
@@ -287,6 +287,9 @@ class L10nZh extends L10n {
|
||||
@override
|
||||
String get insufficientBalance => '余额不足';
|
||||
|
||||
@override
|
||||
String get feeExceedsAmount => '手续费超过发送金额';
|
||||
|
||||
@override
|
||||
String tokenCreationError(String error) {
|
||||
return '创建代币错误:$error';
|
||||
@@ -1202,4 +1205,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就是不拥有它';
|
||||
}
|
||||
|
||||
+24
-1
@@ -118,6 +118,7 @@
|
||||
"confirm": "Confirmar",
|
||||
"cancel": "Cancelar",
|
||||
"insufficientBalance": "Saldo insuficiente",
|
||||
"feeExceedsAmount": "A taxa excede o valor a enviar",
|
||||
"tokenCreationError": "Erro ao criar token: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -559,5 +560,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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -118,6 +118,7 @@
|
||||
"confirm": "Подтвердить",
|
||||
"cancel": "Отмена",
|
||||
"insufficientBalance": "Недостаточный баланс",
|
||||
"feeExceedsAmount": "Комиссия превышает сумму отправки",
|
||||
"tokenCreationError": "Ошибка создания токена: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -559,5 +560,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не иметь их"
|
||||
}
|
||||
|
||||
+24
-1
@@ -118,6 +118,7 @@
|
||||
"confirm": "Thibitisha",
|
||||
"cancel": "Ghairi",
|
||||
"insufficientBalance": "Salio halitoshi",
|
||||
"feeExceedsAmount": "Ada inazidi kiasi cha kutuma",
|
||||
"tokenCreationError": "Hitilafu ya kuunda tokeni: {error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -559,5 +560,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"
|
||||
}
|
||||
|
||||
+24
-1
@@ -118,6 +118,7 @@
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"insufficientBalance": "余额不足",
|
||||
"feeExceedsAmount": "手续费超过发送金额",
|
||||
"tokenCreationError": "创建代币错误:{error}",
|
||||
"@tokenCreationError": {
|
||||
"placeholders": {
|
||||
@@ -559,5 +560,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就是不拥有它"
|
||||
}
|
||||
|
||||
@@ -68,6 +68,10 @@ class WalletProvider extends ChangeNotifier {
|
||||
/// Caché de MintInfo por URL (nombre, logo, contactos, etc.)
|
||||
final Map<String, MintInfo> _mintInfoCache = {};
|
||||
|
||||
/// Caché del balance stream para evitar recrear StreamSink en cada build().
|
||||
Stream<BigInt>? _cachedBalanceStream;
|
||||
String? _cachedBalanceKey;
|
||||
|
||||
/// Mint activo actualmente
|
||||
String? _activeMintUrl;
|
||||
|
||||
@@ -352,6 +356,10 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
debugPrint('Mints restaurados: ${_mintUnits.keys.length}');
|
||||
|
||||
// Re-detectar unidades en background (sin bloquear arranque).
|
||||
// Si el mint ahora soporta más unidades (ej: usd), se actualizan.
|
||||
_refreshAllMintUnits();
|
||||
} else {
|
||||
// Primera vez - agregar mint por defecto
|
||||
await addMint('https://mint.cubabitcoin.org');
|
||||
@@ -403,6 +411,62 @@ class WalletProvider extends ChangeNotifier {
|
||||
return await getWallet(_activeMintUrl!, _activeUnit);
|
||||
}
|
||||
|
||||
/// Re-detecta unidades de todos los mints conocidos via NUT-04.
|
||||
/// Corre en background sin bloquear el arranque. Solo notifica si hay cambios.
|
||||
void _refreshAllMintUnits() async {
|
||||
bool changed = false;
|
||||
|
||||
for (final mintUrl in _mintUnits.keys.toList()) {
|
||||
try {
|
||||
final mintInfo = await getMintInfo(mintUrl: mintUrl);
|
||||
final units = <String>{};
|
||||
for (final method in mintInfo.nuts.nut04.methods) {
|
||||
units.add(method.unit);
|
||||
}
|
||||
final unitList = units.isEmpty ? ['sat'] : units.toList();
|
||||
unitList.sort((a, b) {
|
||||
if (a == 'sat') return -1;
|
||||
if (b == 'sat') return 1;
|
||||
return a.compareTo(b);
|
||||
});
|
||||
|
||||
final previous = _mintUnits[mintUrl];
|
||||
if (!_listEquals(previous, unitList)) {
|
||||
_mintUnits[mintUrl] = unitList;
|
||||
changed = true;
|
||||
debugPrint('Unidades actualizadas para $mintUrl: $unitList');
|
||||
|
||||
// Validar _activeUnit si este es el mint activo
|
||||
if (mintUrl == _activeMintUrl && !unitList.contains(_activeUnit)) {
|
||||
_activeUnit = unitList.first;
|
||||
debugPrint('Active unit reset to ${unitList.first} for $mintUrl');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error refrescando unidades de $mintUrl: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
try {
|
||||
await _saveMints();
|
||||
} catch (e) {
|
||||
debugPrint('Error guardando mints actualizados: $e');
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Compara dos listas de strings por valor.
|
||||
bool _listEquals(List<String>? a, List<String>? b) {
|
||||
if (a == null || b == null) return a == b;
|
||||
if (a.length != b.length) return false;
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MULTI-MINT OPERATIONS
|
||||
// ============================================================
|
||||
@@ -602,8 +666,21 @@ class WalletProvider extends ChangeNotifier {
|
||||
// ============================================================
|
||||
|
||||
/// Stream de balance del wallet activo (reactivo).
|
||||
/// Cacheado para que StreamBuilder reciba el mismo objeto entre rebuilds
|
||||
/// y no mate el StreamSink de Rust.
|
||||
Stream<BigInt>? streamBalance() {
|
||||
return activeWallet?.streamBalance();
|
||||
final wallet = activeWallet;
|
||||
if (wallet == null) {
|
||||
_cachedBalanceStream = null;
|
||||
_cachedBalanceKey = null;
|
||||
return null;
|
||||
}
|
||||
final key = '$_activeMintUrl:$_activeUnit';
|
||||
if (_cachedBalanceKey != key) {
|
||||
_cachedBalanceStream = wallet.streamBalance().asBroadcastStream();
|
||||
_cachedBalanceKey = key;
|
||||
}
|
||||
return _cachedBalanceStream;
|
||||
}
|
||||
|
||||
/// Obtiene el balance del wallet activo.
|
||||
@@ -844,7 +921,20 @@ class WalletProvider extends ChangeNotifier {
|
||||
// DEBUG: transacciones ANTES del receive
|
||||
await _debugLogTransactions(wallet, 'BEFORE receive');
|
||||
|
||||
final amount = await wallet.receive(token: token, opts: opts);
|
||||
BigInt amount;
|
||||
try {
|
||||
amount = await wallet.receive(token: token, opts: opts);
|
||||
} catch (e) {
|
||||
final errorStr = e.toString().toLowerCase();
|
||||
if (errorStr.contains('already signed')) {
|
||||
debugPrint('[RECEIVE] Already signed error, restoring wallet counters...');
|
||||
await wallet.restore();
|
||||
// Retry once after counter resync
|
||||
amount = await wallet.receive(token: token, opts: opts);
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG: counters después de receive
|
||||
await KeysetDebug.logCounters('AFTER receive ($unit)');
|
||||
@@ -966,45 +1056,33 @@ class WalletProvider extends ChangeNotifier {
|
||||
Future<String> confirmSend(PreparedSend prepared, String? memo) async {
|
||||
final wallet = await getActiveWallet();
|
||||
|
||||
final token = await wallet.send(
|
||||
final result = await wallet.send(
|
||||
send: prepared,
|
||||
memo: memo,
|
||||
includeMemo: memo != null && memo.isNotEmpty,
|
||||
);
|
||||
|
||||
// Guardar token en storage local para mostrar en detalles del historial.
|
||||
// Usamos hash del token como key temporal; después buscaremos la transacción.
|
||||
await _saveTokenForRecentTransaction(token.encoded);
|
||||
|
||||
notifyListeners();
|
||||
return token.encoded;
|
||||
}
|
||||
|
||||
/// Guarda el token para la transacción más reciente de tipo send.
|
||||
Future<void> _saveTokenForRecentTransaction(String tokenEncoded) async {
|
||||
try {
|
||||
// Obtener transacciones outgoing más recientes
|
||||
final wallet = await getActiveWallet();
|
||||
final txs = await wallet.listTransactions(
|
||||
direction: TransactionDirection.outgoing,
|
||||
);
|
||||
|
||||
if (txs.isNotEmpty) {
|
||||
// La más reciente debería ser la que acabamos de crear
|
||||
final recentTx = txs.first;
|
||||
|
||||
// Save token metadata using the deterministic transaction ID returned by CDK
|
||||
// (SHA-256 of sorted proof Y values — no racy listTransactions needed)
|
||||
// Best-effort: don't fail the send if metadata persistence fails
|
||||
final txId = result.transactionId;
|
||||
if (txId != null && txId.isNotEmpty) {
|
||||
try {
|
||||
await _txMetaStorage.save(
|
||||
recentTx.id,
|
||||
txId,
|
||||
TransactionMeta(
|
||||
type: TransactionType.cashu,
|
||||
token: tokenEncoded,
|
||||
token: result.token.encoded,
|
||||
),
|
||||
);
|
||||
debugPrint('Token guardado para tx ${recentTx.id}');
|
||||
debugPrint('Token guardado para tx $txId');
|
||||
} catch (e) {
|
||||
debugPrint('Error guardando send metadata: $e');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error guardando token metadata: $e');
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return result.token.encoded;
|
||||
}
|
||||
|
||||
/// Cancela un envío preparado (libera proofs reservados).
|
||||
@@ -1116,12 +1194,14 @@ class WalletProvider extends ChangeNotifier {
|
||||
_activeMintController!.close();
|
||||
}
|
||||
|
||||
// StreamController que la UI puede escuchar y cancelar libremente
|
||||
final controller = StreamController<MintQuote>();
|
||||
// StreamController que la UI puede escuchar y cancelar libremente.
|
||||
// sync: true para no perder eventos rápidos (paid → issued en µs).
|
||||
final controller = StreamController<MintQuote>(sync: true);
|
||||
_activeMintController = controller;
|
||||
|
||||
// Cancelar suscripción anterior si existe (await evita race de callbacks)
|
||||
await _activeMintSubscription?.cancel();
|
||||
// Cancelar suscripción anterior sin await: el task de Rust (polling + WS)
|
||||
// puede tardar en cerrar y bloquearía la creación de una nueva factura.
|
||||
_activeMintSubscription?.cancel();
|
||||
_activeMintSubscription = null;
|
||||
|
||||
// Suscribirse al stream del CDK desde el provider (persiste sin UI)
|
||||
@@ -1129,7 +1209,7 @@ class WalletProvider extends ChangeNotifier {
|
||||
amount: amount,
|
||||
description: description,
|
||||
).listen(
|
||||
(quote) {
|
||||
(quote) async {
|
||||
// Guardar invoice temprano en SharedPreferences
|
||||
if (quote.state == MintQuoteState.unpaid) {
|
||||
invoiceBolt11 = quote.request;
|
||||
@@ -1138,8 +1218,10 @@ class WalletProvider extends ChangeNotifier {
|
||||
|
||||
// Cuando se completa, guardar metadata, confetti, limpiar pending
|
||||
if (quote.state == MintQuoteState.issued && invoiceBolt11 != null) {
|
||||
_saveMintMetadata(wallet, invoiceBolt11!);
|
||||
_removePendingMintInvoice(quote.id);
|
||||
final saved = await _saveMintMetadata(wallet, invoiceBolt11!, quote.transactionId);
|
||||
// Only remove pending invoice if metadata was saved;
|
||||
// otherwise _matchPendingMintInvoices can recover it on next startup
|
||||
if (saved) _removePendingMintInvoice(quote.id);
|
||||
}
|
||||
|
||||
// Reenviar a la UI (si sigue escuchando)
|
||||
@@ -1373,28 +1455,30 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Guarda metadata para una transacción de mint (Lightning deposit).
|
||||
Future<void> _saveMintMetadata(Wallet wallet, String invoice) async {
|
||||
/// Returns true if metadata was actually saved.
|
||||
Future<bool> _saveMintMetadata(Wallet wallet, String invoice, String? transactionId) async {
|
||||
try {
|
||||
final txs = await wallet.listTransactions(
|
||||
direction: TransactionDirection.incoming,
|
||||
);
|
||||
|
||||
if (txs.isNotEmpty) {
|
||||
final recentTx = txs.first;
|
||||
|
||||
if (transactionId != null && transactionId.isNotEmpty) {
|
||||
await _txMetaStorage.save(
|
||||
recentTx.id,
|
||||
transactionId,
|
||||
TransactionMeta(
|
||||
type: TransactionType.lightning,
|
||||
invoice: invoice,
|
||||
),
|
||||
);
|
||||
debugPrint('Mint metadata guardada para tx ${recentTx.id}');
|
||||
debugPrint('Mint metadata guardada para tx $transactionId');
|
||||
confettiController.fire();
|
||||
notifyListeners();
|
||||
return true;
|
||||
} else {
|
||||
debugPrint('Mint metadata: no transaction ID available');
|
||||
confettiController.fire();
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error guardando mint metadata: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1414,19 +1498,57 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Ejecuta el pago del invoice.
|
||||
/// Guarda metadata type=lightning para identificar en historial.
|
||||
/// Usa prepare/confirm/cancel para recuperar proofs si el pago falla.
|
||||
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) {
|
||||
try {
|
||||
await wallet.cancelMelt(melt: prepared);
|
||||
} catch (cancelErr) {
|
||||
debugPrint('[MELT] cancelMelt failed: $cancelErr');
|
||||
}
|
||||
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 {count, amount} total de proofs recuperadas.
|
||||
Future<ReclaimResult> reclaimPendingProofs({String? mintUrl}) async {
|
||||
var totalCount = BigInt.zero;
|
||||
var totalAmount = BigInt.zero;
|
||||
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();
|
||||
totalCount += result.count;
|
||||
totalAmount += result.amount;
|
||||
} catch (e) {
|
||||
debugPrint('Reclaim pending proofs failed for ${entry.key}:$unit: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (totalCount > BigInt.zero) {
|
||||
notifyListeners();
|
||||
}
|
||||
return ReclaimResult(count: totalCount, amount: totalAmount);
|
||||
}
|
||||
|
||||
/// Guarda metadata para una transacción de melt (Lightning withdrawal).
|
||||
@@ -1453,6 +1575,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
|
||||
// ============================================================
|
||||
@@ -1558,6 +1732,14 @@ class WalletProvider extends ChangeNotifier {
|
||||
// Silencioso - puede fallar offline
|
||||
debugPrint('Check pending failed: $e');
|
||||
}
|
||||
|
||||
try {
|
||||
// Recuperar proofs huérfanas (melts fallidos, sends no completados)
|
||||
await wallet.reclaimPendingProofs();
|
||||
} catch (e) {
|
||||
debugPrint('Reclaim pending proofs failed: $e');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('Error getting wallet ${entry.key}:$unit: $e');
|
||||
}
|
||||
@@ -1722,7 +1904,7 @@ class WalletProvider extends ChangeNotifier {
|
||||
// Enviar todo el balance
|
||||
final prepared =
|
||||
await tempWallet.prepareSend(amount: tempBalance);
|
||||
final token = await tempWallet.send(
|
||||
final result = await tempWallet.send(
|
||||
send: prepared,
|
||||
memo: 'Recuperación El Caju',
|
||||
includeMemo: true,
|
||||
@@ -1730,7 +1912,7 @@ class WalletProvider extends ChangeNotifier {
|
||||
|
||||
// Reclamar en nuestro wallet
|
||||
final ourWallet = await getWallet(mintUrl, unit);
|
||||
final received = await ourWallet.receive(token: token);
|
||||
final received = await ourWallet.receive(token: result.token);
|
||||
totalRecovered += received;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -1769,6 +1951,8 @@ class WalletProvider extends ChangeNotifier {
|
||||
_activeUnit = 'sat';
|
||||
_mnemonic = null;
|
||||
_db = null;
|
||||
_cachedBalanceStream = null;
|
||||
_cachedBalanceKey = null;
|
||||
|
||||
// Limpiar metadata de transacciones
|
||||
await _txMetaStorage.clear();
|
||||
@@ -1797,6 +1981,8 @@ class WalletProvider extends ChangeNotifier {
|
||||
_activeUnit = 'sat';
|
||||
_mnemonic = null;
|
||||
_db = null;
|
||||
_cachedBalanceStream = null;
|
||||
_cachedBalanceKey = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -1972,6 +2158,8 @@ class WalletProvider extends ChangeNotifier {
|
||||
if (_activeMintController != null && !_activeMintController!.isClosed) {
|
||||
_activeMintController!.close();
|
||||
}
|
||||
_cachedBalanceStream = null;
|
||||
_cachedBalanceKey = null;
|
||||
confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ 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/bip321_builder.dart';
|
||||
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// import '../../core/utils/bip321_builder.dart';
|
||||
import '../../core/services/nfc_service.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
@@ -45,7 +46,9 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
// Payment data
|
||||
String? _creqB;
|
||||
String? _bolt11;
|
||||
QrMode _activeMode = QrMode.cashu;
|
||||
// TODO: re-enable QrMode.cashu / universal once CDK fixes NIP-17 sender pubkey bug
|
||||
// See: https://github.com/cashubtc/cdk/issues/1807
|
||||
// QrMode _activeMode = QrMode.lightning;
|
||||
bool _paymentHandled = false;
|
||||
|
||||
// Listeners
|
||||
@@ -59,22 +62,26 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
// Success
|
||||
BigInt _receivedAmount = BigInt.zero;
|
||||
|
||||
late final WalletProvider _walletProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_activeUnit = context.read<WalletProvider>().activeUnit;
|
||||
_walletProvider = context.read<WalletProvider>();
|
||||
_activeUnit = _walletProvider.activeUnit;
|
||||
_checkNfc();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_descriptionController.dispose();
|
||||
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// No-op while Nostr is disabled (_nostrSubscription is always null, _creqB is always null)
|
||||
_nostrSubscription?.cancel();
|
||||
_mintSubscription?.cancel();
|
||||
if (_nfcEmulating) NfcService.stopEmulating();
|
||||
// Clear persisted request if user abandoned without receiving payment
|
||||
if (!_paymentHandled && _creqB != null) {
|
||||
context.read<WalletProvider>().removePendingNostrRequest();
|
||||
_walletProvider.removePendingNostrRequest();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
@@ -346,42 +353,37 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
version: QrVersions.auto,
|
||||
size: 260,
|
||||
backgroundColor: Colors.white,
|
||||
errorCorrectionLevel: _activeMode == QrMode.universal
|
||||
? QrErrorCorrectLevel.M
|
||||
: QrErrorCorrectLevel.H,
|
||||
// TODO: use QrErrorCorrectLevel.M for universal mode once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.H,
|
||||
),
|
||||
if (_activeMode == QrMode.cashu)
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Image.asset(
|
||||
'assets/img/cashu.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
// TODO: re-enable Cashu logo overlay once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// if (_activeMode == QrMode.cashu)
|
||||
// Container(
|
||||
// width: 48, height: 48,
|
||||
// decoration: BoxDecoration(
|
||||
// color: Colors.white,
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// border: Border.all(color: Colors.white, width: 4),
|
||||
// ),
|
||||
// child: ClipRRect(
|
||||
// borderRadius: BorderRadius.circular(6),
|
||||
// child: Image.asset('assets/img/cashu.png', fit: BoxFit.contain),
|
||||
// ),
|
||||
// ),
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.bitcoinOrange,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
),
|
||||
if (_activeMode == QrMode.lightning)
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.bitcoinOrange,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.zap,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
child: const Icon(
|
||||
LucideIcons.zap,
|
||||
color: Colors.white,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -434,73 +436,66 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
}
|
||||
|
||||
Widget _buildModeToggle() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildToggleButton(
|
||||
label: L10n.of(context)!.universal,
|
||||
mode: QrMode.universal,
|
||||
enabled: _bolt11 != null,
|
||||
),
|
||||
_buildToggleButton(
|
||||
label: 'Cashu',
|
||||
mode: QrMode.cashu,
|
||||
enabled: true,
|
||||
),
|
||||
_buildToggleButton(
|
||||
label: 'Lightning',
|
||||
mode: QrMode.lightning,
|
||||
enabled: _bolt11 != null,
|
||||
),
|
||||
],
|
||||
),
|
||||
// TODO: re-enable Universal/Cashu tabs once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// Hidden while only Lightning is available (single button toggle is a no-op)
|
||||
return const SizedBox.shrink(
|
||||
// child: Container(
|
||||
// padding: const EdgeInsets.all(4),
|
||||
// decoration: BoxDecoration(
|
||||
// color: Colors.white.withValues(alpha: 0.1),
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// ),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// _buildToggleButton(label: L10n.of(context)!.universal, mode: QrMode.universal, enabled: _bolt11 != null),
|
||||
// _buildToggleButton(label: 'Cashu', mode: QrMode.cashu, enabled: true),
|
||||
// _buildToggleButton(label: 'Lightning', mode: QrMode.lightning, enabled: _bolt11 != null),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggleButton({
|
||||
required String label,
|
||||
required QrMode mode,
|
||||
required bool enabled,
|
||||
}) {
|
||||
final isActive = _activeMode == mode;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: enabled
|
||||
? () {
|
||||
setState(() => _activeMode = mode);
|
||||
_updateNfcPayload();
|
||||
}
|
||||
: null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
gradient: isActive
|
||||
? const LinearGradient(colors: AppColors.buttonGradient)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 13,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
|
||||
color: enabled
|
||||
? (isActive ? Colors.white : AppColors.textSecondary)
|
||||
: Colors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// Widget _buildToggleButton({
|
||||
// required String label,
|
||||
// required QrMode mode,
|
||||
// required bool enabled,
|
||||
// }) {
|
||||
// final isActive = _activeMode == mode;
|
||||
// return Expanded(
|
||||
// child: GestureDetector(
|
||||
// onTap: enabled
|
||||
// ? () {
|
||||
// setState(() => _activeMode = mode);
|
||||
// _updateNfcPayload();
|
||||
// }
|
||||
// : null,
|
||||
// child: Container(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
// decoration: BoxDecoration(
|
||||
// gradient: isActive
|
||||
// ? const LinearGradient(colors: AppColors.buttonGradient)
|
||||
// : null,
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// ),
|
||||
// child: Center(
|
||||
// child: Text(
|
||||
// label,
|
||||
// style: TextStyle(
|
||||
// fontFamily: 'Inter',
|
||||
// fontSize: 13,
|
||||
// fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
|
||||
// color: enabled
|
||||
// ? (isActive ? Colors.white : AppColors.textSecondary)
|
||||
// : Colors.white.withValues(alpha: 0.3),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Row(
|
||||
@@ -709,11 +704,12 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
|
||||
// ─── Logic ───
|
||||
|
||||
static const List<String> _defaultNostrRelays = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.primal.net',
|
||||
'wss://nos.lol',
|
||||
];
|
||||
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// static const List<String> _defaultNostrRelays = [
|
||||
// 'wss://relay.damus.io',
|
||||
// 'wss://relay.primal.net',
|
||||
// 'wss://nos.lol',
|
||||
// ];
|
||||
|
||||
Future<void> _generateRequest() async {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
@@ -731,7 +727,9 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
_paymentHandled = false;
|
||||
_creqB = null;
|
||||
_bolt11 = null;
|
||||
_activeMode = QrMode.cashu;
|
||||
// TODO: re-enable Cashu/Nostr payment request once CDK fixes NIP-17 sender pubkey bug
|
||||
// See: https://github.com/cashubtc/cdk/issues/1807
|
||||
// _activeMode = QrMode.lightning;
|
||||
|
||||
setState(() => _status = RequestStatus.generating);
|
||||
|
||||
@@ -748,35 +746,39 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
? _descriptionController.text
|
||||
: null;
|
||||
|
||||
// 1. Create payment request (creqB + Nostr keys)
|
||||
final request = await wallet.createPaymentRequest(
|
||||
params: CreateRequestParams(
|
||||
amount: _amount,
|
||||
unit: _activeUnit,
|
||||
description: description,
|
||||
nostrRelays: _defaultNostrRelays,
|
||||
),
|
||||
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// // 1. Create payment request (creqB + Nostr keys)
|
||||
// final request = await wallet.createPaymentRequest(
|
||||
// params: CreateRequestParams(
|
||||
// amount: _amount,
|
||||
// unit: _activeUnit,
|
||||
// description: description,
|
||||
// nostrRelays: _defaultNostrRelays,
|
||||
// ),
|
||||
// );
|
||||
//
|
||||
// _creqB = request.creqB;
|
||||
//
|
||||
// // Persist handle for recovery if app is killed
|
||||
// await walletProvider.savePendingNostrRequest(
|
||||
// request.listenerHandle.toPersisted(),
|
||||
// );
|
||||
//
|
||||
// // 2. Start Nostr listener
|
||||
// _nostrSubscription = wallet
|
||||
// .waitForNostrPayment(handle: request.listenerHandle)
|
||||
// .listen(_onNostrEvent);
|
||||
|
||||
// Lightning invoice generation via wallet provider
|
||||
// (walletProvider.mintTokens handles pending invoice persistence and metadata)
|
||||
final mintStream = await walletProvider.mintTokens(
|
||||
_amount,
|
||||
description,
|
||||
);
|
||||
|
||||
_creqB = request.creqB;
|
||||
|
||||
// Persist handle for recovery if app is killed
|
||||
await walletProvider.savePendingNostrRequest(
|
||||
request.listenerHandle.toPersisted(),
|
||||
);
|
||||
|
||||
// 2. Start Nostr listener
|
||||
_nostrSubscription = wallet
|
||||
.waitForNostrPayment(handle: request.listenerHandle)
|
||||
.listen(_onNostrEvent);
|
||||
|
||||
// 3. Start Lightning invoice generation directly via CDK
|
||||
_mintSubscription = wallet
|
||||
.mint(amount: _amount, description: description)
|
||||
.listen(_onMintEvent);
|
||||
|
||||
setState(() => _status = RequestStatus.waiting);
|
||||
if (!mounted) return;
|
||||
_mintSubscription = mintStream.listen(_onMintEvent);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_status = RequestStatus.error;
|
||||
_errorMessage = e.toString();
|
||||
@@ -784,14 +786,15 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onNostrEvent(NostrPaymentEvent event) {
|
||||
if (!mounted || _paymentHandled) return;
|
||||
if (event.state == NostrPaymentState.received) {
|
||||
_paymentHandled = true;
|
||||
_mintSubscription?.cancel();
|
||||
_onPaymentSuccess(event.amount ?? _amount);
|
||||
}
|
||||
}
|
||||
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// void _onNostrEvent(NostrPaymentEvent event) {
|
||||
// if (!mounted || _paymentHandled) return;
|
||||
// if (event.state == NostrPaymentState.received) {
|
||||
// _paymentHandled = true;
|
||||
// _mintSubscription?.cancel();
|
||||
// _onPaymentSuccess(event.amount ?? _amount);
|
||||
// }
|
||||
// }
|
||||
|
||||
void _onMintEvent(MintQuote quote) {
|
||||
if (!mounted) return;
|
||||
@@ -800,7 +803,9 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
if (_bolt11 == null) {
|
||||
setState(() {
|
||||
_bolt11 = quote.request;
|
||||
_activeMode = QrMode.universal;
|
||||
// TODO: switch to QrMode.universal once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// _activeMode = QrMode.lightning;
|
||||
_status = RequestStatus.waiting;
|
||||
});
|
||||
_updateNfcPayload();
|
||||
}
|
||||
@@ -813,7 +818,17 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
}
|
||||
break;
|
||||
case MintQuoteState.error:
|
||||
// Lightning failed, but Nostr listener continues
|
||||
// No Nostr fallback while CDK NIP-17 bug is open (cashubtc/cdk#1807)
|
||||
if (!_paymentHandled) {
|
||||
if (_nfcEmulating) {
|
||||
NfcService.stopEmulating();
|
||||
_nfcEmulating = false;
|
||||
}
|
||||
setState(() {
|
||||
_status = RequestStatus.error;
|
||||
_errorMessage = quote.error ?? L10n.of(context)!.unknownError;
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -828,20 +843,22 @@ class _RequestScreenState extends State<RequestScreen> {
|
||||
});
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
walletProvider.confettiController.fire();
|
||||
await walletProvider.removePendingNostrRequest();
|
||||
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// Only remove pending Nostr request if this screen created one.
|
||||
// Disabled while Nostr payment requests are disabled to avoid
|
||||
// deleting a recovery record from a previous session.
|
||||
// await walletProvider.removePendingNostrRequest();
|
||||
}
|
||||
|
||||
// ─── QR Content ───
|
||||
|
||||
String _getActiveQrContent() {
|
||||
switch (_activeMode) {
|
||||
case QrMode.universal:
|
||||
return buildUnifiedUri(creqB: _creqB!, bolt11: _bolt11);
|
||||
case QrMode.cashu:
|
||||
return _creqB!.toUpperCase();
|
||||
case QrMode.lightning:
|
||||
return _bolt11?.toUpperCase() ?? '';
|
||||
}
|
||||
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
|
||||
// case QrMode.universal:
|
||||
// return buildUnifiedUri(creqB: _creqB!, bolt11: _bolt11);
|
||||
// case QrMode.cashu:
|
||||
// return _creqB!.toUpperCase();
|
||||
return _bolt11?.toUpperCase() ?? '';
|
||||
}
|
||||
|
||||
// ─── Actions ───
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
@@ -5,10 +6,12 @@ import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/models/proof.dart';
|
||||
import '../../core/services/proof_service.dart';
|
||||
import '../../src/rust/api/token.dart' as cdk;
|
||||
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.
|
||||
@@ -45,7 +48,7 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_memoController.dispose();
|
||||
_proofService.close();
|
||||
if (!_isCreating) _proofService.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -321,32 +324,55 @@ 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(() {
|
||||
_isCreating = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final selectedProofs = _selectedProofs;
|
||||
|
||||
// Marcar proofs como PENDING_SPENT
|
||||
await _proofService.markProofsPendingSpent(selectedProofs);
|
||||
proofsMarkedPending = true;
|
||||
|
||||
// Crear token V3
|
||||
// Crear token via CDK (produce V4/cashuB válido)
|
||||
final memo = _memoController.text.isNotEmpty ? _memoController.text : null;
|
||||
final token = _proofService.createTokenV3(
|
||||
final proofsJson = jsonEncode(
|
||||
selectedProofs.map((p) => p.toTokenProof()).toList(),
|
||||
);
|
||||
final cdkToken = cdk.createOfflineToken(
|
||||
mintUrl: widget.mintUrl,
|
||||
proofs: selectedProofs,
|
||||
proofsJson: proofsJson,
|
||||
memo: memo,
|
||||
unit: widget.unit,
|
||||
);
|
||||
final token = cdkToken.encoded;
|
||||
|
||||
if (mounted) {
|
||||
_isCreating = false;
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareTokenScreen(
|
||||
token: token,
|
||||
amount: _selectedTotal,
|
||||
amount: selectedTotal,
|
||||
unit: widget.unit,
|
||||
memo: memo,
|
||||
),
|
||||
@@ -354,7 +380,17 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
if (proofsMarkedPending) {
|
||||
try {
|
||||
await _proofService.markProofsUnspent(selectedProofs);
|
||||
} catch (rollbackErr) {
|
||||
debugPrint('Failed to rollback proofs to UNSPENT: $rollbackErr');
|
||||
}
|
||||
}
|
||||
if (!mounted) {
|
||||
_proofService.close();
|
||||
return;
|
||||
}
|
||||
final errorMessage = L10n.of(context)!.creatingTokenError(e.toString());
|
||||
setState(() {
|
||||
_errorMessage = errorMessage;
|
||||
|
||||
@@ -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,6 +8,7 @@ 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';
|
||||
@@ -572,15 +574,37 @@ 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,
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => _ConfirmationModal(
|
||||
amount: _amount,
|
||||
unit: _activeUnit,
|
||||
@@ -594,6 +618,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 {
|
||||
|
||||
@@ -72,8 +72,13 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
|
||||
}
|
||||
|
||||
void _encodeTokenToUR() {
|
||||
// Parsear el token string a objeto Token de cdk-flutter
|
||||
final token = cdk.Token.parse(encoded: widget.token);
|
||||
// Construir Token directamente sin Token.parse para evitar validación
|
||||
// estricta de keyset IDs. encodeQrToken solo usa token.encoded.
|
||||
final token = cdk.Token(
|
||||
encoded: widget.token,
|
||||
amount: widget.amount,
|
||||
mintUrl: '',
|
||||
);
|
||||
|
||||
// Codificar a fragmentos UR usando cdk-flutter
|
||||
// maxFragmentLength por defecto es 150 bytes
|
||||
|
||||
@@ -249,6 +249,7 @@ class _MintScreenState extends State<MintScreen> {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => _ConfirmationModal(
|
||||
amount: _amount,
|
||||
unit: _activeUnit,
|
||||
|
||||
@@ -404,6 +404,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 +433,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;
|
||||
|
||||
@@ -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,249 @@
|
||||
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';
|
||||
|
||||
/// 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 Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: 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,
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
|
||||
// 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: GestureDetector(
|
||||
onTap: widget.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.cancel,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppDimensions.paddingMedium),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: _canDelete ? widget.onConfirm : null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: _canDelete
|
||||
? AppColors.error
|
||||
: AppColors.error.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
l10n.deleteWallet,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _canDelete
|
||||
? Colors.white
|
||||
: Colors.white.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,721 @@
|
||||
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';
|
||||
|
||||
/// 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 Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: 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,
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
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),
|
||||
),
|
||||
),
|
||||
|
||||
// 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 reclaim = await walletProvider.reclaimPendingProofs();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Sumar monto del reclaim a los detalles de recuperación
|
||||
if (reclaim.count > BigInt.zero) {
|
||||
final activeUnit = walletProvider.activeUnit;
|
||||
final formatted = UnitFormatter.formatBalance(reclaim.amount, activeUnit);
|
||||
final label = UnitFormatter.getUnitLabel(activeUnit);
|
||||
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 reclaim = await walletProvider.reclaimPendingProofs(mintUrl: _selectedMintUrl);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
// Sumar monto del reclaim a los detalles
|
||||
if (reclaim.count > BigInt.zero) {
|
||||
final activeUnit = walletProvider.activeUnit;
|
||||
final formatted = UnitFormatter.formatBalance(reclaim.amount, activeUnit);
|
||||
final label = UnitFormatter.getUnitLabel(activeUnit);
|
||||
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
@@ -10,6 +10,21 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
// These functions are ignored because they are not marked as `pub`: `proofs`
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `fmt`, `from_str`, `try_from`, `try_into`, `try_into`
|
||||
|
||||
/// Create a token from raw proof data without network access.
|
||||
/// Uses CDK's CdkToken::new to produce a valid V4 (cashuB) token.
|
||||
/// `proofs_json` is a JSON array of proofs: [{"id":"...","amount":1,"secret":"...","C":"..."}, ...]
|
||||
Token createOfflineToken({
|
||||
required String mintUrl,
|
||||
required String proofsJson,
|
||||
String? memo,
|
||||
String? unit,
|
||||
}) => RustLib.instance.api.crateApiTokenCreateOfflineToken(
|
||||
mintUrl: mintUrl,
|
||||
proofsJson: proofsJson,
|
||||
memo: memo,
|
||||
unit: unit,
|
||||
);
|
||||
|
||||
List<String> encodeQrToken({required Token token, BigInt? maxFragmentLength}) =>
|
||||
RustLib.instance.api.crateApiTokenEncodeQrToken(
|
||||
token: token,
|
||||
|
||||
@@ -13,6 +13,25 @@ import 'token.dart';
|
||||
// These functions are ignored because they are not marked as `pub`: `mint_url`, `unit`, `update_balance_streams`
|
||||
// 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 +63,16 @@ 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();
|
||||
|
||||
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 +90,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,15 +118,21 @@ 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 count and total amount of proofs recovered.
|
||||
Future<ReclaimResult> reclaimPendingProofs();
|
||||
|
||||
Future<void> recoverIncompleteSagas();
|
||||
|
||||
Future<void> restore();
|
||||
|
||||
Future<Token> send({
|
||||
Future<SendResult> send({
|
||||
required PreparedSend send,
|
||||
String? memo,
|
||||
bool? includeMemo,
|
||||
@@ -178,6 +205,9 @@ class MintQuote {
|
||||
final Token? token;
|
||||
final String? error;
|
||||
|
||||
/// Deterministic transaction ID (set when proofs are available, typically on Issued)
|
||||
final String? transactionId;
|
||||
|
||||
const MintQuote({
|
||||
required this.id,
|
||||
required this.request,
|
||||
@@ -186,6 +216,7 @@ class MintQuote {
|
||||
required this.state,
|
||||
this.token,
|
||||
this.error,
|
||||
this.transactionId,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -196,7 +227,8 @@ class MintQuote {
|
||||
expiry.hashCode ^
|
||||
state.hashCode ^
|
||||
token.hashCode ^
|
||||
error.hashCode;
|
||||
error.hashCode ^
|
||||
transactionId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -209,7 +241,8 @@ class MintQuote {
|
||||
expiry == other.expiry &&
|
||||
state == other.state &&
|
||||
token == other.token &&
|
||||
error == other.error;
|
||||
error == other.error &&
|
||||
transactionId == other.transactionId;
|
||||
}
|
||||
|
||||
enum MintQuoteState { unpaid, paid, issued, error }
|
||||
@@ -235,6 +268,24 @@ class ReceiveOptions {
|
||||
preimages == other.preimages;
|
||||
}
|
||||
|
||||
class ReclaimResult {
|
||||
final BigInt count;
|
||||
final BigInt amount;
|
||||
|
||||
const ReclaimResult({required this.count, required this.amount});
|
||||
|
||||
@override
|
||||
int get hashCode => count.hashCode ^ amount.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ReclaimResult &&
|
||||
runtimeType == other.runtimeType &&
|
||||
count == other.count &&
|
||||
amount == other.amount;
|
||||
}
|
||||
|
||||
class SendOptions {
|
||||
final String? pubkey;
|
||||
final bool? includeFee;
|
||||
@@ -256,6 +307,29 @@ class SendOptions {
|
||||
includeFee == other.includeFee;
|
||||
}
|
||||
|
||||
/// Result of a confirmed send, carrying both the ecash token and the
|
||||
/// deterministic transaction ID so Dart can save metadata without a racy
|
||||
/// listTransactions lookup.
|
||||
class SendResult {
|
||||
final Token token;
|
||||
|
||||
/// Deterministic transaction ID (None if computation failed)
|
||||
final String? transactionId;
|
||||
|
||||
const SendResult({required this.token, this.transactionId});
|
||||
|
||||
@override
|
||||
int get hashCode => token.hashCode ^ transactionId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SendResult &&
|
||||
runtimeType == other.runtimeType &&
|
||||
token == other.token &&
|
||||
transactionId == other.transactionId;
|
||||
}
|
||||
|
||||
class Transaction {
|
||||
final String id;
|
||||
final String mintUrl;
|
||||
|
||||
+860
-104
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,12 +400,18 @@ 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);
|
||||
|
||||
@protected
|
||||
SendOptions dco_decode_send_options(dynamic raw);
|
||||
|
||||
@protected
|
||||
SendResult dco_decode_send_result(dynamic raw);
|
||||
|
||||
@protected
|
||||
SupportedSettings dco_decode_supported_settings(dynamic raw);
|
||||
|
||||
@@ -423,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(
|
||||
@@ -453,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(
|
||||
@@ -483,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(
|
||||
@@ -524,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(
|
||||
@@ -773,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,
|
||||
@@ -781,6 +842,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
SendOptions sse_decode_send_options(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
SendResult sse_decode_send_result(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
SupportedSettings sse_decode_supported_settings(SseDeserializer deserializer);
|
||||
|
||||
@@ -833,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(
|
||||
@@ -868,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(
|
||||
@@ -903,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(
|
||||
@@ -951,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(
|
||||
@@ -1264,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,
|
||||
@@ -1273,6 +1368,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_send_options(SendOptions self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_send_result(SendResult self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_supported_settings(
|
||||
SupportedSettings self,
|
||||
@@ -1395,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,12 +402,18 @@ 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);
|
||||
|
||||
@protected
|
||||
SendOptions dco_decode_send_options(dynamic raw);
|
||||
|
||||
@protected
|
||||
SendResult dco_decode_send_result(dynamic raw);
|
||||
|
||||
@protected
|
||||
SupportedSettings dco_decode_supported_settings(dynamic raw);
|
||||
|
||||
@@ -425,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(
|
||||
@@ -455,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(
|
||||
@@ -485,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(
|
||||
@@ -526,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(
|
||||
@@ -775,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,
|
||||
@@ -783,6 +844,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
SendOptions sse_decode_send_options(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
SendResult sse_decode_send_result(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
SupportedSettings sse_decode_supported_settings(SseDeserializer deserializer);
|
||||
|
||||
@@ -835,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(
|
||||
@@ -870,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(
|
||||
@@ -905,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(
|
||||
@@ -953,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(
|
||||
@@ -1266,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,
|
||||
@@ -1275,6 +1370,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
void sse_encode_send_options(SendOptions self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_send_result(SendResult self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_supported_settings(
|
||||
SupportedSettings self,
|
||||
@@ -1352,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,
|
||||
@@ -1443,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,
|
||||
|
||||
@@ -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,
|
||||
@@ -461,21 +452,33 @@ async fn wait_for_nostr_payment_inner(
|
||||
.await
|
||||
.map_err(|_| Error::Network("Relay connection timed out".to_string()))?;
|
||||
|
||||
// Create the broadcast receiver BEFORE subscribing so we capture historical
|
||||
// events the relay sends back in response to our subscription filter.
|
||||
// (broadcast::Receiver only sees messages sent after its creation)
|
||||
let mut notifications = client.notifications();
|
||||
|
||||
// Subscribe to NIP-17 gift-wrap events (kind 1059) addressed to our ephemeral pubkey
|
||||
let filter = Filter::new().pubkey(pubkey).kind(Kind::GiftWrap);
|
||||
client
|
||||
.subscribe(filter, None)
|
||||
.await
|
||||
.map_err(|e| Error::Cdk(format!("Subscription failed: {e}")))?;
|
||||
|
||||
// Listen for notifications with periodic cancellation check.
|
||||
// Every 5s we check if the Dart stream is still alive by attempting
|
||||
// a sink.add(). If it fails, the Dart side disposed the stream and
|
||||
// we disconnect cleanly.
|
||||
let mut notifications = client.notifications();
|
||||
loop {
|
||||
match tokio::time::timeout(Duration::from_secs(5), notifications.recv()).await {
|
||||
match tokio::time::timeout(Duration::from_secs(1), notifications.recv()).await {
|
||||
Ok(Ok(notification)) => {
|
||||
// Check sink is alive before processing
|
||||
if sink
|
||||
.add(NostrPaymentEvent {
|
||||
state: NostrPaymentState::Waiting,
|
||||
amount: None,
|
||||
error: None,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
client.disconnect().await;
|
||||
return Err(Error::Network("Listener cancelled by client".to_string()));
|
||||
}
|
||||
|
||||
if let nostr_sdk::RelayPoolNotification::Event { event, .. } = notification {
|
||||
// Try to unwrap NIP-17 gift-wrap
|
||||
let unwrapped = match client.unwrap_gift_wrap(&event).await {
|
||||
|
||||
+25
-1
@@ -4,7 +4,10 @@ use std::{
|
||||
};
|
||||
|
||||
use bc_ur::{prelude::CBOR, MultipartDecoder, MultipartEncoder, UR};
|
||||
use cdk::nuts::Token as CdkToken;
|
||||
use cdk::{
|
||||
mint_url::MintUrl,
|
||||
nuts::{CurrencyUnit, Proof, Token as CdkToken},
|
||||
};
|
||||
use cdk_common::{KeySetInfo, Proofs};
|
||||
use flutter_rust_bridge::frb;
|
||||
|
||||
@@ -92,6 +95,27 @@ impl TryInto<CdkToken> for &Token {
|
||||
}
|
||||
}
|
||||
|
||||
// === Offline token creation ===
|
||||
|
||||
/// Create a token from raw proof data without network access.
|
||||
/// Uses CDK's CdkToken::new to produce a valid V4 (cashuB) token.
|
||||
/// `proofs_json` is a JSON array of proofs: [{"id":"...","amount":1,"secret":"...","C":"..."}, ...]
|
||||
#[frb(sync)]
|
||||
pub fn create_offline_token(
|
||||
mint_url: &str,
|
||||
proofs_json: &str,
|
||||
memo: Option<String>,
|
||||
unit: Option<String>,
|
||||
) -> Result<Token, Error> {
|
||||
let mint_url = MintUrl::from_str(mint_url)?;
|
||||
let unit = unit
|
||||
.map(|u| CurrencyUnit::from_str(&u).unwrap_or(CurrencyUnit::Custom(u)))
|
||||
.unwrap_or(CurrencyUnit::Sat);
|
||||
let proofs: Vec<Proof> = serde_json::from_str(proofs_json)?;
|
||||
let token = CdkToken::new(mint_url, proofs, memo, unit);
|
||||
Token::try_from(token)
|
||||
}
|
||||
|
||||
// === QR Multiframe (bc-ur) ===
|
||||
|
||||
pub struct TokenDecoder {
|
||||
|
||||
+322
-102
@@ -12,19 +12,21 @@ use cdk::{
|
||||
wallet::{
|
||||
MeltQuote as CdkMeltQuote, MintQuote as CdkMintQuote, PreparedSend as CdkPreparedSend,
|
||||
ReceiveOptions as CdkReceiveOptions, SendMemo, SendOptions as CdkSendOptions,
|
||||
Wallet as CdkWallet,
|
||||
Wallet as CdkWallet, WalletSubscription,
|
||||
},
|
||||
};
|
||||
use cdk_common::{
|
||||
util::unix_time,
|
||||
wallet::{
|
||||
Transaction as CdkTransaction, TransactionDirection as CdkTransactionDirection,
|
||||
TransactionId,
|
||||
},
|
||||
NotificationPayload,
|
||||
};
|
||||
use cdk_sqlite::WalletSqliteDatabase;
|
||||
use flutter_rust_bridge::frb;
|
||||
use log::info;
|
||||
use tokio::{sync::broadcast, time::sleep};
|
||||
use tokio::sync::broadcast;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::frb_generated::StreamSink;
|
||||
@@ -131,12 +133,12 @@ impl Wallet {
|
||||
send: PreparedSend,
|
||||
memo: Option<String>,
|
||||
include_memo: Option<bool>,
|
||||
) -> Result<Token, Error> {
|
||||
) -> Result<SendResult, Error> {
|
||||
let send_memo = memo.map(|m| SendMemo {
|
||||
memo: m,
|
||||
include_memo: include_memo.unwrap_or_default(),
|
||||
});
|
||||
let token = self
|
||||
let cdk_token = self
|
||||
.inner
|
||||
.confirm_send(
|
||||
send.operation_id,
|
||||
@@ -148,10 +150,35 @@ impl Wallet {
|
||||
send.cdk_send_fee,
|
||||
send_memo,
|
||||
)
|
||||
.await?
|
||||
.to_string();
|
||||
.await?;
|
||||
|
||||
// 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(cdk::wallet::KeysetFilter::Active).await {
|
||||
Ok(keysets) => match cdk_token.proofs(&keysets) {
|
||||
Ok(proofs) => TransactionId::try_from(proofs)
|
||||
.map(|id| id.to_string())
|
||||
.map_err(|e| info!("Failed to compute tx ID from proofs: {e}"))
|
||||
.ok(),
|
||||
Err(e) => {
|
||||
info!("Failed to extract proofs from token: {e}");
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
info!("Failed to fetch keysets for tx ID: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let token_str = cdk_token.to_string();
|
||||
self.update_balance_streams().await;
|
||||
Ok(Token::from_str(&token)?)
|
||||
|
||||
Ok(SendResult {
|
||||
token: Token::from_str(&token_str)?,
|
||||
transaction_id: tx_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn cancel_send(&self, send: PreparedSend) -> Result<(), Error> {
|
||||
@@ -184,113 +211,193 @@ impl Wallet {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Try WebSocket (NUT-17) subscription — if unavailable, HTTP polling still works
|
||||
let subscription = match self
|
||||
.inner
|
||||
.subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote.id.clone()]))
|
||||
.await
|
||||
{
|
||||
Ok(sub) => Some(sub),
|
||||
Err(e) => {
|
||||
info!(
|
||||
"Mint quote {}: WebSocket unavailable, using HTTP polling only: {e}",
|
||||
quote.id
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let _self = self.clone();
|
||||
flutter_rust_bridge::spawn(async move {
|
||||
loop {
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
// Timeout: time until quote expires + 30s buffer, or 1 hour if no expiry
|
||||
let remaining = quote.expiry.saturating_sub(unix_time());
|
||||
let timeout_dur = if quote.expiry == 0 {
|
||||
Duration::from_secs(3600)
|
||||
} else {
|
||||
Duration::from_secs(remaining + 30)
|
||||
};
|
||||
|
||||
// Check if the Dart stream is still alive before polling
|
||||
if sink
|
||||
.add(MintQuote {
|
||||
// Clone for the timeout error path (originals are moved into the async block)
|
||||
let expired_id = quote.id.clone();
|
||||
let expired_request = quote.request.clone();
|
||||
let expired_amount = quote.amount;
|
||||
let expired_expiry = quote.expiry;
|
||||
|
||||
// Detect payment via two parallel paths:
|
||||
// - Path A: WebSocket (NUT-17) — fast when it works (sats on most mints)
|
||||
// - Path B: HTTP polling every 5s — fallback for mints that don't send
|
||||
// WebSocket notifications for all units (e.g. Nutshell 0.20.0 + USD)
|
||||
// First one to detect payment wins.
|
||||
let quote_id_for_poll = quote.id.clone();
|
||||
let poll_wallet = _self.clone();
|
||||
|
||||
// Timeout only covers detection, not minting.
|
||||
// If payment is detected near expiry, mint() must still complete.
|
||||
enum Detected {
|
||||
Paid,
|
||||
Issued,
|
||||
}
|
||||
|
||||
let mut subscription = subscription;
|
||||
let detected = tokio::time::timeout(timeout_dur, async {
|
||||
tokio::select! {
|
||||
// Path A: WebSocket subscription (skipped if unavailable)
|
||||
result = async {
|
||||
let Some(ref mut sub) = subscription else {
|
||||
return std::future::pending::<Detected>().await;
|
||||
};
|
||||
while let Some(event) = sub.recv().await {
|
||||
match event.into_inner() {
|
||||
NotificationPayload::MintQuoteBolt11Response(info)
|
||||
if info.state == CdkMintQuoteState::Paid =>
|
||||
{
|
||||
info!("Mint quote {} paid via WebSocket", quote.id);
|
||||
return Detected::Paid;
|
||||
}
|
||||
NotificationPayload::MintQuoteBolt11Response(info)
|
||||
if info.state == CdkMintQuoteState::Issued =>
|
||||
{
|
||||
return Detected::Issued;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
std::future::pending::<Detected>().await
|
||||
} => result,
|
||||
|
||||
// Path B: HTTP polling fallback
|
||||
result = async {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
match poll_wallet.inner.check_mint_quote_status("e_id_for_poll).await {
|
||||
Ok(q) if q.state == CdkMintQuoteState::Paid => {
|
||||
info!("Mint quote {} paid via HTTP polling", quote_id_for_poll);
|
||||
return Detected::Paid;
|
||||
}
|
||||
Ok(q) if q.state == CdkMintQuoteState::Issued => {
|
||||
return Detected::Issued;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
} => result,
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
// Handle detection result — mint logic runs outside the timeout
|
||||
match detected {
|
||||
Err(_) => {
|
||||
// Timeout: quote expired
|
||||
let _ = sink.add(MintQuote {
|
||||
id: expired_id,
|
||||
request: expired_request,
|
||||
amount: expired_amount.map(|a| a.into()),
|
||||
expiry: Some(expired_expiry),
|
||||
state: MintQuoteState::Error,
|
||||
token: None,
|
||||
error: Some("Quote expired".to_string()),
|
||||
transaction_id: None,
|
||||
});
|
||||
}
|
||||
Ok(Detected::Paid) => {
|
||||
// Notify Dart: payment detected
|
||||
let _ = sink.add(MintQuote {
|
||||
id: quote.id.clone(),
|
||||
request: quote.request.clone(),
|
||||
amount: quote.amount.map(|a| a.into()),
|
||||
expiry: Some(quote.expiry),
|
||||
state: MintQuoteState::Unpaid,
|
||||
state: CdkMintQuoteState::Paid.into(),
|
||||
token: None,
|
||||
error: None,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
info!("Mint polling stopped: Dart stream closed for {}", quote.id);
|
||||
break;
|
||||
}
|
||||
transaction_id: None,
|
||||
});
|
||||
|
||||
info!("Checking mint quote state for {}", quote.id);
|
||||
match _self.inner.check_mint_quote_status("e.id).await {
|
||||
Ok(state_res) => match state_res.state {
|
||||
CdkMintQuoteState::Unpaid => {
|
||||
if state_res.expiry < unix_time() {
|
||||
let _ = sink.add(MintQuote {
|
||||
id: quote.id,
|
||||
request: quote.request,
|
||||
amount: quote.amount.map(|a| a.into()),
|
||||
expiry: Some(state_res.expiry),
|
||||
state: MintQuoteState::Error,
|
||||
token: None,
|
||||
error: Some("Quote expired".to_string()),
|
||||
});
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
CdkMintQuoteState::Issued => {
|
||||
break;
|
||||
}
|
||||
CdkMintQuoteState::Paid => {
|
||||
// Mint the ecash tokens (outside timeout)
|
||||
match _self
|
||||
.inner
|
||||
.mint("e.id, SplitTarget::None, None)
|
||||
.await
|
||||
{
|
||||
Ok(mint_proofs) => {
|
||||
let tx_id = match TransactionId::try_from(
|
||||
mint_proofs.clone(),
|
||||
) {
|
||||
Ok(id) => Some(id.to_string()),
|
||||
Err(e) => {
|
||||
info!("Failed to compute mint tx ID: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let mint_amount =
|
||||
mint_proofs.total_amount().unwrap_or_default();
|
||||
let _ = sink.add(MintQuote {
|
||||
id: quote.id.clone(),
|
||||
request: quote.request.clone(),
|
||||
id: quote.id,
|
||||
request: quote.request,
|
||||
amount: Some(mint_amount.into()),
|
||||
expiry: Some(quote.expiry),
|
||||
state: CdkMintQuoteState::Issued.into(),
|
||||
token: Token::try_from(CdkToken::new(
|
||||
mint_url,
|
||||
mint_proofs,
|
||||
None,
|
||||
unit,
|
||||
))
|
||||
.ok(),
|
||||
error: None,
|
||||
transaction_id: tx_id,
|
||||
});
|
||||
_self.update_balance_streams().await;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = sink.add(MintQuote {
|
||||
id: quote.id,
|
||||
request: quote.request,
|
||||
amount: quote.amount.map(|a| a.into()),
|
||||
expiry: Some(quote.expiry),
|
||||
state: CdkMintQuoteState::Paid.into(),
|
||||
state: MintQuoteState::Error,
|
||||
token: None,
|
||||
error: None,
|
||||
error: Some(e.to_string()),
|
||||
transaction_id: None,
|
||||
});
|
||||
match _self
|
||||
.inner
|
||||
.mint("e.id, SplitTarget::None, None)
|
||||
.await
|
||||
{
|
||||
Ok(mint_proofs) => {
|
||||
let mint_amount =
|
||||
mint_proofs.total_amount().unwrap_or_default();
|
||||
let _ = sink.add(MintQuote {
|
||||
id: quote.id,
|
||||
request: quote.request,
|
||||
amount: Some(mint_amount.into()),
|
||||
expiry: Some(quote.expiry),
|
||||
state: CdkMintQuoteState::Issued.into(),
|
||||
token: Token::try_from(CdkToken::new(
|
||||
mint_url,
|
||||
mint_proofs,
|
||||
None,
|
||||
unit,
|
||||
))
|
||||
.ok(),
|
||||
error: None,
|
||||
});
|
||||
_self.update_balance_streams().await;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = sink.add(MintQuote {
|
||||
id: quote.id,
|
||||
request: quote.request,
|
||||
amount: quote.amount.map(|a| a.into()),
|
||||
expiry: Some(quote.expiry),
|
||||
state: MintQuoteState::Error,
|
||||
token: None,
|
||||
error: Some(e.to_string()),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let _ = sink.add(MintQuote {
|
||||
id: quote.id,
|
||||
request: quote.request,
|
||||
amount: quote.amount.map(|a| a.into()),
|
||||
expiry: Some(quote.expiry),
|
||||
state: MintQuoteState::Error,
|
||||
token: None,
|
||||
error: Some(e.to_string()),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Detected::Issued) => {
|
||||
// Already issued (recovered from previous session)
|
||||
let _ = sink.add(MintQuote {
|
||||
id: quote.id.clone(),
|
||||
request: quote.request.clone(),
|
||||
amount: quote.amount.map(|a| a.into()),
|
||||
expiry: Some(quote.expiry),
|
||||
state: CdkMintQuoteState::Issued.into(),
|
||||
token: None,
|
||||
error: None,
|
||||
transaction_id: None,
|
||||
});
|
||||
_self.update_balance_streams().await;
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
@@ -314,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 ===
|
||||
@@ -384,11 +528,56 @@ impl Wallet {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// === Reclaim orphaned proofs ===
|
||||
|
||||
/// Check pending-spent proofs with the mint and revert unspent ones.
|
||||
/// Returns count and total amount of proofs recovered.
|
||||
pub async fn reclaim_pending_proofs(&self) -> Result<ReclaimResult, Error> {
|
||||
let pending = self.inner.get_pending_spent_proofs().await?;
|
||||
if pending.is_empty() {
|
||||
return Ok(ReclaimResult { count: 0, amount: 0 });
|
||||
}
|
||||
|
||||
// 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?;
|
||||
|
||||
// Collect Y values of proofs the mint says are NOT spent
|
||||
// Only reclaim proofs the mint explicitly reports as Unspent.
|
||||
// Pending proofs (still being processed) must not be unreserved.
|
||||
let unspent_ys: Vec<PublicKey> = states
|
||||
.into_iter()
|
||||
.filter(|s| s.state == ProofState::Unspent)
|
||||
.map(|s| s.y)
|
||||
.collect();
|
||||
|
||||
let count = unspent_ys.len() as u64;
|
||||
let amount: u64 = unspent_ys.iter()
|
||||
.filter_map(|y| amount_by_y.get(y))
|
||||
.sum();
|
||||
|
||||
if count > 0 {
|
||||
// Revert from PendingSpent to Unspent
|
||||
self.inner.unreserve_proofs(unspent_ys).await?;
|
||||
}
|
||||
|
||||
// Always refresh: check_proofs_spent may have removed spent proofs
|
||||
self.update_balance_streams().await;
|
||||
Ok(ReclaimResult { count, amount })
|
||||
}
|
||||
|
||||
// === 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)?)
|
||||
@@ -427,6 +616,15 @@ impl Wallet {
|
||||
// Types
|
||||
// ========================================================================
|
||||
|
||||
/// Result of a confirmed send, carrying both the ecash token and the
|
||||
/// deterministic transaction ID so Dart can save metadata without a racy
|
||||
/// listTransactions lookup.
|
||||
pub struct SendResult {
|
||||
pub token: Token,
|
||||
/// Deterministic transaction ID (None if computation failed)
|
||||
pub transaction_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct MintQuote {
|
||||
pub id: String,
|
||||
pub request: String,
|
||||
@@ -435,6 +633,8 @@ pub struct MintQuote {
|
||||
pub state: MintQuoteState,
|
||||
pub token: Option<Token>,
|
||||
pub error: Option<String>,
|
||||
/// Deterministic transaction ID (set when proofs are available, typically on Issued)
|
||||
pub transaction_id: Option<String>,
|
||||
}
|
||||
|
||||
impl From<CdkMintQuote> for MintQuote {
|
||||
@@ -447,6 +647,7 @@ impl From<CdkMintQuote> for MintQuote {
|
||||
state: quote.state.into(),
|
||||
token: None,
|
||||
error: None,
|
||||
transaction_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -525,6 +726,25 @@ impl<'a> From<CdkPreparedSend<'a>> for PreparedSend {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReclaimResult {
|
||||
pub count: u64,
|
||||
pub amount: u64,
|
||||
}
|
||||
|
||||
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>>,
|
||||
|
||||
+898
-111
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