Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8251c433c | ||
|
|
812f28d3b9 | ||
|
|
0465034a2b | ||
|
|
9abb2c03e1 | ||
|
|
f842561864 | ||
|
|
5fe1ec5c90 | ||
|
|
336f4316aa | ||
|
|
7f902f7f75 | ||
|
|
bb07a8f2da |
@@ -0,0 +1,61 @@
|
||||
name: Release APK
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
|
||||
|
||||
- name: Install Android NDK
|
||||
run: echo "y" | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "ndk;27.0.12077973"
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Generate localizations
|
||||
run: flutter gen-l10n
|
||||
|
||||
- name: Build release APKs
|
||||
run: flutter build apk --release --split-per-abi
|
||||
|
||||
- name: Rename APKs
|
||||
run: |
|
||||
cd build/app/outputs/flutter-apk
|
||||
mv app-arm64-v8a-release.apk elcaju-${{ github.ref_name }}-arm64.apk
|
||||
mv app-armeabi-v7a-release.apk elcaju-${{ github.ref_name }}-armeabi-v7a.apk
|
||||
mv app-x86_64-release.apk elcaju-${{ github.ref_name }}-x86_64.apk
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-arm64.apk
|
||||
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-armeabi-v7a.apk
|
||||
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-x86_64.apk
|
||||
generate_release_notes: true
|
||||
@@ -2,3 +2,4 @@ arb-dir: lib/l10n
|
||||
template-arb-file: app_es.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-class: L10n
|
||||
synthetic-package: false
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/// Utilidad para leer datos del SQLite de CDK.
|
||||
/// Usado para:
|
||||
/// - Debug de keyset counters
|
||||
/// - Leer input_fee_ppk para cálculo de fees
|
||||
/// - Contar proofs unspent para consolidación P2PK
|
||||
library;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class KeysetDebug {
|
||||
static Database? _db;
|
||||
|
||||
/// Abre la DB de CDK en modo solo lectura.
|
||||
static Future<Database> _getDb() async {
|
||||
if (_db != null && _db!.isOpen) return _db!;
|
||||
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final dbPath = '${dir.path}/elcaju_wallet.sqlite';
|
||||
|
||||
_db = await openDatabase(dbPath, readOnly: true);
|
||||
return _db!;
|
||||
}
|
||||
|
||||
/// Lee todos los keysets con sus counters y los imprime.
|
||||
/// Retorna el counter del keyset activo (si hay uno).
|
||||
static Future<void> logCounters(String label) async {
|
||||
try {
|
||||
final db = await _getDb();
|
||||
|
||||
final rows = await db.rawQuery(
|
||||
'SELECT id, unit, active, counter FROM keyset ORDER BY active DESC, unit ASC',
|
||||
);
|
||||
|
||||
debugPrint('[COUNTER DEBUG] ===== $label =====');
|
||||
debugPrint('[COUNTER DEBUG] Total keysets: ${rows.length}');
|
||||
|
||||
for (final row in rows) {
|
||||
final id = row['id'] as String?;
|
||||
final unit = row['unit'] as String?;
|
||||
final active = row['active'] as int?;
|
||||
final counter = row['counter'] as int?;
|
||||
final shortId = (id != null && id.length > 12) ? id.substring(0, 12) : id;
|
||||
|
||||
debugPrint(
|
||||
'[COUNTER DEBUG] keyset=$shortId unit=$unit active=$active counter=$counter',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[COUNTER DEBUG] Error leyendo counters: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Lee el input_fee_ppk del keyset activo para un mint y unidad.
|
||||
///
|
||||
/// IMPORTANTE: Lee directamente del schema interno de CDK (tabla `keyset`,
|
||||
/// columna `input_fee_ppk`). Escrito para CDK 0.13.4 (cdk-flutter actual).
|
||||
/// Si CDK cambia el schema (ej: en 0.14.x con rusqlite), este query puede
|
||||
/// fallar. En caso de error retorna -1 (asume fees > 0) para bloquear P2PK
|
||||
/// de forma segura — nunca retornar 0 en error porque permitiria P2PK en
|
||||
/// mints con fees, causando perdida de fondos.
|
||||
///
|
||||
/// TODO: Revisar este codigo al actualizar cdk-flutter a CDK 0.14.x.
|
||||
/// Idealmente cdk-flutter deberia exponer getActiveKeyset().inputFeePpk
|
||||
/// via API publica en lugar de leer SQLite directamente.
|
||||
static Future<int> getInputFeePpk(String mintUrl, String unit) async {
|
||||
try {
|
||||
final db = await _getDb();
|
||||
final rows = await db.rawQuery(
|
||||
'SELECT input_fee_ppk FROM keyset WHERE active=1 AND unit=? AND mint_url=?',
|
||||
[unit, mintUrl],
|
||||
);
|
||||
if (rows.isEmpty) {
|
||||
debugPrint('[KEYSET DEBUG] No active keyset found for $mintUrl/$unit');
|
||||
return -1;
|
||||
}
|
||||
return (rows.first['input_fee_ppk'] as int?) ?? 0;
|
||||
} catch (e) {
|
||||
debugPrint('[KEYSET DEBUG] Error leyendo input_fee_ppk: $e');
|
||||
// Fail-safe: asumir fees > 0 para bloquear P2PK ante schema desconocido
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cuenta los proofs UNSPENT de un mint y unidad.
|
||||
/// Retorna 0 si no se encuentra o hay error.
|
||||
static Future<int> getUnspentProofCount(String mintUrl, String unit) async {
|
||||
try {
|
||||
final db = await _getDb();
|
||||
final rows = await db.rawQuery(
|
||||
'SELECT COUNT(*) as count FROM proof WHERE state=? AND mint_url=? AND unit=?',
|
||||
['UNSPENT', mintUrl, unit],
|
||||
);
|
||||
if (rows.isEmpty) return 0;
|
||||
return (rows.first['count'] as int?) ?? 0;
|
||||
} catch (e) {
|
||||
debugPrint('[KEYSET DEBUG] Error contando proofs: $e');
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cierra la DB (llamar al final si es necesario).
|
||||
static Future<void> close() async {
|
||||
await _db?.close();
|
||||
_db = null;
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -501,7 +501,7 @@
|
||||
"p2pkImportNsec": "nsec importieren",
|
||||
"p2pkImport": "Importieren",
|
||||
"p2pkEnterLabel": "Name für diesen Schlüssel",
|
||||
"p2pkLockToKey": "An öffentlichen Schlüssel sperren",
|
||||
"p2pkLockToKey": "Senden mit P2PK-Signatur",
|
||||
"p2pkLockDescription": "Nur der Empfänger kann einlösen",
|
||||
"p2pkReceiverPubkey": "npub1... oder hex (64/66 Zeichen)",
|
||||
"p2pkInvalidPubkey": "Ungültiger öffentlicher Schlüssel",
|
||||
@@ -517,5 +517,6 @@
|
||||
"p2pkErrorInvalidNsec": "Ungültiger nsec",
|
||||
"p2pkErrorKeyAlreadyExists": "Dieser Schlüssel existiert bereits",
|
||||
"p2pkErrorKeyNotFound": "Schlüssel nicht gefunden",
|
||||
"p2pkErrorCannotDeletePrimary": "Primärschlüssel kann nicht gelöscht werden"
|
||||
"p2pkErrorCannotDeletePrimary": "Primärschlüssel kann nicht gelöscht werden",
|
||||
"p2pkSendComingSoon": "Demnächst verfügbar"
|
||||
}
|
||||
|
||||
+3
-2
@@ -382,7 +382,7 @@
|
||||
"p2pkImportNsec": "Import nsec",
|
||||
"p2pkImport": "Import",
|
||||
"p2pkEnterLabel": "Name for this key",
|
||||
"p2pkLockToKey": "Lock to public key",
|
||||
"p2pkLockToKey": "Send with P2PK signature",
|
||||
"p2pkLockDescription": "Only the recipient can claim",
|
||||
"p2pkReceiverPubkey": "npub1... or hex (64/66 chars)",
|
||||
"p2pkInvalidPubkey": "Invalid public key",
|
||||
@@ -398,5 +398,6 @@
|
||||
"p2pkErrorInvalidNsec": "Invalid nsec",
|
||||
"p2pkErrorKeyAlreadyExists": "This key already exists",
|
||||
"p2pkErrorKeyNotFound": "Key not found",
|
||||
"p2pkErrorCannotDeletePrimary": "Cannot delete primary key"
|
||||
"p2pkErrorCannotDeletePrimary": "Cannot delete primary key",
|
||||
"p2pkSendComingSoon": "Coming soon"
|
||||
}
|
||||
|
||||
+3
-2
@@ -502,7 +502,7 @@
|
||||
"p2pkImportNsec": "Importar nsec",
|
||||
"p2pkImport": "Importar",
|
||||
"p2pkEnterLabel": "Nombre para esta clave",
|
||||
"p2pkLockToKey": "Bloquear a clave pública",
|
||||
"p2pkLockToKey": "Envío con firma P2PK",
|
||||
"p2pkLockDescription": "Solo el destinatario podrá reclamar",
|
||||
"p2pkReceiverPubkey": "npub1... o hex (64/66 caracteres)",
|
||||
"p2pkInvalidPubkey": "Clave pública inválida",
|
||||
@@ -518,5 +518,6 @@
|
||||
"p2pkErrorInvalidNsec": "nsec inválido",
|
||||
"p2pkErrorKeyAlreadyExists": "Esta clave ya existe",
|
||||
"p2pkErrorKeyNotFound": "Clave no encontrada",
|
||||
"p2pkErrorCannotDeletePrimary": "No se puede eliminar la clave principal"
|
||||
"p2pkErrorCannotDeletePrimary": "No se puede eliminar la clave principal",
|
||||
"p2pkSendComingSoon": "Disponible próximamente"
|
||||
}
|
||||
|
||||
+3
-2
@@ -501,7 +501,7 @@
|
||||
"p2pkImportNsec": "Importer nsec",
|
||||
"p2pkImport": "Importer",
|
||||
"p2pkEnterLabel": "Nom pour cette clé",
|
||||
"p2pkLockToKey": "Verrouiller à la clé publique",
|
||||
"p2pkLockToKey": "Envoi avec signature P2PK",
|
||||
"p2pkLockDescription": "Seul le destinataire peut réclamer",
|
||||
"p2pkReceiverPubkey": "npub1... ou hex (64/66 caractères)",
|
||||
"p2pkInvalidPubkey": "Clé publique invalide",
|
||||
@@ -517,5 +517,6 @@
|
||||
"p2pkErrorInvalidNsec": "nsec invalide",
|
||||
"p2pkErrorKeyAlreadyExists": "Cette clé existe déjà",
|
||||
"p2pkErrorKeyNotFound": "Clé non trouvée",
|
||||
"p2pkErrorCannotDeletePrimary": "Impossible de supprimer la clé principale"
|
||||
"p2pkErrorCannotDeletePrimary": "Impossible de supprimer la clé principale",
|
||||
"p2pkSendComingSoon": "Bientôt disponible"
|
||||
}
|
||||
|
||||
+3
-2
@@ -501,7 +501,7 @@
|
||||
"p2pkImportNsec": "Importa nsec",
|
||||
"p2pkImport": "Importa",
|
||||
"p2pkEnterLabel": "Nome per questa chiave",
|
||||
"p2pkLockToKey": "Blocca a chiave pubblica",
|
||||
"p2pkLockToKey": "Invio con firma P2PK",
|
||||
"p2pkLockDescription": "Solo il destinatario può riscattare",
|
||||
"p2pkReceiverPubkey": "npub1... o hex (64/66 caratteri)",
|
||||
"p2pkInvalidPubkey": "Chiave pubblica non valida",
|
||||
@@ -517,5 +517,6 @@
|
||||
"p2pkErrorInvalidNsec": "nsec non valido",
|
||||
"p2pkErrorKeyAlreadyExists": "Questa chiave esiste già",
|
||||
"p2pkErrorKeyNotFound": "Chiave non trovata",
|
||||
"p2pkErrorCannotDeletePrimary": "Impossibile eliminare la chiave principale"
|
||||
"p2pkErrorCannotDeletePrimary": "Impossibile eliminare la chiave principale",
|
||||
"p2pkSendComingSoon": "Disponibile prossimamente"
|
||||
}
|
||||
|
||||
+3
-2
@@ -501,7 +501,7 @@
|
||||
"p2pkImportNsec": "nsecをインポート",
|
||||
"p2pkImport": "インポート",
|
||||
"p2pkEnterLabel": "この鍵の名前",
|
||||
"p2pkLockToKey": "公開鍵にロック",
|
||||
"p2pkLockToKey": "P2PK署名で送信",
|
||||
"p2pkLockDescription": "受取人のみが請求可能",
|
||||
"p2pkReceiverPubkey": "npub1... または hex(64/66文字)",
|
||||
"p2pkInvalidPubkey": "無効な公開鍵",
|
||||
@@ -517,5 +517,6 @@
|
||||
"p2pkErrorInvalidNsec": "無効なnsec",
|
||||
"p2pkErrorKeyAlreadyExists": "この鍵は既に存在します",
|
||||
"p2pkErrorKeyNotFound": "鍵が見つかりません",
|
||||
"p2pkErrorCannotDeletePrimary": "プライマリ鍵は削除できません"
|
||||
"p2pkErrorCannotDeletePrimary": "プライマリ鍵は削除できません",
|
||||
"p2pkSendComingSoon": "近日公開"
|
||||
}
|
||||
|
||||
+3
-2
@@ -501,7 +501,7 @@
|
||||
"p2pkImportNsec": "nsec 가져오기",
|
||||
"p2pkImport": "가져오기",
|
||||
"p2pkEnterLabel": "이 키의 이름",
|
||||
"p2pkLockToKey": "공개 키로 잠금",
|
||||
"p2pkLockToKey": "P2PK 서명으로 전송",
|
||||
"p2pkLockDescription": "수신자만 청구 가능",
|
||||
"p2pkReceiverPubkey": "npub1... 또는 hex (64/66자)",
|
||||
"p2pkInvalidPubkey": "유효하지 않은 공개 키",
|
||||
@@ -517,5 +517,6 @@
|
||||
"p2pkErrorInvalidNsec": "유효하지 않은 nsec",
|
||||
"p2pkErrorKeyAlreadyExists": "이 키는 이미 존재합니다",
|
||||
"p2pkErrorKeyNotFound": "키를 찾을 수 없습니다",
|
||||
"p2pkErrorCannotDeletePrimary": "기본 키는 삭제할 수 없습니다"
|
||||
"p2pkErrorCannotDeletePrimary": "기본 키는 삭제할 수 없습니다",
|
||||
"p2pkSendComingSoon": "곧 출시 예정"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -501,7 +501,7 @@
|
||||
"p2pkImportNsec": "Importar nsec",
|
||||
"p2pkImport": "Importar",
|
||||
"p2pkEnterLabel": "Nome para esta chave",
|
||||
"p2pkLockToKey": "Bloquear para chave pública",
|
||||
"p2pkLockToKey": "Envio com assinatura P2PK",
|
||||
"p2pkLockDescription": "Apenas o destinatário pode resgatar",
|
||||
"p2pkReceiverPubkey": "npub1... ou hex (64/66 caracteres)",
|
||||
"p2pkInvalidPubkey": "Chave pública inválida",
|
||||
@@ -517,5 +517,6 @@
|
||||
"p2pkErrorInvalidNsec": "nsec inválido",
|
||||
"p2pkErrorKeyAlreadyExists": "Esta chave já existe",
|
||||
"p2pkErrorKeyNotFound": "Chave não encontrada",
|
||||
"p2pkErrorCannotDeletePrimary": "Não é possível excluir a chave principal"
|
||||
"p2pkErrorCannotDeletePrimary": "Não é possível excluir a chave principal",
|
||||
"p2pkSendComingSoon": "Em breve disponível"
|
||||
}
|
||||
|
||||
+3
-2
@@ -501,7 +501,7 @@
|
||||
"p2pkImportNsec": "Импортировать nsec",
|
||||
"p2pkImport": "Импортировать",
|
||||
"p2pkEnterLabel": "Имя для этого ключа",
|
||||
"p2pkLockToKey": "Заблокировать на публичный ключ",
|
||||
"p2pkLockToKey": "Отправка с подписью P2PK",
|
||||
"p2pkLockDescription": "Только получатель может получить",
|
||||
"p2pkReceiverPubkey": "npub1... или hex (64/66 символов)",
|
||||
"p2pkInvalidPubkey": "Недействительный публичный ключ",
|
||||
@@ -517,5 +517,6 @@
|
||||
"p2pkErrorInvalidNsec": "Недействительный nsec",
|
||||
"p2pkErrorKeyAlreadyExists": "Этот ключ уже существует",
|
||||
"p2pkErrorKeyNotFound": "Ключ не найден",
|
||||
"p2pkErrorCannotDeletePrimary": "Невозможно удалить основной ключ"
|
||||
"p2pkErrorCannotDeletePrimary": "Невозможно удалить основной ключ",
|
||||
"p2pkSendComingSoon": "Скоро будет доступно"
|
||||
}
|
||||
|
||||
+3
-2
@@ -501,7 +501,7 @@
|
||||
"p2pkImportNsec": "Ingiza nsec",
|
||||
"p2pkImport": "Ingiza",
|
||||
"p2pkEnterLabel": "Jina la ufunguo huu",
|
||||
"p2pkLockToKey": "Funga kwa ufunguo wa umma",
|
||||
"p2pkLockToKey": "Tuma kwa saini ya P2PK",
|
||||
"p2pkLockDescription": "Mpokeaji pekee anaweza kudai",
|
||||
"p2pkReceiverPubkey": "npub1... au hex (herufi 64/66)",
|
||||
"p2pkInvalidPubkey": "Ufunguo wa umma batili",
|
||||
@@ -517,5 +517,6 @@
|
||||
"p2pkErrorInvalidNsec": "nsec batili",
|
||||
"p2pkErrorKeyAlreadyExists": "Ufunguo huu tayari upo",
|
||||
"p2pkErrorKeyNotFound": "Ufunguo haujapatikana",
|
||||
"p2pkErrorCannotDeletePrimary": "Haiwezekani kufuta ufunguo mkuu"
|
||||
"p2pkErrorCannotDeletePrimary": "Haiwezekani kufuta ufunguo mkuu",
|
||||
"p2pkSendComingSoon": "Inakuja hivi karibuni"
|
||||
}
|
||||
|
||||
+3
-2
@@ -501,7 +501,7 @@
|
||||
"p2pkImportNsec": "导入nsec",
|
||||
"p2pkImport": "导入",
|
||||
"p2pkEnterLabel": "此密钥的名称",
|
||||
"p2pkLockToKey": "锁定到公钥",
|
||||
"p2pkLockToKey": "P2PK签名发送",
|
||||
"p2pkLockDescription": "只有接收者可以领取",
|
||||
"p2pkReceiverPubkey": "npub1... 或 hex(64/66字符)",
|
||||
"p2pkInvalidPubkey": "无效的公钥",
|
||||
@@ -517,5 +517,6 @@
|
||||
"p2pkErrorInvalidNsec": "无效的nsec",
|
||||
"p2pkErrorKeyAlreadyExists": "此密钥已存在",
|
||||
"p2pkErrorKeyNotFound": "未找到密钥",
|
||||
"p2pkErrorCannotDeletePrimary": "无法删除主密钥"
|
||||
"p2pkErrorCannotDeletePrimary": "无法删除主密钥",
|
||||
"p2pkSendComingSoon": "即将推出"
|
||||
}
|
||||
|
||||
+10
-1
@@ -3,12 +3,13 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import 'core/theme/app_theme.dart';
|
||||
import 'providers/wallet_provider.dart';
|
||||
import 'providers/settings_provider.dart';
|
||||
import 'providers/price_provider.dart';
|
||||
import 'providers/p2pk_provider.dart';
|
||||
import 'widgets/effects/cashu_confetti.dart';
|
||||
import 'screens/1_splash/splash_screen.dart';
|
||||
|
||||
void main() async {
|
||||
@@ -83,6 +84,14 @@ class ElCajuApp extends StatelessWidget {
|
||||
],
|
||||
locale: Locale(settingsProvider.locale),
|
||||
|
||||
builder: (context, child) {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
return CashuConfetti(
|
||||
controller: walletProvider.confettiController,
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
|
||||
home: const SplashScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -10,6 +11,8 @@ import 'package:uuid/uuid.dart';
|
||||
import '../data/transaction_meta_storage.dart';
|
||||
import '../data/pending_token.dart';
|
||||
import '../data/pending_token_storage.dart';
|
||||
import '../core/utils/keyset_debug.dart';
|
||||
import '../widgets/effects/cashu_confetti.dart';
|
||||
|
||||
/// Helper class para info de token parseado
|
||||
class TokenInfo {
|
||||
@@ -39,6 +42,9 @@ class WalletProvider extends ChangeNotifier {
|
||||
/// Storage para tokens pendientes de reclamar (Receive Later)
|
||||
final PendingTokenStorage _pendingTokenStorage = PendingTokenStorage();
|
||||
|
||||
/// Controller global de confetti para celebrar recepciones
|
||||
final CashuConfettiController confettiController = CashuConfettiController();
|
||||
|
||||
/// Generador de UUIDs
|
||||
static const _uuid = Uuid();
|
||||
|
||||
@@ -67,6 +73,7 @@ class WalletProvider extends ChangeNotifier {
|
||||
static const _mintsKey = 'wallet_mints';
|
||||
static const _activeMintKey = 'wallet_active_mint';
|
||||
static const _activeUnitKey = 'wallet_active_unit';
|
||||
static const _pendingMintInvoicesKey = 'pending_mint_invoices';
|
||||
|
||||
/// Mint de Cuba Bitcoin - siempre aparece primero en la lista
|
||||
static const cubaBitcoinMint = 'https://mint.cubabitcoin.org';
|
||||
@@ -846,16 +853,55 @@ class WalletProvider extends ChangeNotifier {
|
||||
? ReceiveOptions(signingKeys: [p2pkPrivateKey])
|
||||
: null;
|
||||
|
||||
// DEBUG: counters antes de receive
|
||||
await KeysetDebug.logCounters('BEFORE receive ($unit)');
|
||||
|
||||
// DEBUG: transacciones ANTES del receive
|
||||
await _debugLogTransactions(wallet, 'BEFORE receive');
|
||||
|
||||
final amount = await wallet.receive(token: token, opts: opts);
|
||||
|
||||
// DEBUG: counters después de receive
|
||||
await KeysetDebug.logCounters('AFTER receive ($unit)');
|
||||
|
||||
// DEBUG: transacciones DESPUÉS del receive (antes de checkPending)
|
||||
await _debugLogTransactions(wallet, 'AFTER receive (before checkPending)');
|
||||
|
||||
// Guardar metadata para la transacción recién creada
|
||||
await _saveMetaForRecentReceive(wallet, encodedToken);
|
||||
|
||||
// Verificar transacciones pendientes para actualizar outgoing pending → settled
|
||||
try {
|
||||
await wallet.checkPendingTransactions();
|
||||
} catch (e) {
|
||||
debugPrint('Check pending after receive failed: $e');
|
||||
}
|
||||
|
||||
// DEBUG: transacciones DESPUÉS de checkPending
|
||||
await _debugLogTransactions(wallet, 'AFTER checkPending');
|
||||
|
||||
debugPrint('Token recibido: $amount $unit en $mintUrl');
|
||||
confettiController.fire();
|
||||
notifyListeners();
|
||||
return amount;
|
||||
}
|
||||
|
||||
/// DEBUG: Lista todas las transacciones de un wallet para diagnóstico.
|
||||
Future<void> _debugLogTransactions(Wallet wallet, String label) async {
|
||||
try {
|
||||
final allTxs = await wallet.listTransactions();
|
||||
debugPrint('[TX DEBUG] ===== $label =====');
|
||||
debugPrint('[TX DEBUG] Total transacciones: ${allTxs.length}');
|
||||
for (final tx in allTxs) {
|
||||
final dir = tx.direction == TransactionDirection.incoming ? 'IN' : 'OUT';
|
||||
final status = tx.status == TransactionStatus.pending ? 'PENDING' : 'SETTLED';
|
||||
debugPrint('[TX DEBUG] $dir ${tx.amount} ${tx.unit} [$status] fee=${tx.fee} id=${tx.id.substring(0, 16)}...');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[TX DEBUG] Error listando transacciones: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Guarda metadata para la transacción de receive más reciente.
|
||||
Future<void> _saveMetaForRecentReceive(Wallet wallet, String tokenEncoded) async {
|
||||
try {
|
||||
@@ -983,38 +1029,91 @@ class WalletProvider extends ChangeNotifier {
|
||||
|
||||
/// Método de conveniencia: prepara y confirma en un solo paso.
|
||||
Future<String> sendTokens(BigInt amount, String? memo) async {
|
||||
final wallet = await getActiveWallet();
|
||||
final balanceBefore = await wallet.balance();
|
||||
debugPrint('[SEND DEBUG] Normal send - balance=$balanceBefore, amount=$amount');
|
||||
await KeysetDebug.logCounters('BEFORE normal send');
|
||||
final prepared = await prepareSend(amount);
|
||||
return await confirmSend(prepared, memo);
|
||||
debugPrint('[SEND DEBUG] prepareSend OK - fee=${prepared.fee}');
|
||||
final token = await confirmSend(prepared, memo);
|
||||
final balanceAfter = await wallet.balance();
|
||||
debugPrint('[SEND DEBUG] confirmSend OK - balance after=$balanceAfter');
|
||||
await KeysetDebug.logCounters('AFTER normal send');
|
||||
return token;
|
||||
}
|
||||
|
||||
/// Envía tokens P2PK.
|
||||
/// Workaround CDK bug: limpia txs pending antes de enviar.
|
||||
/// Ver: https://github.com/cashubtc/cdk-flutter/issues/3
|
||||
Future<String> sendTokensP2pk(BigInt amount, String pubkey, String? memo) async {
|
||||
// Workaround CDK bug: limpiar txs pending y proofs reservados antes de P2PK send
|
||||
await _settlePendingTransactions();
|
||||
/// Verifica si el mint activo soporta P2PK sin el bug de fees.
|
||||
/// Bug CDK: prepare_send con P2PK (force_swap=true) no reserva proofs
|
||||
/// para el swap fee. Solo mints con ppk=0 funcionan correctamente.
|
||||
/// Ver P2PK_SEND_BUG.md para análisis completo.
|
||||
Future<void> _checkP2pkMintCompatibility() async {
|
||||
final mintUrl = _activeMintUrl!;
|
||||
final unit = _activeUnit;
|
||||
final ppk = await KeysetDebug.getInputFeePpk(mintUrl, unit);
|
||||
|
||||
final prepared = await prepareSendP2pk(amount, pubkey);
|
||||
try {
|
||||
return await confirmSend(prepared, memo);
|
||||
} catch (e) {
|
||||
// Liberar proofs reservados si el envío falla
|
||||
try {
|
||||
await cancelSend(prepared);
|
||||
} catch (_) {}
|
||||
rethrow;
|
||||
debugPrint('[P2PK] mintUrl=$mintUrl, unit=$unit, ppk=$ppk');
|
||||
|
||||
if (ppk > 0) {
|
||||
throw Exception(
|
||||
'P2PK no disponible en este mint. '
|
||||
'El mint $mintUrl cobra input fees (ppk=$ppk) y CDK tiene un bug '
|
||||
'que impide P2PK sends en mints con fees. '
|
||||
'Usa un mint con ppk=0 (ej: mint.cubabitcoin.org).',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Intenta liquidar transacciones pending sincronizando con el mint.
|
||||
/// Workaround para bug de CDK donde P2PK sends pending bloquean envíos subsiguientes.
|
||||
Future<void> _settlePendingTransactions() async {
|
||||
/// Envía tokens P2PK (bloqueados a una clave pública).
|
||||
/// Solo funciona en mints con ppk=0 debido a bug en CDK core.
|
||||
/// Ver P2PK_SEND_BUG.md para detalles.
|
||||
Future<String> sendTokensP2pk(BigInt amount, String pubkey, String? memo) async {
|
||||
final wallet = await getActiveWallet();
|
||||
|
||||
// Verificar compatibilidad del mint con P2PK
|
||||
await _checkP2pkMintCompatibility();
|
||||
|
||||
// DEBUG: estado antes del P2PK send
|
||||
final balanceBefore = await wallet.balance();
|
||||
debugPrint('[P2PK DEBUG] ===== BEFORE P2PK SEND =====');
|
||||
debugPrint('[P2PK DEBUG] Balance: $balanceBefore');
|
||||
debugPrint('[P2PK DEBUG] Amount to send: $amount');
|
||||
debugPrint('[P2PK DEBUG] Pubkey: ${pubkey.length > 16 ? pubkey.substring(0, 16) : pubkey}...');
|
||||
|
||||
// DEBUG: counters antes de prepareSend
|
||||
await KeysetDebug.logCounters('BEFORE P2PK prepareSend');
|
||||
|
||||
// Paso 1: prepareSend P2PK
|
||||
debugPrint('[P2PK DEBUG] Calling prepareSendP2pk...');
|
||||
final prepared = await prepareSendP2pk(amount, pubkey);
|
||||
debugPrint('[P2PK DEBUG] prepareSendP2pk OK - fee=${prepared.fee}');
|
||||
|
||||
final balanceAfterPrepare = await wallet.balance();
|
||||
debugPrint('[P2PK DEBUG] Balance after prepare: $balanceAfterPrepare');
|
||||
|
||||
// DEBUG: counters después de prepareSend
|
||||
await KeysetDebug.logCounters('AFTER P2PK prepareSend');
|
||||
|
||||
// Paso 2: confirmSend
|
||||
debugPrint('[P2PK DEBUG] Calling confirmSend...');
|
||||
try {
|
||||
final wallet = await getActiveWallet();
|
||||
await wallet.checkPendingTransactions();
|
||||
await wallet.reclaimReserved();
|
||||
final token = await confirmSend(prepared, memo);
|
||||
final balanceAfterSend = await wallet.balance();
|
||||
debugPrint('[P2PK DEBUG] confirmSend OK!');
|
||||
debugPrint('[P2PK DEBUG] Balance after send: $balanceAfterSend');
|
||||
|
||||
// DEBUG: counters después de send exitoso
|
||||
await KeysetDebug.logCounters('AFTER P2PK send OK');
|
||||
|
||||
return token;
|
||||
} catch (e) {
|
||||
debugPrint('settlePendingTransactions failed (may be offline): $e');
|
||||
final balanceAfterError = await wallet.balance();
|
||||
debugPrint('[P2PK DEBUG] confirmSend FAILED: $e');
|
||||
debugPrint('[P2PK DEBUG] Balance after error: $balanceAfterError');
|
||||
|
||||
// DEBUG: counters después de send fallido
|
||||
await KeysetDebug.logCounters('AFTER P2PK send FAILED');
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1035,34 +1134,161 @@ class WalletProvider extends ChangeNotifier {
|
||||
// MINT (Depositar via Lightning)
|
||||
// ============================================================
|
||||
|
||||
/// Suscripción activa al stream de mint (vive en el provider, no en la UI).
|
||||
StreamSubscription<MintQuote>? _activeMintSubscription;
|
||||
|
||||
/// Controller del stream actual de mint (para cerrar al iniciar uno nuevo).
|
||||
StreamController<MintQuote>? _activeMintController;
|
||||
|
||||
/// Inicia un depósito via Lightning.
|
||||
/// Retorna Stream con estados: unpaid -> paid -> issued.
|
||||
/// Guarda metadata type=lightning cuando se completa.
|
||||
Stream<MintQuote> mintTokens(BigInt amount, String? description) {
|
||||
/// La suscripción al CDK vive en el provider para que los side effects
|
||||
/// (guardar metadata, confetti) ocurran aunque la UI se cierre.
|
||||
Future<Stream<MintQuote>> mintTokens(BigInt amount, String? description) async {
|
||||
final wallet = activeWallet;
|
||||
if (wallet == null) {
|
||||
throw Exception('No hay wallet activo');
|
||||
}
|
||||
|
||||
final mintUrl = _activeMintUrl!;
|
||||
final unit = _activeUnit;
|
||||
String? invoiceBolt11;
|
||||
|
||||
// Wrapper del stream para capturar el invoice y guardar metadata
|
||||
return wallet.mint(
|
||||
// Cerrar controller anterior si existe (evitar leak)
|
||||
if (_activeMintController != null && !_activeMintController!.isClosed) {
|
||||
_activeMintController!.close();
|
||||
}
|
||||
|
||||
// StreamController que la UI puede escuchar y cancelar libremente
|
||||
final controller = StreamController<MintQuote>();
|
||||
_activeMintController = controller;
|
||||
|
||||
// Cancelar suscripción anterior si existe (await evita race de callbacks)
|
||||
await _activeMintSubscription?.cancel();
|
||||
_activeMintSubscription = null;
|
||||
|
||||
// Suscribirse al stream del CDK desde el provider (persiste sin UI)
|
||||
_activeMintSubscription = wallet.mint(
|
||||
amount: amount,
|
||||
description: description,
|
||||
).map((quote) {
|
||||
// Capturar el invoice cuando está en estado unpaid
|
||||
if (quote.state == MintQuoteState.unpaid) {
|
||||
invoiceBolt11 = quote.request;
|
||||
).listen(
|
||||
(quote) {
|
||||
// Guardar invoice temprano en SharedPreferences
|
||||
if (quote.state == MintQuoteState.unpaid) {
|
||||
invoiceBolt11 = quote.request;
|
||||
_savePendingMintInvoice(quote.id, quote.request, mintUrl, unit, amount);
|
||||
}
|
||||
|
||||
// Cuando se completa, guardar metadata, confetti, limpiar pending
|
||||
if (quote.state == MintQuoteState.issued && invoiceBolt11 != null) {
|
||||
_saveMintMetadata(wallet, invoiceBolt11!);
|
||||
_removePendingMintInvoice(quote.id);
|
||||
}
|
||||
|
||||
// Reenviar a la UI (si sigue escuchando)
|
||||
if (!controller.isClosed) {
|
||||
controller.add(quote);
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (!controller.isClosed) {
|
||||
controller.addError(error);
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
_activeMintSubscription = null;
|
||||
if (!controller.isClosed) {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
/// Guarda un invoice de mint pendiente para recuperarlo después.
|
||||
Future<void> _savePendingMintInvoice(
|
||||
String quoteId, String invoice, String mintUrl, String unit, BigInt amount,
|
||||
) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonStr = prefs.getString(_pendingMintInvoicesKey) ?? '{}';
|
||||
final map = Map<String, dynamic>.from(jsonDecode(jsonStr));
|
||||
map[quoteId] = {
|
||||
'invoice': invoice,
|
||||
'mintUrl': mintUrl,
|
||||
'unit': unit,
|
||||
'amount': amount.toString(),
|
||||
'createdAt': DateTime.now().toIso8601String(),
|
||||
};
|
||||
await prefs.setString(_pendingMintInvoicesKey, jsonEncode(map));
|
||||
debugPrint('Pending mint invoice guardado: $quoteId');
|
||||
} catch (e) {
|
||||
debugPrint('Error guardando pending mint invoice: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Elimina un invoice de mint pendiente (ya fue procesado).
|
||||
Future<void> _removePendingMintInvoice(String quoteId) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonStr = prefs.getString(_pendingMintInvoicesKey) ?? '{}';
|
||||
final map = Map<String, dynamic>.from(jsonDecode(jsonStr));
|
||||
map.remove(quoteId);
|
||||
await prefs.setString(_pendingMintInvoicesKey, jsonEncode(map));
|
||||
debugPrint('Pending mint invoice eliminado: $quoteId');
|
||||
} catch (e) {
|
||||
debugPrint('Error eliminando pending mint invoice: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// TTL para invoices de mint pendientes (24 horas).
|
||||
static const _pendingMintInvoiceTtl = Duration(hours: 24);
|
||||
|
||||
/// Obtiene todos los invoices de mint pendientes, limpiando expirados.
|
||||
Future<Map<String, dynamic>> _getPendingMintInvoices() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonStr = prefs.getString(_pendingMintInvoicesKey) ?? '{}';
|
||||
final map = Map<String, dynamic>.from(jsonDecode(jsonStr));
|
||||
|
||||
// Filtrar expirados
|
||||
final now = DateTime.now();
|
||||
final expired = <String>[];
|
||||
for (final entry in map.entries) {
|
||||
final data = Map<String, dynamic>.from(entry.value);
|
||||
final createdAt = DateTime.tryParse(data['createdAt'] ?? '');
|
||||
if (createdAt == null || now.difference(createdAt) > _pendingMintInvoiceTtl) {
|
||||
expired.add(entry.key);
|
||||
}
|
||||
}
|
||||
|
||||
// Cuando se completa, guardar metadata
|
||||
if (quote.state == MintQuoteState.issued && invoiceBolt11 != null) {
|
||||
_saveMintMetadata(wallet, invoiceBolt11!);
|
||||
// Limpiar expirados si hay
|
||||
if (expired.isNotEmpty) {
|
||||
for (final key in expired) {
|
||||
map.remove(key);
|
||||
}
|
||||
await prefs.setString(_pendingMintInvoicesKey, jsonEncode(map));
|
||||
debugPrint('Pending mint invoices expirados eliminados: ${expired.length}');
|
||||
}
|
||||
|
||||
return quote;
|
||||
});
|
||||
return map;
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Busca un invoice pendiente que coincida con un mintUrl y unit.
|
||||
/// Retorna el invoice string o null.
|
||||
Future<String?> findPendingMintInvoice(String mintUrl, String unit) async {
|
||||
final pending = await _getPendingMintInvoices();
|
||||
for (final entry in pending.values) {
|
||||
final data = Map<String, dynamic>.from(entry);
|
||||
if (data['mintUrl'] == mintUrl && data['unit'] == unit) {
|
||||
return data['invoice'] as String?;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Guarda metadata para una transacción de mint (Lightning deposit).
|
||||
@@ -1083,6 +1309,8 @@ class WalletProvider extends ChangeNotifier {
|
||||
),
|
||||
);
|
||||
debugPrint('Mint metadata guardada para tx ${recentTx.id}');
|
||||
confettiController.fire();
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error guardando mint metadata: $e');
|
||||
@@ -1180,8 +1408,20 @@ class WalletProvider extends ChangeNotifier {
|
||||
|
||||
/// Obtiene el tipo de una transacción (cashu o lightning).
|
||||
/// Busca primero en metadata del CDK, luego en storage local.
|
||||
/// Si no hay metadata y es incoming, asume Lightning (los receives Cashu
|
||||
/// siempre guardan metadata inmediatamente).
|
||||
/// Limitación conocida: transacciones anteriores al sistema de metadata
|
||||
/// o donde el guardado falló silenciosamente serían clasificadas como Lightning.
|
||||
TransactionType getTransactionType(Transaction tx) {
|
||||
return _txMetaStorage.getType(tx.id, tx.metadata);
|
||||
final type = _txMetaStorage.getType(tx.id, tx.metadata);
|
||||
// Si el storage devuelve cashu por defecto pero no tiene metadata real,
|
||||
// y la transacción es incoming → probablemente es Lightning
|
||||
if (!_txMetaStorage.has(tx.id) &&
|
||||
tx.metadata['type'] == null &&
|
||||
tx.direction == TransactionDirection.incoming) {
|
||||
return TransactionType.lightning;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/// Obtiene metadata adicional de una transacción.
|
||||
@@ -1200,6 +1440,7 @@ class WalletProvider extends ChangeNotifier {
|
||||
|
||||
/// Verifica proofs pendientes en todos los wallets.
|
||||
/// Llamar en background al iniciar la app.
|
||||
/// También vincula transacciones incoming sin metadata con invoices pendientes.
|
||||
Future<void> checkPendingTransactions() async {
|
||||
for (final wallet in _wallets.values) {
|
||||
try {
|
||||
@@ -1209,6 +1450,59 @@ class WalletProvider extends ChangeNotifier {
|
||||
debugPrint('Check pending failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Vincular transacciones incoming sin metadata con pending invoices
|
||||
await _matchPendingMintInvoices();
|
||||
}
|
||||
|
||||
/// Busca transacciones incoming sin metadata y las vincula con
|
||||
/// invoices de mint pendientes guardados en SharedPreferences.
|
||||
Future<void> _matchPendingMintInvoices() async {
|
||||
try {
|
||||
final pending = await _getPendingMintInvoices();
|
||||
if (pending.isEmpty) return;
|
||||
|
||||
// Obtener todas las transacciones incoming
|
||||
final allTxs = await getAllTransactions(
|
||||
direction: TransactionDirection.incoming,
|
||||
);
|
||||
|
||||
for (final tx in allTxs) {
|
||||
// Solo procesar transacciones sin metadata
|
||||
if (_txMetaStorage.has(tx.id)) continue;
|
||||
|
||||
// Buscar un pending invoice que coincida con mintUrl, unit y amount
|
||||
String? matchedQuoteId;
|
||||
String? matchedInvoice;
|
||||
|
||||
for (final entry in pending.entries) {
|
||||
final data = Map<String, dynamic>.from(entry.value);
|
||||
if (data['mintUrl'] == tx.mintUrl && data['unit'] == tx.unit && data['amount'] == tx.amount.toString()) {
|
||||
matchedQuoteId = entry.key;
|
||||
matchedInvoice = data['invoice'] as String?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedQuoteId != null && matchedInvoice != null) {
|
||||
await _txMetaStorage.save(
|
||||
tx.id,
|
||||
TransactionMeta(
|
||||
type: TransactionType.lightning,
|
||||
invoice: matchedInvoice,
|
||||
),
|
||||
);
|
||||
// Limpiar el pending invoice ya vinculado
|
||||
await _removePendingMintInvoice(matchedQuoteId);
|
||||
pending.remove(matchedQuoteId);
|
||||
debugPrint('Matched pending mint invoice → tx ${tx.id}');
|
||||
confettiController.fire();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error matching pending mint invoices: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifica si un token específico fue gastado.
|
||||
@@ -1544,4 +1838,14 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_activeMintSubscription?.cancel();
|
||||
if (_activeMintController != null && !_activeMintController!.isClosed) {
|
||||
_activeMintController!.close();
|
||||
}
|
||||
confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/utils/incoming_data_parser.dart';
|
||||
import '../../widgets/scanner/qr_scanner_widget.dart';
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
@@ -9,7 +9,6 @@ import '../../core/utils/incoming_data_parser.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/animated_action_button.dart';
|
||||
import '../../widgets/effects/cashu_confetti.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
import '../4_receive/receive_screen.dart';
|
||||
@@ -34,9 +33,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
// Estado local
|
||||
bool _isBalanceVisible = true;
|
||||
|
||||
// Controller para el efecto confeti
|
||||
final CashuConfettiController _confettiController = CashuConfettiController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -46,11 +42,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
/// Dispara confetti global desde WalletProvider
|
||||
void _fireConfetti() => context.read<WalletProvider>().confettiController.fire();
|
||||
|
||||
/// Verifica y reclama automáticamente tokens pendientes
|
||||
Future<void> _checkPendingTokens() async {
|
||||
@@ -64,9 +57,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
final unit = (result['unit'] as String?) ?? walletProvider.activeUnit;
|
||||
|
||||
if (claimed > 0 && mounted) {
|
||||
// Disparar confetti
|
||||
_confettiController.fire();
|
||||
|
||||
// Confetti se dispara globalmente desde WalletProvider.receiveToken
|
||||
// Mostrar snackbar
|
||||
final l10n = L10n.of(context)!;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -84,9 +75,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CashuConfetti(
|
||||
controller: _confettiController,
|
||||
child: GradientBackground(
|
||||
return GradientBackground(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: SafeArea(
|
||||
@@ -122,7 +111,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,7 +149,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
// El Caju (derecha) - toca para confeti
|
||||
GestureDetector(
|
||||
onTap: () => _confettiController.fire(),
|
||||
onTap: () => _fireConfetti(),
|
||||
child: Image.asset(
|
||||
'assets/img/elcajucubano.png',
|
||||
width: 56,
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
@@ -11,7 +11,6 @@ import '../../core/utils/nostr_utils.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/common/primary_button.dart';
|
||||
import '../../widgets/effects/cashu_confetti.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../providers/p2pk_provider.dart';
|
||||
import '../10_scanner/scan_screen.dart';
|
||||
@@ -29,8 +28,6 @@ class ReceiveScreen extends StatefulWidget {
|
||||
|
||||
class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
final TextEditingController _tokenController = TextEditingController();
|
||||
final CashuConfettiController _confettiController = CashuConfettiController();
|
||||
|
||||
// Estado del token
|
||||
bool _isValidToken = false;
|
||||
bool _isProcessing = false;
|
||||
@@ -65,15 +62,12 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
void dispose() {
|
||||
_tokenController.dispose();
|
||||
_manualKeyController.dispose();
|
||||
_confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CashuConfetti(
|
||||
controller: _confettiController,
|
||||
child: GradientBackground(
|
||||
return GradientBackground(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
@@ -96,7 +90,6 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
child: _showSuccess ? _buildSuccessView() : _buildReceiveForm(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1027,8 +1020,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
|
||||
_isProcessing = false;
|
||||
});
|
||||
|
||||
// Disparar confetti
|
||||
_confettiController.fire();
|
||||
// Confetti se dispara globalmente desde WalletProvider.receiveToken
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/models/proof.dart';
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
@@ -276,134 +276,57 @@ class _SendScreenState extends State<SendScreen> {
|
||||
Widget _buildP2PKSection() {
|
||||
final l10n = L10n.of(context)!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Toggle P2PK
|
||||
GlassCard(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimensions.paddingMedium,
|
||||
vertical: AppDimensions.paddingSmall,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.lock,
|
||||
color: _useP2PK ? AppColors.primaryAction : AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.p2pkLockToKey,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
l10n.p2pkLockDescription,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: _useP2PK,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_useP2PK = value;
|
||||
if (!value) {
|
||||
_pubkeyController.clear();
|
||||
_pubkeyError = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
activeColor: AppColors.primaryAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
// P2PK Send deshabilitado temporalmente por bug en CDK (cdk-flutter usa CDK 0.13.4).
|
||||
// Se habilitará cuando cdk-flutter actualice a CDK 0.14.x con el fix de include_fee.
|
||||
const bool p2pkSendEnabled = false;
|
||||
|
||||
return Opacity(
|
||||
opacity: p2pkSendEnabled ? 1.0 : 0.5,
|
||||
child: GlassCard(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimensions.paddingMedium,
|
||||
vertical: AppDimensions.paddingSmall,
|
||||
),
|
||||
|
||||
// Campo pubkey (visible solo si P2PK está activo)
|
||||
if (_useP2PK) ...[
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
|
||||
GlassCard(
|
||||
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
|
||||
child: TextField(
|
||||
controller: _pubkeyController,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.p2pkReceiverPubkey,
|
||||
hintStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
errorText: _pubkeyError,
|
||||
errorStyle: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.error,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
LucideIcons.key,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
size: 18,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
LucideIcons.clipboard,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
size: 18,
|
||||
),
|
||||
onPressed: _pastePubkey,
|
||||
),
|
||||
),
|
||||
onChanged: _validatePubkey,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.lock,
|
||||
color: AppColors.textSecondary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: AppDimensions.paddingSmall),
|
||||
|
||||
// Advertencia experimental
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.alertTriangle,
|
||||
size: 14,
|
||||
color: AppColors.warning.withValues(alpha: 0.8),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.p2pkExperimental,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 11,
|
||||
color: AppColors.warning.withValues(alpha: 0.8),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.p2pkLockToKey,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: p2pkSendEnabled ? Colors.white : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
l10n.p2pkSendComingSoon,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
Switch(
|
||||
value: false,
|
||||
onChanged: null,
|
||||
activeColor: AppColors.primaryAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -590,17 +513,6 @@ class _SendScreenState extends State<SendScreen> {
|
||||
|
||||
// P2PK: bloquear a clave pública si está habilitado
|
||||
if (_useP2PK && _pubkeyController.text.isNotEmpty) {
|
||||
// Verificar si hay txs P2PK pending (workaround CDK bug)
|
||||
final hasPending = await walletProvider.hasPendingOutgoingTransactions();
|
||||
if (hasPending) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_errorMessage = l10n.p2pkPendingSendWarning;
|
||||
_isProcessing = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Usar normalizeToCompressedHex para obtener formato SEC1 (66 chars)
|
||||
final pubkeyHex = NostrUtils.normalizeToCompressedHex(_pubkeyController.text);
|
||||
if (pubkeyHex == null) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' as cdk;
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
|
||||
@@ -5,13 +5,12 @@ import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
import '../../widgets/common/glass_card.dart';
|
||||
import '../../widgets/effects/cashu_confetti.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
|
||||
/// Estados del proceso de mint
|
||||
@@ -39,7 +38,6 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
String? _invoice;
|
||||
String? _errorMessage;
|
||||
StreamSubscription<MintQuote>? _mintSubscription;
|
||||
final CashuConfettiController _confettiController = CashuConfettiController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -50,15 +48,14 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_mintSubscription?.cancel();
|
||||
_confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startMintProcess() {
|
||||
Future<void> _startMintProcess() async {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
|
||||
try {
|
||||
final mintStream = walletProvider.mintTokens(
|
||||
final mintStream = await walletProvider.mintTokens(
|
||||
widget.amount,
|
||||
widget.description,
|
||||
);
|
||||
@@ -107,8 +104,7 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
}
|
||||
|
||||
void _onMintCompleted() {
|
||||
// Disparar confetti inmediatamente
|
||||
_confettiController.fire();
|
||||
// Confetti se dispara globalmente desde WalletProvider._saveMintMetadata
|
||||
|
||||
// Esperar a que termine el confetti antes de navegar
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
@@ -133,25 +129,19 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CashuConfetti(
|
||||
controller: _confettiController,
|
||||
child: GradientBackground(
|
||||
child: PopScope(
|
||||
canPop: _status == MintStatus.issued || _status == MintStatus.error,
|
||||
child: Scaffold(
|
||||
return GradientBackground(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: (_status == MintStatus.issued || _status == MintStatus.error)
|
||||
? IconButton(
|
||||
icon: const Icon(
|
||||
LucideIcons.arrowLeft,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
)
|
||||
: null,
|
||||
leading: IconButton(
|
||||
icon: const Icon(
|
||||
LucideIcons.arrowLeft,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context)!.payInvoiceTitle,
|
||||
style: const TextStyle(
|
||||
@@ -181,9 +171,7 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
|
||||
child: _buildContent(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' show MintInfo, ContactInfo;
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../widgets/common/gradient_background.dart';
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' show MintInfo;
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' as cdk;
|
||||
import 'package:cdk_flutter/cdk_flutter.dart' show Transaction, TransactionDirection, TransactionStatus;
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/constants/dimensions.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
@@ -704,6 +704,11 @@ class _TransactionDetailScreenState extends State<_TransactionDetailScreen> {
|
||||
final meta = widget.walletProvider.getTransactionMeta(widget.transaction.id);
|
||||
_tokenOrInvoice = meta?.token ?? meta?.invoice;
|
||||
|
||||
// Si es Lightning incoming sin invoice, buscar en pending mint invoices
|
||||
if (_tokenOrInvoice == null && _isLightning && _isIncoming) {
|
||||
_loadPendingMintInvoice();
|
||||
}
|
||||
|
||||
// Mostrar QR para todo EXCEPTO Lightning saliente
|
||||
_shouldShowQR = !_isLightning || _isIncoming;
|
||||
|
||||
@@ -713,6 +718,19 @@ class _TransactionDetailScreenState extends State<_TransactionDetailScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Busca el invoice en pending mint invoices si no hay metadata.
|
||||
Future<void> _loadPendingMintInvoice() async {
|
||||
final invoice = await widget.walletProvider.findPendingMintInvoice(
|
||||
widget.transaction.mintUrl,
|
||||
widget.transaction.unit,
|
||||
);
|
||||
if (invoice != null && mounted) {
|
||||
setState(() {
|
||||
_tokenOrInvoice = invoice;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationTimer?.cancel();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:elcaju/l10n/app_localizations.dart';
|
||||
import 'package:cdk_flutter/cdk_flutter.dart';
|
||||
import '../../core/constants/colors.dart';
|
||||
import '../../core/utils/incoming_data_parser.dart';
|
||||
|
||||
Reference in New Issue
Block a user