Compare commits

...
Author SHA1 Message Date
Forte11Cuba f66668f0ec fix: normalize privacyBody split to filter empty lines 2026-04-11 02:32:46 -06:00
Forte11Cuba 100a4943eb fix: handle privacyConclusion lines robustly for any locale line count 2026-04-11 02:27:46 -06:00
Forte11Cuba 5b1317c3e4 fix: address CodeRabbit review Portuguese register, scrollable layout 2026-04-11 02:22:28 -06:00
Forte11Cuba d85b6562dc feat: add privacy policy screen with humorous tone 2026-04-11 01:32:43 -06:00
Forte11andGitHub 56585319cc Merge pull request #101 from Forte11Cuba/feat/swap-chart-improvements
feat: improve swap price chart — real data only, 24h stats, min/max
2026-04-11 00:54:52 -06:00
Forte11Cuba 79ca30c5ed fix: filter non-finite and zero price samples before rendering chart 2026-04-11 00:37:57 -06:00
Forte11Cuba 0bd988a7a6 fix: address CodeRabbit review handle 2 samples, use i18n template for minMax 2026-04-11 00:31:53 -06:00
Forte11Cuba 218d798d2a feat: improve swap price chart — real data only, 24h stats, min/max 2026-04-11 00:19:43 -06:00
Forte11andGitHub e8d8789413 Merge pull request #100 from Forte11Cuba/feat/swap-screen
feat: add swap screen UI (sats -> USD) with price chart
2026-04-10 04:47:37 -06:00
Forte11Cuba a2866d035a fix: truncate conversion amounts with floor instead of round to prevent overpaying 2026-04-10 04:44:32 -06:00
Forte11Cuba 1a8e3e601b fix: show mint name in AppBar, make confirmation sheet non-dismissible, hardcode Cuba Bitcoin mint 2026-04-10 04:06:40 -06:00
Forte11Cuba b3dde35141 fix: add try/catch to _loadBalances, localize fee/total labels, remove redundant ternary 2026-04-10 03:48:58 -06:00
Forte11Cuba 8541222da6 fix: remove redundant ternary, capture provider ref outside async callback 2026-04-10 03:26:49 -06:00
Forte11Cuba d6085f7754 fix: improve swap reliability — remove unsafe tx fallback, add mint stream timeout and issued handler 2026-04-10 03:18:27 -06:00
Forte11Cuba d2035ba416 feat: implement swap logic (sats <-> USD) with bidirectional input, fee preview, and mint validation 2026-04-10 02:57:55 -06:00
Forte11Cuba 42bab5d18a feat: add swap screen UI (sats -> USD) with price chart 2026-04-09 04:54:48 -06:00
Forte11andGitHub 6c05785391 Merge pull request #99 from Forte11Cuba/chore/upgrade-cdk-v0.16.0
docs: update CDK version to 0.16.0 in README
2026-04-09 03:57:31 -06:00
Forte11Cuba 65224314e1 docs: update CDK version to 0.16.0 in README 2026-04-09 03:53:02 -06:00
Forte11andGitHub 2534cf5d1e Merge pull request #98 from Forte11Cuba/chore/upgrade-cdk-v0.16.0
chore: upgrade CDK from v0.15.1 to v0.16.0
2026-04-05 20:32:14 -06:00
Forte11Cuba 262b049c20 chore: upgrade CDK from v0.15.1 to v0.16.0 2026-04-05 20:09:55 -06:00
Forte11Cuba 697f6fa555 chore: upgrade CDK from v0.15.1 to v0.16.0 2026-04-05 19:56:34 -06:00
Forte11andGitHub 02fec140e1 Merge pull request #97 from Forte11Cuba/release/v0.2.0
Release APK / build (push) Canceled after 0s
chore: bump version to v0.2.0+3
2026-04-05 16:08:16 -06:00
Forte11Cuba 7cc92d6732 chore: bump version to v0.2.0+3 2026-04-05 15:51:01 -06:00
Forte11andGitHub 013560d25b Merge pull request #96 from Forte11Cuba/chore/rename-package-id
chore: rename package identifier from com.example.elcaju to me.elcaju
2026-04-03 18:44:20 -06:00
Forte11Cuba d19a58df84 chore: rename package identifier from com.example.elcaju to me.elcaju 2026-04-03 17:51:08 -06:00
Forte11andGitHub fd64d2f269 Merge pull request #95 from Forte11Cuba/fix/mint-http-polling-fallback
fix: add HTTP polling fallback for mint quote payment detection
2026-04-03 17:46:23 -06:00
Forte11Cuba 426b63456d fix: move mint logic outside timeout to prevent expired race 2026-04-03 17:25:38 -06:00
Forte11Cuba 4d2ad39323 fix: add HTTP polling fallback for mint quote payment detection 2026-04-03 16:56:22 -06:00
Forte11andGitHub 060c6acd4e Merge pull request #94 from Forte11Cuba/fix/refresh-mint-units-on-startup
fix: refresh mint units on startup via NUT-04
2026-04-03 16:48:24 -06:00
Forte11Cuba 14da243b89 fix: wrap _saveMints in try-catch inside async void method 2026-04-03 16:20:23 -06:00
Forte11Cuba 4f91a2be1c fix: validate activeUnit after refreshing mint units 2026-04-03 15:48:02 -06:00
Forte11Cuba ab13e8f6c2 fix: refresh mint units on startup via NUT-04 2026-04-03 15:40:51 -06:00
Forte11andGitHub f431f825c5 Merge pull request #93 from Forte11Cuba/fix/cache-balance-stream-and-sync-controller
fix: cache balance stream and use sync StreamController
2026-04-03 15:30:20 -06:00
39 changed files with 2865 additions and 178 deletions
+2 -2
View File
@@ -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)
+6 -6
View File
@@ -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;
+56
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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데이터를 갖지 않는 것입니다"
}
+126
View File
@@ -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> {
+72
View File
@@ -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';
}
+71
View File
@@ -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';
}
+72
View File
@@ -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';
}
+72
View File
@@ -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';
}
+72
View File
@@ -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';
}
+69
View File
@@ -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データを持たないことです';
}
+69
View File
@@ -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데이터를 갖지 않는 것입니다';
}
+72
View File
@@ -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';
}
+71
View File
@@ -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не иметь их';
}
+71
View File
@@ -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';
}
+69
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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就是不拥有它"
}
+112
View File
@@ -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
+131
View File
@@ -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,
),
);
}),
],
),
),
),
),
),
),
),
);
}
}
+40 -17
View File
@@ -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;
}
}
+1 -1
View File
@@ -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.
+3 -3
View File
@@ -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";
+2 -2
View File
@@ -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
View File
@@ -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
+39 -15
View File
@@ -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
View File
@@ -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
+3 -12
View File
@@ -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
View File
@@ -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(&quote.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(&quote_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(&quote.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)?)
+2 -2
View File
@@ -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"