Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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});
|
||||
}
|
||||
|
||||
+23
-1
@@ -559,5 +559,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"
|
||||
}
|
||||
|
||||
+23
-1
@@ -452,5 +452,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"
|
||||
}
|
||||
|
||||
+39
-1
@@ -572,5 +572,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"
|
||||
}
|
||||
|
||||
+23
-1
@@ -559,5 +559,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"
|
||||
}
|
||||
|
||||
+23
-1
@@ -559,5 +559,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"
|
||||
}
|
||||
|
||||
+23
-1
@@ -559,5 +559,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データを持たないことです"
|
||||
}
|
||||
|
||||
+23
-1
@@ -559,5 +559,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데이터를 갖지 않는 것입니다"
|
||||
}
|
||||
|
||||
@@ -2352,6 +2352,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> {
|
||||
|
||||
@@ -1241,4 +1241,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';
|
||||
}
|
||||
|
||||
@@ -1222,4 +1222,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';
|
||||
}
|
||||
|
||||
@@ -1230,4 +1230,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';
|
||||
}
|
||||
|
||||
@@ -1246,4 +1246,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';
|
||||
}
|
||||
|
||||
@@ -1234,4 +1234,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';
|
||||
}
|
||||
|
||||
@@ -1207,4 +1207,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データを持たないことです';
|
||||
}
|
||||
|
||||
@@ -1209,4 +1209,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데이터를 갖지 않는 것입니다';
|
||||
}
|
||||
|
||||
@@ -1234,4 +1234,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';
|
||||
}
|
||||
|
||||
@@ -1230,4 +1230,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не иметь их';
|
||||
}
|
||||
|
||||
@@ -1231,4 +1231,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';
|
||||
}
|
||||
|
||||
@@ -1202,4 +1202,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就是不拥有它';
|
||||
}
|
||||
|
||||
+23
-1
@@ -559,5 +559,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"
|
||||
}
|
||||
|
||||
+23
-1
@@ -559,5 +559,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не иметь их"
|
||||
}
|
||||
|
||||
+23
-1
@@ -559,5 +559,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"
|
||||
}
|
||||
|
||||
+23
-1
@@ -559,5 +559,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就是不拥有它"
|
||||
}
|
||||
|
||||
@@ -356,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');
|
||||
@@ -407,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
|
||||
// ============================================================
|
||||
@@ -1477,6 +1537,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
|
||||
// ============================================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,10 @@ import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../2_onboarding/backup_seed_screen.dart';
|
||||
import 'mints_screen.dart';
|
||||
import 'privacy_screen.dart';
|
||||
import 'language_screen.dart';
|
||||
import 'p2pk_keys_screen.dart';
|
||||
import '../13_swap/swap_screen.dart';
|
||||
|
||||
/// Pantalla de configuración
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
@@ -77,6 +79,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// Sección WALLET
|
||||
_buildSectionHeader(l10n.walletSection),
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
_buildSettingTile(
|
||||
icon: LucideIcons.arrowLeftRight,
|
||||
title: l10n.swap,
|
||||
subtitle: l10n.swapDescription,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SwapScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildSettingTile(
|
||||
icon: LucideIcons.key,
|
||||
title: l10n.backupSeedPhrase,
|
||||
@@ -106,11 +121,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
value: settingsProvider.pinEnabled,
|
||||
onChanged: (value) =>
|
||||
_togglePin(context, settingsProvider, value),
|
||||
activeColor: AppColors.primaryAction,
|
||||
activeThumbColor: AppColors.primaryAction,
|
||||
),
|
||||
),
|
||||
_buildSettingTile(
|
||||
icon: LucideIcons.refreshCw,
|
||||
icon: LucideIcons.searchCode,
|
||||
title: l10n.recoverTokens,
|
||||
subtitle: l10n.scanMintsWithSeed,
|
||||
onTap: () => _showRecoverTokensDialog(context, settingsProvider),
|
||||
@@ -151,6 +166,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
title: l10n.about,
|
||||
onTap: () => _showAboutDialog(context),
|
||||
),
|
||||
_buildSettingTile(
|
||||
icon: LucideIcons.shield,
|
||||
title: l10n.privacyPolicy,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const PrivacyScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildSettingTile(
|
||||
icon: LucideIcons.github,
|
||||
title: 'GitHub',
|
||||
@@ -472,7 +497,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// Obtener mnemonic
|
||||
final mnemonic = await settingsProvider.getMnemonic();
|
||||
if (mnemonic == null) {
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.mnemonicNotFound),
|
||||
@@ -484,7 +509,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
}
|
||||
|
||||
// Navegar a BackupSeedScreen
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -505,7 +530,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final pin = await _showCreatePinDialog(context);
|
||||
if (pin != null && pin.length == 4) {
|
||||
await settingsProvider.setPin(pin);
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.pinActivated),
|
||||
@@ -519,7 +544,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final verified = await _verifyPinDialog(context, settingsProvider);
|
||||
if (verified) {
|
||||
await settingsProvider.removePin();
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.pinDeactivated),
|
||||
@@ -548,6 +573,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
if (firstPin == null) return null;
|
||||
|
||||
// Confirmar PIN
|
||||
if (!context.mounted) return null;
|
||||
final confirmPin = await showDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
@@ -560,7 +586,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
if (confirmPin == null) return null;
|
||||
|
||||
if (firstPin != confirmPin) {
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.pinMismatch),
|
||||
@@ -591,7 +617,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
if (settingsProvider.verifyPin(pin)) {
|
||||
return true;
|
||||
} else {
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.incorrectPin),
|
||||
@@ -732,7 +758,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
try {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.couldNotOpenLink),
|
||||
@@ -799,7 +825,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
await walletProvider.deleteDatabase();
|
||||
await settingsProvider.deleteWallet();
|
||||
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
// Navegar a welcome screen
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context,
|
||||
@@ -808,7 +834,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.deleteError(e.toString())),
|
||||
@@ -993,7 +1019,9 @@ class _PinDialogState extends State<_PinDialog> {
|
||||
if (_pin.length == 4) {
|
||||
// PIN completo, cerrar con resultado
|
||||
Future.delayed(const Duration(milliseconds: 200), () {
|
||||
Navigator.pop(context, _pin.join());
|
||||
if (mounted) {
|
||||
Navigator.pop(context, _pin.join());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1811,7 +1839,6 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
|
||||
// Escanear todos los mints (retorna Map<String, Map<String, BigInt>>)
|
||||
final results = await walletProvider.restoreAllMints();
|
||||
|
||||
BigInt totalRecovered = BigInt.zero;
|
||||
int mintsScanned = 0;
|
||||
int mintsWithError = 0;
|
||||
final recoveredDetails = <String>[];
|
||||
@@ -1819,15 +1846,12 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
|
||||
for (final mintEntry in results.entries) {
|
||||
final unitBalances = mintEntry.value;
|
||||
bool hasError = false;
|
||||
BigInt mintTotal = BigInt.zero;
|
||||
|
||||
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) {
|
||||
mintTotal += balance;
|
||||
final formatted = UnitFormatter.formatBalance(balance, unit);
|
||||
final label = UnitFormatter.getUnitLabel(unit);
|
||||
recoveredDetails.add('$formatted $label');
|
||||
@@ -1838,7 +1862,6 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
|
||||
mintsWithError++;
|
||||
} else {
|
||||
mintsScanned++;
|
||||
totalRecovered += mintTotal;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+158
-102
@@ -155,7 +155,7 @@ impl Wallet {
|
||||
// Compute the deterministic transaction ID from the token's proofs
|
||||
// (SHA-256 of sorted Y values — same as CDK uses internally)
|
||||
// Best-effort: send is already committed, don't fail on ID computation
|
||||
let tx_id = match self.inner.get_mint_keysets().await {
|
||||
let tx_id = match self.inner.get_mint_keysets(cdk::wallet::KeysetFilter::Active).await {
|
||||
Ok(keysets) => match cdk_token.proofs(&keysets) {
|
||||
Ok(proofs) => TransactionId::try_from(proofs)
|
||||
.map(|id| id.to_string())
|
||||
@@ -211,12 +211,21 @@ impl Wallet {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Subscribe to quote state changes via WebSocket (NUT-17) with HTTP polling fallback
|
||||
let mut subscription = self
|
||||
// Try WebSocket (NUT-17) subscription — if unavailable, HTTP polling still works
|
||||
let subscription = match self
|
||||
.inner
|
||||
.subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote.id.clone()]))
|
||||
.await
|
||||
.map_err(|e| Error::Cdk(e.to_string()))?;
|
||||
{
|
||||
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 {
|
||||
@@ -234,114 +243,161 @@ impl Wallet {
|
||||
let expired_amount = quote.amount;
|
||||
let expired_expiry = quote.expiry;
|
||||
|
||||
let result = tokio::time::timeout(timeout_dur, async {
|
||||
while let Some(event) = subscription.recv().await {
|
||||
match event.into_inner() {
|
||||
NotificationPayload::MintQuoteBolt11Response(info)
|
||||
if info.state == CdkMintQuoteState::Paid =>
|
||||
{
|
||||
info!("Mint quote {} paid via subscription", quote.id);
|
||||
// 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();
|
||||
|
||||
// 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: CdkMintQuoteState::Paid.into(),
|
||||
token: None,
|
||||
error: None,
|
||||
transaction_id: None,
|
||||
});
|
||||
// Timeout only covers detection, not minting.
|
||||
// If payment is detected near expiry, mint() must still complete.
|
||||
enum Detected {
|
||||
Paid,
|
||||
Issued,
|
||||
}
|
||||
|
||||
// Mint the ecash tokens
|
||||
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,
|
||||
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;
|
||||
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;
|
||||
}
|
||||
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()),
|
||||
transaction_id: None,
|
||||
});
|
||||
NotificationPayload::MintQuoteBolt11Response(info)
|
||||
if info.state == CdkMintQuoteState::Issued =>
|
||||
{
|
||||
return Detected::Issued;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
return;
|
||||
}
|
||||
NotificationPayload::MintQuoteBolt11Response(info)
|
||||
if info.state == CdkMintQuoteState::Issued =>
|
||||
{
|
||||
// Already issued (recovered from previous session) — notify Dart
|
||||
// so it can clean up pending metadata and show success UI
|
||||
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;
|
||||
return;
|
||||
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,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
} => result,
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if result.is_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,
|
||||
});
|
||||
// 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: CdkMintQuoteState::Paid.into(),
|
||||
token: None,
|
||||
error: None,
|
||||
transaction_id: None,
|
||||
});
|
||||
|
||||
// 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,
|
||||
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: MintQuoteState::Error,
|
||||
token: None,
|
||||
error: Some(e.to_string()),
|
||||
transaction_id: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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(())
|
||||
@@ -472,7 +528,7 @@ impl Wallet {
|
||||
|
||||
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)?)
|
||||
|
||||
@@ -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