Compare commits

...
Author SHA1 Message Date
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
32 changed files with 2162 additions and 47 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)
+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});
}
+17 -1
View File
@@ -559,5 +559,21 @@
"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}"
}
+17 -1
View File
@@ -452,5 +452,21 @@
"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}"
}
+33 -1
View File
@@ -572,5 +572,37 @@
"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" }
}
}
}
+17 -1
View File
@@ -559,5 +559,21 @@
"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}"
}
+17 -1
View File
@@ -559,5 +559,21 @@
"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}"
}
+17 -1
View File
@@ -559,5 +559,21 @@
"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}"
}
+17 -1
View File
@@ -559,5 +559,21 @@
"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}"
}
+90
View File
@@ -2352,6 +2352,96 @@ 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);
}
class _L10nDelegate extends LocalizationsDelegate<L10n> {
+52
View File
@@ -1241,4 +1241,56 @@ 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';
}
}
+51
View File
@@ -1222,4 +1222,55 @@ 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';
}
}
+52
View File
@@ -1230,4 +1230,56 @@ 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';
}
}
+52
View File
@@ -1246,4 +1246,56 @@ 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';
}
}
+52
View File
@@ -1234,4 +1234,56 @@ 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';
}
}
+51
View File
@@ -1207,4 +1207,55 @@ 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';
}
}
+51
View File
@@ -1209,4 +1209,55 @@ 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';
}
}
+52
View File
@@ -1234,4 +1234,56 @@ 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';
}
}
+51
View File
@@ -1230,4 +1230,55 @@ 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';
}
}
+51
View File
@@ -1231,4 +1231,55 @@ 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';
}
}
+51
View File
@@ -1202,4 +1202,55 @@ 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';
}
}
+17 -1
View File
@@ -559,5 +559,21 @@
"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}"
}
+17 -1
View File
@@ -559,5 +559,21 @@
"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}"
}
+17 -1
View File
@@ -559,5 +559,21 @@
"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}"
}
+17 -1
View File
@@ -559,5 +559,21 @@
"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}"
}
+52
View File
@@ -1537,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
+15 -1
View File
@@ -15,6 +15,7 @@ import '../2_onboarding/backup_seed_screen.dart';
import 'mints_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 +78,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,
@@ -110,7 +124,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
_buildSettingTile(
icon: LucideIcons.refreshCw,
icon: LucideIcons.searchCode,
title: l10n.recoverTokens,
subtitle: l10n.scanMintsWithSeed,
onTap: () => _showRecoverTokensDialog(context, settingsProvider),
+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,
+2 -2
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())
@@ -528,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)?)