Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9a70bf1ed | ||
|
|
22434245f2 | ||
|
|
fb76bde5b7 | ||
|
|
365047e73c | ||
|
|
c374c042c7 | ||
|
|
0f7e89a687 | ||
|
|
73582c4a69 | ||
|
|
3e9fea34eb | ||
|
|
348ef1d987 | ||
|
|
fc72bb33b2 | ||
|
|
7a9ce65e55 | ||
|
|
604dd65266 | ||
|
|
42f59e877f | ||
|
|
8b0e0dcd15 | ||
|
|
0ac2a8b3ad | ||
|
|
a97d7d45bc | ||
|
|
bf2c4771dd | ||
|
|
5821ea2be5 | ||
|
|
36b900113f | ||
|
|
91078729aa | ||
|
|
61293d991a | ||
|
|
4cb37a8de2 | ||
|
|
1b5045b5ef | ||
|
|
7a1cb21d35 | ||
|
|
b8649348c8 | ||
|
|
d9de23b425 | ||
|
|
28d95fdae0 | ||
|
|
3b72367ff1 | ||
|
|
a8251c433c | ||
|
|
812f28d3b9 | ||
|
|
0465034a2b | ||
|
|
9abb2c03e1 | ||
|
|
f842561864 | ||
|
|
5fe1ec5c90 | ||
|
|
336f4316aa | ||
|
|
7f902f7f75 | ||
|
|
bb07a8f2da |
@@ -0,0 +1,68 @@
|
||||
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 (split per ABI)
|
||||
run: flutter build apk --release --split-per-abi
|
||||
|
||||
- name: Build universal APK
|
||||
run: flutter build apk --release
|
||||
|
||||
- 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
|
||||
mv app-release.apk elcaju-${{ github.ref_name }}-universal.apk
|
||||
cp elcaju-${{ github.ref_name }}-universal.apk elcaju-universal.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
|
||||
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-universal.apk
|
||||
build/app/outputs/flutter-apk/elcaju-universal.apk
|
||||
generate_release_notes: true
|
||||
@@ -44,3 +44,6 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Rust/CargoKit compiled native libraries
|
||||
android/app/src/main/jniLibs/
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(du brauchst 12 oder 24)",
|
||||
"restoreScanningMint": "Mint wird nach vorhandenen Token durchsucht...",
|
||||
"restoreError": "Wiederherstellungsfehler: {error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "Löschen",
|
||||
|
||||
"offlineSend": "Offline senden",
|
||||
"selectAll": "Alle",
|
||||
"deselectAll": "Keine",
|
||||
"selectNotesToSend": "Wähle die zu sendenden Notizen:",
|
||||
"totalToSend": "Gesamt zu senden",
|
||||
"notesSelected": "{count} Notizen ausgewählt",
|
||||
@@ -501,7 +504,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",
|
||||
|
||||
+4
-1
@@ -47,6 +47,7 @@
|
||||
"seedPlaceholder": "word1 word2 word3 ...",
|
||||
"wordCount": "{count} words",
|
||||
"needWords": "(you need 12 or 24)",
|
||||
"restoreScanningMint": "Scanning mint for existing tokens...",
|
||||
"restoreError": "Restore error: {error}",
|
||||
|
||||
"homeTitle": "Home",
|
||||
@@ -296,6 +297,8 @@
|
||||
"delete": "Delete",
|
||||
|
||||
"offlineSend": "Offline Send",
|
||||
"selectAll": "All",
|
||||
"deselectAll": "None",
|
||||
"selectNotesToSend": "Select the notes you want to send:",
|
||||
"totalToSend": "Total to send",
|
||||
"notesSelected": "{count} notes selected",
|
||||
@@ -382,7 +385,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",
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(necesitas 12 o 24)",
|
||||
"restoreScanningMint": "Escaneando mint en busca de tokens...",
|
||||
"restoreError": "Error al restaurar: {error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "Eliminar",
|
||||
|
||||
"offlineSend": "Envío Offline",
|
||||
"selectAll": "Todo",
|
||||
"deselectAll": "Ninguno",
|
||||
"selectNotesToSend": "Selecciona las notas que deseas enviar:",
|
||||
"totalToSend": "Total a enviar",
|
||||
"notesSelected": "{count} notas seleccionadas",
|
||||
@@ -502,7 +505,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",
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(vous avez besoin de 12 ou 24)",
|
||||
"restoreScanningMint": "Recherche de tokens sur le mint...",
|
||||
"restoreError": "Erreur de restauration : {error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "Supprimer",
|
||||
|
||||
"offlineSend": "Envoi hors ligne",
|
||||
"selectAll": "Tout",
|
||||
"deselectAll": "Aucun",
|
||||
"selectNotesToSend": "Sélectionnez les notes à envoyer :",
|
||||
"totalToSend": "Total à envoyer",
|
||||
"notesSelected": "{count} notes sélectionnées",
|
||||
@@ -501,7 +504,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",
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(servono 12 o 24)",
|
||||
"restoreScanningMint": "Scansione del mint per token esistenti...",
|
||||
"restoreError": "Errore di ripristino: {error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "Elimina",
|
||||
|
||||
"offlineSend": "Invio offline",
|
||||
"selectAll": "Tutto",
|
||||
"deselectAll": "Nessuno",
|
||||
"selectNotesToSend": "Seleziona le note da inviare:",
|
||||
"totalToSend": "Totale da inviare",
|
||||
"notesSelected": "{count} note selezionate",
|
||||
@@ -501,7 +504,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",
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(12語または24語が必要)",
|
||||
"restoreScanningMint": "ミントで既存のトークンをスキャン中...",
|
||||
"restoreError": "復元エラー:{error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "削除",
|
||||
|
||||
"offlineSend": "オフライン送金",
|
||||
"selectAll": "すべて",
|
||||
"deselectAll": "なし",
|
||||
"selectNotesToSend": "送信するノートを選択:",
|
||||
"totalToSend": "送金合計",
|
||||
"notesSelected": "{count}件のノートを選択",
|
||||
@@ -501,7 +504,7 @@
|
||||
"p2pkImportNsec": "nsecをインポート",
|
||||
"p2pkImport": "インポート",
|
||||
"p2pkEnterLabel": "この鍵の名前",
|
||||
"p2pkLockToKey": "公開鍵にロック",
|
||||
"p2pkLockToKey": "P2PK署名で送信",
|
||||
"p2pkLockDescription": "受取人のみが請求可能",
|
||||
"p2pkReceiverPubkey": "npub1... または hex(64/66文字)",
|
||||
"p2pkInvalidPubkey": "無効な公開鍵",
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(12개 또는 24개 필요)",
|
||||
"restoreScanningMint": "민트에서 기존 토큰 검색 중...",
|
||||
"restoreError": "복구 오류: {error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "삭제",
|
||||
|
||||
"offlineSend": "오프라인 전송",
|
||||
"selectAll": "전체",
|
||||
"deselectAll": "없음",
|
||||
"selectNotesToSend": "보낼 노트를 선택하세요:",
|
||||
"totalToSend": "보낼 총액",
|
||||
"notesSelected": "{count}개 노트 선택됨",
|
||||
@@ -501,7 +504,7 @@
|
||||
"p2pkImportNsec": "nsec 가져오기",
|
||||
"p2pkImport": "가져오기",
|
||||
"p2pkEnterLabel": "이 키의 이름",
|
||||
"p2pkLockToKey": "공개 키로 잠금",
|
||||
"p2pkLockToKey": "P2PK 서명으로 전송",
|
||||
"p2pkLockDescription": "수신자만 청구 가능",
|
||||
"p2pkReceiverPubkey": "npub1... 또는 hex (64/66자)",
|
||||
"p2pkInvalidPubkey": "유효하지 않은 공개 키",
|
||||
|
||||
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
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(você precisa de 12 ou 24)",
|
||||
"restoreScanningMint": "Escaneando mint em busca de tokens...",
|
||||
"restoreError": "Erro ao restaurar: {error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "Excluir",
|
||||
|
||||
"offlineSend": "Envio Offline",
|
||||
"selectAll": "Tudo",
|
||||
"deselectAll": "Nenhum",
|
||||
"selectNotesToSend": "Selecione as notas que deseja enviar:",
|
||||
"totalToSend": "Total a enviar",
|
||||
"notesSelected": "{count} notas selecionadas",
|
||||
@@ -501,7 +504,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",
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(нужно 12 или 24)",
|
||||
"restoreScanningMint": "Сканирование минта на наличие токенов...",
|
||||
"restoreError": "Ошибка восстановления: {error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "Удалить",
|
||||
|
||||
"offlineSend": "Офлайн отправка",
|
||||
"selectAll": "Все",
|
||||
"deselectAll": "Ничего",
|
||||
"selectNotesToSend": "Выберите заметки для отправки:",
|
||||
"totalToSend": "Итого к отправке",
|
||||
"notesSelected": "{count} заметок выбрано",
|
||||
@@ -501,7 +504,7 @@
|
||||
"p2pkImportNsec": "Импортировать nsec",
|
||||
"p2pkImport": "Импортировать",
|
||||
"p2pkEnterLabel": "Имя для этого ключа",
|
||||
"p2pkLockToKey": "Заблокировать на публичный ключ",
|
||||
"p2pkLockToKey": "Отправка с подписью P2PK",
|
||||
"p2pkLockDescription": "Только получатель может получить",
|
||||
"p2pkReceiverPubkey": "npub1... или hex (64/66 символов)",
|
||||
"p2pkInvalidPubkey": "Недействительный публичный ключ",
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(unahitaji 12 au 24)",
|
||||
"restoreScanningMint": "Inatafuta tokeni zilizopo kwenye mint...",
|
||||
"restoreError": "Hitilafu ya kurejesha: {error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "Futa",
|
||||
|
||||
"offlineSend": "Tuma Nje ya Mtandao",
|
||||
"selectAll": "Zote",
|
||||
"deselectAll": "Hakuna",
|
||||
"selectNotesToSend": "Chagua noti unazotaka kutuma:",
|
||||
"totalToSend": "Jumla ya kutuma",
|
||||
"notesSelected": "noti {count} zimechaguliwa",
|
||||
@@ -501,7 +504,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",
|
||||
|
||||
+4
-1
@@ -52,6 +52,7 @@
|
||||
}
|
||||
},
|
||||
"needWords": "(需要12或24个)",
|
||||
"restoreScanningMint": "正在扫描铸造厂中的现有代币...",
|
||||
"restoreError": "恢复错误:{error}",
|
||||
"@restoreError": {
|
||||
"placeholders": {
|
||||
@@ -401,6 +402,8 @@
|
||||
"delete": "删除",
|
||||
|
||||
"offlineSend": "离线发送",
|
||||
"selectAll": "全部",
|
||||
"deselectAll": "无",
|
||||
"selectNotesToSend": "选择要发送的票据:",
|
||||
"totalToSend": "发送总额",
|
||||
"notesSelected": "已选择 {count} 张票据",
|
||||
@@ -501,7 +504,7 @@
|
||||
"p2pkImportNsec": "导入nsec",
|
||||
"p2pkImport": "导入",
|
||||
"p2pkEnterLabel": "此密钥的名称",
|
||||
"p2pkLockToKey": "锁定到公钥",
|
||||
"p2pkLockToKey": "P2PK签名发送",
|
||||
"p2pkLockDescription": "只有接收者可以领取",
|
||||
"p2pkReceiverPubkey": "npub1... 或 hex(64/66字符)",
|
||||
"p2pkInvalidPubkey": "无效的公钥",
|
||||
|
||||
+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 {
|
||||
@@ -921,11 +967,12 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Prepara un envío P2PK (bloqueado a una clave pública).
|
||||
/// includeFee: true asegura que se reserven proofs para cubrir el swap fee (CDK 0.15+).
|
||||
Future<PreparedSend> prepareSendP2pk(BigInt amount, String pubkey) async {
|
||||
final wallet = await getActiveWallet();
|
||||
return await wallet.prepareSend(
|
||||
amount: amount,
|
||||
opts: SendOptions(pubkey: pubkey),
|
||||
opts: SendOptions(pubkey: pubkey, includeFee: true),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -982,39 +1029,42 @@ class WalletProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Método de conveniencia: prepara y confirma en un solo paso.
|
||||
/// Si confirmSend falla, libera proofs reservados con cancelSend.
|
||||
Future<String> sendTokens(BigInt amount, String? memo) async {
|
||||
final prepared = await prepareSend(amount);
|
||||
return await confirmSend(prepared, memo);
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
final prepared = await prepareSendP2pk(amount, pubkey);
|
||||
debugPrint('[SEND] prepareSend OK - fee=${prepared.fee}');
|
||||
try {
|
||||
return await confirmSend(prepared, memo);
|
||||
final token = await confirmSend(prepared, memo);
|
||||
debugPrint('[SEND] Send completed');
|
||||
return token;
|
||||
} catch (e) {
|
||||
// Liberar proofs reservados si el envío falla
|
||||
try {
|
||||
await cancelSend(prepared);
|
||||
} catch (_) {}
|
||||
} catch (cancelErr) {
|
||||
debugPrint('[SEND] cancelSend failed: $cancelErr');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
/// CDK 0.15+ con includeFee: true maneja correctamente mints con ppk>0.
|
||||
/// Si confirmSend falla, libera proofs reservados con cancelSend.
|
||||
Future<String> sendTokensP2pk(BigInt amount, String pubkey, String? memo) async {
|
||||
debugPrint('[P2PK] Sending $amount to ${pubkey.length > 16 ? pubkey.substring(0, 16) : pubkey}...');
|
||||
final prepared = await prepareSendP2pk(amount, pubkey);
|
||||
debugPrint('[P2PK] prepareSend OK - fee=${prepared.fee}');
|
||||
try {
|
||||
final wallet = await getActiveWallet();
|
||||
await wallet.checkPendingTransactions();
|
||||
await wallet.reclaimReserved();
|
||||
final token = await confirmSend(prepared, memo);
|
||||
debugPrint('[P2PK] Send completed');
|
||||
return token;
|
||||
} catch (e) {
|
||||
debugPrint('settlePendingTransactions failed (may be offline): $e');
|
||||
try {
|
||||
await cancelSend(prepared);
|
||||
} catch (cancelErr) {
|
||||
debugPrint('[P2PK] cancelSend failed: $cancelErr');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1035,34 +1085,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 +1260,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 +1359,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 +1391,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 +1401,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 +1789,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';
|
||||
@@ -25,6 +25,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
bool _isRestoring = false;
|
||||
String? _errorMessage;
|
||||
String? _statusMessage;
|
||||
|
||||
int get _wordCount {
|
||||
final text = _seedController.text.trim();
|
||||
@@ -56,18 +57,19 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
|
||||
});
|
||||
|
||||
try {
|
||||
final mnemonic = _seedController.text.trim().toLowerCase();
|
||||
final mnemonic =
|
||||
_seedController.text.trim().toLowerCase().replaceAll(RegExp(r'\s+'), ' ');
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final settingsProvider = context.read<SettingsProvider>();
|
||||
final p2pkProvider = context.read<P2PKProvider>();
|
||||
|
||||
// Guardar mnemonic de forma segura
|
||||
await settingsProvider.saveMnemonic(mnemonic);
|
||||
|
||||
// Inicializar wallet (esto valida el mnemonic internamente)
|
||||
// Si el mnemonic es inválido, cdk_flutter lanzará una excepción
|
||||
// Inicializar wallet primero (valida el mnemonic internamente).
|
||||
// Si es inválido, cdk_flutter lanzará una excepción antes de persistir.
|
||||
await walletProvider.initialize(mnemonic);
|
||||
|
||||
// Solo guardar mnemonic si initialize() pasó sin error
|
||||
await settingsProvider.saveMnemonic(mnemonic);
|
||||
|
||||
// Inicializar P2PK (derivar clave principal del mnemonic)
|
||||
try {
|
||||
await p2pkProvider.initialize(mnemonic);
|
||||
@@ -75,6 +77,21 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
|
||||
debugPrint('[RestoreWalletScreen] Error initializing P2PK (non-fatal): $e');
|
||||
}
|
||||
|
||||
// Escanear mint activo para recuperar tokens existentes (NUT-13)
|
||||
final activeMint = walletProvider.activeMintUrl;
|
||||
if (activeMint != null && mounted) {
|
||||
final l10n = L10n.of(context)!;
|
||||
setState(() {
|
||||
_statusMessage = l10n.restoreScanningMint;
|
||||
});
|
||||
|
||||
try {
|
||||
await walletProvider.restoreFromMint(activeMint);
|
||||
} catch (e) {
|
||||
debugPrint('[RestoreWalletScreen] Error scanning mint (non-fatal): $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
@@ -83,9 +100,13 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Revertir el guardado del mnemonic si falló la inicialización
|
||||
final settingsProvider = context.read<SettingsProvider>();
|
||||
await settingsProvider.deleteWallet();
|
||||
// Limpiar cualquier estado parcial si algo falló
|
||||
try {
|
||||
final settingsProvider = context.read<SettingsProvider>();
|
||||
await settingsProvider.deleteWallet();
|
||||
} catch (cleanupError) {
|
||||
debugPrint('Error during restore cleanup: $cleanupError');
|
||||
}
|
||||
|
||||
final l10n = L10n.of(context)!;
|
||||
setState(() {
|
||||
@@ -260,6 +281,22 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
|
||||
? _restoreWallet
|
||||
: null,
|
||||
),
|
||||
|
||||
if (_isRestoring && _statusMessage != null) ...[
|
||||
const SizedBox(height: AppDimensions.paddingMedium),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
_statusMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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';
|
||||
@@ -132,12 +132,23 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
|
||||
actions: [
|
||||
// Botón seleccionar/deseleccionar todos
|
||||
if (_availableProofs.isNotEmpty)
|
||||
IconButton(
|
||||
TextButton.icon(
|
||||
icon: Icon(
|
||||
_selectedIds.length == _availableProofs.length
|
||||
? LucideIcons.checkSquare
|
||||
: LucideIcons.square,
|
||||
color: AppColors.primaryAction,
|
||||
size: 20,
|
||||
),
|
||||
label: Text(
|
||||
_selectedIds.length == _availableProofs.length
|
||||
? L10n.of(context)!.deselectAll
|
||||
: L10n.of(context)!.selectAll,
|
||||
style: const TextStyle(
|
||||
color: AppColors.primaryAction,
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
onPressed: _selectedIds.length == _availableProofs.length
|
||||
? _clearSelection
|
||||
|
||||
@@ -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';
|
||||
@@ -13,6 +13,7 @@ import '../../widgets/common/primary_button.dart';
|
||||
import '../../widgets/common/numpad_widget.dart';
|
||||
import '../../providers/wallet_provider.dart';
|
||||
import '../../core/utils/nostr_utils.dart';
|
||||
import '../../widgets/scanner/qr_scanner_widget.dart';
|
||||
import 'share_token_screen.dart';
|
||||
import 'offline_send_screen.dart';
|
||||
|
||||
@@ -277,9 +278,8 @@ class _SendScreenState extends State<SendScreen> {
|
||||
final l10n = L10n.of(context)!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Toggle P2PK
|
||||
GlassCard(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppDimensions.paddingMedium,
|
||||
@@ -319,88 +319,64 @@ class _SendScreenState extends State<SendScreen> {
|
||||
),
|
||||
Switch(
|
||||
value: _useP2PK,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_useP2PK = value;
|
||||
if (!value) {
|
||||
_pubkeyController.clear();
|
||||
_pubkeyError = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
onChanged: (value) => setState(() => _useP2PK = value),
|
||||
activeColor: AppColors.primaryAction,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Campo pubkey (visible solo si P2PK está activo)
|
||||
// Campo de pubkey cuando 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,
|
||||
),
|
||||
),
|
||||
|
||||
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(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _pubkeyController,
|
||||
onChanged: _validatePubkey,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 11,
|
||||
color: AppColors.warning.withValues(alpha: 0.8),
|
||||
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,
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.clipboard, size: 20),
|
||||
color: AppColors.textSecondary,
|
||||
onPressed: _pastePubkey,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(LucideIcons.scanLine, size: 20),
|
||||
color: AppColors.textSecondary,
|
||||
tooltip: l10n.scanQrCode,
|
||||
onPressed: _scanPubkey,
|
||||
),
|
||||
],
|
||||
),
|
||||
errorText: _pubkeyError,
|
||||
errorStyle: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -421,12 +397,71 @@ class _SendScreenState extends State<SendScreen> {
|
||||
|
||||
Future<void> _pastePubkey() async {
|
||||
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (clipboardData?.text != null) {
|
||||
_pubkeyController.text = clipboardData!.text!;
|
||||
_validatePubkey(clipboardData.text!);
|
||||
if (!mounted) return;
|
||||
final text = clipboardData?.text?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
_pubkeyController.text = text;
|
||||
_validatePubkey(text);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _scanPubkey() async {
|
||||
final result = await Navigator.push<String>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(LucideIcons.x, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
L10n.of(context)!.scanQrCode,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: QrScannerWidget(
|
||||
onDetect: (data) {
|
||||
final trimmed = data.trim();
|
||||
if (NostrUtils.isValidP2PKPubkey(trimmed)) {
|
||||
Navigator.pop(context, trimmed);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.p2pkInvalidPubkey),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('[P2PK Scan] $error');
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.cameraPermissionDenied),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (!mounted || result == null) return;
|
||||
_pubkeyController.text = result;
|
||||
_validatePubkey(result);
|
||||
}
|
||||
|
||||
Widget _buildErrorMessage() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: AppDimensions.paddingMedium),
|
||||
@@ -590,17 +625,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';
|
||||
@@ -691,8 +691,9 @@ class _TransactionDetailScreenState extends State<_TransactionDetailScreen> {
|
||||
// Datos de la transacción
|
||||
late final bool _isIncoming;
|
||||
late final bool _isLightning;
|
||||
late final String? _tokenOrInvoice;
|
||||
String? _tokenOrInvoice;
|
||||
late final bool _shouldShowQR;
|
||||
bool _isLoadingPendingInvoice = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -704,6 +705,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 +719,26 @@ class _TransactionDetailScreenState extends State<_TransactionDetailScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Busca el invoice en pending mint invoices si no hay metadata.
|
||||
Future<void> _loadPendingMintInvoice() async {
|
||||
setState(() => _isLoadingPendingInvoice = true);
|
||||
String? invoice;
|
||||
try {
|
||||
invoice = await widget.walletProvider.findPendingMintInvoice(
|
||||
widget.transaction.mintUrl,
|
||||
widget.transaction.unit,
|
||||
);
|
||||
} catch (e, st) {
|
||||
debugPrint('findPendingMintInvoice failed: $e\n$st');
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingPendingInvoice = false;
|
||||
if (invoice != null) _tokenOrInvoice = invoice;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationTimer?.cancel();
|
||||
@@ -851,8 +877,8 @@ class _TransactionDetailScreenState extends State<_TransactionDetailScreen> {
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
|
||||
// Mensaje si no hay token/invoice
|
||||
if (_tokenOrInvoice == null) ...[
|
||||
// Mensaje si no hay token/invoice (ocultar mientras carga)
|
||||
if (_tokenOrInvoice == null && !_isLoadingPendingInvoice) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -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';
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "3e0c77c22c807459d5d355e270ef4b5ad6a490c1"
|
||||
resolved-ref: "7fb8580530d963c9ff09f23e55a1e56bccd8676b"
|
||||
url: "https://github.com/cashubtc/cdk_flutter.git"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: elcaju
|
||||
description: "ElCaju - Tu wallet de ecash privado"
|
||||
publish_to: 'none'
|
||||
version: 0.0.1+1
|
||||
version: 0.1.0+2
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.0
|
||||
|
||||
Reference in New Issue
Block a user