Compare commits

...
Author SHA1 Message Date
Forte11Cuba f01fc66dcf fix: reclaim orphaned proofs automatically on app startup 2026-04-15 16:06:29 -06:00
Forte11andGitHub 9a14af33a2 Merge pull request #107 from Forte11Cuba/fix/melt-proofs-lost-and-min-amount-validation
fix: recover reserved proofs on melt failure and validate minimum viable amounts
2026-04-15 06:10:08 -06:00
Forte11Cuba 770868a773 fix: add fee pre-validation to swap and log cancelMelt failures 2026-04-15 04:48:03 -06:00
Forte11Cuba dc78f8958c fix: recover reserved proofs on melt failure and validate minimum viable amounts 2026-04-15 04:20:53 -06:00
Forte11andGitHub 03ba0e532f Merge pull request #103 from Forte11Cuba/refactor/split-settings-screen
refactor: split settings_screen.dart into separate files
2026-04-12 16:34:06 -06:00
Forte11Cuba a6a4365a0d fix: remove unnecessary async from synchronous _loadMints 2026-04-12 16:33:26 -06:00
Forte11Cuba ac23574ae2 fix: count only mints that actually recovered tokens in restore message 2026-04-12 16:07:24 -06:00
Forte11Cuba 87f8e52036 fix: use addPostFrameCallback for safe context access in RecoverTokensModal 2026-04-12 15:56:26 -06:00
Forte11Cuba eb9319fca0 refactor: split settings_screen.dart into separate files 2026-04-12 15:43:08 -06:00
Forte11andGitHub a9e4272762 Merge pull request #102 from Forte11Cuba/feat/privacy-screen
feat: add privacy policy screen with humorous tone
2026-04-11 03:01:53 -06:00
Forte11Cuba f66668f0ec fix: normalize privacyBody split to filter empty lines 2026-04-11 02:32:46 -06:00
Forte11Cuba 100a4943eb fix: handle privacyConclusion lines robustly for any locale line count 2026-04-11 02:27:46 -06:00
Forte11Cuba 5b1317c3e4 fix: address CodeRabbit review Portuguese register, scrollable layout 2026-04-11 02:22:28 -06:00
Forte11Cuba d85b6562dc feat: add privacy policy screen with humorous tone 2026-04-11 01:32:43 -06:00
Forte11andGitHub 56585319cc Merge pull request #101 from Forte11Cuba/feat/swap-chart-improvements
feat: improve swap price chart — real data only, 24h stats, min/max
2026-04-11 00:54:52 -06:00
Forte11Cuba 79ca30c5ed fix: filter non-finite and zero price samples before rendering chart 2026-04-11 00:37:57 -06:00
Forte11Cuba 0bd988a7a6 fix: address CodeRabbit review handle 2 samples, use i18n template for minMax 2026-04-11 00:31:53 -06:00
Forte11Cuba 218d798d2a feat: improve swap price chart — real data only, 24h stats, min/max 2026-04-11 00:19:43 -06:00
Forte11andGitHub e8d8789413 Merge pull request #100 from Forte11Cuba/feat/swap-screen
feat: add swap screen UI (sats -> USD) with price chart
2026-04-10 04:47:37 -06:00
Forte11Cuba a2866d035a fix: truncate conversion amounts with floor instead of round to prevent overpaying 2026-04-10 04:44:32 -06:00
Forte11Cuba 1a8e3e601b fix: show mint name in AppBar, make confirmation sheet non-dismissible, hardcode Cuba Bitcoin mint 2026-04-10 04:06:40 -06:00
Forte11Cuba b3dde35141 fix: add try/catch to _loadBalances, localize fee/total labels, remove redundant ternary 2026-04-10 03:48:58 -06:00
Forte11Cuba 8541222da6 fix: remove redundant ternary, capture provider ref outside async callback 2026-04-10 03:26:49 -06:00
Forte11Cuba d6085f7754 fix: improve swap reliability — remove unsafe tx fallback, add mint stream timeout and issued handler 2026-04-10 03:18:27 -06:00
Forte11Cuba d2035ba416 feat: implement swap logic (sats <-> USD) with bidirectional input, fee preview, and mint validation 2026-04-10 02:57:55 -06:00
Forte11Cuba 42bab5d18a feat: add swap screen UI (sats -> USD) with price chart 2026-04-09 04:54:48 -06:00
Forte11andGitHub 6c05785391 Merge pull request #99 from Forte11Cuba/chore/upgrade-cdk-v0.16.0
docs: update CDK version to 0.16.0 in README
2026-04-09 03:57:31 -06:00
Forte11Cuba 65224314e1 docs: update CDK version to 0.16.0 in README 2026-04-09 03:53:02 -06:00
Forte11andGitHub 2534cf5d1e Merge pull request #98 from Forte11Cuba/chore/upgrade-cdk-v0.16.0
chore: upgrade CDK from v0.15.1 to v0.16.0
2026-04-05 20:32:14 -06:00
Forte11Cuba 262b049c20 chore: upgrade CDK from v0.15.1 to v0.16.0 2026-04-05 20:09:55 -06:00
Forte11Cuba 697f6fa555 chore: upgrade CDK from v0.15.1 to v0.16.0 2026-04-05 19:56:34 -06:00
Forte11andGitHub 02fec140e1 Merge pull request #97 from Forte11Cuba/release/v0.2.0
Release APK / build (push) Canceled after 0s
chore: bump version to v0.2.0+3
2026-04-05 16:08:16 -06:00
Forte11Cuba 7cc92d6732 chore: bump version to v0.2.0+3 2026-04-05 15:51:01 -06:00
Forte11andGitHub 013560d25b Merge pull request #96 from Forte11Cuba/chore/rename-package-id
chore: rename package identifier from com.example.elcaju to me.elcaju
2026-04-03 18:44:20 -06:00
Forte11Cuba d19a58df84 chore: rename package identifier from com.example.elcaju to me.elcaju 2026-04-03 17:51:08 -06:00
Forte11andGitHub fd64d2f269 Merge pull request #95 from Forte11Cuba/fix/mint-http-polling-fallback
fix: add HTTP polling fallback for mint quote payment detection
2026-04-03 17:46:23 -06:00
Forte11Cuba 426b63456d fix: move mint logic outside timeout to prevent expired race 2026-04-03 17:25:38 -06:00
Forte11Cuba 4d2ad39323 fix: add HTTP polling fallback for mint quote payment detection 2026-04-03 16:56:22 -06:00
Forte11andGitHub 060c6acd4e Merge pull request #94 from Forte11Cuba/fix/refresh-mint-units-on-startup
fix: refresh mint units on startup via NUT-04
2026-04-03 16:48:24 -06:00
Forte11Cuba 14da243b89 fix: wrap _saveMints in try-catch inside async void method 2026-04-03 16:20:23 -06:00
Forte11Cuba 4f91a2be1c fix: validate activeUnit after refreshing mint units 2026-04-03 15:48:02 -06:00
Forte11Cuba ab13e8f6c2 fix: refresh mint units on startup via NUT-04 2026-04-03 15:40:51 -06:00
Forte11andGitHub f431f825c5 Merge pull request #93 from Forte11Cuba/fix/cache-balance-stream-and-sync-controller
fix: cache balance stream and use sync StreamController
2026-04-03 15:30:20 -06:00
Forte11Cuba b56aa5ce62 fix: clear balance stream cache in deleteDatabase() and reset() 2026-04-03 15:25:11 -06:00
Forte11Cuba c69de2b41b fix: cache balance stream and use sync StreamController 2026-04-03 15:05:22 -06:00
Forte11andGitHub e5285058a8 Merge pull request #91 from Forte11Cuba/fix/send-token-metadata-race-condition
fix: use deterministic transaction ID for send/mint metadata
2026-04-02 01:58:26 -06:00
Forte11Cuba b01b658b93 fix: add mounted guards after async mintTokens call 2026-04-02 01:52:53 -06:00
Forte11Cuba 6596aa963a refactor: log mint tx ID computation failure instead of swallowing 2026-04-02 01:52:14 -06:00
Forte11Cuba ed309d18e8 refactor: add debug logs for tx ID computation failures 2026-04-02 01:08:36 -06:00
Forte11Cuba 1b30d9836b fix: make transaction ID computation best-effort after committed send 2026-04-01 22:54:05 -06:00
Forte11Cuba 588a127f9a refactor: use Option<String> for SendResult.transaction_id 2026-04-01 20:14:11 -06:00
Forte11Cuba 362e45e40f fix: use deterministic transaction ID for send/mint metadata 2026-04-01 19:42:17 -06:00
Forte11andGitHub fae57cc46e Merge pull request #90 from Forte11Cuba/fix/disable-nostr-payment-request-nip17-bug
fix: disable Cashu/Nostr payment request until CDK fixes NIP-17 sender pubkey
2026-04-01 16:45:30 -06:00
Forte11Cuba 80ab0fd9e1 refactor: remove dead code paths while Nostr is disabled 2026-04-01 16:38:02 -06:00
Forte11Cuba 29895f4079 fix: treat Lightning mint error as terminal without Nostr fallback 2026-04-01 14:45:40 -06:00
Forte11Cuba 41fa32d1b4 refactor: hide mode toggle and simplify QR content while Nostr is disabled 2026-04-01 14:33:58 -06:00
Forte11Cuba 63ce12d745 fix: defer QR display until Lightning invoice is ready 2026-04-01 14:10:39 -06:00
Forte11Cuba 7245dbb669 fix: disable Cashu/Nostr payment request until CDK fixes NIP-17 sender pubkey 2026-04-01 13:59:04 -06:00
Forte11andGitHub e124e0839a Merge pull request #89 from Forte11Cuba/feat/mint-websocket-subscription
feat: replace 3s polling with CDK WebSocket subscription (NUT-17) for mint quote
2026-04-01 12:42:25 -06:00
Forte11Cuba 1a31b47c6b fix: handle expired-quote timeout and emit Issued on recovery path 2026-04-01 12:39:17 -06:00
Forte11Cuba a5cd50af13 feat: replace 3s polling with CDK WebSocket subscription (NUT-17) for mint quote 2026-04-01 12:24:12 -06:00
Forte11andGitHub 66119f6b56 Merge pull request #88 from Forte11Cuba/fix/receive-already-signed-retry
fix: handle 'already signed' error in receive with counter resync
2026-03-31 03:42:05 -06:00
Forte11Cuba 484f73a33b fix: only reclaim proofs explicitly reported as Unspent by mint 2026-03-31 03:36:16 -06:00
Forte11Cuba 67204a12f8 fix: handle 'already signed' error in receive with counter resync 2026-03-31 02:36:43 -06:00
Forte11andGitHub b4a62fe9e5 Merge pull request #86 from Forte11Cuba/fix/offline-send-token-v4
fix: use CDK to create valid V4 tokens for offline send
2026-03-31 01:49:26 -06:00
Forte11Cuba 638b288b7e fix: prevent ProofService leak and snapshot selectedTotal 2026-03-31 00:42:17 -06:00
Forte11Cuba 357cc6827f fix: prevent ProofService close during in-flight token creation 2026-03-31 00:31:11 -06:00
Forte11Cuba 682ec4ba7d fix: log rollback error instead of swallowing silently 2026-03-30 23:36:52 -06:00
Forte11Cuba 03cf1d2119 fix: rollback proofs to UNSPENT if offline token creation fails 2026-03-30 23:30:00 -06:00
Forte11Cuba 2309a2edda fix: use CDK to create valid V4 tokens for offline send 2026-03-30 23:09:40 -06:00
Forte11andGitHub ac1ac11075 Merge pull request #85 from Forte11Cuba/fix/dispose-crash-modal-overlap
fix: dispose crash and modal overlap
2026-03-30 00:20:13 -06:00
Forte11Cuba aafa9f97eb fix: dispose crash and modal overlap 2026-03-30 00:10:34 -06:00
Forte11andGitHub 7d3fe5ac58 Merge pull request #84 from Forte11Cuba/fix/nostr-listener-fail-to-post
fix: reduce Nostr listener cancellation delay from 5s to 1s
2026-03-29 23:53:06 -06:00
Forte11Cuba b9c01ce865 fix: reduce Nostr listener cancellation delay from 5s to 1s 2026-03-29 23:45:36 -06:00
Forte11andGitHub 679aa6be5f Merge pull request #83 from Forte11Cuba/feat/nostr-key-persistence
feat: persist Nostr ephemeral keys for payment request recovery
2026-03-29 23:35:54 -06:00
Forte11Cuba c44a17efbd fix: address CodeRabbit review on Nostr key persistence 2026-03-29 23:30:56 -06:00
Forte11Cuba 27a2baefcc feat: persist Nostr ephemeral keys for payment request recovery 2026-03-29 22:49:13 -06:00
Forte11andGitHub e23c5748ce Merge pull request #82 from Forte11Cuba/fix/qr-logo-overlay
feat: add logo overlay to QR codes in RequestScreen
2026-03-29 21:54:15 -06:00
Forte11Cuba c9781b2ff2 refactor: extract bitcoinOrange to AppColors constants 2026-03-29 21:53:50 -06:00
Forte11Cuba b9485d3296 feat: add logo overlay to QR codes in RequestScreen 2026-03-29 21:31:08 -06:00
Forte11andGitHub c222fff82e Merge pull request #81 from Forte11Cuba/feat/nut18-receiver-ui
feat: add RequestScreen UI with unified payment (Phase 2)
2026-03-29 21:22:01 -06:00
Forte11Cuba f839031340 fix: align scanner invoiceOnly handler with BIP-321 validation 2026-03-29 21:18:16 -06:00
Forte11Cuba 41b8743b29 fix: address CodeRabbit Phase 2 review 2026-03-29 21:05:55 -06:00
Forte11Cuba 056f4c1286 feat: add RequestScreen UI with unified payment (Phase 2) 2026-03-29 20:33:00 -06:00
Forte11andGitHub cfed450521 Merge pull request #79 from Forte11Cuba/feat/nut18-receiver-bridge
feat: expose create_payment_request() and wait_for_nostr_payment() in Rust bridge
2026-03-29 18:56:06 -06:00
Forte11Cuba 438603c97b fix: validate wallet matches handle before starting listener 2026-03-29 18:29:23 -06:00
Forte11Cuba c92c438efe fix: skip spawning listener if Dart stream is already closed 2026-03-29 15:22:23 -06:00
Forte11Cuba 7d5efcfb75 fix: apply nitpick improvements from CodeRabbit 2026-03-29 14:19:04 -06:00
Forte11Cuba bb942ef72c fix: return error on Dart stream cancellation instead of Ok(0) 2026-03-29 14:07:45 -06:00
Forte11Cuba b28a529ffc fix: validate incoming payment and reject empty relay list 2026-03-29 13:45:20 -06:00
Forte11Cuba 1ff3a6ec16 feat: address CodeRabbit review feedback 2026-03-29 13:27:52 -06:00
Forte11Cuba f1b679d912 feat: expose create_payment_request() and wait_for_nostr_payment() in Rust bridge 2026-03-29 13:01:36 -06:00
Forte11andGitHub 8b514337a6 Merge pull request #78 from Forte11Cuba/feat/nut18-payment-requests
feat: implement NUT-18/26 payment request support (payer side)
2026-03-29 04:31:42 -06:00
Forte11Cuba d7f6803f1f fix: normalize mint URLs before acceptance check 2026-03-29 04:25:05 -06:00
Forte11Cuba d91a40c41d fix: address CodeRabbit review feedback 2026-03-29 04:06:56 -06:00
Forte11Cuba 0ed49789d0 feat: implement NUT-18/26 payment request support (payer side) 2026-03-29 03:40:13 -06:00
Forte11andGitHub a41b99644c Merge pull request #76 from Forte11Cuba/fix/p2pk-pending-token-claim
fix: pass P2PK signing key when claiming pending tokens
2026-03-29 02:35:58 -06:00
Forte11Cuba 286eae3812 fix: route pending P2PK claims through receiveToken for full bookkeeping 2026-03-29 01:58:12 -06:00
Forte11Cuba e1e8057be5 fix: pass P2PK signing key when claiming pending tokens (#71) 2026-03-29 01:44:27 -06:00
Forte11andGitHub a1ce50bcd4 Merge pull request #75 from Forte11Cuba/docs/update-readme
docs: update README to reflect current project state
2026-03-29 01:28:57 -06:00
70 changed files with 13987 additions and 1880 deletions
+2 -2
View File
@@ -214,7 +214,7 @@ flutter build apk --release --target-platform android-arm64
│ flutter_rust_bridge v2.11.1 (FFI) │
├─────────────────────────────────────────┤
│ elcaju_core (Rust interno) │
│ CDK 0.15.1 + SQLite + BIP39 + Tokio │
│ CDK 0.16.0 + SQLite + BIP39 + Tokio │
└─────────────────────────────────────────┘
```
@@ -256,7 +256,7 @@ elcaju/
│ │ └── scanner/ # QR scanner widget
│ └── l10n/ # 11 languages (354 translation keys)
├── rust/
│ ├── Cargo.toml # elcaju_core: CDK 0.15.1, flutter_rust_bridge 2.11.1
│ ├── Cargo.toml # elcaju_core: CDK 0.16.0, flutter_rust_bridge 2.11.1
│ └── src/api/ # wallet, token, keys, mint_info, error
├── rust_builder/ # Cargokit integration (auto Rust compilation)
├── android/ # Android config (NDK 27.0, NFC HCE service)
+6 -6
View File
@@ -368,7 +368,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -547,7 +547,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
+1
View File
@@ -22,6 +22,7 @@ class AppColors {
// === ACCENT COLORS ===
static const Color primaryAction = Color(0xFFE35D33); // Naranja vibrante
static const Color secondaryAction = Color(0xFFFFB74D); // Amarillo pulpa/Bitcoin
static const Color bitcoinOrange = Color(0xFFF7931A); // Bitcoin brand orange
static const Color success = Color(0xFF00E676); // Verde neón
static const Color error = Color(0xFFFF5252); // Rojo suave
static const Color warning = Color(0xFFFFB74D); // Amarillo
+4 -2
View File
@@ -473,11 +473,13 @@ class NfcService {
return null;
}
/// Check if a string looks like a Cashu token.
/// Check if a string looks like a Cashu token or payment request.
static bool _isCashuToken(String text) {
final lower = text.toLowerCase().trim();
return lower.startsWith('cashua') ||
lower.startsWith('cashub') ||
lower.startsWith('creqa');
lower.startsWith('creqa') ||
lower.startsWith('creqb') ||
lower.startsWith('bitcoin:');
}
}
+56
View File
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart' as http;
/// Servicio para obtener precios de Bitcoin usando Yadio API
@@ -86,4 +87,59 @@ class PriceService {
// Retornar en centavos
return BigInt.from((fiatAmount * 100).round());
}
// --- Blink API (precios históricos) ---
static const _blinkUrl = 'https://api.blink.sv/graphql';
/// Obtiene precios históricos de BTC desde Blink API.
/// range: ONE_DAY, ONE_WEEK, ONE_MONTH, ONE_YEAR, FIVE_YEARS
static Future<List<PricePoint>> getHistoricalPrices({
String range = 'ONE_DAY',
}) async {
try {
final response = await http.post(
Uri.parse(_blinkUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'query':
'query { btcPriceList(range: $range) { price { base offset } timestamp } }',
}),
).timeout(const Duration(seconds: 15));
if (response.statusCode != 200) {
throw Exception('Error HTTP: ${response.statusCode}');
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
final data = json['data'] as Map<String, dynamic>?;
if (data == null) throw Exception('No data in response');
final list = data['btcPriceList'] as List?;
if (list == null || list.isEmpty) {
throw Exception('No historical prices in response');
}
return list.map((point) {
final price = point['price'] as Map<String, dynamic>;
final base = (price['base'] as num).toDouble();
final offset = (price['offset'] as num).toInt();
// base / 10^offset = USD cents → /100 = USD
final priceUsd = base / pow(10, offset) / 100;
final timestamp = (point['timestamp'] as num).toInt();
return PricePoint(timestamp: timestamp, priceUsd: priceUsd);
}).toList();
} catch (e) {
throw Exception('Error obteniendo precios históricos: $e');
}
}
}
/// Punto de precio histórico de BTC
class PricePoint {
final int timestamp;
final double priceUsd;
PricePoint({required this.timestamp, required this.priceUsd});
}
+1 -1
View File
@@ -33,7 +33,7 @@ class ProofService {
final dir = await getApplicationDocumentsDirectory();
final dbPath = '${dir.path}/elcaju_wallet.sqlite';
_db = await openDatabase(dbPath);
_db = await openDatabase(dbPath, singleInstance: false);
return _db!;
}
+20
View File
@@ -0,0 +1,20 @@
/// Builds a BIP-321 unified payment URI.
///
/// Format: BITCOIN:?LIGHTNING=LNBC...&CREQ=CREQB1...
/// All uppercase for optimal QR encoding (alphanumeric mode).
///
/// Ref: NUT-26 "BIP-321 Integration" section.
String buildUnifiedUri({
required String creqB,
String? bolt11,
}) {
final buffer = StringBuffer('bitcoin:?');
if (bolt11 != null) {
buffer.write('lightning=');
buffer.write(bolt11.toUpperCase());
buffer.write('&');
}
buffer.write('creq=');
buffer.write(creqB.toUpperCase());
return buffer.toString();
}
+55 -4
View File
@@ -15,7 +15,7 @@ enum IncomingDataType {
cashuToken, // cashuA... / cashuB...
lightningInvoice, // lnbc... / lntb... / lnbcrt...
mintUrl, // https://...
paymentRequest, // creqA... (post-MVP, Cashu payment request)
paymentRequest, // creqA/creqB/bitcoin:?creq= (NUT-18/26 payment request)
unknown,
}
@@ -103,8 +103,31 @@ class IncomingDataParser {
);
}
// Payment Request (creqA...)
if (lower.startsWith('creqa')) {
// BIP-321 unified URI: bitcoin:?lightning=...&creq=...
// Detect as paymentRequest but also extract the lightning invoice
if (lower.startsWith('bitcoin:')) {
final bolt11 = _extractBip321Param(trimmed, 'lightning');
final creq = _extractBip321Param(trimmed, 'creq');
if (creq != null) {
return ParsedData(
type: IncomingDataType.paymentRequest,
raw: trimmed,
invoiceBolt11: bolt11,
);
}
// bitcoin: URI with only lightning= (no creq)
if (bolt11 != null) {
return ParsedData(
type: IncomingDataType.lightningInvoice,
raw: trimmed,
invoiceBolt11: bolt11,
);
}
}
// Payment Request: creqA (NUT-18), creqB/CREQB1 (NUT-26)
if (lower.startsWith('creqa') || lower.startsWith('creqb')) {
return ParsedData(
type: IncomingDataType.paymentRequest,
raw: trimmed,
@@ -193,7 +216,35 @@ class IncomingDataParser {
case ScanMode.cashuOnly:
return data.type == IncomingDataType.cashuToken;
case ScanMode.invoiceOnly:
return data.type == IncomingDataType.lightningInvoice;
// Accept pure invoices and BIP-321 URIs that contain a lightning invoice
return data.type == IncomingDataType.lightningInvoice ||
(data.type == IncomingDataType.paymentRequest &&
data.invoiceBolt11 != null);
}
}
/// Extract a query parameter value from a BIP-321 URI.
/// Case-insensitive key matching, handles percent-encoding.
static String? _extractBip321Param(String uri, String key) {
final qIndex = uri.indexOf('?');
if (qIndex < 0) return null;
final query = uri.substring(qIndex + 1);
for (final param in query.split('&')) {
final eqIndex = param.indexOf('=');
if (eqIndex < 0) continue;
final k = param.substring(0, eqIndex);
if (k.toLowerCase() == key.toLowerCase()) {
final rawValue = param.substring(eqIndex + 1);
if (rawValue.isEmpty) continue;
try {
final value = Uri.decodeComponent(rawValue);
if (value.isEmpty) continue;
return value;
} on ArgumentError {
continue;
}
}
}
return null;
}
}
+50 -2
View File
@@ -118,6 +118,7 @@
"confirm": "Bestätigen",
"cancel": "Abbrechen",
"insufficientBalance": "Unzureichendes Guthaben",
"feeExceedsAmount": "Die Gebühr übersteigt den Sendebetrag",
"tokenCreationError": "Fehler beim Erstellen des Tokens: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -502,7 +503,22 @@
"scanLightningInvoiceHint": "Scanne eine Lightning Rechnung (lnbc...)",
"addMintQuestion": "Diesen Mint hinzufügen?",
"cameraPermissionDenied": "Kamera-Berechtigung verweigert",
"paymentRequestNotSupported": "Zahlungsanfragen werden noch nicht unterstützt",
"paymentRequestTitle": "Zahlungsanfrage",
"paymentRequestFrom": "Anfrage von",
"paymentRequestAmount": "Angeforderter Betrag",
"paymentRequestDescription": "Beschreibung",
"paymentRequestMints": "Akzeptierte Mints",
"paymentRequestAnyMint": "Jeder Mint",
"paymentRequestPay": "Bezahlen",
"paymentRequestPaying": "Bezahle...",
"paymentRequestSuccess": "Zahlung erfolgreich gesendet",
"paymentRequestNoTransport": "Diese Anfrage hat keine konfigurierte Zustellmethode",
"paymentRequestTransport": "Transport",
"paymentRequestMintNotAccepted": "Dein aktiver Mint ist nicht in der Liste der akzeptierten Mints",
"paymentRequestUnitMismatch": "Inkompatible Einheit: Anfrage erfordert {unit}",
"paymentRequestInsufficientBalance": "Unzureichendes Guthaben",
"paymentRequestErrorParsing": "Fehler beim Lesen der Zahlungsanfrage",
"p2pkTitle": "P2PK-Schlüssel",
"p2pkSettingsDescription": "Gesperrtes ecash empfangen",
"p2pkExperimental": "P2PK ist experimentell. Mit Vorsicht verwenden.",
@@ -534,5 +550,37 @@
"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",
"request": "Anfordern",
"requestPayment": "Zahlung anfordern",
"requestPaymentDescription": "Einheitliche Zahlungsanforderung erstellen",
"generateRequest": "Anforderung erstellen",
"generatingRequest": "Anforderung wird erstellt...",
"requestPaymentReceived": "Zahlung erhalten",
"requestDescriptionHint": "Beschreibung (optional)",
"universal": "Universal",
"copiedToClipboard": "In die Zwischenablage kopiert",
"swap": "Tauschen",
"swapDescription": "Zwischen Sats und USD wechseln",
"swapFrom": "Von",
"swapTo": "Nach",
"swapAction": "Tauschen",
"swapEstimatedFee": "Geschätzte Gebühr",
"swapUseAll": "Alles verwenden",
"swapMinimum": "Minimum: {amount}",
"swapProcessing": "Swap wird verarbeitet...",
"swapSuccess": "Swap abgeschlossen",
"swapErrorInsufficient": "Unzureichendes Guthaben",
"swapErrorExpired": "Angebot abgelaufen",
"swapErrorGeneric": "Swap-Fehler: {error}",
"swapChartUnavailable": "Preis nicht verfügbar · Tippen zum Wiederholen",
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
"privacyPolicy": "Datenschutz",
"privacyTitle": "WIR SAMMELN NICHTS",
"privacyGoodbye": "TSCHÜSS",
"privacyKeepReading": "(lies weiter, wenn du willst…)",
"privacyBody": "Wir wissen nicht, wer du bist\nWir wissen nicht, wie viel du hast\nWir wissen nicht, was du tust",
"privacyConclusion": "Der beste Weg, deine Daten zu schützen,\nist sie nicht zu haben"
}
+54 -2
View File
@@ -103,6 +103,7 @@
"confirm": "Confirm",
"cancel": "Cancel",
"insufficientBalance": "Insufficient balance",
"feeExceedsAmount": "Fee exceeds the amount to send",
"tokenCreationError": "Error creating token: {error}",
"tokenCreated": "Token created",
@@ -390,7 +391,26 @@
"scanLightningInvoiceHint": "Scan a Lightning invoice (lnbc...)",
"addMintQuestion": "Add this mint?",
"cameraPermissionDenied": "Camera permission denied",
"paymentRequestNotSupported": "Payment requests are not yet supported",
"paymentRequestTitle": "Payment Request",
"paymentRequestFrom": "Request from",
"paymentRequestAmount": "Requested amount",
"paymentRequestDescription": "Description",
"paymentRequestMints": "Accepted mints",
"paymentRequestAnyMint": "Any mint",
"paymentRequestPay": "Pay",
"paymentRequestPaying": "Paying...",
"paymentRequestSuccess": "Payment sent successfully",
"paymentRequestNoTransport": "This request has no delivery method configured",
"paymentRequestTransport": "Transport",
"paymentRequestMintNotAccepted": "Your active mint is not in the list of accepted mints",
"paymentRequestUnitMismatch": "Incompatible unit: request requires {unit}",
"@paymentRequestUnitMismatch": {
"placeholders": {
"unit": { "type": "String" }
}
},
"paymentRequestInsufficientBalance": "Insufficient balance",
"paymentRequestErrorParsing": "Error reading payment request",
"p2pkTitle": "P2PK Keys",
"p2pkSettingsDescription": "Receive locked ecash",
@@ -423,5 +443,37 @@
"p2pkErrorInvalidNsec": "Invalid nsec",
"p2pkErrorKeyAlreadyExists": "This key already exists",
"p2pkErrorKeyNotFound": "Key not found",
"p2pkErrorCannotDeletePrimary": "Cannot delete primary key"
"p2pkErrorCannotDeletePrimary": "Cannot delete primary key",
"request": "Request",
"requestPayment": "Request Payment",
"requestPaymentDescription": "Generate unified payment request",
"generateRequest": "Generate Request",
"generatingRequest": "Generating request...",
"requestPaymentReceived": "Payment received",
"requestDescriptionHint": "Description (optional)",
"universal": "Universal",
"copiedToClipboard": "Copied to clipboard",
"swap": "Swap",
"swapDescription": "Convert between sats and USD",
"swapFrom": "From",
"swapTo": "To",
"swapAction": "Swap",
"swapEstimatedFee": "Estimated fee",
"swapUseAll": "Use all",
"swapMinimum": "Minimum: {amount}",
"swapProcessing": "Processing swap...",
"swapSuccess": "Swap completed",
"swapErrorInsufficient": "Insufficient balance",
"swapErrorExpired": "Quote has expired",
"swapErrorGeneric": "Swap error: {error}",
"swapChartUnavailable": "Price unavailable · Tap to retry",
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
"privacyPolicy": "Privacy policy",
"privacyTitle": "WE COLLECT NOTHING",
"privacyGoodbye": "BYE",
"privacyKeepReading": "(keep reading if you want…)",
"privacyBody": "We don't know who you are\nWe don't know how much you have\nWe don't know what you do",
"privacyConclusion": "The best way to protect your data\nis not to have it"
}
+70 -2
View File
@@ -118,6 +118,7 @@
"confirm": "Confirmar",
"cancel": "Cancelar",
"insufficientBalance": "Balance insuficiente",
"feeExceedsAmount": "La comisión supera el monto a enviar",
"tokenCreationError": "Error al crear token: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -510,7 +511,26 @@
"scanLightningInvoiceHint": "Escanea un invoice Lightning (lnbc...)",
"addMintQuestion": "¿Agregar este mint?",
"cameraPermissionDenied": "Permiso de cámara denegado",
"paymentRequestNotSupported": "Los payment requests aún no están soportados",
"paymentRequestTitle": "Solicitud de pago",
"paymentRequestFrom": "Solicitud de",
"paymentRequestAmount": "Monto solicitado",
"paymentRequestDescription": "Descripción",
"paymentRequestMints": "Mints aceptados",
"paymentRequestAnyMint": "Cualquier mint",
"paymentRequestPay": "Pagar",
"paymentRequestPaying": "Pagando...",
"paymentRequestSuccess": "Pago enviado correctamente",
"paymentRequestNoTransport": "Esta solicitud no tiene método de entrega configurado",
"paymentRequestTransport": "Transporte",
"paymentRequestMintNotAccepted": "Tu mint activo no está en la lista de mints aceptados",
"paymentRequestUnitMismatch": "Unidad incompatible: la solicitud requiere {unit}",
"@paymentRequestUnitMismatch": {
"placeholders": {
"unit": { "type": "String" }
}
},
"paymentRequestInsufficientBalance": "Balance insuficiente",
"paymentRequestErrorParsing": "Error al leer la solicitud de pago",
"p2pkTitle": "Claves P2PK",
"p2pkSettingsDescription": "Recibir ecash bloqueado",
@@ -543,5 +563,53 @@
"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",
"request": "Solicitar",
"requestPayment": "Solicitar pago",
"requestPaymentDescription": "Generar solicitud de pago unificada",
"generateRequest": "Generar solicitud",
"generatingRequest": "Generando solicitud...",
"requestPaymentReceived": "Pago recibido",
"requestDescriptionHint": "Descripción (opcional)",
"universal": "Universal",
"copiedToClipboard": "Copiado al portapapeles",
"swap": "Cambiar",
"swapDescription": "Convertir entre sats y USD",
"swapFrom": "De",
"swapTo": "A",
"swapAction": "Cambiar",
"swapEstimatedFee": "Fee estimado",
"swapUseAll": "Usar todo",
"swapMinimum": "Mínimo: {amount}",
"@swapMinimum": {
"placeholders": {
"amount": { "type": "String" }
}
},
"swapProcessing": "Procesando swap...",
"swapSuccess": "Swap completado",
"swapErrorInsufficient": "Saldo insuficiente",
"swapErrorExpired": "La cotización ha expirado",
"swapErrorGeneric": "Error en el swap: {error}",
"@swapErrorGeneric": {
"placeholders": {
"error": { "type": "String" }
}
},
"swapChartUnavailable": "Precio no disponible · Toca para reintentar",
"swapChartMinMax": "24h Mín: {minPrice} — Máx: {maxPrice}",
"@swapChartMinMax": {
"placeholders": {
"minPrice": { "type": "String" },
"maxPrice": { "type": "String" }
}
},
"privacyPolicy": "Política de privacidad",
"privacyTitle": "NO RECOPILAMOS NADA",
"privacyGoodbye": "ADIÓS",
"privacyKeepReading": "(sigue leyendo si quieres…)",
"privacyBody": "No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces",
"privacyConclusion": "La mejor forma de proteger tus datos\nes no tenerlos"
}
+50 -2
View File
@@ -118,6 +118,7 @@
"confirm": "Confirmer",
"cancel": "Annuler",
"insufficientBalance": "Solde insuffisant",
"feeExceedsAmount": "Les frais dépassent le montant à envoyer",
"tokenCreationError": "Erreur de création du token : {error}",
"@tokenCreationError": {
"placeholders": {
@@ -502,7 +503,22 @@
"scanLightningInvoiceHint": "Scannez une facture Lightning (lnbc...)",
"addMintQuestion": "Ajouter ce mint ?",
"cameraPermissionDenied": "Permission de la caméra refusée",
"paymentRequestNotSupported": "Les demandes de paiement ne sont pas encore prises en charge",
"paymentRequestTitle": "Demande de paiement",
"paymentRequestFrom": "Demande de",
"paymentRequestAmount": "Montant demandé",
"paymentRequestDescription": "Description",
"paymentRequestMints": "Mints acceptés",
"paymentRequestAnyMint": "N'importe quel mint",
"paymentRequestPay": "Payer",
"paymentRequestPaying": "Paiement en cours...",
"paymentRequestSuccess": "Paiement envoyé avec succès",
"paymentRequestNoTransport": "Cette demande n'a pas de méthode de livraison configurée",
"paymentRequestTransport": "Transport",
"paymentRequestMintNotAccepted": "Votre mint actif n'est pas dans la liste des mints acceptés",
"paymentRequestUnitMismatch": "Unité incompatible : la demande nécessite {unit}",
"paymentRequestInsufficientBalance": "Solde insuffisant",
"paymentRequestErrorParsing": "Erreur lors de la lecture de la demande de paiement",
"p2pkTitle": "Clés P2PK",
"p2pkSettingsDescription": "Recevoir ecash verrouillé",
"p2pkExperimental": "P2PK est expérimental. Utiliser avec prudence.",
@@ -534,5 +550,37 @@
"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",
"request": "Demander",
"requestPayment": "Demander un paiement",
"requestPaymentDescription": "Générer une demande de paiement unifiée",
"generateRequest": "Générer la demande",
"generatingRequest": "Génération en cours...",
"requestPaymentReceived": "Paiement reçu",
"requestDescriptionHint": "Description (facultatif)",
"universal": "Universel",
"copiedToClipboard": "Copié dans le presse-papiers",
"swap": "Échanger",
"swapDescription": "Convertir entre sats et USD",
"swapFrom": "De",
"swapTo": "Vers",
"swapAction": "Échanger",
"swapEstimatedFee": "Frais estimés",
"swapUseAll": "Tout utiliser",
"swapMinimum": "Minimum : {amount}",
"swapProcessing": "Swap en cours...",
"swapSuccess": "Swap terminé",
"swapErrorInsufficient": "Solde insuffisant",
"swapErrorExpired": "Le devis a expiré",
"swapErrorGeneric": "Erreur de swap : {error}",
"swapChartUnavailable": "Prix indisponible · Appuyez pour réessayer",
"swapChartMinMax": "24h Min : {minPrice} — Max : {maxPrice}",
"privacyPolicy": "Politique de confidentialité",
"privacyTitle": "NOUS NE COLLECTONS RIEN",
"privacyGoodbye": "AU REVOIR",
"privacyKeepReading": "(continue à lire si tu veux…)",
"privacyBody": "Nous ne savons pas qui tu es\nNous ne savons pas combien tu as\nNous ne savons pas ce que tu fais",
"privacyConclusion": "La meilleure façon de protéger tes données,\nc'est de ne pas les avoir"
}
+50 -2
View File
@@ -118,6 +118,7 @@
"confirm": "Conferma",
"cancel": "Annulla",
"insufficientBalance": "Saldo insufficiente",
"feeExceedsAmount": "La commissione supera l'importo da inviare",
"tokenCreationError": "Errore creazione token: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -502,7 +503,22 @@
"scanLightningInvoiceHint": "Scansiona una fattura Lightning (lnbc...)",
"addMintQuestion": "Aggiungere questo mint?",
"cameraPermissionDenied": "Permesso fotocamera negato",
"paymentRequestNotSupported": "Le richieste di pagamento non sono ancora supportate",
"paymentRequestTitle": "Richiesta di pagamento",
"paymentRequestFrom": "Richiesta da",
"paymentRequestAmount": "Importo richiesto",
"paymentRequestDescription": "Descrizione",
"paymentRequestMints": "Mint accettati",
"paymentRequestAnyMint": "Qualsiasi mint",
"paymentRequestPay": "Paga",
"paymentRequestPaying": "Pagamento in corso...",
"paymentRequestSuccess": "Pagamento inviato con successo",
"paymentRequestNoTransport": "Questa richiesta non ha un metodo di consegna configurato",
"paymentRequestTransport": "Trasporto",
"paymentRequestMintNotAccepted": "Il tuo mint attivo non è nella lista dei mint accettati",
"paymentRequestUnitMismatch": "Unità incompatibile: la richiesta richiede {unit}",
"paymentRequestInsufficientBalance": "Saldo insufficiente",
"paymentRequestErrorParsing": "Errore nella lettura della richiesta di pagamento",
"p2pkTitle": "Chiavi P2PK",
"p2pkSettingsDescription": "Ricevi ecash bloccato",
"p2pkExperimental": "P2PK è sperimentale. Usare con cautela.",
@@ -534,5 +550,37 @@
"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",
"request": "Richiedi",
"requestPayment": "Richiedi pagamento",
"requestPaymentDescription": "Genera richiesta di pagamento unificata",
"generateRequest": "Genera richiesta",
"generatingRequest": "Generazione in corso...",
"requestPaymentReceived": "Pagamento ricevuto",
"requestDescriptionHint": "Descrizione (opzionale)",
"universal": "Universale",
"copiedToClipboard": "Copiato negli appunti",
"swap": "Scambia",
"swapDescription": "Converti tra sats e USD",
"swapFrom": "Da",
"swapTo": "A",
"swapAction": "Scambia",
"swapEstimatedFee": "Commissione stimata",
"swapUseAll": "Usa tutto",
"swapMinimum": "Minimo: {amount}",
"swapProcessing": "Swap in corso...",
"swapSuccess": "Swap completato",
"swapErrorInsufficient": "Saldo insufficiente",
"swapErrorExpired": "Il preventivo è scaduto",
"swapErrorGeneric": "Errore swap: {error}",
"swapChartUnavailable": "Prezzo non disponibile · Tocca per riprovare",
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
"privacyPolicy": "Informativa sulla privacy",
"privacyTitle": "NON RACCOGLIAMO NULLA",
"privacyGoodbye": "CIAO",
"privacyKeepReading": "(continua a leggere se vuoi…)",
"privacyBody": "Non sappiamo chi sei\nNon sappiamo quanto hai\nNon sappiamo cosa fai",
"privacyConclusion": "Il modo migliore per proteggere i tuoi dati\nè non averli"
}
+50 -2
View File
@@ -118,6 +118,7 @@
"confirm": "確認",
"cancel": "キャンセル",
"insufficientBalance": "残高不足",
"feeExceedsAmount": "手数料が送金額を超えています",
"tokenCreationError": "トークン作成エラー:{error}",
"@tokenCreationError": {
"placeholders": {
@@ -502,7 +503,22 @@
"scanLightningInvoiceHint": "Lightningインボイスをスキャン(lnbc...",
"addMintQuestion": "このMintを追加しますか?",
"cameraPermissionDenied": "カメラの許可が拒否されました",
"paymentRequestNotSupported": "支払いリクエストはまだサポートされていません",
"paymentRequestTitle": "支払いリクエスト",
"paymentRequestFrom": "リクエスト元",
"paymentRequestAmount": "リクエスト金額",
"paymentRequestDescription": "説明",
"paymentRequestMints": "対応ミント",
"paymentRequestAnyMint": "すべてのミント",
"paymentRequestPay": "支払う",
"paymentRequestPaying": "支払い中...",
"paymentRequestSuccess": "支払いが正常に送信されました",
"paymentRequestNoTransport": "このリクエストには配信方法が設定されていません",
"paymentRequestTransport": "トランスポート",
"paymentRequestMintNotAccepted": "アクティブなミントは対応ミントリストにありません",
"paymentRequestUnitMismatch": "単位が互換性がありません:リクエストには{unit}が必要です",
"paymentRequestInsufficientBalance": "残高不足",
"paymentRequestErrorParsing": "支払いリクエストの読み取りエラー",
"p2pkTitle": "P2PK鍵",
"p2pkSettingsDescription": "ロックされたecashを受け取る",
"p2pkExperimental": "P2PKは実験的機能です。注意してご使用ください。",
@@ -534,5 +550,37 @@
"p2pkErrorInvalidNsec": "無効なnsec",
"p2pkErrorKeyAlreadyExists": "この鍵は既に存在します",
"p2pkErrorKeyNotFound": "鍵が見つかりません",
"p2pkErrorCannotDeletePrimary": "プライマリ鍵は削除できません"
"p2pkErrorCannotDeletePrimary": "プライマリ鍵は削除できません",
"request": "リクエスト",
"requestPayment": "支払いをリクエスト",
"requestPaymentDescription": "統合支払いリクエストを生成",
"generateRequest": "リクエストを生成",
"generatingRequest": "生成中...",
"requestPaymentReceived": "支払いを受け取りました",
"requestDescriptionHint": "説明(任意)",
"universal": "ユニバーサル",
"copiedToClipboard": "クリップボードにコピーしました",
"swap": "交換",
"swapDescription": "SatsとUSDを変換",
"swapFrom": "送信元",
"swapTo": "送信先",
"swapAction": "交換",
"swapEstimatedFee": "推定手数料",
"swapUseAll": "全額使用",
"swapMinimum": "最小: {amount}",
"swapProcessing": "スワップ処理中...",
"swapSuccess": "スワップ完了",
"swapErrorInsufficient": "残高不足",
"swapErrorExpired": "見積もりの有効期限切れ",
"swapErrorGeneric": "スワップエラー: {error}",
"swapChartUnavailable": "価格を取得できません・タップで再試行",
"swapChartMinMax": "24h 安値: {minPrice} — 高値: {maxPrice}",
"privacyPolicy": "プライバシーポリシー",
"privacyTitle": "何も収集しません",
"privacyGoodbye": "さようなら",
"privacyKeepReading": "(読み続けたければどうぞ…)",
"privacyBody": "あなたが誰か知りません\nいくら持っているか知りません\n何をしているか知りません",
"privacyConclusion": "データを守る最善の方法は\nデータを持たないことです"
}
+50 -2
View File
@@ -118,6 +118,7 @@
"confirm": "확인",
"cancel": "취소",
"insufficientBalance": "잔액 부족",
"feeExceedsAmount": "수수료가 송금액을 초과합니다",
"tokenCreationError": "토큰 생성 오류: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -502,7 +503,22 @@
"scanLightningInvoiceHint": "Lightning 인보이스 스캔 (lnbc...)",
"addMintQuestion": "이 mint를 추가하시겠습니까?",
"cameraPermissionDenied": "카메라 권한이 거부되었습니다",
"paymentRequestNotSupported": "결제 요청은 아직 지원되지 않습니다",
"paymentRequestTitle": "결제 요청",
"paymentRequestFrom": "요청자",
"paymentRequestAmount": "요청 금액",
"paymentRequestDescription": "설명",
"paymentRequestMints": "허용된 민트",
"paymentRequestAnyMint": "모든 민트",
"paymentRequestPay": "결제",
"paymentRequestPaying": "결제 중...",
"paymentRequestSuccess": "결제가 성공적으로 전송되었습니다",
"paymentRequestNoTransport": "이 요청에는 전달 방법이 설정되지 않았습니다",
"paymentRequestTransport": "전송 방식",
"paymentRequestMintNotAccepted": "활성 민트가 허용된 민트 목록에 없습니다",
"paymentRequestUnitMismatch": "호환되지 않는 단위: 요청에 {unit}이(가) 필요합니다",
"paymentRequestInsufficientBalance": "잔액 부족",
"paymentRequestErrorParsing": "결제 요청을 읽는 중 오류 발생",
"p2pkTitle": "P2PK 키",
"p2pkSettingsDescription": "잠긴 ecash 받기",
"p2pkExperimental": "P2PK는 실험적 기능입니다. 주의하여 사용하세요.",
@@ -534,5 +550,37 @@
"p2pkErrorInvalidNsec": "유효하지 않은 nsec",
"p2pkErrorKeyAlreadyExists": "이 키는 이미 존재합니다",
"p2pkErrorKeyNotFound": "키를 찾을 수 없습니다",
"p2pkErrorCannotDeletePrimary": "기본 키는 삭제할 수 없습니다"
"p2pkErrorCannotDeletePrimary": "기본 키는 삭제할 수 없습니다",
"request": "요청",
"requestPayment": "결제 요청",
"requestPaymentDescription": "통합 결제 요청 생성",
"generateRequest": "요청 생성",
"generatingRequest": "생성 중...",
"requestPaymentReceived": "결제가 수신되었습니다",
"requestDescriptionHint": "설명 (선택사항)",
"universal": "유니버설",
"copiedToClipboard": "클립보드에 복사되었습니다",
"swap": "교환",
"swapDescription": "Sats와 USD 간 변환",
"swapFrom": "에서",
"swapTo": "으로",
"swapAction": "교환",
"swapEstimatedFee": "예상 수수료",
"swapUseAll": "전액 사용",
"swapMinimum": "최소: {amount}",
"swapProcessing": "교환 처리 중...",
"swapSuccess": "교환 완료",
"swapErrorInsufficient": "잔액 부족",
"swapErrorExpired": "견적이 만료되었습니다",
"swapErrorGeneric": "교환 오류: {error}",
"swapChartUnavailable": "가격 불러오기 실패 · 탭하여 재시도",
"swapChartMinMax": "24h 최저: {minPrice} — 최고: {maxPrice}",
"privacyPolicy": "개인정보 처리방침",
"privacyTitle": "우리는 아무것도 수집하지 않습니다",
"privacyGoodbye": "안녕",
"privacyKeepReading": "(계속 읽고 싶으면…)",
"privacyBody": "당신이 누구인지 모릅니다\n얼마를 가지고 있는지 모릅니다\n무엇을 하는지 모릅니다",
"privacyConclusion": "데이터를 보호하는 가장 좋은 방법은\n데이터를 갖지 않는 것입니다"
}
+273 -3
View File
@@ -661,6 +661,12 @@ abstract class L10n {
/// **'Balance insuficiente'**
String get insufficientBalance;
/// No description provided for @feeExceedsAmount.
///
/// In es, this message translates to:
/// **'La comisión supera el monto a enviar'**
String get feeExceedsAmount;
/// No description provided for @tokenCreationError.
///
/// In es, this message translates to:
@@ -2017,11 +2023,95 @@ abstract class L10n {
/// **'Permiso de cámara denegado'**
String get cameraPermissionDenied;
/// No description provided for @paymentRequestNotSupported.
/// No description provided for @paymentRequestTitle.
///
/// In es, this message translates to:
/// **'Los payment requests aún no están soportados'**
String get paymentRequestNotSupported;
/// **'Solicitud de pago'**
String get paymentRequestTitle;
/// No description provided for @paymentRequestFrom.
///
/// In es, this message translates to:
/// **'Solicitud de'**
String get paymentRequestFrom;
/// No description provided for @paymentRequestAmount.
///
/// In es, this message translates to:
/// **'Monto solicitado'**
String get paymentRequestAmount;
/// No description provided for @paymentRequestDescription.
///
/// In es, this message translates to:
/// **'Descripción'**
String get paymentRequestDescription;
/// No description provided for @paymentRequestMints.
///
/// In es, this message translates to:
/// **'Mints aceptados'**
String get paymentRequestMints;
/// No description provided for @paymentRequestAnyMint.
///
/// In es, this message translates to:
/// **'Cualquier mint'**
String get paymentRequestAnyMint;
/// No description provided for @paymentRequestPay.
///
/// In es, this message translates to:
/// **'Pagar'**
String get paymentRequestPay;
/// No description provided for @paymentRequestPaying.
///
/// In es, this message translates to:
/// **'Pagando...'**
String get paymentRequestPaying;
/// No description provided for @paymentRequestSuccess.
///
/// In es, this message translates to:
/// **'Pago enviado correctamente'**
String get paymentRequestSuccess;
/// No description provided for @paymentRequestNoTransport.
///
/// In es, this message translates to:
/// **'Esta solicitud no tiene método de entrega configurado'**
String get paymentRequestNoTransport;
/// No description provided for @paymentRequestTransport.
///
/// In es, this message translates to:
/// **'Transporte'**
String get paymentRequestTransport;
/// No description provided for @paymentRequestMintNotAccepted.
///
/// In es, this message translates to:
/// **'Tu mint activo no está en la lista de mints aceptados'**
String get paymentRequestMintNotAccepted;
/// No description provided for @paymentRequestUnitMismatch.
///
/// In es, this message translates to:
/// **'Unidad incompatible: la solicitud requiere {unit}'**
String paymentRequestUnitMismatch(String unit);
/// No description provided for @paymentRequestInsufficientBalance.
///
/// In es, this message translates to:
/// **'Balance insuficiente'**
String get paymentRequestInsufficientBalance;
/// No description provided for @paymentRequestErrorParsing.
///
/// In es, this message translates to:
/// **'Error al leer la solicitud de pago'**
String get paymentRequestErrorParsing;
/// No description provided for @p2pkTitle.
///
@@ -2214,6 +2304,186 @@ abstract class L10n {
/// In es, this message translates to:
/// **'No se puede eliminar la clave principal'**
String get p2pkErrorCannotDeletePrimary;
/// No description provided for @request.
///
/// In es, this message translates to:
/// **'Solicitar'**
String get request;
/// No description provided for @requestPayment.
///
/// In es, this message translates to:
/// **'Solicitar pago'**
String get requestPayment;
/// No description provided for @requestPaymentDescription.
///
/// In es, this message translates to:
/// **'Generar solicitud de pago unificada'**
String get requestPaymentDescription;
/// No description provided for @generateRequest.
///
/// In es, this message translates to:
/// **'Generar solicitud'**
String get generateRequest;
/// No description provided for @generatingRequest.
///
/// In es, this message translates to:
/// **'Generando solicitud...'**
String get generatingRequest;
/// No description provided for @requestPaymentReceived.
///
/// In es, this message translates to:
/// **'Pago recibido'**
String get requestPaymentReceived;
/// No description provided for @requestDescriptionHint.
///
/// In es, this message translates to:
/// **'Descripción (opcional)'**
String get requestDescriptionHint;
/// No description provided for @universal.
///
/// In es, this message translates to:
/// **'Universal'**
String get universal;
/// No description provided for @copiedToClipboard.
///
/// In es, this message translates to:
/// **'Copiado al portapapeles'**
String get copiedToClipboard;
/// No description provided for @swap.
///
/// In es, this message translates to:
/// **'Cambiar'**
String get swap;
/// No description provided for @swapDescription.
///
/// In es, this message translates to:
/// **'Convertir entre sats y USD'**
String get swapDescription;
/// No description provided for @swapFrom.
///
/// In es, this message translates to:
/// **'De'**
String get swapFrom;
/// No description provided for @swapTo.
///
/// In es, this message translates to:
/// **'A'**
String get swapTo;
/// No description provided for @swapAction.
///
/// In es, this message translates to:
/// **'Cambiar'**
String get swapAction;
/// No description provided for @swapEstimatedFee.
///
/// In es, this message translates to:
/// **'Fee estimado'**
String get swapEstimatedFee;
/// No description provided for @swapUseAll.
///
/// In es, this message translates to:
/// **'Usar todo'**
String get swapUseAll;
/// No description provided for @swapMinimum.
///
/// In es, this message translates to:
/// **'Mínimo: {amount}'**
String swapMinimum(String amount);
/// No description provided for @swapProcessing.
///
/// In es, this message translates to:
/// **'Procesando swap...'**
String get swapProcessing;
/// No description provided for @swapSuccess.
///
/// In es, this message translates to:
/// **'Swap completado'**
String get swapSuccess;
/// No description provided for @swapErrorInsufficient.
///
/// In es, this message translates to:
/// **'Saldo insuficiente'**
String get swapErrorInsufficient;
/// No description provided for @swapErrorExpired.
///
/// In es, this message translates to:
/// **'La cotización ha expirado'**
String get swapErrorExpired;
/// No description provided for @swapErrorGeneric.
///
/// In es, this message translates to:
/// **'Error en el swap: {error}'**
String swapErrorGeneric(String error);
/// No description provided for @swapChartUnavailable.
///
/// In es, this message translates to:
/// **'Precio no disponible · Toca para reintentar'**
String get swapChartUnavailable;
/// No description provided for @swapChartMinMax.
///
/// In es, this message translates to:
/// **'24h Mín: {minPrice} — Máx: {maxPrice}'**
String swapChartMinMax(String minPrice, String maxPrice);
/// No description provided for @privacyPolicy.
///
/// In es, this message translates to:
/// **'Política de privacidad'**
String get privacyPolicy;
/// No description provided for @privacyTitle.
///
/// In es, this message translates to:
/// **'NO RECOPILAMOS NADA'**
String get privacyTitle;
/// No description provided for @privacyGoodbye.
///
/// In es, this message translates to:
/// **'ADIÓS'**
String get privacyGoodbye;
/// No description provided for @privacyKeepReading.
///
/// In es, this message translates to:
/// **'(sigue leyendo si quieres…)'**
String get privacyKeepReading;
/// No description provided for @privacyBody.
///
/// In es, this message translates to:
/// **'No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces'**
String get privacyBody;
/// No description provided for @privacyConclusion.
///
/// In es, this message translates to:
/// **'La mejor forma de proteger tus datos\nes no tenerlos'**
String get privacyConclusion;
}
class _L10nDelegate extends LocalizationsDelegate<L10n> {
+151 -2
View File
@@ -295,6 +295,9 @@ class L10nDe extends L10n {
@override
String get insufficientBalance => 'Unzureichendes Guthaben';
@override
String get feeExceedsAmount => 'Die Gebühr übersteigt den Sendebetrag';
@override
String tokenCreationError(String error) {
return 'Fehler beim Erstellen des Tokens: $error';
@@ -1063,8 +1066,54 @@ class L10nDe extends L10n {
String get cameraPermissionDenied => 'Kamera-Berechtigung verweigert';
@override
String get paymentRequestNotSupported =>
'Zahlungsanfragen werden noch nicht unterstützt';
String get paymentRequestTitle => 'Zahlungsanfrage';
@override
String get paymentRequestFrom => 'Anfrage von';
@override
String get paymentRequestAmount => 'Angeforderter Betrag';
@override
String get paymentRequestDescription => 'Beschreibung';
@override
String get paymentRequestMints => 'Akzeptierte Mints';
@override
String get paymentRequestAnyMint => 'Jeder Mint';
@override
String get paymentRequestPay => 'Bezahlen';
@override
String get paymentRequestPaying => 'Bezahle...';
@override
String get paymentRequestSuccess => 'Zahlung erfolgreich gesendet';
@override
String get paymentRequestNoTransport =>
'Diese Anfrage hat keine konfigurierte Zustellmethode';
@override
String get paymentRequestTransport => 'Transport';
@override
String get paymentRequestMintNotAccepted =>
'Dein aktiver Mint ist nicht in der Liste der akzeptierten Mints';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Inkompatible Einheit: Anfrage erfordert $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Unzureichendes Guthaben';
@override
String get paymentRequestErrorParsing =>
'Fehler beim Lesen der Zahlungsanfrage';
@override
String get p2pkTitle => 'P2PK-Schlüssel';
@@ -1167,4 +1216,104 @@ class L10nDe extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'Primärschlüssel kann nicht gelöscht werden';
@override
String get request => 'Anfordern';
@override
String get requestPayment => 'Zahlung anfordern';
@override
String get requestPaymentDescription =>
'Einheitliche Zahlungsanforderung erstellen';
@override
String get generateRequest => 'Anforderung erstellen';
@override
String get generatingRequest => 'Anforderung wird erstellt...';
@override
String get requestPaymentReceived => 'Zahlung erhalten';
@override
String get requestDescriptionHint => 'Beschreibung (optional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'In die Zwischenablage kopiert';
@override
String get swap => 'Tauschen';
@override
String get swapDescription => 'Zwischen Sats und USD wechseln';
@override
String get swapFrom => 'Von';
@override
String get swapTo => 'Nach';
@override
String get swapAction => 'Tauschen';
@override
String get swapEstimatedFee => 'Geschätzte Gebühr';
@override
String get swapUseAll => 'Alles verwenden';
@override
String swapMinimum(String amount) {
return 'Minimum: $amount';
}
@override
String get swapProcessing => 'Swap wird verarbeitet...';
@override
String get swapSuccess => 'Swap abgeschlossen';
@override
String get swapErrorInsufficient => 'Unzureichendes Guthaben';
@override
String get swapErrorExpired => 'Angebot abgelaufen';
@override
String swapErrorGeneric(String error) {
return 'Swap-Fehler: $error';
}
@override
String get swapChartUnavailable =>
'Preis nicht verfügbar · Tippen zum Wiederholen';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min: $minPrice — Max: $maxPrice';
}
@override
String get privacyPolicy => 'Datenschutz';
@override
String get privacyTitle => 'WIR SAMMELN NICHTS';
@override
String get privacyGoodbye => 'TSCHÜSS';
@override
String get privacyKeepReading => '(lies weiter, wenn du willst…)';
@override
String get privacyBody =>
'Wir wissen nicht, wer du bist\nWir wissen nicht, wie viel du hast\nWir wissen nicht, was du tust';
@override
String get privacyConclusion =>
'Der beste Weg, deine Daten zu schützen,\nist sie nicht zu haben';
}
+148 -2
View File
@@ -292,6 +292,9 @@ class L10nEn extends L10n {
@override
String get insufficientBalance => 'Insufficient balance';
@override
String get feeExceedsAmount => 'Fee exceeds the amount to send';
@override
String tokenCreationError(String error) {
return 'Error creating token: $error';
@@ -1050,8 +1053,53 @@ class L10nEn extends L10n {
String get cameraPermissionDenied => 'Camera permission denied';
@override
String get paymentRequestNotSupported =>
'Payment requests are not yet supported';
String get paymentRequestTitle => 'Payment Request';
@override
String get paymentRequestFrom => 'Request from';
@override
String get paymentRequestAmount => 'Requested amount';
@override
String get paymentRequestDescription => 'Description';
@override
String get paymentRequestMints => 'Accepted mints';
@override
String get paymentRequestAnyMint => 'Any mint';
@override
String get paymentRequestPay => 'Pay';
@override
String get paymentRequestPaying => 'Paying...';
@override
String get paymentRequestSuccess => 'Payment sent successfully';
@override
String get paymentRequestNoTransport =>
'This request has no delivery method configured';
@override
String get paymentRequestTransport => 'Transport';
@override
String get paymentRequestMintNotAccepted =>
'Your active mint is not in the list of accepted mints';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Incompatible unit: request requires $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Insufficient balance';
@override
String get paymentRequestErrorParsing => 'Error reading payment request';
@override
String get p2pkTitle => 'P2PK Keys';
@@ -1150,4 +1198,102 @@ class L10nEn extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'Cannot delete primary key';
@override
String get request => 'Request';
@override
String get requestPayment => 'Request Payment';
@override
String get requestPaymentDescription => 'Generate unified payment request';
@override
String get generateRequest => 'Generate Request';
@override
String get generatingRequest => 'Generating request...';
@override
String get requestPaymentReceived => 'Payment received';
@override
String get requestDescriptionHint => 'Description (optional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Copied to clipboard';
@override
String get swap => 'Swap';
@override
String get swapDescription => 'Convert between sats and USD';
@override
String get swapFrom => 'From';
@override
String get swapTo => 'To';
@override
String get swapAction => 'Swap';
@override
String get swapEstimatedFee => 'Estimated fee';
@override
String get swapUseAll => 'Use all';
@override
String swapMinimum(String amount) {
return 'Minimum: $amount';
}
@override
String get swapProcessing => 'Processing swap...';
@override
String get swapSuccess => 'Swap completed';
@override
String get swapErrorInsufficient => 'Insufficient balance';
@override
String get swapErrorExpired => 'Quote has expired';
@override
String swapErrorGeneric(String error) {
return 'Swap error: $error';
}
@override
String get swapChartUnavailable => 'Price unavailable · Tap to retry';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min: $minPrice — Max: $maxPrice';
}
@override
String get privacyPolicy => 'Privacy policy';
@override
String get privacyTitle => 'WE COLLECT NOTHING';
@override
String get privacyGoodbye => 'BYE';
@override
String get privacyKeepReading => '(keep reading if you want…)';
@override
String get privacyBody =>
'We don\'t know who you are\nWe don\'t know how much you have\nWe don\'t know what you do';
@override
String get privacyConclusion =>
'The best way to protect your data\nis not to have it';
}
+149 -2
View File
@@ -292,6 +292,9 @@ class L10nEs extends L10n {
@override
String get insufficientBalance => 'Balance insuficiente';
@override
String get feeExceedsAmount => 'La comisión supera el monto a enviar';
@override
String tokenCreationError(String error) {
return 'Error al crear token: $error';
@@ -1055,8 +1058,53 @@ class L10nEs extends L10n {
String get cameraPermissionDenied => 'Permiso de cámara denegado';
@override
String get paymentRequestNotSupported =>
'Los payment requests aún no están soportados';
String get paymentRequestTitle => 'Solicitud de pago';
@override
String get paymentRequestFrom => 'Solicitud de';
@override
String get paymentRequestAmount => 'Monto solicitado';
@override
String get paymentRequestDescription => 'Descripción';
@override
String get paymentRequestMints => 'Mints aceptados';
@override
String get paymentRequestAnyMint => 'Cualquier mint';
@override
String get paymentRequestPay => 'Pagar';
@override
String get paymentRequestPaying => 'Pagando...';
@override
String get paymentRequestSuccess => 'Pago enviado correctamente';
@override
String get paymentRequestNoTransport =>
'Esta solicitud no tiene método de entrega configurado';
@override
String get paymentRequestTransport => 'Transporte';
@override
String get paymentRequestMintNotAccepted =>
'Tu mint activo no está en la lista de mints aceptados';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unidad incompatible: la solicitud requiere $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Balance insuficiente';
@override
String get paymentRequestErrorParsing => 'Error al leer la solicitud de pago';
@override
String get p2pkTitle => 'Claves P2PK';
@@ -1158,4 +1206,103 @@ class L10nEs extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'No se puede eliminar la clave principal';
@override
String get request => 'Solicitar';
@override
String get requestPayment => 'Solicitar pago';
@override
String get requestPaymentDescription => 'Generar solicitud de pago unificada';
@override
String get generateRequest => 'Generar solicitud';
@override
String get generatingRequest => 'Generando solicitud...';
@override
String get requestPaymentReceived => 'Pago recibido';
@override
String get requestDescriptionHint => 'Descripción (opcional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Copiado al portapapeles';
@override
String get swap => 'Cambiar';
@override
String get swapDescription => 'Convertir entre sats y USD';
@override
String get swapFrom => 'De';
@override
String get swapTo => 'A';
@override
String get swapAction => 'Cambiar';
@override
String get swapEstimatedFee => 'Fee estimado';
@override
String get swapUseAll => 'Usar todo';
@override
String swapMinimum(String amount) {
return 'Mínimo: $amount';
}
@override
String get swapProcessing => 'Procesando swap...';
@override
String get swapSuccess => 'Swap completado';
@override
String get swapErrorInsufficient => 'Saldo insuficiente';
@override
String get swapErrorExpired => 'La cotización ha expirado';
@override
String swapErrorGeneric(String error) {
return 'Error en el swap: $error';
}
@override
String get swapChartUnavailable =>
'Precio no disponible · Toca para reintentar';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Mín: $minPrice — Máx: $maxPrice';
}
@override
String get privacyPolicy => 'Política de privacidad';
@override
String get privacyTitle => 'NO RECOPILAMOS NADA';
@override
String get privacyGoodbye => 'ADIÓS';
@override
String get privacyKeepReading => '(sigue leyendo si quieres…)';
@override
String get privacyBody =>
'No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces';
@override
String get privacyConclusion =>
'La mejor forma de proteger tus datos\nes no tenerlos';
}
+151 -2
View File
@@ -296,6 +296,9 @@ class L10nFr extends L10n {
@override
String get insufficientBalance => 'Solde insuffisant';
@override
String get feeExceedsAmount => 'Les frais dépassent le montant à envoyer';
@override
String tokenCreationError(String error) {
return 'Erreur de création du token : $error';
@@ -1068,8 +1071,54 @@ class L10nFr extends L10n {
String get cameraPermissionDenied => 'Permission de la caméra refusée';
@override
String get paymentRequestNotSupported =>
'Les demandes de paiement ne sont pas encore prises en charge';
String get paymentRequestTitle => 'Demande de paiement';
@override
String get paymentRequestFrom => 'Demande de';
@override
String get paymentRequestAmount => 'Montant demandé';
@override
String get paymentRequestDescription => 'Description';
@override
String get paymentRequestMints => 'Mints acceptés';
@override
String get paymentRequestAnyMint => 'N\'importe quel mint';
@override
String get paymentRequestPay => 'Payer';
@override
String get paymentRequestPaying => 'Paiement en cours...';
@override
String get paymentRequestSuccess => 'Paiement envoyé avec succès';
@override
String get paymentRequestNoTransport =>
'Cette demande n\'a pas de méthode de livraison configurée';
@override
String get paymentRequestTransport => 'Transport';
@override
String get paymentRequestMintNotAccepted =>
'Votre mint actif n\'est pas dans la liste des mints acceptés';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unité incompatible : la demande nécessite $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Solde insuffisant';
@override
String get paymentRequestErrorParsing =>
'Erreur lors de la lecture de la demande de paiement';
@override
String get p2pkTitle => 'Clés P2PK';
@@ -1172,4 +1221,104 @@ class L10nFr extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'Impossible de supprimer la clé principale';
@override
String get request => 'Demander';
@override
String get requestPayment => 'Demander un paiement';
@override
String get requestPaymentDescription =>
'Générer une demande de paiement unifiée';
@override
String get generateRequest => 'Générer la demande';
@override
String get generatingRequest => 'Génération en cours...';
@override
String get requestPaymentReceived => 'Paiement reçu';
@override
String get requestDescriptionHint => 'Description (facultatif)';
@override
String get universal => 'Universel';
@override
String get copiedToClipboard => 'Copié dans le presse-papiers';
@override
String get swap => 'Échanger';
@override
String get swapDescription => 'Convertir entre sats et USD';
@override
String get swapFrom => 'De';
@override
String get swapTo => 'Vers';
@override
String get swapAction => 'Échanger';
@override
String get swapEstimatedFee => 'Frais estimés';
@override
String get swapUseAll => 'Tout utiliser';
@override
String swapMinimum(String amount) {
return 'Minimum : $amount';
}
@override
String get swapProcessing => 'Swap en cours...';
@override
String get swapSuccess => 'Swap terminé';
@override
String get swapErrorInsufficient => 'Solde insuffisant';
@override
String get swapErrorExpired => 'Le devis a expiré';
@override
String swapErrorGeneric(String error) {
return 'Erreur de swap : $error';
}
@override
String get swapChartUnavailable =>
'Prix indisponible · Appuyez pour réessayer';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min : $minPrice — Max : $maxPrice';
}
@override
String get privacyPolicy => 'Politique de confidentialité';
@override
String get privacyTitle => 'NOUS NE COLLECTONS RIEN';
@override
String get privacyGoodbye => 'AU REVOIR';
@override
String get privacyKeepReading => '(continue à lire si tu veux…)';
@override
String get privacyBody =>
'Nous ne savons pas qui tu es\nNous ne savons pas combien tu as\nNous ne savons pas ce que tu fais';
@override
String get privacyConclusion =>
'La meilleure façon de protéger tes données,\nc\'est de ne pas les avoir';
}
+151 -2
View File
@@ -293,6 +293,9 @@ class L10nIt extends L10n {
@override
String get insufficientBalance => 'Saldo insufficiente';
@override
String get feeExceedsAmount => 'La commissione supera l\'importo da inviare';
@override
String tokenCreationError(String error) {
return 'Errore creazione token: $error';
@@ -1058,8 +1061,54 @@ class L10nIt extends L10n {
String get cameraPermissionDenied => 'Permesso fotocamera negato';
@override
String get paymentRequestNotSupported =>
'Le richieste di pagamento non sono ancora supportate';
String get paymentRequestTitle => 'Richiesta di pagamento';
@override
String get paymentRequestFrom => 'Richiesta da';
@override
String get paymentRequestAmount => 'Importo richiesto';
@override
String get paymentRequestDescription => 'Descrizione';
@override
String get paymentRequestMints => 'Mint accettati';
@override
String get paymentRequestAnyMint => 'Qualsiasi mint';
@override
String get paymentRequestPay => 'Paga';
@override
String get paymentRequestPaying => 'Pagamento in corso...';
@override
String get paymentRequestSuccess => 'Pagamento inviato con successo';
@override
String get paymentRequestNoTransport =>
'Questa richiesta non ha un metodo di consegna configurato';
@override
String get paymentRequestTransport => 'Trasporto';
@override
String get paymentRequestMintNotAccepted =>
'Il tuo mint attivo non è nella lista dei mint accettati';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unità incompatibile: la richiesta richiede $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Saldo insufficiente';
@override
String get paymentRequestErrorParsing =>
'Errore nella lettura della richiesta di pagamento';
@override
String get p2pkTitle => 'Chiavi P2PK';
@@ -1160,4 +1209,104 @@ class L10nIt extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'Impossibile eliminare la chiave principale';
@override
String get request => 'Richiedi';
@override
String get requestPayment => 'Richiedi pagamento';
@override
String get requestPaymentDescription =>
'Genera richiesta di pagamento unificata';
@override
String get generateRequest => 'Genera richiesta';
@override
String get generatingRequest => 'Generazione in corso...';
@override
String get requestPaymentReceived => 'Pagamento ricevuto';
@override
String get requestDescriptionHint => 'Descrizione (opzionale)';
@override
String get universal => 'Universale';
@override
String get copiedToClipboard => 'Copiato negli appunti';
@override
String get swap => 'Scambia';
@override
String get swapDescription => 'Converti tra sats e USD';
@override
String get swapFrom => 'Da';
@override
String get swapTo => 'A';
@override
String get swapAction => 'Scambia';
@override
String get swapEstimatedFee => 'Commissione stimata';
@override
String get swapUseAll => 'Usa tutto';
@override
String swapMinimum(String amount) {
return 'Minimo: $amount';
}
@override
String get swapProcessing => 'Swap in corso...';
@override
String get swapSuccess => 'Swap completato';
@override
String get swapErrorInsufficient => 'Saldo insufficiente';
@override
String get swapErrorExpired => 'Il preventivo è scaduto';
@override
String swapErrorGeneric(String error) {
return 'Errore swap: $error';
}
@override
String get swapChartUnavailable =>
'Prezzo non disponibile · Tocca per riprovare';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min: $minPrice — Max: $maxPrice';
}
@override
String get privacyPolicy => 'Informativa sulla privacy';
@override
String get privacyTitle => 'NON RACCOGLIAMO NULLA';
@override
String get privacyGoodbye => 'CIAO';
@override
String get privacyKeepReading => '(continua a leggere se vuoi…)';
@override
String get privacyBody =>
'Non sappiamo chi sei\nNon sappiamo quanto hai\nNon sappiamo cosa fai';
@override
String get privacyConclusion =>
'Il modo migliore per proteggere i tuoi dati\nè non averli';
}
+144 -1
View File
@@ -288,6 +288,9 @@ class L10nJa extends L10n {
@override
String get insufficientBalance => '残高不足';
@override
String get feeExceedsAmount => '手数料が送金額を超えています';
@override
String tokenCreationError(String error) {
return 'トークン作成エラー:$error';
@@ -1038,7 +1041,51 @@ class L10nJa extends L10n {
String get cameraPermissionDenied => 'カメラの許可が拒否されました';
@override
String get paymentRequestNotSupported => '支払いリクエストはまだサポートされていません';
String get paymentRequestTitle => '支払いリクエスト';
@override
String get paymentRequestFrom => 'リクエスト元';
@override
String get paymentRequestAmount => 'リクエスト金額';
@override
String get paymentRequestDescription => '説明';
@override
String get paymentRequestMints => '対応ミント';
@override
String get paymentRequestAnyMint => 'すべてのミント';
@override
String get paymentRequestPay => '支払う';
@override
String get paymentRequestPaying => '支払い中...';
@override
String get paymentRequestSuccess => '支払いが正常に送信されました';
@override
String get paymentRequestNoTransport => 'このリクエストには配信方法が設定されていません';
@override
String get paymentRequestTransport => 'トランスポート';
@override
String get paymentRequestMintNotAccepted => 'アクティブなミントは対応ミントリストにありません';
@override
String paymentRequestUnitMismatch(String unit) {
return '単位が互換性がありません:リクエストには$unitが必要です';
}
@override
String get paymentRequestInsufficientBalance => '残高不足';
@override
String get paymentRequestErrorParsing => '支払いリクエストの読み取りエラー';
@override
String get p2pkTitle => 'P2PK鍵';
@@ -1136,4 +1183,100 @@ class L10nJa extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'プライマリ鍵は削除できません';
@override
String get request => 'リクエスト';
@override
String get requestPayment => '支払いをリクエスト';
@override
String get requestPaymentDescription => '統合支払いリクエストを生成';
@override
String get generateRequest => 'リクエストを生成';
@override
String get generatingRequest => '生成中...';
@override
String get requestPaymentReceived => '支払いを受け取りました';
@override
String get requestDescriptionHint => '説明(任意)';
@override
String get universal => 'ユニバーサル';
@override
String get copiedToClipboard => 'クリップボードにコピーしました';
@override
String get swap => '交換';
@override
String get swapDescription => 'SatsとUSDを変換';
@override
String get swapFrom => '送信元';
@override
String get swapTo => '送信先';
@override
String get swapAction => '交換';
@override
String get swapEstimatedFee => '推定手数料';
@override
String get swapUseAll => '全額使用';
@override
String swapMinimum(String amount) {
return '最小: $amount';
}
@override
String get swapProcessing => 'スワップ処理中...';
@override
String get swapSuccess => 'スワップ完了';
@override
String get swapErrorInsufficient => '残高不足';
@override
String get swapErrorExpired => '見積もりの有効期限切れ';
@override
String swapErrorGeneric(String error) {
return 'スワップエラー: $error';
}
@override
String get swapChartUnavailable => '価格を取得できません・タップで再試行';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h 安値: $minPrice — 高値: $maxPrice';
}
@override
String get privacyPolicy => 'プライバシーポリシー';
@override
String get privacyTitle => '何も収集しません';
@override
String get privacyGoodbye => 'さようなら';
@override
String get privacyKeepReading => '(読み続けたければどうぞ…)';
@override
String get privacyBody => 'あなたが誰か知りません\nいくら持っているか知りません\n何をしているか知りません';
@override
String get privacyConclusion => 'データを守る最善の方法は\nデータを持たないことです';
}
+144 -1
View File
@@ -290,6 +290,9 @@ class L10nKo extends L10n {
@override
String get insufficientBalance => '잔액 부족';
@override
String get feeExceedsAmount => '수수료가 송금액을 초과합니다';
@override
String tokenCreationError(String error) {
return '토큰 생성 오류: $error';
@@ -1040,7 +1043,51 @@ class L10nKo extends L10n {
String get cameraPermissionDenied => '카메라 권한이 거부되었습니다';
@override
String get paymentRequestNotSupported => '결제 요청은 아직 지원되지 않습니다';
String get paymentRequestTitle => '결제 요청';
@override
String get paymentRequestFrom => '요청자';
@override
String get paymentRequestAmount => '요청 금액';
@override
String get paymentRequestDescription => '설명';
@override
String get paymentRequestMints => '허용된 민트';
@override
String get paymentRequestAnyMint => '모든 민트';
@override
String get paymentRequestPay => '결제';
@override
String get paymentRequestPaying => '결제 중...';
@override
String get paymentRequestSuccess => '결제가 성공적으로 전송되었습니다';
@override
String get paymentRequestNoTransport => '이 요청에는 전달 방법이 설정되지 않았습니다';
@override
String get paymentRequestTransport => '전송 방식';
@override
String get paymentRequestMintNotAccepted => '활성 민트가 허용된 민트 목록에 없습니다';
@override
String paymentRequestUnitMismatch(String unit) {
return '호환되지 않는 단위: 요청에 $unit이(가) 필요합니다';
}
@override
String get paymentRequestInsufficientBalance => '잔액 부족';
@override
String get paymentRequestErrorParsing => '결제 요청을 읽는 중 오류 발생';
@override
String get p2pkTitle => 'P2PK 키';
@@ -1138,4 +1185,100 @@ class L10nKo extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => '기본 키는 삭제할 수 없습니다';
@override
String get request => '요청';
@override
String get requestPayment => '결제 요청';
@override
String get requestPaymentDescription => '통합 결제 요청 생성';
@override
String get generateRequest => '요청 생성';
@override
String get generatingRequest => '생성 중...';
@override
String get requestPaymentReceived => '결제가 수신되었습니다';
@override
String get requestDescriptionHint => '설명 (선택사항)';
@override
String get universal => '유니버설';
@override
String get copiedToClipboard => '클립보드에 복사되었습니다';
@override
String get swap => '교환';
@override
String get swapDescription => 'Sats와 USD 간 변환';
@override
String get swapFrom => '에서';
@override
String get swapTo => '으로';
@override
String get swapAction => '교환';
@override
String get swapEstimatedFee => '예상 수수료';
@override
String get swapUseAll => '전액 사용';
@override
String swapMinimum(String amount) {
return '최소: $amount';
}
@override
String get swapProcessing => '교환 처리 중...';
@override
String get swapSuccess => '교환 완료';
@override
String get swapErrorInsufficient => '잔액 부족';
@override
String get swapErrorExpired => '견적이 만료되었습니다';
@override
String swapErrorGeneric(String error) {
return '교환 오류: $error';
}
@override
String get swapChartUnavailable => '가격 불러오기 실패 · 탭하여 재시도';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h 최저: $minPrice — 최고: $maxPrice';
}
@override
String get privacyPolicy => '개인정보 처리방침';
@override
String get privacyTitle => '우리는 아무것도 수집하지 않습니다';
@override
String get privacyGoodbye => '안녕';
@override
String get privacyKeepReading => '(계속 읽고 싶으면…)';
@override
String get privacyBody => '당신이 누구인지 모릅니다\n얼마를 가지고 있는지 모릅니다\n무엇을 하는지 모릅니다';
@override
String get privacyConclusion => '데이터를 보호하는 가장 좋은 방법은\n데이터를 갖지 않는 것입니다';
}
+151 -2
View File
@@ -293,6 +293,9 @@ class L10nPt extends L10n {
@override
String get insufficientBalance => 'Saldo insuficiente';
@override
String get feeExceedsAmount => 'A taxa excede o valor a enviar';
@override
String tokenCreationError(String error) {
return 'Erro ao criar token: $error';
@@ -1057,8 +1060,54 @@ class L10nPt extends L10n {
String get cameraPermissionDenied => 'Permissão de câmera negada';
@override
String get paymentRequestNotSupported =>
'Solicitações de pagamento ainda não são suportadas';
String get paymentRequestTitle => 'Solicitação de pagamento';
@override
String get paymentRequestFrom => 'Solicitação de';
@override
String get paymentRequestAmount => 'Valor solicitado';
@override
String get paymentRequestDescription => 'Descrição';
@override
String get paymentRequestMints => 'Mints aceitos';
@override
String get paymentRequestAnyMint => 'Qualquer mint';
@override
String get paymentRequestPay => 'Pagar';
@override
String get paymentRequestPaying => 'Pagando...';
@override
String get paymentRequestSuccess => 'Pagamento enviado com sucesso';
@override
String get paymentRequestNoTransport =>
'Esta solicitação não tem método de entrega configurado';
@override
String get paymentRequestTransport => 'Transporte';
@override
String get paymentRequestMintNotAccepted =>
'Seu mint ativo não está na lista de mints aceitos';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unidade incompatível: a solicitação requer $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Saldo insuficiente';
@override
String get paymentRequestErrorParsing =>
'Erro ao ler a solicitação de pagamento';
@override
String get p2pkTitle => 'Chaves P2PK';
@@ -1160,4 +1209,104 @@ class L10nPt extends L10n {
@override
String get p2pkErrorCannotDeletePrimary =>
'Não é possível excluir a chave principal';
@override
String get request => 'Solicitar';
@override
String get requestPayment => 'Solicitar pagamento';
@override
String get requestPaymentDescription =>
'Gerar solicitação de pagamento unificada';
@override
String get generateRequest => 'Gerar solicitação';
@override
String get generatingRequest => 'Gerando solicitação...';
@override
String get requestPaymentReceived => 'Pagamento recebido';
@override
String get requestDescriptionHint => 'Descrição (opcional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Copiado para a área de transferência';
@override
String get swap => 'Trocar';
@override
String get swapDescription => 'Converter entre sats e USD';
@override
String get swapFrom => 'De';
@override
String get swapTo => 'Para';
@override
String get swapAction => 'Trocar';
@override
String get swapEstimatedFee => 'Taxa estimada';
@override
String get swapUseAll => 'Usar tudo';
@override
String swapMinimum(String amount) {
return 'Mínimo: $amount';
}
@override
String get swapProcessing => 'Processando swap...';
@override
String get swapSuccess => 'Swap concluído';
@override
String get swapErrorInsufficient => 'Saldo insuficiente';
@override
String get swapErrorExpired => 'A cotação expirou';
@override
String swapErrorGeneric(String error) {
return 'Erro no swap: $error';
}
@override
String get swapChartUnavailable =>
'Preço indisponível · Toque para tentar novamente';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Mín: $minPrice — Máx: $maxPrice';
}
@override
String get privacyPolicy => 'Política de privacidade';
@override
String get privacyTitle => 'NÃO RECOLHEMOS NADA';
@override
String get privacyGoodbye => 'ADEUS';
@override
String get privacyKeepReading => '(continue lendo se quiser…)';
@override
String get privacyBody =>
'Não sabemos quem você é\nNão sabemos quanto você tem\nNão sabemos o que você faz';
@override
String get privacyConclusion =>
'A melhor forma de proteger seus dados\né não tê-los';
}
+149 -2
View File
@@ -292,6 +292,9 @@ class L10nRu extends L10n {
@override
String get insufficientBalance => 'Недостаточный баланс';
@override
String get feeExceedsAmount => 'Комиссия превышает сумму отправки';
@override
String tokenCreationError(String error) {
return 'Ошибка создания токена: $error';
@@ -1054,8 +1057,53 @@ class L10nRu extends L10n {
String get cameraPermissionDenied => 'Доступ к камере запрещён';
@override
String get paymentRequestNotSupported =>
'Запросы на оплату пока не поддерживаются';
String get paymentRequestTitle => 'Запрос на оплату';
@override
String get paymentRequestFrom => 'Запрос от';
@override
String get paymentRequestAmount => 'Запрошенная сумма';
@override
String get paymentRequestDescription => 'Описание';
@override
String get paymentRequestMints => 'Принимаемые минты';
@override
String get paymentRequestAnyMint => 'Любой минт';
@override
String get paymentRequestPay => 'Оплатить';
@override
String get paymentRequestPaying => 'Оплата...';
@override
String get paymentRequestSuccess => 'Платёж успешно отправлен';
@override
String get paymentRequestNoTransport =>
'В этом запросе не настроен метод доставки';
@override
String get paymentRequestTransport => 'Транспорт';
@override
String get paymentRequestMintNotAccepted =>
'Ваш активный минт не в списке принимаемых';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Несовместимая единица: запрос требует $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Недостаточный баланс';
@override
String get paymentRequestErrorParsing => 'Ошибка чтения запроса на оплату';
@override
String get p2pkTitle => 'Ключи P2PK';
@@ -1157,4 +1205,103 @@ class L10nRu extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'Невозможно удалить основной ключ';
@override
String get request => 'Запросить';
@override
String get requestPayment => 'Запросить платёж';
@override
String get requestPaymentDescription =>
'Создать унифицированный запрос на оплату';
@override
String get generateRequest => 'Создать запрос';
@override
String get generatingRequest => 'Создание запроса...';
@override
String get requestPaymentReceived => 'Платёж получен';
@override
String get requestDescriptionHint => 'Описание (необязательно)';
@override
String get universal => 'Универсальный';
@override
String get copiedToClipboard => 'Скопировано в буфер обмена';
@override
String get swap => 'Обменять';
@override
String get swapDescription => 'Конвертировать между sats и USD';
@override
String get swapFrom => 'Из';
@override
String get swapTo => 'В';
@override
String get swapAction => 'Обменять';
@override
String get swapEstimatedFee => 'Ориентировочная комиссия';
@override
String get swapUseAll => 'Использовать всё';
@override
String swapMinimum(String amount) {
return 'Минимум: $amount';
}
@override
String get swapProcessing => 'Обработка обмена...';
@override
String get swapSuccess => 'Обмен завершён';
@override
String get swapErrorInsufficient => 'Недостаточный баланс';
@override
String get swapErrorExpired => 'Котировка истекла';
@override
String swapErrorGeneric(String error) {
return 'Ошибка обмена: $error';
}
@override
String get swapChartUnavailable => 'Цена недоступна · Нажмите для повтора';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Мин: $minPrice — Макс: $maxPrice';
}
@override
String get privacyPolicy => 'Политика конфиденциальности';
@override
String get privacyTitle => 'МЫ НЕ СОБИРАЕМ НИЧЕГО';
@override
String get privacyGoodbye => 'ПОКА';
@override
String get privacyKeepReading => '(читай дальше, если хочешь…)';
@override
String get privacyBody =>
'Мы не знаем, кто ты\nМы не знаем, сколько у тебя\nМы не знаем, что ты делаешь';
@override
String get privacyConclusion =>
'Лучший способ защитить твои данные —\nне иметь их';
}
+149 -1
View File
@@ -293,6 +293,9 @@ class L10nSw extends L10n {
@override
String get insufficientBalance => 'Salio halitoshi';
@override
String get feeExceedsAmount => 'Ada inazidi kiasi cha kutuma';
@override
String tokenCreationError(String error) {
return 'Hitilafu ya kuunda tokeni: $error';
@@ -1057,7 +1060,54 @@ class L10nSw extends L10n {
String get cameraPermissionDenied => 'Ruhusa ya kamera imekataliwa';
@override
String get paymentRequestNotSupported => 'Maombi ya malipo bado hayatumiki';
String get paymentRequestTitle => 'Ombi la malipo';
@override
String get paymentRequestFrom => 'Ombi kutoka';
@override
String get paymentRequestAmount => 'Kiasi kilichoombwa';
@override
String get paymentRequestDescription => 'Maelezo';
@override
String get paymentRequestMints => 'Mint zinazokubaliwa';
@override
String get paymentRequestAnyMint => 'Mint yoyote';
@override
String get paymentRequestPay => 'Lipa';
@override
String get paymentRequestPaying => 'Inalipa...';
@override
String get paymentRequestSuccess => 'Malipo yametumwa kwa mafanikio';
@override
String get paymentRequestNoTransport =>
'Ombi hili halina njia ya uwasilishaji iliyosanidiwa';
@override
String get paymentRequestTransport => 'Njia ya usafirishaji';
@override
String get paymentRequestMintNotAccepted =>
'Mint yako hai haiko kwenye orodha ya mint zinazokubaliwa';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Kitengo kisichooana: ombi linahitaji $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Salio haitoshi';
@override
String get paymentRequestErrorParsing =>
'Hitilafu wakati wa kusoma ombi la malipo';
@override
String get p2pkTitle => 'Funguo za P2PK';
@@ -1157,4 +1207,102 @@ class L10nSw extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'Haiwezekani kufuta ufunguo mkuu';
@override
String get request => 'Omba';
@override
String get requestPayment => 'Omba malipo';
@override
String get requestPaymentDescription => 'Tengeneza ombi la malipo la pamoja';
@override
String get generateRequest => 'Tengeneza ombi';
@override
String get generatingRequest => 'Inatengeneza ombi...';
@override
String get requestPaymentReceived => 'Malipo limepokelewa';
@override
String get requestDescriptionHint => 'Maelezo (si lazima)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Imenakiliwa kwenye ubao wa kunakili';
@override
String get swap => 'Badilisha';
@override
String get swapDescription => 'Badilisha kati ya sats na USD';
@override
String get swapFrom => 'Kutoka';
@override
String get swapTo => 'Kwenda';
@override
String get swapAction => 'Badilisha';
@override
String get swapEstimatedFee => 'Ada inayokadiriwa';
@override
String get swapUseAll => 'Tumia yote';
@override
String swapMinimum(String amount) {
return 'Kiwango cha chini: $amount';
}
@override
String get swapProcessing => 'Inashughulikia ubadilishaji...';
@override
String get swapSuccess => 'Ubadilishaji umekamilika';
@override
String get swapErrorInsufficient => 'Salio haitoshi';
@override
String get swapErrorExpired => 'Bei imeisha muda';
@override
String swapErrorGeneric(String error) {
return 'Kosa la ubadilishaji: $error';
}
@override
String get swapChartUnavailable => 'Bei haipatikani · Gusa kurudia';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Chini: $minPrice — Juu: $maxPrice';
}
@override
String get privacyPolicy => 'Sera ya faragha';
@override
String get privacyTitle => 'HATUKUSANYI CHOCHOTE';
@override
String get privacyGoodbye => 'KWAHERI';
@override
String get privacyKeepReading => '(endelea kusoma ukitaka…)';
@override
String get privacyBody =>
'Hatujui wewe ni nani\nHatujui una kiasi gani\nHatujui unafanya nini';
@override
String get privacyConclusion =>
'Njia bora ya kulinda data yako\nni kutokuwa nayo';
}
+144 -1
View File
@@ -287,6 +287,9 @@ class L10nZh extends L10n {
@override
String get insufficientBalance => '余额不足';
@override
String get feeExceedsAmount => '手续费超过发送金额';
@override
String tokenCreationError(String error) {
return '创建代币错误:$error';
@@ -1034,7 +1037,51 @@ class L10nZh extends L10n {
String get cameraPermissionDenied => '相机权限被拒绝';
@override
String get paymentRequestNotSupported => '付款请求尚不支持';
String get paymentRequestTitle => '付款请求';
@override
String get paymentRequestFrom => '来自';
@override
String get paymentRequestAmount => '请求金额';
@override
String get paymentRequestDescription => '描述';
@override
String get paymentRequestMints => '接受的铸造厂';
@override
String get paymentRequestAnyMint => '任何铸造厂';
@override
String get paymentRequestPay => '支付';
@override
String get paymentRequestPaying => '支付中...';
@override
String get paymentRequestSuccess => '付款发送成功';
@override
String get paymentRequestNoTransport => '此请求未配置交付方式';
@override
String get paymentRequestTransport => '传输方式';
@override
String get paymentRequestMintNotAccepted => '您的活跃铸造厂不在接受的铸造厂列表中';
@override
String paymentRequestUnitMismatch(String unit) {
return '单位不兼容:请求需要 $unit';
}
@override
String get paymentRequestInsufficientBalance => '余额不足';
@override
String get paymentRequestErrorParsing => '读取付款请求时出错';
@override
String get p2pkTitle => 'P2PK密钥';
@@ -1131,4 +1178,100 @@ class L10nZh extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => '无法删除主密钥';
@override
String get request => '请求';
@override
String get requestPayment => '请求付款';
@override
String get requestPaymentDescription => '生成统一付款请求';
@override
String get generateRequest => '生成请求';
@override
String get generatingRequest => '正在生成请求...';
@override
String get requestPaymentReceived => '已收到付款';
@override
String get requestDescriptionHint => '描述(可选)';
@override
String get universal => '通用';
@override
String get copiedToClipboard => '已复制到剪贴板';
@override
String get swap => '兑换';
@override
String get swapDescription => '在 Sats 和 USD 之间转换';
@override
String get swapFrom => '';
@override
String get swapTo => '';
@override
String get swapAction => '兑换';
@override
String get swapEstimatedFee => '预估手续费';
@override
String get swapUseAll => '全部使用';
@override
String swapMinimum(String amount) {
return '最低: $amount';
}
@override
String get swapProcessing => '兑换处理中...';
@override
String get swapSuccess => '兑换完成';
@override
String get swapErrorInsufficient => '余额不足';
@override
String get swapErrorExpired => '报价已过期';
@override
String swapErrorGeneric(String error) {
return '兑换错误: $error';
}
@override
String get swapChartUnavailable => '价格不可用 · 点击重试';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h 最低: $minPrice — 最高: $maxPrice';
}
@override
String get privacyPolicy => '隐私政策';
@override
String get privacyTitle => '我们什么都不收集';
@override
String get privacyGoodbye => '再见';
@override
String get privacyKeepReading => '(想继续看就看吧…)';
@override
String get privacyBody => '我们不知道你是谁\n我们不知道你有多少\n我们不知道你在做什么';
@override
String get privacyConclusion => '保护数据的最好方式\n就是不拥有它';
}
+50 -2
View File
@@ -118,6 +118,7 @@
"confirm": "Confirmar",
"cancel": "Cancelar",
"insufficientBalance": "Saldo insuficiente",
"feeExceedsAmount": "A taxa excede o valor a enviar",
"tokenCreationError": "Erro ao criar token: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -502,7 +503,22 @@
"scanLightningInvoiceHint": "Escaneie um invoice Lightning (lnbc...)",
"addMintQuestion": "Adicionar este mint?",
"cameraPermissionDenied": "Permissão de câmera negada",
"paymentRequestNotSupported": "Solicitações de pagamento ainda não são suportadas",
"paymentRequestTitle": "Solicitação de pagamento",
"paymentRequestFrom": "Solicitação de",
"paymentRequestAmount": "Valor solicitado",
"paymentRequestDescription": "Descrição",
"paymentRequestMints": "Mints aceitos",
"paymentRequestAnyMint": "Qualquer mint",
"paymentRequestPay": "Pagar",
"paymentRequestPaying": "Pagando...",
"paymentRequestSuccess": "Pagamento enviado com sucesso",
"paymentRequestNoTransport": "Esta solicitação não tem método de entrega configurado",
"paymentRequestTransport": "Transporte",
"paymentRequestMintNotAccepted": "Seu mint ativo não está na lista de mints aceitos",
"paymentRequestUnitMismatch": "Unidade incompatível: a solicitação requer {unit}",
"paymentRequestInsufficientBalance": "Saldo insuficiente",
"paymentRequestErrorParsing": "Erro ao ler a solicitação de pagamento",
"p2pkTitle": "Chaves P2PK",
"p2pkSettingsDescription": "Receber ecash bloqueado",
"p2pkExperimental": "P2PK é experimental. Use com cautela.",
@@ -534,5 +550,37 @@
"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",
"request": "Solicitar",
"requestPayment": "Solicitar pagamento",
"requestPaymentDescription": "Gerar solicitação de pagamento unificada",
"generateRequest": "Gerar solicitação",
"generatingRequest": "Gerando solicitação...",
"requestPaymentReceived": "Pagamento recebido",
"requestDescriptionHint": "Descrição (opcional)",
"universal": "Universal",
"copiedToClipboard": "Copiado para a área de transferência",
"swap": "Trocar",
"swapDescription": "Converter entre sats e USD",
"swapFrom": "De",
"swapTo": "Para",
"swapAction": "Trocar",
"swapEstimatedFee": "Taxa estimada",
"swapUseAll": "Usar tudo",
"swapMinimum": "Mínimo: {amount}",
"swapProcessing": "Processando swap...",
"swapSuccess": "Swap concluído",
"swapErrorInsufficient": "Saldo insuficiente",
"swapErrorExpired": "A cotação expirou",
"swapErrorGeneric": "Erro no swap: {error}",
"swapChartUnavailable": "Preço indisponível · Toque para tentar novamente",
"swapChartMinMax": "24h Mín: {minPrice} — Máx: {maxPrice}",
"privacyPolicy": "Política de privacidade",
"privacyTitle": "NÃO RECOLHEMOS NADA",
"privacyGoodbye": "ADEUS",
"privacyKeepReading": "(continue lendo se quiser…)",
"privacyBody": "Não sabemos quem você é\nNão sabemos quanto você tem\nNão sabemos o que você faz",
"privacyConclusion": "A melhor forma de proteger seus dados\né não tê-los"
}
+50 -2
View File
@@ -118,6 +118,7 @@
"confirm": "Подтвердить",
"cancel": "Отмена",
"insufficientBalance": "Недостаточный баланс",
"feeExceedsAmount": "Комиссия превышает сумму отправки",
"tokenCreationError": "Ошибка создания токена: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -502,7 +503,22 @@
"scanLightningInvoiceHint": "Сканируйте Lightning счёт (lnbc...)",
"addMintQuestion": "Добавить этот mint?",
"cameraPermissionDenied": "Доступ к камере запрещён",
"paymentRequestNotSupported": "Запросы на оплату пока не поддерживаются",
"paymentRequestTitle": "Запрос на оплату",
"paymentRequestFrom": "Запрос от",
"paymentRequestAmount": "Запрошенная сумма",
"paymentRequestDescription": "Описание",
"paymentRequestMints": "Принимаемые минты",
"paymentRequestAnyMint": "Любой минт",
"paymentRequestPay": "Оплатить",
"paymentRequestPaying": "Оплата...",
"paymentRequestSuccess": "Платёж успешно отправлен",
"paymentRequestNoTransport": "В этом запросе не настроен метод доставки",
"paymentRequestTransport": "Транспорт",
"paymentRequestMintNotAccepted": "Ваш активный минт не в списке принимаемых",
"paymentRequestUnitMismatch": "Несовместимая единица: запрос требует {unit}",
"paymentRequestInsufficientBalance": "Недостаточный баланс",
"paymentRequestErrorParsing": "Ошибка чтения запроса на оплату",
"p2pkTitle": "Ключи P2PK",
"p2pkSettingsDescription": "Получить заблокированный ecash",
"p2pkExperimental": "P2PK экспериментальный. Используйте с осторожностью.",
@@ -534,5 +550,37 @@
"p2pkErrorInvalidNsec": "Недействительный nsec",
"p2pkErrorKeyAlreadyExists": "Этот ключ уже существует",
"p2pkErrorKeyNotFound": "Ключ не найден",
"p2pkErrorCannotDeletePrimary": "Невозможно удалить основной ключ"
"p2pkErrorCannotDeletePrimary": "Невозможно удалить основной ключ",
"request": "Запросить",
"requestPayment": "Запросить платёж",
"requestPaymentDescription": "Создать унифицированный запрос на оплату",
"generateRequest": "Создать запрос",
"generatingRequest": "Создание запроса...",
"requestPaymentReceived": "Платёж получен",
"requestDescriptionHint": "Описание (необязательно)",
"universal": "Универсальный",
"copiedToClipboard": "Скопировано в буфер обмена",
"swap": "Обменять",
"swapDescription": "Конвертировать между sats и USD",
"swapFrom": "Из",
"swapTo": "В",
"swapAction": "Обменять",
"swapEstimatedFee": "Ориентировочная комиссия",
"swapUseAll": "Использовать всё",
"swapMinimum": "Минимум: {amount}",
"swapProcessing": "Обработка обмена...",
"swapSuccess": "Обмен завершён",
"swapErrorInsufficient": "Недостаточный баланс",
"swapErrorExpired": "Котировка истекла",
"swapErrorGeneric": "Ошибка обмена: {error}",
"swapChartUnavailable": "Цена недоступна · Нажмите для повтора",
"swapChartMinMax": "24h Мин: {minPrice} — Макс: {maxPrice}",
"privacyPolicy": "Политика конфиденциальности",
"privacyTitle": "МЫ НЕ СОБИРАЕМ НИЧЕГО",
"privacyGoodbye": "ПОКА",
"privacyKeepReading": "(читай дальше, если хочешь…)",
"privacyBody": "Мы не знаем, кто ты\nМы не знаем, сколько у тебя\nМы не знаем, что ты делаешь",
"privacyConclusion": "Лучший способ защитить твои данные —\nне иметь их"
}
+50 -2
View File
@@ -118,6 +118,7 @@
"confirm": "Thibitisha",
"cancel": "Ghairi",
"insufficientBalance": "Salio halitoshi",
"feeExceedsAmount": "Ada inazidi kiasi cha kutuma",
"tokenCreationError": "Hitilafu ya kuunda tokeni: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -502,7 +503,22 @@
"scanLightningInvoiceHint": "Changanua ankara ya Lightning (lnbc...)",
"addMintQuestion": "Ongeza mint hii?",
"cameraPermissionDenied": "Ruhusa ya kamera imekataliwa",
"paymentRequestNotSupported": "Maombi ya malipo bado hayatumiki",
"paymentRequestTitle": "Ombi la malipo",
"paymentRequestFrom": "Ombi kutoka",
"paymentRequestAmount": "Kiasi kilichoombwa",
"paymentRequestDescription": "Maelezo",
"paymentRequestMints": "Mint zinazokubaliwa",
"paymentRequestAnyMint": "Mint yoyote",
"paymentRequestPay": "Lipa",
"paymentRequestPaying": "Inalipa...",
"paymentRequestSuccess": "Malipo yametumwa kwa mafanikio",
"paymentRequestNoTransport": "Ombi hili halina njia ya uwasilishaji iliyosanidiwa",
"paymentRequestTransport": "Njia ya usafirishaji",
"paymentRequestMintNotAccepted": "Mint yako hai haiko kwenye orodha ya mint zinazokubaliwa",
"paymentRequestUnitMismatch": "Kitengo kisichooana: ombi linahitaji {unit}",
"paymentRequestInsufficientBalance": "Salio haitoshi",
"paymentRequestErrorParsing": "Hitilafu wakati wa kusoma ombi la malipo",
"p2pkTitle": "Funguo za P2PK",
"p2pkSettingsDescription": "Pokea ecash iliyofungwa",
"p2pkExperimental": "P2PK ni ya majaribio. Tumia kwa uangalifu.",
@@ -534,5 +550,37 @@
"p2pkErrorInvalidNsec": "nsec batili",
"p2pkErrorKeyAlreadyExists": "Ufunguo huu tayari upo",
"p2pkErrorKeyNotFound": "Ufunguo haujapatikana",
"p2pkErrorCannotDeletePrimary": "Haiwezekani kufuta ufunguo mkuu"
"p2pkErrorCannotDeletePrimary": "Haiwezekani kufuta ufunguo mkuu",
"request": "Omba",
"requestPayment": "Omba malipo",
"requestPaymentDescription": "Tengeneza ombi la malipo la pamoja",
"generateRequest": "Tengeneza ombi",
"generatingRequest": "Inatengeneza ombi...",
"requestPaymentReceived": "Malipo limepokelewa",
"requestDescriptionHint": "Maelezo (si lazima)",
"universal": "Universal",
"copiedToClipboard": "Imenakiliwa kwenye ubao wa kunakili",
"swap": "Badilisha",
"swapDescription": "Badilisha kati ya sats na USD",
"swapFrom": "Kutoka",
"swapTo": "Kwenda",
"swapAction": "Badilisha",
"swapEstimatedFee": "Ada inayokadiriwa",
"swapUseAll": "Tumia yote",
"swapMinimum": "Kiwango cha chini: {amount}",
"swapProcessing": "Inashughulikia ubadilishaji...",
"swapSuccess": "Ubadilishaji umekamilika",
"swapErrorInsufficient": "Salio haitoshi",
"swapErrorExpired": "Bei imeisha muda",
"swapErrorGeneric": "Kosa la ubadilishaji: {error}",
"swapChartUnavailable": "Bei haipatikani · Gusa kurudia",
"swapChartMinMax": "24h Chini: {minPrice} — Juu: {maxPrice}",
"privacyPolicy": "Sera ya faragha",
"privacyTitle": "HATUKUSANYI CHOCHOTE",
"privacyGoodbye": "KWAHERI",
"privacyKeepReading": "(endelea kusoma ukitaka…)",
"privacyBody": "Hatujui wewe ni nani\nHatujui una kiasi gani\nHatujui unafanya nini",
"privacyConclusion": "Njia bora ya kulinda data yako\nni kutokuwa nayo"
}
+50 -2
View File
@@ -118,6 +118,7 @@
"confirm": "确认",
"cancel": "取消",
"insufficientBalance": "余额不足",
"feeExceedsAmount": "手续费超过发送金额",
"tokenCreationError": "创建代币错误:{error}",
"@tokenCreationError": {
"placeholders": {
@@ -502,7 +503,22 @@
"scanLightningInvoiceHint": "扫描闪电发票(lnbc...",
"addMintQuestion": "添加此铸造厂?",
"cameraPermissionDenied": "相机权限被拒绝",
"paymentRequestNotSupported": "付款请求尚不支持",
"paymentRequestTitle": "付款请求",
"paymentRequestFrom": "来自",
"paymentRequestAmount": "请求金额",
"paymentRequestDescription": "描述",
"paymentRequestMints": "接受的铸造厂",
"paymentRequestAnyMint": "任何铸造厂",
"paymentRequestPay": "支付",
"paymentRequestPaying": "支付中...",
"paymentRequestSuccess": "付款发送成功",
"paymentRequestNoTransport": "此请求未配置交付方式",
"paymentRequestTransport": "传输方式",
"paymentRequestMintNotAccepted": "您的活跃铸造厂不在接受的铸造厂列表中",
"paymentRequestUnitMismatch": "单位不兼容:请求需要 {unit}",
"paymentRequestInsufficientBalance": "余额不足",
"paymentRequestErrorParsing": "读取付款请求时出错",
"p2pkTitle": "P2PK密钥",
"p2pkSettingsDescription": "接收锁定的ecash",
"p2pkExperimental": "P2PK是实验性功能。请谨慎使用。",
@@ -534,5 +550,37 @@
"p2pkErrorInvalidNsec": "无效的nsec",
"p2pkErrorKeyAlreadyExists": "此密钥已存在",
"p2pkErrorKeyNotFound": "未找到密钥",
"p2pkErrorCannotDeletePrimary": "无法删除主密钥"
"p2pkErrorCannotDeletePrimary": "无法删除主密钥",
"request": "请求",
"requestPayment": "请求付款",
"requestPaymentDescription": "生成统一付款请求",
"generateRequest": "生成请求",
"generatingRequest": "正在生成请求...",
"requestPaymentReceived": "已收到付款",
"requestDescriptionHint": "描述(可选)",
"universal": "通用",
"copiedToClipboard": "已复制到剪贴板",
"swap": "兑换",
"swapDescription": "在 Sats 和 USD 之间转换",
"swapFrom": "从",
"swapTo": "到",
"swapAction": "兑换",
"swapEstimatedFee": "预估手续费",
"swapUseAll": "全部使用",
"swapMinimum": "最低: {amount}",
"swapProcessing": "兑换处理中...",
"swapSuccess": "兑换完成",
"swapErrorInsufficient": "余额不足",
"swapErrorExpired": "报价已过期",
"swapErrorGeneric": "兑换错误: {error}",
"swapChartUnavailable": "价格不可用 · 点击重试",
"swapChartMinMax": "24h 最低: {minPrice} — 最高: {maxPrice}",
"privacyPolicy": "隐私政策",
"privacyTitle": "我们什么都不收集",
"privacyGoodbye": "再见",
"privacyKeepReading": "(想继续看就看吧…)",
"privacyBody": "我们不知道你是谁\n我们不知道你有多少\n我们不知道你在做什么",
"privacyConclusion": "保护数据的最好方式\n就是不拥有它"
}
+410 -59
View File
@@ -7,13 +7,16 @@ import '../src/rust/api/wallet.dart';
import '../src/rust/api/token.dart';
import '../src/rust/api/mint_info.dart';
import '../src/rust/api/keys.dart';
import '../src/rust/api/payment_request.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
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 '../core/utils/p2pk_utils.dart';
import '../widgets/effects/cashu_confetti.dart';
/// Helper class para info de token parseado
@@ -65,6 +68,10 @@ class WalletProvider extends ChangeNotifier {
/// Caché de MintInfo por URL (nombre, logo, contactos, etc.)
final Map<String, MintInfo> _mintInfoCache = {};
/// Caché del balance stream para evitar recrear StreamSink en cada build().
Stream<BigInt>? _cachedBalanceStream;
String? _cachedBalanceKey;
/// Mint activo actualmente
String? _activeMintUrl;
@@ -76,6 +83,7 @@ class WalletProvider extends ChangeNotifier {
static const _activeMintKey = 'wallet_active_mint';
static const _activeUnitKey = 'wallet_active_unit';
static const _pendingMintInvoicesKey = 'pending_mint_invoices';
static const _pendingNostrRequestKey = 'pending_nostr_request';
/// Mint de Cuba Bitcoin - siempre aparece primero en la lista
static const cubaBitcoinMint = 'https://mint.cubabitcoin.org';
@@ -348,6 +356,10 @@ class WalletProvider extends ChangeNotifier {
}
}
debugPrint('Mints restaurados: ${_mintUnits.keys.length}');
// Re-detectar unidades en background (sin bloquear arranque).
// Si el mint ahora soporta más unidades (ej: usd), se actualizan.
_refreshAllMintUnits();
} else {
// Primera vez - agregar mint por defecto
await addMint('https://mint.cubabitcoin.org');
@@ -399,6 +411,62 @@ class WalletProvider extends ChangeNotifier {
return await getWallet(_activeMintUrl!, _activeUnit);
}
/// Re-detecta unidades de todos los mints conocidos via NUT-04.
/// Corre en background sin bloquear el arranque. Solo notifica si hay cambios.
void _refreshAllMintUnits() async {
bool changed = false;
for (final mintUrl in _mintUnits.keys.toList()) {
try {
final mintInfo = await getMintInfo(mintUrl: mintUrl);
final units = <String>{};
for (final method in mintInfo.nuts.nut04.methods) {
units.add(method.unit);
}
final unitList = units.isEmpty ? ['sat'] : units.toList();
unitList.sort((a, b) {
if (a == 'sat') return -1;
if (b == 'sat') return 1;
return a.compareTo(b);
});
final previous = _mintUnits[mintUrl];
if (!_listEquals(previous, unitList)) {
_mintUnits[mintUrl] = unitList;
changed = true;
debugPrint('Unidades actualizadas para $mintUrl: $unitList');
// Validar _activeUnit si este es el mint activo
if (mintUrl == _activeMintUrl && !unitList.contains(_activeUnit)) {
_activeUnit = unitList.first;
debugPrint('Active unit reset to ${unitList.first} for $mintUrl');
}
}
} catch (e) {
debugPrint('Error refrescando unidades de $mintUrl: $e');
}
}
if (changed) {
try {
await _saveMints();
} catch (e) {
debugPrint('Error guardando mints actualizados: $e');
}
notifyListeners();
}
}
/// Compara dos listas de strings por valor.
bool _listEquals(List<String>? a, List<String>? b) {
if (a == null || b == null) return a == b;
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
// ============================================================
// MULTI-MINT OPERATIONS
// ============================================================
@@ -598,8 +666,21 @@ class WalletProvider extends ChangeNotifier {
// ============================================================
/// Stream de balance del wallet activo (reactivo).
/// Cacheado para que StreamBuilder reciba el mismo objeto entre rebuilds
/// y no mate el StreamSink de Rust.
Stream<BigInt>? streamBalance() {
return activeWallet?.streamBalance();
final wallet = activeWallet;
if (wallet == null) {
_cachedBalanceStream = null;
_cachedBalanceKey = null;
return null;
}
final key = '$_activeMintUrl:$_activeUnit';
if (_cachedBalanceKey != key) {
_cachedBalanceStream = wallet.streamBalance().asBroadcastStream();
_cachedBalanceKey = key;
}
return _cachedBalanceStream;
}
/// Obtiene el balance del wallet activo.
@@ -840,7 +921,20 @@ class WalletProvider extends ChangeNotifier {
// DEBUG: transacciones ANTES del receive
await _debugLogTransactions(wallet, 'BEFORE receive');
final amount = await wallet.receive(token: token, opts: opts);
BigInt amount;
try {
amount = await wallet.receive(token: token, opts: opts);
} catch (e) {
final errorStr = e.toString().toLowerCase();
if (errorStr.contains('already signed')) {
debugPrint('[RECEIVE] Already signed error, restoring wallet counters...');
await wallet.restore();
// Retry once after counter resync
amount = await wallet.receive(token: token, opts: opts);
} else {
rethrow;
}
}
// DEBUG: counters después de receive
await KeysetDebug.logCounters('AFTER receive ($unit)');
@@ -962,45 +1056,33 @@ class WalletProvider extends ChangeNotifier {
Future<String> confirmSend(PreparedSend prepared, String? memo) async {
final wallet = await getActiveWallet();
final token = await wallet.send(
final result = await wallet.send(
send: prepared,
memo: memo,
includeMemo: memo != null && memo.isNotEmpty,
);
// Guardar token en storage local para mostrar en detalles del historial.
// Usamos hash del token como key temporal; después buscaremos la transacción.
await _saveTokenForRecentTransaction(token.encoded);
notifyListeners();
return token.encoded;
}
/// Guarda el token para la transacción más reciente de tipo send.
Future<void> _saveTokenForRecentTransaction(String tokenEncoded) async {
try {
// Obtener transacciones outgoing más recientes
final wallet = await getActiveWallet();
final txs = await wallet.listTransactions(
direction: TransactionDirection.outgoing,
);
if (txs.isNotEmpty) {
// La más reciente debería ser la que acabamos de crear
final recentTx = txs.first;
// Save token metadata using the deterministic transaction ID returned by CDK
// (SHA-256 of sorted proof Y values — no racy listTransactions needed)
// Best-effort: don't fail the send if metadata persistence fails
final txId = result.transactionId;
if (txId != null && txId.isNotEmpty) {
try {
await _txMetaStorage.save(
recentTx.id,
txId,
TransactionMeta(
type: TransactionType.cashu,
token: tokenEncoded,
token: result.token.encoded,
),
);
debugPrint('Token guardado para tx ${recentTx.id}');
debugPrint('Token guardado para tx $txId');
} catch (e) {
debugPrint('Error guardando send metadata: $e');
}
} catch (e) {
debugPrint('Error guardando token metadata: $e');
}
notifyListeners();
return result.token.encoded;
}
/// Cancela un envío preparado (libera proofs reservados).
@@ -1062,6 +1144,27 @@ class WalletProvider extends ChangeNotifier {
}
}
// ============================================================
// PAYMENT REQUEST (NUT-18/26)
// ============================================================
/// Paga un Payment Request (NUT-18 creqA / NUT-26 creqB / BIP-321).
/// CDK maneja: NUT-10 spending conditions, transporte (Nostr/HTTP POST),
/// preparación del token y entrega automática.
Future<void> payPaymentRequest(
String encodedRequest, {
BigInt? customAmount,
}) async {
final wallet = await getActiveWallet();
debugPrint('[PAY_REQUEST] Paying payment request via ${wallet.mintUrl}');
await wallet.payPaymentRequest(
encoded: encodedRequest,
customAmount: customAmount,
);
debugPrint('[PAY_REQUEST] Payment delivered successfully');
notifyListeners();
}
// ============================================================
// MINT (Depositar via Lightning)
// ============================================================
@@ -1091,12 +1194,14 @@ class WalletProvider extends ChangeNotifier {
_activeMintController!.close();
}
// StreamController que la UI puede escuchar y cancelar libremente
final controller = StreamController<MintQuote>();
// StreamController que la UI puede escuchar y cancelar libremente.
// sync: true para no perder eventos rápidos (paid → issued en µs).
final controller = StreamController<MintQuote>(sync: true);
_activeMintController = controller;
// Cancelar suscripción anterior si existe (await evita race de callbacks)
await _activeMintSubscription?.cancel();
// Cancelar suscripción anterior sin await: el task de Rust (polling + WS)
// puede tardar en cerrar y bloquearía la creación de una nueva factura.
_activeMintSubscription?.cancel();
_activeMintSubscription = null;
// Suscribirse al stream del CDK desde el provider (persiste sin UI)
@@ -1104,7 +1209,7 @@ class WalletProvider extends ChangeNotifier {
amount: amount,
description: description,
).listen(
(quote) {
(quote) async {
// Guardar invoice temprano en SharedPreferences
if (quote.state == MintQuoteState.unpaid) {
invoiceBolt11 = quote.request;
@@ -1113,8 +1218,10 @@ class WalletProvider extends ChangeNotifier {
// Cuando se completa, guardar metadata, confetti, limpiar pending
if (quote.state == MintQuoteState.issued && invoiceBolt11 != null) {
_saveMintMetadata(wallet, invoiceBolt11!);
_removePendingMintInvoice(quote.id);
final saved = await _saveMintMetadata(wallet, invoiceBolt11!, quote.transactionId);
// Only remove pending invoice if metadata was saved;
// otherwise _matchPendingMintInvoices can recover it on next startup
if (saved) _removePendingMintInvoice(quote.id);
}
// Reenviar a la UI (si sigue escuchando)
@@ -1223,29 +1330,155 @@ class WalletProvider extends ChangeNotifier {
return null;
}
/// Guarda metadata para una transacción de mint (Lightning deposit).
Future<void> _saveMintMetadata(Wallet wallet, String invoice) async {
// ============================================================
// PENDING NOSTR PAYMENT REQUESTS
// ============================================================
/// TTL for pending Nostr requests (24 hours).
static const _pendingNostrRequestTtl = Duration(hours: 24);
StreamSubscription<NostrPaymentEvent>? _pendingNostrSubscription;
bool _pendingNostrResumeAttempted = false;
static const _pendingNostrSecretKey = 'pending_nostr_secret';
static const _secureStorage = FlutterSecureStorage();
/// Save a pending Nostr payment request for recovery after app restart.
/// Secret key goes to FlutterSecureStorage, metadata to SharedPreferences.
Future<void> savePendingNostrRequest(PersistedRequestData data) async {
try {
final txs = await wallet.listTransactions(
direction: TransactionDirection.incoming,
// Secret key in encrypted storage
await _secureStorage.write(
key: _pendingNostrSecretKey,
value: data.secretHex,
);
// Metadata in SharedPreferences (no secrets)
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_pendingNostrRequestKey, jsonEncode({
'pubkeyHex': data.pubkeyHex,
'relays': data.relays,
'amount': data.amount?.toString(),
'unit': data.unit,
'mintUrl': data.mintUrl,
'createdAt': DateTime.now().toIso8601String(),
}));
} catch (e) {
debugPrint('Error saving pending Nostr request: $e');
}
}
/// Remove the pending Nostr payment request (payment received or cancelled).
Future<void> removePendingNostrRequest() async {
try {
await _secureStorage.delete(key: _pendingNostrSecretKey);
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_pendingNostrRequestKey);
} catch (e) {
debugPrint('Error removing pending Nostr request: $e');
}
}
/// Check for a pending Nostr payment request and resume listening.
/// Called automatically during app startup. Guarded against duplicate calls.
Future<void> resumePendingNostrRequest() async {
if (_pendingNostrResumeAttempted) return;
_pendingNostrResumeAttempted = true;
await _pendingNostrSubscription?.cancel();
_pendingNostrSubscription = null;
try {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString(_pendingNostrRequestKey);
if (jsonStr == null) return;
final map = Map<String, dynamic>.from(jsonDecode(jsonStr));
// Check TTL
final createdAt = DateTime.tryParse(map['createdAt'] ?? '');
if (createdAt == null ||
DateTime.now().difference(createdAt) > _pendingNostrRequestTtl) {
await removePendingNostrRequest();
debugPrint('Pending Nostr request expired, removed');
return;
}
// Read secret from secure storage
final secretHex = await _secureStorage.read(key: _pendingNostrSecretKey);
if (secretHex == null) {
await prefs.remove(_pendingNostrRequestKey);
debugPrint('Pending Nostr request has no secret key, removed');
return;
}
// Reconstruct handle
final data = PersistedRequestData(
secretHex: secretHex,
pubkeyHex: map['pubkeyHex'] as String,
relays: List<String>.from(map['relays']),
amount: map['amount'] != null
? BigInt.tryParse(map['amount'] as String)
: null,
unit: map['unit'] as String,
mintUrl: map['mintUrl'] as String,
);
if (txs.isNotEmpty) {
final recentTx = txs.first;
final handle = NostrListenerHandle.fromPersisted(data: data);
// Get or create the wallet for this mint+unit (lazy instantiation)
Wallet wallet;
try {
wallet = await getWallet(data.mintUrl, data.unit);
} catch (e) {
debugPrint('No wallet for pending Nostr request: $e');
await removePendingNostrRequest();
return;
}
// Resume listening in background
debugPrint('Resuming pending Nostr payment request...');
_pendingNostrSubscription = wallet.waitForNostrPayment(handle: handle).listen(
(event) {
if (event.state == NostrPaymentState.received) {
debugPrint('Pending Nostr payment received: ${event.amount}');
removePendingNostrRequest();
confettiController.fire();
_pendingNostrSubscription?.cancel();
_pendingNostrSubscription = null;
}
},
onError: (error) {
debugPrint('Pending Nostr listener error (kept for retry): $error');
_pendingNostrSubscription = null;
_pendingNostrResumeAttempted = false; // Allow retry on next startup
},
);
} catch (e) {
debugPrint('Error resuming pending Nostr request: $e');
}
}
/// Guarda metadata para una transacción de mint (Lightning deposit).
/// Returns true if metadata was actually saved.
Future<bool> _saveMintMetadata(Wallet wallet, String invoice, String? transactionId) async {
try {
if (transactionId != null && transactionId.isNotEmpty) {
await _txMetaStorage.save(
recentTx.id,
transactionId,
TransactionMeta(
type: TransactionType.lightning,
invoice: invoice,
),
);
debugPrint('Mint metadata guardada para tx ${recentTx.id}');
debugPrint('Mint metadata guardada para tx $transactionId');
confettiController.fire();
notifyListeners();
return true;
} else {
debugPrint('Mint metadata: no transaction ID available');
confettiController.fire();
notifyListeners();
return false;
}
} catch (e) {
debugPrint('Error guardando mint metadata: $e');
return false;
}
}
@@ -1265,19 +1498,57 @@ class WalletProvider extends ChangeNotifier {
}
/// Ejecuta el pago del invoice.
/// Guarda metadata type=lightning para identificar en historial.
/// Usa prepare/confirm/cancel para recuperar proofs si el pago falla.
Future<BigInt> melt(MeltQuote quote) async {
final wallet = await getActiveWallet();
final totalPaid = await wallet.melt(quote: quote);
final prepared = await wallet.prepareMelt(quote: quote);
try {
final totalPaid = await wallet.confirmMelt(melt: prepared);
// Guardar metadata con el invoice
if (_pendingMeltInvoice != null) {
await _saveMeltMetadata(wallet, _pendingMeltInvoice!);
_pendingMeltInvoice = null;
// Guardar metadata con el invoice
if (_pendingMeltInvoice != null) {
await _saveMeltMetadata(wallet, _pendingMeltInvoice!);
_pendingMeltInvoice = null;
}
notifyListeners();
return totalPaid;
} catch (e) {
try {
await wallet.cancelMelt(melt: prepared);
} catch (cancelErr) {
debugPrint('[MELT] cancelMelt failed: $cancelErr');
}
rethrow;
}
}
notifyListeners();
return totalPaid;
/// Recupera proofs huérfanas en PendingSpent consultando el mint.
/// Solo revierte proofs que el mint confirma como no gastadas.
/// Si [mintUrl] es null, escanea todos los mints.
/// Retorna {count, amount} total de proofs recuperadas.
Future<ReclaimResult> reclaimPendingProofs({String? mintUrl}) async {
var totalCount = BigInt.zero;
var totalAmount = BigInt.zero;
final mints = mintUrl != null
? {mintUrl: _mintUnits[mintUrl] ?? ['sat']}
: _mintUnits;
for (final entry in mints.entries) {
for (final unit in entry.value) {
try {
final wallet = await getWallet(entry.key, unit);
final result = await wallet.reclaimPendingProofs();
totalCount += result.count;
totalAmount += result.amount;
} catch (e) {
debugPrint('Reclaim pending proofs failed for ${entry.key}:$unit: $e');
}
}
}
if (totalCount > BigInt.zero) {
notifyListeners();
}
return ReclaimResult(count: totalCount, amount: totalAmount);
}
/// Guarda metadata para una transacción de melt (Lightning withdrawal).
@@ -1304,6 +1575,58 @@ class WalletProvider extends ChangeNotifier {
}
}
/// Guarda metadata para el lado melt (enviado) de un swap.
/// Busca la tx outgoing más reciente que coincida con el monto.
Future<void> saveSwapMeltMetadata(Wallet wallet, String invoice, BigInt amount) async {
try {
final txs = await wallet.listTransactions(
direction: TransactionDirection.outgoing,
);
final tx = txs.cast<Transaction?>().firstWhere(
(t) => t!.amount == amount && !_txMetaStorage.has(t.id),
orElse: () => null,
);
if (tx != null) {
await _txMetaStorage.save(
tx.id,
TransactionMeta(type: TransactionType.lightning, invoice: invoice),
);
debugPrint('Swap melt metadata guardada para tx ${tx.id}');
} else {
debugPrint('No matching untagged outgoing tx found for amount $amount');
}
} catch (e) {
debugPrint('Error guardando swap melt metadata: $e');
}
notifyListeners();
}
/// Guarda metadata para el lado mint (recibido) de un swap.
/// Busca la tx incoming más reciente que coincida con el monto.
Future<void> saveSwapMintMetadata(Wallet wallet, String invoice, BigInt amount) async {
try {
final txs = await wallet.listTransactions(
direction: TransactionDirection.incoming,
);
final tx = txs.cast<Transaction?>().firstWhere(
(t) => t!.amount == amount && !_txMetaStorage.has(t.id),
orElse: () => null,
);
if (tx != null) {
await _txMetaStorage.save(
tx.id,
TransactionMeta(type: TransactionType.lightning, invoice: invoice),
);
debugPrint('Swap mint metadata guardada para tx ${tx.id}');
} else {
debugPrint('No matching untagged incoming tx found for amount $amount');
}
} catch (e) {
debugPrint('Error guardando swap mint metadata: $e');
}
notifyListeners();
}
// ============================================================
// HISTORIAL
// ============================================================
@@ -1409,6 +1732,14 @@ class WalletProvider extends ChangeNotifier {
// Silencioso - puede fallar offline
debugPrint('Check pending failed: $e');
}
try {
// Recuperar proofs huérfanas (melts fallidos, sends no completados)
await wallet.reclaimPendingProofs();
} catch (e) {
debugPrint('Reclaim pending proofs failed: $e');
}
} catch (e) {
debugPrint('Error getting wallet ${entry.key}:$unit: $e');
}
@@ -1417,6 +1748,9 @@ class WalletProvider extends ChangeNotifier {
// Vincular transacciones incoming sin metadata con pending invoices
await _matchPendingMintInvoices();
// Resume pending Nostr payment request if app was killed mid-wait
await resumePendingNostrRequest();
}
/// Busca transacciones incoming sin metadata y las vincula con
@@ -1570,7 +1904,7 @@ class WalletProvider extends ChangeNotifier {
// Enviar todo el balance
final prepared =
await tempWallet.prepareSend(amount: tempBalance);
final token = await tempWallet.send(
final result = await tempWallet.send(
send: prepared,
memo: 'Recuperación El Caju',
includeMemo: true,
@@ -1578,7 +1912,7 @@ class WalletProvider extends ChangeNotifier {
// Reclamar en nuestro wallet
final ourWallet = await getWallet(mintUrl, unit);
final received = await ourWallet.receive(token: token);
final received = await ourWallet.receive(token: result.token);
totalRecovered += received;
}
} catch (e) {
@@ -1617,6 +1951,8 @@ class WalletProvider extends ChangeNotifier {
_activeUnit = 'sat';
_mnemonic = null;
_db = null;
_cachedBalanceStream = null;
_cachedBalanceKey = null;
// Limpiar metadata de transacciones
await _txMetaStorage.clear();
@@ -1645,6 +1981,8 @@ class WalletProvider extends ChangeNotifier {
_activeUnit = 'sat';
_mnemonic = null;
_db = null;
_cachedBalanceStream = null;
_cachedBalanceKey = null;
notifyListeners();
}
@@ -1701,7 +2039,8 @@ class WalletProvider extends ChangeNotifier {
/// Retorna el monto recibido si tiene éxito, o lanza excepción.
/// Si el token está gastado o es inválido, lo elimina automáticamente.
/// Verifica conectividad al mint antes de intentar reclamar.
Future<BigInt> claimPendingToken(String id) async {
/// [p2pkPrivateKey] clave privada para desbloquear tokens P2PK.
Future<BigInt> claimPendingToken(String id, {String? p2pkPrivateKey}) async {
final pending = _pendingTokenStorage.get(id);
if (pending == null) {
throw Exception('Token pendiente no encontrado');
@@ -1714,8 +2053,16 @@ class WalletProvider extends ChangeNotifier {
}
try {
// Intentar reclamar usando el método existente
final amount = await receiveToken(pending.encoded);
BigInt amount;
if (P2PKUtils.isP2PKLocked(pending.encoded) && p2pkPrivateKey == null) {
throw Exception('P2PK token requires a private key to claim');
}
amount = await receiveToken(
pending.encoded,
p2pkPrivateKey: p2pkPrivateKey,
);
// Éxito: eliminar de pending
await _pendingTokenStorage.remove(id);
@@ -1744,7 +2091,8 @@ class WalletProvider extends ChangeNotifier {
/// Verifica y reclama automáticamente tokens pendientes.
/// Retorna un mapa con estadísticas: claimed, failed, removed, totalClaimed, unit.
Future<Map<String, dynamic>> checkPendingTokens() async {
/// [p2pkKeyResolver] función que dado un token encoded retorna la clave privada P2PK (o null).
Future<Map<String, dynamic>> checkPendingTokens({String? Function(String encodedToken)? p2pkKeyResolver}) async {
final tokens = _pendingTokenStorage.listValid();
if (tokens.isEmpty) {
return {'claimed': 0, 'failed': 0, 'removed': 0, 'totalClaimed': BigInt.zero, 'unit': _activeUnit};
@@ -1758,7 +2106,8 @@ class WalletProvider extends ChangeNotifier {
for (final token in tokens) {
try {
final amount = await claimPendingToken(token.id);
final p2pkKey = p2pkKeyResolver?.call(token.encoded);
final amount = await claimPendingToken(token.id, p2pkPrivateKey: p2pkKey);
claimed++;
totalClaimed += amount;
claimedUnit ??= token.unit; // Usar la unidad del primer token reclamado
@@ -1809,6 +2158,8 @@ class WalletProvider extends ChangeNotifier {
if (_activeMintController != null && !_activeMintController!.isClosed) {
_activeMintController!.close();
}
_cachedBalanceStream = null;
_cachedBalanceKey = null;
confettiController.dispose();
super.dispose();
}
+16 -4
View File
@@ -8,6 +8,7 @@ import '../../widgets/scanner/qr_scanner_widget.dart';
import '../../providers/wallet_provider.dart';
import '../4_receive/receive_screen.dart';
import '../7_melt/melt_screen.dart';
import '../11_payment_request/payment_request_screen.dart';
/// Pantalla de escaneo QR con soporte para diferentes modos
class ScanScreen extends StatefulWidget {
@@ -195,8 +196,17 @@ class _ScanScreenState extends State<ScanScreen> {
break;
case IncomingDataType.paymentRequest:
// TODO: Implementar manejo de payment requests (post-MVP)
_showError(l10n.paymentRequestNotSupported);
if (mounted) {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PaymentRequestScreen(
encodedRequest: data.raw,
),
),
);
}
break;
case IncomingDataType.unknown:
@@ -221,8 +231,10 @@ class _ScanScreenState extends State<ScanScreen> {
}
void _handleInvoiceOnlyMode(ParsedData data) {
if (data.type == IncomingDataType.lightningInvoice) {
// Retornar el invoice vía callback
if (data.type == IncomingDataType.lightningInvoice ||
(data.type == IncomingDataType.paymentRequest &&
data.invoiceBolt11 != null)) {
// Return the invoice — works for pure BOLT11 and BIP-321 URIs with lightning=
Navigator.pop(context, data.invoiceBolt11 ?? data.raw);
widget.onDataScanned?.call(data.invoiceBolt11 ?? data.raw);
} else {
@@ -0,0 +1,338 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../src/rust/api/payment_request.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../providers/wallet_provider.dart';
/// Pantalla de confirmación para pagar un Payment Request (NUT-18/26)
class PaymentRequestScreen extends StatefulWidget {
/// Raw encoded payment request (creqA, CREQB1, or bitcoin:?creq=)
final String encodedRequest;
const PaymentRequestScreen({super.key, required this.encodedRequest});
@override
State<PaymentRequestScreen> createState() => _PaymentRequestScreenState();
}
class _PaymentRequestScreenState extends State<PaymentRequestScreen> {
PaymentRequestInfo? _info;
String? _parseError;
bool _isPaying = false;
String? _payError;
@override
void initState() {
super.initState();
_parseRequest();
}
void _parseRequest() {
try {
final info = PaymentRequestInfo.parse(encoded: widget.encodedRequest);
setState(() => _info = info);
} catch (e) {
setState(() => _parseError = e.toString());
}
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(
l10n.paymentRequestTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
),
),
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft),
onPressed: () => Navigator.pop(context),
),
),
body: _parseError != null
? _buildError(l10n)
: _info == null
? const Center(child: CircularProgressIndicator())
: _buildContent(l10n),
),
);
}
Widget _buildError(L10n l10n) {
return Center(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: GlassCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(LucideIcons.alertCircle, color: Colors.redAccent, size: 48),
const SizedBox(height: 16),
Text(
l10n.paymentRequestErrorParsing,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_parseError!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 13,
color: Colors.white.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildContent(L10n l10n) {
final info = _info!;
final walletProvider = context.watch<WalletProvider>();
final activeUnit = walletProvider.activeUnit;
final requestUnit = info.unit ?? 'sat';
// Validaciones
final unitMismatch = info.unit != null && info.unit != activeUnit;
String stripSlash(String url) =>
url.endsWith('/') ? url.substring(0, url.length - 1) : url;
final normalizedMints = info.mints.map(stripSlash).toSet();
final activeMint = walletProvider.activeMintUrl;
final mintNotAccepted = normalizedMints.isNotEmpty &&
(activeMint == null || !normalizedMints.contains(stripSlash(activeMint)));
final hasTransport = info.transports.isNotEmpty;
return SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Monto
if (info.amount != null)
_buildAmountCard(info.amount!, requestUnit, l10n),
const SizedBox(height: 16),
// Detalles
GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Descripción
if (info.description != null && info.description!.isNotEmpty) ...[
_buildDetailRow(
LucideIcons.fileText,
l10n.paymentRequestDescription,
info.description!,
),
const SizedBox(height: 12),
],
// Mints aceptados
_buildDetailRow(
LucideIcons.server,
l10n.paymentRequestMints,
info.mints.isEmpty
? l10n.paymentRequestAnyMint
: info.mints.map((m) => Uri.parse(m).host).join(', '),
),
// Transporte (solo mostrar si hay transporte configurado)
if (info.transports.isNotEmpty) ...[
const SizedBox(height: 12),
_buildDetailRow(
LucideIcons.send,
l10n.paymentRequestTransport,
info.transports
.map((t) => t.transportType == 'nostr' ? 'Nostr (NIP-17)' : 'HTTP POST')
.join(', '),
),
],
],
),
),
const SizedBox(height: 16),
// Warnings
if (unitMismatch)
_buildWarning(
LucideIcons.alertTriangle,
l10n.paymentRequestUnitMismatch(requestUnit),
),
if (mintNotAccepted)
_buildWarning(
LucideIcons.alertTriangle,
l10n.paymentRequestMintNotAccepted,
),
if (!hasTransport)
_buildWarning(
LucideIcons.info,
l10n.paymentRequestNoTransport,
),
if (_payError != null) ...[
const SizedBox(height: 12),
_buildWarning(LucideIcons.alertCircle, _payError!),
],
const SizedBox(height: 32),
// Botón pagar
PrimaryButton(
text: _isPaying ? l10n.paymentRequestPaying : l10n.paymentRequestPay,
icon: LucideIcons.zap,
isLoading: _isPaying,
onPressed: (!hasTransport || _isPaying)
? null
: _pay,
),
],
),
);
}
Widget _buildAmountCard(BigInt amount, String unit, L10n l10n) {
return GlassCard(
child: Column(
children: [
Text(
l10n.paymentRequestAmount,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white.withValues(alpha: 0.7),
),
),
const SizedBox(height: 8),
Text(
UnitFormatter.formatBalanceWithUnit(amount, unit),
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 36,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
);
}
Widget _buildDetailRow(IconData icon, String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: AppColors.primaryAction),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white.withValues(alpha: 0.6),
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
),
],
),
),
],
);
}
Widget _buildWarning(IconData icon, String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: GlassCard(
child: Row(
children: [
Icon(icon, size: 20, color: Colors.amber),
const SizedBox(width: 10),
Expanded(
child: Text(
text,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 13,
color: Colors.amber,
),
),
),
],
),
),
);
}
Future<void> _pay() async {
setState(() {
_isPaying = true;
_payError = null;
});
try {
final walletProvider = context.read<WalletProvider>();
await walletProvider.payPaymentRequest(
widget.encodedRequest,
customAmount: _info!.amount,
);
if (!mounted) return;
// Éxito — mostrar snackbar y volver
final l10n = L10n.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.paymentRequestSuccess),
backgroundColor: Colors.green.shade700,
),
);
Navigator.pop(context, true);
} catch (e) {
if (!mounted) return;
setState(() {
_isPaying = false;
_payError = e.toString();
});
}
}
}
+909
View File
@@ -0,0 +1,909 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:share_plus/share_plus.dart';
import '../../src/rust/api/wallet.dart';
import '../../src/rust/api/payment_request.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// import '../../core/utils/bip321_builder.dart';
import '../../core/services/nfc_service.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/numpad_widget.dart';
import '../../providers/wallet_provider.dart';
enum RequestStatus { input, generating, waiting, received, error }
enum QrMode { universal, cashu, lightning }
/// Pantalla para solicitar pagos via Payment Request unificado.
/// Genera creqB (Cashu/Nostr) + Lightning invoice en paralelo.
class RequestScreen extends StatefulWidget {
const RequestScreen({super.key});
@override
State<RequestScreen> createState() => _RequestScreenState();
}
class _RequestScreenState extends State<RequestScreen> {
final TextEditingController _descriptionController = TextEditingController();
// Input state
String _amountValue = '';
late String _activeUnit;
// Request state
RequestStatus _status = RequestStatus.input;
String? _errorMessage;
// Payment data
String? _creqB;
String? _bolt11;
// TODO: re-enable QrMode.cashu / universal once CDK fixes NIP-17 sender pubkey bug
// See: https://github.com/cashubtc/cdk/issues/1807
// QrMode _activeMode = QrMode.lightning;
bool _paymentHandled = false;
// Listeners
StreamSubscription<NostrPaymentEvent>? _nostrSubscription;
StreamSubscription<MintQuote>? _mintSubscription;
// NFC
NfcState _nfcState = NfcState.unsupported;
bool _nfcEmulating = false;
// Success
BigInt _receivedAmount = BigInt.zero;
late final WalletProvider _walletProvider;
@override
void initState() {
super.initState();
_walletProvider = context.read<WalletProvider>();
_activeUnit = _walletProvider.activeUnit;
_checkNfc();
}
@override
void dispose() {
_descriptionController.dispose();
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// No-op while Nostr is disabled (_nostrSubscription is always null, _creqB is always null)
_nostrSubscription?.cancel();
_mintSubscription?.cancel();
if (_nfcEmulating) NfcService.stopEmulating();
if (!_paymentHandled && _creqB != null) {
_walletProvider.removePendingNostrRequest();
}
super.dispose();
}
Future<void> _checkNfc() async {
final state = await NfcService.checkState();
if (mounted) setState(() => _nfcState = state);
}
String get _unitLabel => UnitFormatter.getUnitLabel(_activeUnit);
BigInt get _amount => UnitFormatter.parseRawDigits(_amountValue, _activeUnit);
bool get _isValidAmount => _amount > BigInt.zero;
// ─── Build ───
@override
Widget build(BuildContext context) {
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
L10n.of(context)!.requestPayment,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
actions: [
if (_status == RequestStatus.waiting)
IconButton(
icon: const Icon(LucideIcons.share2, color: Colors.white),
onPressed: _shareRequest,
),
],
),
body: SafeArea(child: _buildBody()),
),
);
}
Widget _buildBody() {
switch (_status) {
case RequestStatus.input:
return _buildInputView();
case RequestStatus.generating:
return _buildGeneratingView();
case RequestStatus.waiting:
return _buildWaitingView();
case RequestStatus.received:
return _buildSuccessView();
case RequestStatus.error:
return _buildErrorView();
}
}
// ─── Input View ───
Widget _buildInputView() {
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildAmountSection(),
const SizedBox(height: AppDimensions.paddingLarge),
_buildDescriptionSection(),
const SizedBox(height: AppDimensions.paddingMedium),
if (_errorMessage != null) _buildErrorMessage(),
],
),
),
),
Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: PrimaryButton(
text: L10n.of(context)!.generateRequest,
onPressed: _isValidAmount ? _generateRequest : null,
),
),
],
);
}
Widget _buildAmountSection() {
final displayAmount =
UnitFormatter.formatRawDigitsForDisplay(_amountValue, _activeUnit);
return Column(
children: [
Text(
displayAmount,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 48,
fontWeight: FontWeight.bold,
color: _isValidAmount || _amountValue.isEmpty
? Colors.white
: AppColors.error,
),
),
const SizedBox(height: 4),
Text(
_unitLabel,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
NumpadWidget(
value: _amountValue,
onChanged: (v) => setState(() {
_errorMessage = null;
_amountValue = v;
}),
),
],
);
}
Widget _buildDescriptionSection() {
final l10n = L10n.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.descriptionOptional,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: TextField(
controller: _descriptionController,
maxLines: 2,
maxLength: 100,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white,
),
decoration: InputDecoration(
hintText: l10n.requestDescriptionHint,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
counterStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
),
),
],
);
}
Widget _buildErrorMessage() {
return Container(
margin: const EdgeInsets.only(bottom: AppDimensions.paddingMedium),
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: AppColors.error, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
);
}
// ─── Generating View ───
Widget _buildGeneratingView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(color: AppColors.primaryAction),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
L10n.of(context)!.generatingRequest,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary,
),
),
],
),
);
}
// ─── Waiting View (QR + sub-toggle + actions) ───
Widget _buildWaitingView() {
final l10n = L10n.of(context)!;
final formattedAmount = UnitFormatter.formatBalance(_amount, _activeUnit);
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
children: [
// Amount header
Text(
'$formattedAmount $_unitLabel',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
// QR Code with logo overlay
Center(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Stack(
alignment: Alignment.center,
children: [
QrImageView(
data: _getActiveQrContent(),
version: QrVersions.auto,
size: 260,
backgroundColor: Colors.white,
// TODO: use QrErrorCorrectLevel.M for universal mode once CDK fixes NIP-17 (cashubtc/cdk#1807)
errorCorrectionLevel: QrErrorCorrectLevel.H,
),
// TODO: re-enable Cashu logo overlay once CDK fixes NIP-17 (cashubtc/cdk#1807)
// if (_activeMode == QrMode.cashu)
// Container(
// width: 48, height: 48,
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(10),
// border: Border.all(color: Colors.white, width: 4),
// ),
// child: ClipRRect(
// borderRadius: BorderRadius.circular(6),
// child: Image.asset('assets/img/cashu.png', fit: BoxFit.contain),
// ),
// ),
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.bitcoinOrange,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white, width: 4),
),
child: const Icon(
LucideIcons.zap,
color: Colors.white,
size: 28,
),
),
],
),
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Sub-toggle: Universal / Cashu / Lightning
_buildModeToggle(),
const SizedBox(height: AppDimensions.paddingMedium),
// Action buttons: Copy, NFC, Share
_buildActionButtons(),
const SizedBox(height: AppDimensions.paddingMedium),
// Status indicator
_buildStatusIndicator(),
],
),
),
),
// Cancel button
Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
l10n.cancel,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
],
);
}
Widget _buildModeToggle() {
// TODO: re-enable Universal/Cashu tabs once CDK fixes NIP-17 (cashubtc/cdk#1807)
// Hidden while only Lightning is available (single button toggle is a no-op)
return const SizedBox.shrink(
// child: Container(
// padding: const EdgeInsets.all(4),
// decoration: BoxDecoration(
// color: Colors.white.withValues(alpha: 0.1),
// borderRadius: BorderRadius.circular(12),
// ),
// child: Row(
// children: [
// _buildToggleButton(label: L10n.of(context)!.universal, mode: QrMode.universal, enabled: _bolt11 != null),
// _buildToggleButton(label: 'Cashu', mode: QrMode.cashu, enabled: true),
// _buildToggleButton(label: 'Lightning', mode: QrMode.lightning, enabled: _bolt11 != null),
// ],
// ),
// ),
);
}
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// Widget _buildToggleButton({
// required String label,
// required QrMode mode,
// required bool enabled,
// }) {
// final isActive = _activeMode == mode;
// return Expanded(
// child: GestureDetector(
// onTap: enabled
// ? () {
// setState(() => _activeMode = mode);
// _updateNfcPayload();
// }
// : null,
// child: Container(
// padding: const EdgeInsets.symmetric(vertical: 10),
// decoration: BoxDecoration(
// gradient: isActive
// ? const LinearGradient(colors: AppColors.buttonGradient)
// : null,
// borderRadius: BorderRadius.circular(10),
// ),
// child: Center(
// child: Text(
// label,
// style: TextStyle(
// fontFamily: 'Inter',
// fontSize: 13,
// fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
// color: enabled
// ? (isActive ? Colors.white : AppColors.textSecondary)
// : Colors.white.withValues(alpha: 0.3),
// ),
// ),
// ),
// ),
// ),
// );
// }
Widget _buildActionButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildActionButton(
icon: LucideIcons.copy,
label: L10n.of(context)!.copy,
onTap: _copyRequest,
),
if (_nfcState == NfcState.enabled) ...[
const SizedBox(width: 16),
_buildActionButton(
icon: _nfcEmulating ? LucideIcons.wifiOff : LucideIcons.wifi,
label: 'NFC',
onTap: _toggleNfc,
active: _nfcEmulating,
),
],
const SizedBox(width: 16),
_buildActionButton(
icon: LucideIcons.share2,
label: L10n.of(context)!.share,
onTap: _shareRequest,
),
],
);
}
Widget _buildActionButton({
required IconData icon,
required String label,
required VoidCallback onTap,
bool active = false,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: active
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: active
? Border.all(color: AppColors.primaryAction.withValues(alpha: 0.5))
: null,
),
child: Row(
children: [
Icon(icon, color: active ? AppColors.primaryAction : AppColors.textSecondary, size: 18),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: active ? AppColors.primaryAction : AppColors.textSecondary,
),
),
],
),
),
);
}
Widget _buildStatusIndicator() {
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.textSecondary),
),
),
const SizedBox(width: 12),
Text(
L10n.of(context)!.waitingForPayment,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
],
),
);
}
// ─── Success View ───
Widget _buildSuccessView() {
final l10n = L10n.of(context)!;
final formattedAmount =
'+${UnitFormatter.formatBalance(_receivedAmount, _activeUnit)}';
final unitLabel = UnitFormatter.getUnitLabel(_activeUnit);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.checkCircle2,
color: AppColors.success,
size: 40,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
'$formattedAmount $unitLabel',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Text(
l10n.requestPaymentReceived,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingLarge * 2),
PrimaryButton(
text: l10n.backToHome,
onPressed: () =>
Navigator.popUntil(context, (route) => route.isFirst),
),
],
),
);
}
// ─── Error View ───
Widget _buildErrorView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.alertCircle,
color: AppColors.error,
size: 40,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
L10n.of(context)!.error,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_errorMessage ?? L10n.of(context)!.unknownError,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
PrimaryButton(
text: L10n.of(context)!.back,
onPressed: () => setState(() {
_status = RequestStatus.input;
_errorMessage = null;
}),
),
],
),
);
}
// ─── Logic ───
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// static const List<String> _defaultNostrRelays = [
// 'wss://relay.damus.io',
// 'wss://relay.primal.net',
// 'wss://nos.lol',
// ];
Future<void> _generateRequest() async {
final walletProvider = context.read<WalletProvider>();
final wallet = walletProvider.activeWallet;
// Clean up previous request state — await to ensure Rust locks are released
await _nostrSubscription?.cancel();
_nostrSubscription = null;
await _mintSubscription?.cancel();
_mintSubscription = null;
if (_nfcEmulating) {
NfcService.stopEmulating();
_nfcEmulating = false;
}
_paymentHandled = false;
_creqB = null;
_bolt11 = null;
// TODO: re-enable Cashu/Nostr payment request once CDK fixes NIP-17 sender pubkey bug
// See: https://github.com/cashubtc/cdk/issues/1807
// _activeMode = QrMode.lightning;
setState(() => _status = RequestStatus.generating);
if (wallet == null) {
setState(() {
_status = RequestStatus.error;
_errorMessage = 'No active wallet';
});
return;
}
try {
final description = _descriptionController.text.isNotEmpty
? _descriptionController.text
: null;
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// // 1. Create payment request (creqB + Nostr keys)
// final request = await wallet.createPaymentRequest(
// params: CreateRequestParams(
// amount: _amount,
// unit: _activeUnit,
// description: description,
// nostrRelays: _defaultNostrRelays,
// ),
// );
//
// _creqB = request.creqB;
//
// // Persist handle for recovery if app is killed
// await walletProvider.savePendingNostrRequest(
// request.listenerHandle.toPersisted(),
// );
//
// // 2. Start Nostr listener
// _nostrSubscription = wallet
// .waitForNostrPayment(handle: request.listenerHandle)
// .listen(_onNostrEvent);
// Lightning invoice generation via wallet provider
// (walletProvider.mintTokens handles pending invoice persistence and metadata)
final mintStream = await walletProvider.mintTokens(
_amount,
description,
);
if (!mounted) return;
_mintSubscription = mintStream.listen(_onMintEvent);
} catch (e) {
if (!mounted) return;
setState(() {
_status = RequestStatus.error;
_errorMessage = e.toString();
});
}
}
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// void _onNostrEvent(NostrPaymentEvent event) {
// if (!mounted || _paymentHandled) return;
// if (event.state == NostrPaymentState.received) {
// _paymentHandled = true;
// _mintSubscription?.cancel();
// _onPaymentSuccess(event.amount ?? _amount);
// }
// }
void _onMintEvent(MintQuote quote) {
if (!mounted) return;
switch (quote.state) {
case MintQuoteState.unpaid:
if (_bolt11 == null) {
setState(() {
_bolt11 = quote.request;
// TODO: switch to QrMode.universal once CDK fixes NIP-17 (cashubtc/cdk#1807)
// _activeMode = QrMode.lightning;
_status = RequestStatus.waiting;
});
_updateNfcPayload();
}
break;
case MintQuoteState.issued:
if (!_paymentHandled) {
_paymentHandled = true;
_nostrSubscription?.cancel();
_onPaymentSuccess(quote.amount ?? _amount);
}
break;
case MintQuoteState.error:
// No Nostr fallback while CDK NIP-17 bug is open (cashubtc/cdk#1807)
if (!_paymentHandled) {
if (_nfcEmulating) {
NfcService.stopEmulating();
_nfcEmulating = false;
}
setState(() {
_status = RequestStatus.error;
_errorMessage = quote.error ?? L10n.of(context)!.unknownError;
});
}
break;
default:
break;
}
}
Future<void> _onPaymentSuccess(BigInt amount) async {
if (_nfcEmulating) NfcService.stopEmulating();
setState(() {
_status = RequestStatus.received;
_receivedAmount = amount;
});
final walletProvider = context.read<WalletProvider>();
walletProvider.confettiController.fire();
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// Only remove pending Nostr request if this screen created one.
// Disabled while Nostr payment requests are disabled to avoid
// deleting a recovery record from a previous session.
// await walletProvider.removePendingNostrRequest();
}
// ─── QR Content ───
String _getActiveQrContent() {
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// case QrMode.universal:
// return buildUnifiedUri(creqB: _creqB!, bolt11: _bolt11);
// case QrMode.cashu:
// return _creqB!.toUpperCase();
return _bolt11?.toUpperCase() ?? '';
}
// ─── Actions ───
Future<void> _copyRequest() async {
final content = _getActiveQrContent();
await Clipboard.setData(ClipboardData(text: content));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.copiedToClipboard),
backgroundColor: AppColors.success,
duration: const Duration(seconds: 2),
),
);
}
}
void _toggleNfc() {
if (_nfcEmulating) {
NfcService.stopEmulating();
setState(() => _nfcEmulating = false);
} else {
NfcService.startEmulating(_getActiveQrContent());
setState(() => _nfcEmulating = true);
}
}
void _updateNfcPayload() {
if (_nfcEmulating) {
NfcService.startEmulating(_getActiveQrContent());
}
}
void _shareRequest() async {
final content = _getActiveQrContent();
final formattedAmount =
UnitFormatter.formatBalance(_amount, _activeUnit);
final unitLabel = UnitFormatter.getUnitLabel(_activeUnit);
await SharePlus.instance.share(
ShareParams(
text: '$formattedAmount $unitLabel\n\n$content',
subject: 'Payment Request - $formattedAmount $unitLabel',
),
);
}
}
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -11,9 +11,10 @@ import '../../widgets/common/glass_card.dart';
import '../../widgets/common/animated_action_button.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/settings_provider.dart';
import '../../providers/p2pk_provider.dart';
import '../4_receive/receive_screen.dart';
import '../5_send/send_screen.dart';
import '../6_mint/mint_screen.dart';
import '../12_request/request_screen.dart';
import '../7_melt/melt_screen.dart';
import '../8_settings/settings_screen.dart';
import '../8_settings/mints_screen.dart';
@@ -51,7 +52,10 @@ class _HomeScreenState extends State<HomeScreen> {
if (!walletProvider.hasPendingTokens) return;
try {
final result = await walletProvider.checkPendingTokens();
final p2pkProvider = context.read<P2PKProvider>();
final result = await walletProvider.checkPendingTokens(
p2pkKeyResolver: p2pkProvider.getPrivateKeyForToken,
);
final claimed = (result['claimed'] as int?) ?? 0;
final totalClaimed = result['totalClaimed'] as BigInt? ?? BigInt.zero;
final unit = (result['unit'] as String?) ?? walletProvider.activeUnit;
@@ -433,14 +437,14 @@ class _HomeScreenState extends State<HomeScreen> {
},
),
_MethodOption(
icon: LucideIcons.zap,
label: l10n.lightning,
description: l10n.generateInvoiceToDeposit,
icon: LucideIcons.bellRing,
label: l10n.request,
description: l10n.requestPaymentDescription,
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MintScreen()),
MaterialPageRoute(builder: (context) => const RequestScreen()),
);
},
),
+20
View File
@@ -16,6 +16,7 @@ import '../../widgets/common/primary_button.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/p2pk_provider.dart';
import '../10_scanner/scan_screen.dart';
import '../11_payment_request/payment_request_screen.dart';
/// Pantalla para recibir tokens Cashu
class ReceiveScreen extends StatefulWidget {
@@ -993,6 +994,25 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
return;
}
// Detectar payment request y redirigir
final parsed = IncomingDataParser.parse(tokenValue);
if (parsed.type == IncomingDataType.paymentRequest) {
_isValidToken = false;
_tokenInfo = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => PaymentRequestScreen(
encodedRequest: tokenValue,
),
),
);
});
return;
}
// Parsear token real con cdk-flutter
final tokenInfo = walletProvider.parseToken(tokenValue);
+44 -8
View File
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
@@ -5,10 +6,12 @@ import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/models/proof.dart';
import '../../core/services/proof_service.dart';
import '../../src/rust/api/token.dart' as cdk;
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/proof/proof_selector.dart';
import '../../core/utils/keyset_debug.dart';
import 'share_token_screen.dart';
/// Pantalla para enviar tokens de forma offline seleccionando proofs manualmente.
@@ -45,7 +48,7 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
@override
void dispose() {
_memoController.dispose();
_proofService.close();
if (!_isCreating) _proofService.close();
super.dispose();
}
@@ -321,32 +324,55 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
}
Future<void> _createOfflineToken() async {
final selectedProofs = _selectedProofs;
final selectedTotal = _proofService.calculateTotal(selectedProofs);
// Validar monto mínimo viable para el receptor
// Leer ppk del keyset local, fallback a 100 (estándar)
var ppk = await KeysetDebug.getInputFeePpk(widget.mintUrl, widget.unit);
if (ppk < 0) ppk = 100; // -1 = error leyendo DB, asumir fee estándar
final minReceiveFee = ppk > 0 ? BigInt.from((ppk + 999) ~/ 1000) : BigInt.zero;
if (selectedTotal <= minReceiveFee) {
if (mounted) {
setState(() {
_errorMessage = L10n.of(context)!.feeExceedsAmount;
});
}
return;
}
var proofsMarkedPending = false;
setState(() {
_isCreating = true;
});
try {
final selectedProofs = _selectedProofs;
// Marcar proofs como PENDING_SPENT
await _proofService.markProofsPendingSpent(selectedProofs);
proofsMarkedPending = true;
// Crear token V3
// Crear token via CDK (produce V4/cashuB válido)
final memo = _memoController.text.isNotEmpty ? _memoController.text : null;
final token = _proofService.createTokenV3(
final proofsJson = jsonEncode(
selectedProofs.map((p) => p.toTokenProof()).toList(),
);
final cdkToken = cdk.createOfflineToken(
mintUrl: widget.mintUrl,
proofs: selectedProofs,
proofsJson: proofsJson,
memo: memo,
unit: widget.unit,
);
final token = cdkToken.encoded;
if (mounted) {
_isCreating = false;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ShareTokenScreen(
token: token,
amount: _selectedTotal,
amount: selectedTotal,
unit: widget.unit,
memo: memo,
),
@@ -354,7 +380,17 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
);
}
} catch (e) {
if (!mounted) return;
if (proofsMarkedPending) {
try {
await _proofService.markProofsUnspent(selectedProofs);
} catch (rollbackErr) {
debugPrint('Failed to rollback proofs to UNSPENT: $rollbackErr');
}
}
if (!mounted) {
_proofService.close();
return;
}
final errorMessage = L10n.of(context)!.creatingTokenError(e.toString());
setState(() {
_errorMessage = errorMessage;
+50
View File
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
@@ -7,6 +8,7 @@ import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../core/utils/keyset_debug.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
@@ -572,15 +574,37 @@ class _SendScreenState extends State<SendScreen> {
});
return;
}
// Offline: verificar monto mínimo con ppk del keyset local (fallback 100)
var ppk = await KeysetDebug.getInputFeePpk(mintUrl, _activeUnit);
if (ppk < 0) ppk = 100;
final minReceiveFee = ppk > 0 ? BigInt.from((ppk + 999) ~/ 1000) : BigInt.zero;
if (!mounted) return;
if (_amount <= minReceiveFee) {
setState(() {
_errorMessage = L10n.of(context)!.feeExceedsAmount;
});
return;
}
// Offline: ir directo a selección de monedas
_goToOfflineModeWithMessage();
return;
}
// Online: verificar monto mínimo con ppk real del mint
final minReceiveFee = await _getMinReceiveFee(mintUrl);
if (!mounted) return;
if (_amount <= minReceiveFee) {
setState(() {
_errorMessage = L10n.of(context)!.feeExceedsAmount;
});
return;
}
// Online: mostrar modal de confirmación normal
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _ConfirmationModal(
amount: _amount,
unit: _activeUnit,
@@ -594,6 +618,32 @@ class _SendScreenState extends State<SendScreen> {
);
}
/// Calcula el fee mínimo que el receptor pagará al reclamar el token.
/// Consulta /v1/keysets del mint para obtener el ppk real.
/// Fallback: ppk=100 (estándar) → fee mínimo = 1 sat.
Future<BigInt> _getMinReceiveFee(String mintUrl) async {
try {
final response = await http.get(
Uri.parse('$mintUrl/v1/keysets'),
).timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
final data = json.decode(response.body) as Map<String, dynamic>;
final keysets = data['keysets'] as List<dynamic>? ?? [];
for (final ks in keysets) {
if (ks['active'] == true && ks['unit'] == _activeUnit) {
final ppk = (ks['input_fee_ppk'] as num?)?.toInt() ?? 0;
if (ppk > 0) {
return BigInt.from((ppk + 999) ~/ 1000); // ceil(ppk / 1000)
}
return BigInt.zero;
}
}
}
} catch (_) {}
// Fallback: ppk=100 estándar
return BigInt.one;
}
/// Verifica conectividad haciendo petición HTTP real al mint.
Future<bool> _checkConnectivity(String mintUrl) async {
try {
+7 -2
View File
@@ -72,8 +72,13 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
}
void _encodeTokenToUR() {
// Parsear el token string a objeto Token de cdk-flutter
final token = cdk.Token.parse(encoded: widget.token);
// Construir Token directamente sin Token.parse para evitar validación
// estricta de keyset IDs. encodeQrToken solo usa token.encoded.
final token = cdk.Token(
encoded: widget.token,
amount: widget.amount,
mintUrl: '',
);
// Codificar a fragmentos UR usando cdk-flutter
// maxFragmentLength por defecto es 150 bytes
+2 -3
View File
@@ -80,10 +80,9 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
_onMintCompleted();
break;
default:
// Cualquier otro estado se trata como error
case MintQuoteState.error:
_status = MintStatus.error;
_errorMessage = L10n.of(context)!.unknownState;
_errorMessage = quote.error ?? L10n.of(context)!.unknownError;
}
});
},
+1
View File
@@ -249,6 +249,7 @@ class _MintScreenState extends State<MintScreen> {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _ConfirmationModal(
amount: _amount,
unit: _activeUnit,
+12 -2
View File
@@ -404,6 +404,8 @@ class _AmountScreenState extends State<AmountScreen> {
Future<void> _processPayment() async {
if (!_canPay) return;
final walletProvider = context.read<WalletProvider>();
setState(() {
_isProcessing = true;
_errorMessage = null;
@@ -431,14 +433,22 @@ class _AmountScreenState extends State<AmountScreen> {
if (!mounted) return;
// 3. Obtener quote del mint (en la unidad del mint)
final walletProvider = context.read<WalletProvider>();
final quote = await walletProvider.getMeltQuote(invoiceResult.invoice);
if (!mounted) return;
final total = quote.amount + quote.feeReserve;
// 4. Verificar balance suficiente
// 4a. Verificar que el fee no supere el monto (operación inviable)
if (quote.amount <= quote.feeReserve) {
setState(() {
_isProcessing = false;
_errorMessage = L10n.of(context)!.feeExceedsAmount;
});
return;
}
// 4b. Verificar balance suficiente
if (total > _availableBalance) {
setState(() {
_isProcessing = false;
+15 -2
View File
@@ -594,15 +594,28 @@ class _MeltScreenState extends State<MeltScreen> {
}
});
// Extract lightning invoice from BIP-321 URI if present
var inputValue = value;
if (value.trim().toLowerCase().startsWith('bitcoin:')) {
final parsed = IncomingDataParser.parse(value.trim());
if (parsed.invoiceBolt11 != null) {
inputValue = parsed.invoiceBolt11!;
_invoiceController.text = inputValue;
_invoiceController.selection = TextSelection.fromPosition(
TextPosition(offset: inputValue.length),
);
}
}
// Detectar tipo de input (sin mostrar error, solo detectar)
final inputType = LnurlService.detectType(value);
final inputType = LnurlService.detectType(inputValue);
setState(() => _inputType = inputType);
// Solo procesar automáticamente invoices BOLT11
// LNURL y Lightning Address requieren botón explícito
if (inputType == LnInputType.bolt11Invoice) {
// BOLT11 invoices son 200+ chars; evitar llamar API con input parcial
final cleaned = LnurlService.cleanInput(value);
final cleaned = LnurlService.cleanInput(inputValue);
if (cleaned.length > 50) {
_getQuote(cleaned);
}
+144
View File
@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
/// Muestra el diálogo "Acerca de" de ElCaju
void showElCajuAboutDialog(BuildContext context, String appVersion) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.deepVoidPurple,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Logo
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.asset(
'assets/img/elcajucubano.png',
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 16),
const Text(
'El Caju',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'v$appVersion',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
Text(
L10n.of(context)!.aboutTagline,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
_buildAboutDescription(context),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
L10n.of(context)!.close,
style: const TextStyle(
fontFamily: 'Inter',
color: AppColors.textSecondary,
),
),
),
],
),
);
}
Widget _buildAboutDescription(BuildContext context) {
final description = L10n.of(context)!.aboutDescription;
const keyword = 'LaChispa';
final index = description.indexOf(keyword);
if (index == -1) {
return Text(
description,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
);
}
final before = description.substring(0, index);
final after = description.substring(index + keyword.length);
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
children: [
TextSpan(text: before),
WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
child: GestureDetector(
onTap: () => _openLaChispa(context),
child: Text(
keyword,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.secondaryAction,
decoration: TextDecoration.underline,
decorationColor: AppColors.secondaryAction,
),
),
),
),
TextSpan(text: after),
],
),
);
}
Future<void> _openLaChispa(BuildContext context) async {
final url = Uri.parse('https://app.lachispa.me');
try {
await launchUrl(url, mode: LaunchMode.externalApplication);
} catch (_) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.couldNotOpenLink),
backgroundColor: AppColors.error,
),
);
}
}
}
@@ -0,0 +1,249 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
/// Modal para confirmar borrado de wallet
class DeleteWalletModal extends StatefulWidget {
final VoidCallback onConfirm;
final VoidCallback onCancel;
const DeleteWalletModal({
super.key,
required this.onConfirm,
required this.onCancel,
});
@override
State<DeleteWalletModal> createState() => _DeleteWalletModalState();
}
class _DeleteWalletModalState extends State<DeleteWalletModal> {
final TextEditingController _controller = TextEditingController();
bool _canDelete = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
final confirmWord = l10n.deleteConfirmWord;
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Icono advertencia
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: Icon(
LucideIcons.alertTriangle,
color: AppColors.error,
size: 32,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Título
Text(
l10n.deleteWalletQuestion,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Advertencia
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Row(
children: [
Icon(
LucideIcons.alertCircle,
color: AppColors.error,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
l10n.actionIrreversible,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.error,
),
),
),
],
),
const SizedBox(height: 8),
Text(
l10n.deleteWalletWarning,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.error.withValues(alpha: 0.8),
),
),
],
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Input de confirmación
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.typeDeleteToConfirm,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 8),
TextField(
controller: _controller,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white,
),
decoration: InputDecoration(
hintText: confirmWord,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.3),
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
onChanged: (value) {
setState(() {
_canDelete = value == confirmWord;
});
},
),
],
),
),
const SizedBox(height: AppDimensions.paddingLarge),
// Botones
Row(
children: [
Expanded(
child: GestureDetector(
onTap: widget.onCancel,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
l10n.cancel,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: GestureDetector(
onTap: _canDelete ? widget.onConfirm : null,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: _canDelete
? AppColors.error
: AppColors.error.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
l10n.deleteWallet,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: _canDelete
? Colors.white
: Colors.white.withValues(alpha: 0.5),
),
),
),
),
),
),
],
),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
),
);
}
}
+194
View File
@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../core/constants/colors.dart';
/// Diálogo para ingresar PIN de 4 dígitos
class PinDialog extends StatefulWidget {
final String title;
final String subtitle;
const PinDialog({
super.key,
required this.title,
required this.subtitle,
});
@override
State<PinDialog> createState() => _PinDialogState();
}
class _PinDialogState extends State<PinDialog> {
final List<String> _pin = [];
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: AppColors.deepVoidPurple,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
title: Text(
widget.title,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.subtitle,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Indicadores de PIN
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) {
final filled = index < _pin.length;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
width: 16,
height: 16,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: filled
? AppColors.primaryAction
: Colors.white.withValues(alpha: 0.2),
border: Border.all(
color: filled
? AppColors.primaryAction
: Colors.white.withValues(alpha: 0.3),
width: 2,
),
),
);
}),
),
const SizedBox(height: 24),
// Teclado numérico
_buildNumPad(),
],
),
);
}
Widget _buildNumPad() {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: ['1', '2', '3'].map((n) => _buildNumKey(n)).toList(),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: ['4', '5', '6'].map((n) => _buildNumKey(n)).toList(),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: ['7', '8', '9'].map((n) => _buildNumKey(n)).toList(),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Cancelar
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(30),
),
child: Icon(
LucideIcons.x,
color: AppColors.textSecondary,
size: 24,
),
),
),
_buildNumKey('0'),
// Borrar
GestureDetector(
onTap: _deleteDigit,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(30),
),
child: Icon(
LucideIcons.delete,
color: AppColors.textSecondary,
size: 24,
),
),
),
],
),
],
);
}
Widget _buildNumKey(String number) {
return GestureDetector(
onTap: () => _addDigit(number),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(30),
),
child: Center(
child: Text(
number,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
);
}
void _addDigit(String digit) {
if (_pin.length < 4) {
setState(() {
_pin.add(digit);
});
if (_pin.length == 4) {
// PIN completo, cerrar con resultado
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
Navigator.pop(context, _pin.join());
}
});
}
}
}
void _deleteDigit() {
if (_pin.isNotEmpty) {
setState(() {
_pin.removeLast();
});
}
}
}
+131
View File
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
class PrivacyScreen extends StatelessWidget {
const PrivacyScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
final conclusionLines = l10n.privacyConclusion
.split('\n')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
l10n.privacyPolicy,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.privacyTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
l10n.privacyGoodbye,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppColors.primaryAction,
),
),
const SizedBox(height: 40),
Text(
l10n.privacyKeepReading,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 13,
fontStyle: FontStyle.italic,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
const SizedBox(height: 28),
...l10n.privacyBody.split('\n')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.map(
(line) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
line,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary.withValues(alpha: 0.85),
),
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 28),
...conclusionLines.asMap().entries.map((entry) {
final isLast = entry.key == conclusionLines.length - 1;
return Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 8),
child: Text(
entry.value,
style: isLast
? const TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.white,
)
: TextStyle(
fontFamily: 'Inter',
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
);
}),
],
),
),
),
),
),
),
),
);
}
}
@@ -0,0 +1,721 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:provider/provider.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../providers/wallet_provider.dart';
/// Modal para recuperar tokens usando NUT-13
class RecoverTokensModal extends StatefulWidget {
const RecoverTokensModal({super.key});
@override
State<RecoverTokensModal> createState() => _RecoverTokensModalState();
}
class _RecoverTokensModalState extends State<RecoverTokensModal> {
final TextEditingController _mnemonicController = TextEditingController();
bool _useCurrentMnemonic = true;
bool _scanAllMints = true;
String? _selectedMintUrl;
List<String> _availableMints = [];
bool _isLoading = false;
bool _isLoadingMints = true;
String? _result;
bool _isSuccess = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _loadMints());
}
void _loadMints() {
final walletProvider = context.read<WalletProvider>();
setState(() {
_availableMints = walletProvider.mintUrls;
_isLoadingMints = false;
if (_availableMints.isNotEmpty) {
_selectedMintUrl = _availableMints.first;
}
});
}
@override
void dispose() {
_mnemonicController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
// Icono
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: Icon(
LucideIcons.refreshCw,
color: AppColors.primaryAction,
size: 32,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Título
Text(
L10n.of(context)!.recoverTokensTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Descripción
Text(
L10n.of(context)!.recoverTokensDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.paddingLarge),
// Opciones de mnemonic
_buildMnemonicOptions(),
const SizedBox(height: AppDimensions.paddingMedium),
// Selector de mint (solo si usa mnemonic actual)
if (_useCurrentMnemonic) _buildMintSelector(),
// Input para mnemonic personalizado
if (!_useCurrentMnemonic) _buildMnemonicInput(),
// Resultado
if (_result != null) _buildResult(),
const SizedBox(height: AppDimensions.paddingMedium),
// Botón de acción
_buildActionButton(),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
),
),
);
}
Widget _buildMnemonicOptions() {
final l10n = L10n.of(context)!;
return Column(
children: [
// Opción: Usar mnemonic actual
GestureDetector(
onTap: () => setState(() => _useCurrentMnemonic = true),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _useCurrentMnemonic
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _useCurrentMnemonic
? AppColors.primaryAction.withValues(alpha: 0.5)
: Colors.transparent,
),
),
child: Row(
children: [
Icon(
_useCurrentMnemonic
? LucideIcons.checkCircle
: LucideIcons.circle,
color: _useCurrentMnemonic
? AppColors.primaryAction
: AppColors.textSecondary,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.useCurrentSeedPhrase,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
Text(
l10n.scanWithSavedWords,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 8),
// Opción: Usar otro mnemonic
GestureDetector(
onTap: () => setState(() => _useCurrentMnemonic = false),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: !_useCurrentMnemonic
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: !_useCurrentMnemonic
? AppColors.primaryAction.withValues(alpha: 0.5)
: Colors.transparent,
),
),
child: Row(
children: [
Icon(
!_useCurrentMnemonic
? LucideIcons.checkCircle
: LucideIcons.circle,
color: !_useCurrentMnemonic
? AppColors.primaryAction
: AppColors.textSecondary,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.useOtherSeedPhrase,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
Text(
l10n.recoverFromOtherWords,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
),
],
),
),
),
],
);
}
Widget _buildMintSelector() {
final l10n = L10n.of(context)!;
if (_isLoadingMints) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: AppColors.primaryAction,
strokeWidth: 2,
),
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.mintsToScan,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 8),
// Opción: Escanear todos
GestureDetector(
onTap: () => setState(() => _scanAllMints = true),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _scanAllMints
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: _scanAllMints
? AppColors.primaryAction.withValues(alpha: 0.5)
: Colors.transparent,
),
),
child: Row(
children: [
Icon(
_scanAllMints ? LucideIcons.checkCircle : LucideIcons.circle,
color: _scanAllMints
? AppColors.primaryAction
: AppColors.textSecondary,
size: 18,
),
const SizedBox(width: 10),
Text(
l10n.allMints(_availableMints.length),
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
fontWeight: _scanAllMints ? FontWeight.w500 : FontWeight.normal,
),
),
],
),
),
),
const SizedBox(height: 8),
// Opción: Mint específico
GestureDetector(
onTap: () => setState(() => _scanAllMints = false),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: !_scanAllMints
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: !_scanAllMints
? AppColors.primaryAction.withValues(alpha: 0.5)
: Colors.transparent,
),
),
child: Row(
children: [
Icon(
!_scanAllMints ? LucideIcons.checkCircle : LucideIcons.circle,
color: !_scanAllMints
? AppColors.primaryAction
: AppColors.textSecondary,
size: 18,
),
const SizedBox(width: 10),
Text(
l10n.specificMint,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
),
],
),
),
),
// Dropdown de mints (solo si selecciona específico)
if (!_scanAllMints && _availableMints.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
),
),
child: DropdownButton<String>(
value: _selectedMintUrl,
isExpanded: true,
dropdownColor: AppColors.deepVoidPurple,
underline: const SizedBox(),
icon: Icon(
LucideIcons.chevronDown,
color: AppColors.textSecondary,
size: 20,
),
items: _availableMints.map((url) {
final host = Uri.parse(url).host;
return DropdownMenuItem(
value: url,
child: Text(
host,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() => _selectedMintUrl = value);
}
},
),
),
),
],
);
}
Widget _buildMnemonicInput() {
return Padding(
padding: const EdgeInsets.only(top: AppDimensions.paddingMedium),
child: TextField(
controller: _mnemonicController,
autocorrect: false,
enableSuggestions: false,
textCapitalization: TextCapitalization.none,
maxLines: 3,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
decoration: InputDecoration(
hintText: L10n.of(context)!.enterMnemonicWords,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white.withValues(alpha: 0.3),
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.all(16),
),
),
);
}
Widget _buildResult() {
return Container(
margin: const EdgeInsets.only(top: AppDimensions.paddingMedium),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _isSuccess
? AppColors.success.withValues(alpha: 0.1)
: AppColors.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _isSuccess
? AppColors.success.withValues(alpha: 0.3)
: AppColors.error.withValues(alpha: 0.3),
),
),
child: Row(
children: [
Icon(
_isSuccess ? LucideIcons.checkCircle : LucideIcons.alertCircle,
color: _isSuccess ? AppColors.success : AppColors.error,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_result!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: _isSuccess ? AppColors.success : AppColors.error,
),
),
),
],
),
);
}
Widget _buildActionButton() {
return SizedBox(
width: double.infinity,
child: GestureDetector(
onTap: _isLoading ? null : _startRecover,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
gradient: _isLoading
? null
: const LinearGradient(
colors: [AppColors.primaryAction, Color(0xFFFF9100)],
),
color: _isLoading ? Colors.grey : null,
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(
L10n.of(context)!.scanMints,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
);
}
Future<void> _startRecover() async {
if (!mounted) return;
final l10n = L10n.of(context)!;
setState(() {
_isLoading = true;
_result = null;
});
try {
final walletProvider = context.read<WalletProvider>();
if (_useCurrentMnemonic) {
// Usar mnemonic actual
if (_scanAllMints) {
// Escanear todos los mints (retorna Map<String, Map<String, BigInt>>)
final results = await walletProvider.restoreAllMints();
int mintsRecovered = 0;
int mintsWithError = 0;
final recoveredDetails = <String>[];
for (final mintEntry in results.entries) {
final unitBalances = mintEntry.value;
bool hasError = false;
bool hasRecovered = false;
for (final unitEntry in unitBalances.entries) {
final unit = unitEntry.key;
final balance = unitEntry.value;
if (balance < BigInt.zero) {
hasError = true;
} else if (balance > BigInt.zero) {
final formatted = UnitFormatter.formatBalance(balance, unit);
final label = UnitFormatter.getUnitLabel(unit);
recoveredDetails.add('$formatted $label');
hasRecovered = true;
}
}
if (hasError) mintsWithError++;
if (hasRecovered) mintsRecovered++;
}
// Recuperar proofs pendientes (sends no reclamados, melts fallidos)
final reclaim = await walletProvider.reclaimPendingProofs();
if (!mounted) return;
// Sumar monto del reclaim a los detalles de recuperación
if (reclaim.count > BigInt.zero) {
final activeUnit = walletProvider.activeUnit;
final formatted = UnitFormatter.formatBalance(reclaim.amount, activeUnit);
final label = UnitFormatter.getUnitLabel(activeUnit);
recoveredDetails.add('$formatted $label (${reclaim.count} proofs)');
mintsRecovered++;
}
setState(() {
_isSuccess = true;
if (recoveredDetails.isNotEmpty) {
_result = l10n.recoveredTokens(recoveredDetails.join(", "), mintsRecovered);
} else {
_result = l10n.scanCompleteNoTokens;
}
if (mintsWithError > 0) {
_result = '$_result ${l10n.mintsWithError(mintsWithError)}';
}
});
} else {
// Escanear mint específico (retorna Map<String, BigInt>)
if (_selectedMintUrl == null) {
if (!mounted) return;
setState(() {
_isSuccess = false;
_result = l10n.selectMintToScan;
});
return;
}
final unitBalances = await walletProvider.restoreFromMint(_selectedMintUrl!);
final mintHost = UnitFormatter.getMintDisplayName(_selectedMintUrl!);
final recoveredDetails = <String>[];
for (final entry in unitBalances.entries) {
final unit = entry.key;
final balance = entry.value;
if (balance > BigInt.zero) {
final formatted = UnitFormatter.formatBalance(balance, unit);
final label = UnitFormatter.getUnitLabel(unit);
recoveredDetails.add('$formatted $label');
}
}
// Recuperar proofs pendientes de este mint
final reclaim = await walletProvider.reclaimPendingProofs(mintUrl: _selectedMintUrl);
if (!mounted) return;
// Sumar monto del reclaim a los detalles
if (reclaim.count > BigInt.zero) {
final activeUnit = walletProvider.activeUnit;
final formatted = UnitFormatter.formatBalance(reclaim.amount, activeUnit);
final label = UnitFormatter.getUnitLabel(activeUnit);
recoveredDetails.add('$formatted $label (${reclaim.count} proofs)');
}
setState(() {
_isSuccess = true;
if (recoveredDetails.isNotEmpty) {
_result = l10n.recoveredFromMint(recoveredDetails.join(", "), mintHost);
} else {
_result = l10n.noTokensFoundInMint(mintHost);
}
});
}
} else {
// Usar otro mnemonic
final mnemonic = _mnemonicController.text.trim().toLowerCase();
final words = mnemonic.split(RegExp(r'\s+'));
if (words.length != 12 && words.length != 24) {
if (!mounted) return;
setState(() {
_isSuccess = false;
_result = l10n.mnemonicMustHaveWords;
});
return;
}
// Obtener lista de mints actuales para escanear
final mintUrls = walletProvider.mintUrls;
if (mintUrls.isEmpty) {
if (!mounted) return;
setState(() {
_isSuccess = false;
_result = l10n.noConnectedMintsToScan;
});
return;
}
final recovered = await walletProvider.restoreWithMnemonic(
mnemonic,
mintUrls,
);
if (!mounted) return;
setState(() {
_isSuccess = true;
if (recovered > BigInt.zero) {
// Usamos la unidad activa como aproximación para el formato
final activeUnit = walletProvider.activeUnit;
final formatted = UnitFormatter.formatBalance(recovered, activeUnit);
final label = UnitFormatter.getUnitLabel(activeUnit);
_result = l10n.recoveredAndTransferred(formatted, label);
} else {
_result = l10n.noTokensForMnemonic;
}
});
}
} catch (e) {
if (!mounted) return;
setState(() {
_isSuccess = false;
_result = '${l10n.error}: $e';
});
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -14,6 +14,8 @@ import '../../core/utils/formatters.dart';
import '../../data/transaction_meta_storage.dart';
import '../../data/pending_token.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/p2pk_provider.dart';
import '../../core/utils/p2pk_utils.dart';
import '../../widgets/common/gradient_background.dart';
/// Filtros disponibles para el historial
@@ -222,9 +224,16 @@ class _HistoryScreenState extends State<HistoryScreen> {
Future<void> _claimPendingToken(PendingToken token) async {
final l10n = L10n.of(context)!;
final walletProvider = context.read<WalletProvider>();
final p2pkProvider = context.read<P2PKProvider>();
try {
final amount = await walletProvider.claimPendingToken(token.id);
// Detectar P2PK y obtener clave privada si es necesario
String? p2pkKey;
if (P2PKUtils.isP2PKLocked(token.encoded)) {
p2pkKey = p2pkProvider.getPrivateKeyForToken(token.encoded);
}
final amount = await walletProvider.claimPendingToken(token.id, p2pkPrivateKey: p2pkKey);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
+251
View File
@@ -0,0 +1,251 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'error.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `extract_creq_from_uri`, `parse_payment_request_inner`, `percent_decode`, `wait_for_nostr_payment_inner`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<CreatedPaymentRequest>>
abstract class CreatedPaymentRequest implements RustOpaqueInterface {
String get creqA;
String get creqB;
NostrListenerHandle get listenerHandle;
set creqA(String creqA);
set creqB(String creqB);
set listenerHandle(NostrListenerHandle listenerHandle);
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<NostrListenerHandle>>
abstract class NostrListenerHandle implements RustOpaqueInterface {
/// Reconstruct a handle from persisted data (e.g. after app restart).
static NostrListenerHandle fromPersisted({
required PersistedRequestData data,
}) => RustLib.instance.api
.crateApiPaymentRequestNostrListenerHandleFromPersisted(data: data);
/// Export handle data for persistence (e.g. SharedPreferences).
/// The secret key is exposed as hex — the caller is responsible
/// for storing it securely.
PersistedRequestData toPersisted();
}
/// Parameters for creating a payment request.
class CreateRequestParams {
/// Amount to request (in smallest unit, e.g. sats)
final BigInt? amount;
/// Currency unit ("sat", "usd", etc.)
final String unit;
/// Human-readable description
final String? description;
/// Nostr relay URLs for the transport
final List<String> nostrRelays;
const CreateRequestParams({
this.amount,
required this.unit,
this.description,
required this.nostrRelays,
});
@override
int get hashCode =>
amount.hashCode ^
unit.hashCode ^
description.hashCode ^
nostrRelays.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CreateRequestParams &&
runtimeType == other.runtimeType &&
amount == other.amount &&
unit == other.unit &&
description == other.description &&
nostrRelays == other.nostrRelays;
}
/// Event emitted by the Nostr payment listener.
class NostrPaymentEvent {
final NostrPaymentState state;
final BigInt? amount;
final String? error;
const NostrPaymentEvent({required this.state, this.amount, this.error});
@override
int get hashCode => state.hashCode ^ amount.hashCode ^ error.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NostrPaymentEvent &&
runtimeType == other.runtimeType &&
state == other.state &&
amount == other.amount &&
error == other.error;
}
/// State of a Nostr payment listener.
enum NostrPaymentState {
/// Connected to relays, waiting for payment
waiting,
/// Payment received and tokens claimed
received,
/// Error occurred
error,
}
/// Parsed payment request info exposed to Flutter.
/// CDK's FromStr auto-detects creqA (CBOR) vs creqB (Bech32m).
class PaymentRequestInfo {
/// Raw encoded string (creqA... or CREQB1...)
final String raw;
/// Payment id
final String? paymentId;
/// Requested amount (in base unit)
final BigInt? amount;
/// Currency unit ("sat", "usd", etc.)
final String? unit;
/// Whether this is a single-use request
final bool? singleUse;
/// Accepted mints (empty = any mint)
final List<String> mints;
/// Human-readable description
final String? description;
/// Available transports
final List<TransportInfo> transports;
/// Whether NUT-10 spending conditions are required
final bool hasNut10;
const PaymentRequestInfo({
required this.raw,
this.paymentId,
this.amount,
this.unit,
this.singleUse,
required this.mints,
this.description,
required this.transports,
required this.hasNut10,
});
/// Parse a payment request string (creqA, CREQB1, or bitcoin:?creq=).
/// Supports NUT-18 (CBOR+base64) and NUT-26 (Bech32m) automatically.
static PaymentRequestInfo parse({required String encoded}) => RustLib
.instance
.api
.crateApiPaymentRequestPaymentRequestInfoParse(encoded: encoded);
@override
int get hashCode =>
raw.hashCode ^
paymentId.hashCode ^
amount.hashCode ^
unit.hashCode ^
singleUse.hashCode ^
mints.hashCode ^
description.hashCode ^
transports.hashCode ^
hasNut10.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PaymentRequestInfo &&
runtimeType == other.runtimeType &&
raw == other.raw &&
paymentId == other.paymentId &&
amount == other.amount &&
unit == other.unit &&
singleUse == other.singleUse &&
mints == other.mints &&
description == other.description &&
transports == other.transports &&
hasNut10 == other.hasNut10;
}
/// Serializable data for persisting a NostrListenerHandle across app restarts.
class PersistedRequestData {
final String secretHex;
final String pubkeyHex;
final List<String> relays;
final BigInt? amount;
final String unit;
final String mintUrl;
const PersistedRequestData({
required this.secretHex,
required this.pubkeyHex,
required this.relays,
this.amount,
required this.unit,
required this.mintUrl,
});
@override
int get hashCode =>
secretHex.hashCode ^
pubkeyHex.hashCode ^
relays.hashCode ^
amount.hashCode ^
unit.hashCode ^
mintUrl.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PersistedRequestData &&
runtimeType == other.runtimeType &&
secretHex == other.secretHex &&
pubkeyHex == other.pubkeyHex &&
relays == other.relays &&
amount == other.amount &&
unit == other.unit &&
mintUrl == other.mintUrl;
}
/// Transport info exposed to Flutter
class TransportInfo {
/// "nostr" or "post"
final String transportType;
/// Target (nprofile or URL)
final String target;
const TransportInfo({required this.transportType, required this.target});
@override
int get hashCode => transportType.hashCode ^ target.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TransportInfo &&
runtimeType == other.runtimeType &&
transportType == other.transportType &&
target == other.target;
}
+15
View File
@@ -10,6 +10,21 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `proofs`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `fmt`, `from_str`, `try_from`, `try_into`, `try_into`
/// Create a token from raw proof data without network access.
/// Uses CDK's CdkToken::new to produce a valid V4 (cashuB) token.
/// `proofs_json` is a JSON array of proofs: [{"id":"...","amount":1,"secret":"...","C":"..."}, ...]
Token createOfflineToken({
required String mintUrl,
required String proofsJson,
String? memo,
String? unit,
}) => RustLib.instance.api.crateApiTokenCreateOfflineToken(
mintUrl: mintUrl,
proofsJson: proofsJson,
memo: memo,
unit: unit,
);
List<String> encodeQrToken({required Token token, BigInt? maxFragmentLength}) =>
RustLib.instance.api.crateApiTokenEncodeQrToken(
token: token,
+107 -5
View File
@@ -7,11 +7,31 @@ import '../frb_generated.dart';
import 'error.dart';
import 'mint_info.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'payment_request.dart';
import 'token.dart';
// These functions are ignored because they are not marked as `pub`: `mint_url`, `unit`, `update_balance_streams`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `clone`, `clone`, `clone`, `clone`, `cmp`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `into`, `partial_cmp`, `try_into`, `try_into`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PreparedMelt>>
abstract class PreparedMelt implements RustOpaqueInterface {
BigInt get amount;
BigInt get feeReserve;
BigInt get inputFee;
BigInt get swapFee;
set amount(BigInt amount);
set feeReserve(BigInt feeReserve);
set inputFee(BigInt inputFee);
set swapFee(BigInt swapFee);
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PreparedSend>>
abstract class PreparedSend implements RustOpaqueInterface {
BigInt get amount;
@@ -43,12 +63,25 @@ abstract class Wallet implements RustOpaqueInterface {
Future<BigInt> balance();
Future<void> cancelMelt({required PreparedMelt melt});
Future<void> cancelSend({required PreparedSend send});
Future<void> checkAllMintQuotes();
Future<void> checkPendingTransactions();
Future<BigInt> confirmMelt({required PreparedMelt melt});
/// Create a NUT-18 payment request with Nostr transport.
///
/// Builds a PaymentRequest with the wallet's mint URL and unit,
/// generates ephemeral Nostr keys, and returns both creqA and creqB
/// encodings plus the keys needed for the Nostr listener.
Future<CreatedPaymentRequest> createPaymentRequest({
required CreateRequestParams params,
});
Future<void> finalizePendingMelts();
Future<MintInfo?> getMint();
@@ -57,8 +90,6 @@ abstract class Wallet implements RustOpaqueInterface {
Future<List<Transaction>> listTransactions({TransactionDirection? direction});
Future<BigInt> melt({required MeltQuote quote});
Future<MeltQuote> meltQuote({required String request});
Stream<MintQuote> mint({required BigInt amount, String? description});
@@ -77,21 +108,45 @@ abstract class Wallet implements RustOpaqueInterface {
db: db,
);
/// Pay a NUT-18 payment request.
/// Uses CDK's pay_request() which handles:
/// - NUT-10 spending conditions
/// - Transport selection (Nostr preferred, HTTP POST fallback)
/// - Token preparation and delivery
Future<void> payPaymentRequest({
required String encoded,
BigInt? customAmount,
});
Future<PreparedMelt> prepareMelt({required MeltQuote quote});
Future<PreparedSend> prepareSend({required BigInt amount, SendOptions? opts});
Future<BigInt> receive({required Token token, ReceiveOptions? opts});
/// Check pending-spent proofs with the mint and revert unspent ones.
/// Returns count and total amount of proofs recovered.
Future<ReclaimResult> reclaimPendingProofs();
Future<void> recoverIncompleteSagas();
Future<void> restore();
Future<Token> send({
Future<SendResult> send({
required PreparedSend send,
String? memo,
bool? includeMemo,
});
Stream<BigInt> streamBalance();
/// Wait for an incoming Nostr payment (NIP-17 gift-wrap).
///
/// Takes the opaque NostrListenerHandle returned by create_payment_request().
/// The secret key never leaves Rust memory.
Stream<NostrPaymentEvent> waitForNostrPayment({
required NostrListenerHandle handle,
});
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<WalletDatabase>>
@@ -150,6 +205,9 @@ class MintQuote {
final Token? token;
final String? error;
/// Deterministic transaction ID (set when proofs are available, typically on Issued)
final String? transactionId;
const MintQuote({
required this.id,
required this.request,
@@ -158,6 +216,7 @@ class MintQuote {
required this.state,
this.token,
this.error,
this.transactionId,
});
@override
@@ -168,7 +227,8 @@ class MintQuote {
expiry.hashCode ^
state.hashCode ^
token.hashCode ^
error.hashCode;
error.hashCode ^
transactionId.hashCode;
@override
bool operator ==(Object other) =>
@@ -181,7 +241,8 @@ class MintQuote {
expiry == other.expiry &&
state == other.state &&
token == other.token &&
error == other.error;
error == other.error &&
transactionId == other.transactionId;
}
enum MintQuoteState { unpaid, paid, issued, error }
@@ -207,6 +268,24 @@ class ReceiveOptions {
preimages == other.preimages;
}
class ReclaimResult {
final BigInt count;
final BigInt amount;
const ReclaimResult({required this.count, required this.amount});
@override
int get hashCode => count.hashCode ^ amount.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ReclaimResult &&
runtimeType == other.runtimeType &&
count == other.count &&
amount == other.amount;
}
class SendOptions {
final String? pubkey;
final bool? includeFee;
@@ -228,6 +307,29 @@ class SendOptions {
includeFee == other.includeFee;
}
/// Result of a confirmed send, carrying both the ecash token and the
/// deterministic transaction ID so Dart can save metadata without a racy
/// listTransactions lookup.
class SendResult {
final Token token;
/// Deterministic transaction ID (None if computation failed)
final String? transactionId;
const SendResult({required this.token, this.transactionId});
@override
int get hashCode => token.hashCode ^ transactionId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SendResult &&
runtimeType == other.runtimeType &&
token == other.token &&
transactionId == other.transactionId;
}
class Transaction {
final String id;
final String mintUrl;
File diff suppressed because it is too large Load Diff
+479
View File
@@ -6,6 +6,7 @@
import 'api/error.dart';
import 'api/keys.dart';
import 'api/mint_info.dart';
import 'api/payment_request.dart';
import 'api/token.dart';
import 'api/wallet.dart';
import 'dart:async';
@@ -22,6 +23,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
required super.portManager,
});
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_CreatedPaymentRequestPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_NostrListenerHandlePtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_PreparedMeltPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_PreparedSendPtr => wire
._rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSendPtr;
@@ -41,6 +54,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
CreatedPaymentRequest
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedMelt
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -65,6 +96,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
CreatedPaymentRequest
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
PreparedMelt
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -83,6 +126,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
CreatedPaymentRequest
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedMelt
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -110,6 +171,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Map<String, String> dco_decode_Map_String_String_None(dynamic raw);
@protected
CreatedPaymentRequest
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedMelt
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
dynamic raw,
);
@protected
PreparedSend
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -137,6 +216,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustStreamSink<MintQuote> dco_decode_StreamSink_mint_quote_Sse(dynamic raw);
@protected
RustStreamSink<NostrPaymentEvent>
dco_decode_StreamSink_nostr_payment_event_Sse(dynamic raw);
@protected
RustStreamSink<BigInt> dco_decode_StreamSink_u_64_Sse(dynamic raw);
@@ -149,6 +232,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool dco_decode_box_autoadd_bool(dynamic raw);
@protected
CreateRequestParams dco_decode_box_autoadd_create_request_params(dynamic raw);
@protected
MeltQuote dco_decode_box_autoadd_melt_quote(dynamic raw);
@@ -158,6 +244,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion dco_decode_box_autoadd_mint_version(dynamic raw);
@protected
PersistedRequestData dco_decode_box_autoadd_persisted_request_data(
dynamic raw,
);
@protected
ReceiveOptions dco_decode_box_autoadd_receive_options(dynamic raw);
@@ -181,6 +272,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ContactInfo dco_decode_contact_info(dynamic raw);
@protected
CreateRequestParams dco_decode_create_request_params(dynamic raw);
@protected
Error dco_decode_error(dynamic raw);
@@ -217,6 +311,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<Transaction> dco_decode_list_transaction(dynamic raw);
@protected
List<TransportInfo> dco_decode_list_transport_info(dynamic raw);
@protected
MeltMethodSettings dco_decode_melt_method_settings(dynamic raw);
@@ -238,6 +335,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion dco_decode_mint_version(dynamic raw);
@protected
NostrPaymentEvent dco_decode_nostr_payment_event(dynamic raw);
@protected
NostrPaymentState dco_decode_nostr_payment_state(dynamic raw);
@protected
Nut04Settings dco_decode_nut_04_settings(dynamic raw);
@@ -288,15 +391,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
PaymentRequestInfo dco_decode_payment_request_info(dynamic raw);
@protected
PersistedRequestData dco_decode_persisted_request_data(dynamic raw);
@protected
ReceiveOptions dco_decode_receive_options(dynamic raw);
@protected
ReclaimResult dco_decode_reclaim_result(dynamic raw);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
SendOptions dco_decode_send_options(dynamic raw);
@protected
SendResult dco_decode_send_result(dynamic raw);
@protected
SupportedSettings dco_decode_supported_settings(dynamic raw);
@@ -312,6 +427,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
TransactionStatus dco_decode_transaction_status(dynamic raw);
@protected
TransportInfo dco_decode_transport_info(dynamic raw);
@protected
BigInt dco_decode_u_64(dynamic raw);
@@ -327,6 +445,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
CreatedPaymentRequest
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedMelt
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -351,6 +487,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
PreparedMelt
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -369,6 +517,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedMelt
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -398,6 +564,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedMelt
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -427,6 +611,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
RustStreamSink<NostrPaymentEvent>
sse_decode_StreamSink_nostr_payment_event_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<BigInt> sse_decode_StreamSink_u_64_Sse(
SseDeserializer deserializer,
@@ -441,6 +629,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool sse_decode_box_autoadd_bool(SseDeserializer deserializer);
@protected
CreateRequestParams sse_decode_box_autoadd_create_request_params(
SseDeserializer deserializer,
);
@protected
MeltQuote sse_decode_box_autoadd_melt_quote(SseDeserializer deserializer);
@@ -450,6 +643,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion sse_decode_box_autoadd_mint_version(SseDeserializer deserializer);
@protected
PersistedRequestData sse_decode_box_autoadd_persisted_request_data(
SseDeserializer deserializer,
);
@protected
ReceiveOptions sse_decode_box_autoadd_receive_options(
SseDeserializer deserializer,
@@ -475,6 +673,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ContactInfo sse_decode_contact_info(SseDeserializer deserializer);
@protected
CreateRequestParams sse_decode_create_request_params(
SseDeserializer deserializer,
);
@protected
Error sse_decode_error(SseDeserializer deserializer);
@@ -517,6 +720,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<Transaction> sse_decode_list_transaction(SseDeserializer deserializer);
@protected
List<TransportInfo> sse_decode_list_transport_info(
SseDeserializer deserializer,
);
@protected
MeltMethodSettings sse_decode_melt_method_settings(
SseDeserializer deserializer,
@@ -542,6 +750,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion sse_decode_mint_version(SseDeserializer deserializer);
@protected
NostrPaymentEvent sse_decode_nostr_payment_event(
SseDeserializer deserializer,
);
@protected
NostrPaymentState sse_decode_nostr_payment_state(
SseDeserializer deserializer,
);
@protected
Nut04Settings sse_decode_nut_04_settings(SseDeserializer deserializer);
@@ -600,9 +818,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
PaymentRequestInfo sse_decode_payment_request_info(
SseDeserializer deserializer,
);
@protected
PersistedRequestData sse_decode_persisted_request_data(
SseDeserializer deserializer,
);
@protected
ReceiveOptions sse_decode_receive_options(SseDeserializer deserializer);
@protected
ReclaimResult sse_decode_reclaim_result(SseDeserializer deserializer);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
@@ -611,6 +842,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
SendOptions sse_decode_send_options(SseDeserializer deserializer);
@protected
SendResult sse_decode_send_result(SseDeserializer deserializer);
@protected
SupportedSettings sse_decode_supported_settings(SseDeserializer deserializer);
@@ -628,6 +862,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
TransactionStatus sse_decode_transaction_status(SseDeserializer deserializer);
@protected
TransportInfo sse_decode_transport_info(SseDeserializer deserializer);
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer);
@@ -646,6 +883,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
PreparedMelt self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -674,6 +932,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
PreparedMelt self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -695,6 +967,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
PreparedMelt self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -729,6 +1022,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
PreparedMelt self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -763,6 +1077,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_nostr_payment_event_Sse(
RustStreamSink<NostrPaymentEvent> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_u_64_Sse(
RustStreamSink<BigInt> self,
@@ -778,6 +1098,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_create_request_params(
CreateRequestParams self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_melt_quote(
MeltQuote self,
@@ -796,6 +1122,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_persisted_request_data(
PersistedRequestData self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_receive_options(
ReceiveOptions self,
@@ -826,6 +1158,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_contact_info(ContactInfo self, SseSerializer serializer);
@protected
void sse_encode_create_request_params(
CreateRequestParams self,
SseSerializer serializer,
);
@protected
void sse_encode_error(Error self, SseSerializer serializer);
@@ -883,6 +1221,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_transport_info(
List<TransportInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_melt_method_settings(
MeltMethodSettings self,
@@ -913,6 +1257,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_mint_version(MintVersion self, SseSerializer serializer);
@protected
void sse_encode_nostr_payment_event(
NostrPaymentEvent self,
SseSerializer serializer,
);
@protected
void sse_encode_nostr_payment_state(
NostrPaymentState self,
SseSerializer serializer,
);
@protected
void sse_encode_nut_04_settings(Nut04Settings self, SseSerializer serializer);
@@ -982,12 +1338,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_payment_request_info(
PaymentRequestInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_persisted_request_data(
PersistedRequestData self,
SseSerializer serializer,
);
@protected
void sse_encode_receive_options(
ReceiveOptions self,
SseSerializer serializer,
);
@protected
void sse_encode_reclaim_result(ReclaimResult self, SseSerializer serializer);
@protected
void sse_encode_record_string_string(
(String, String) self,
@@ -997,6 +1368,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_send_options(SendOptions self, SseSerializer serializer);
@protected
void sse_encode_send_result(SendResult self, SseSerializer serializer);
@protected
void sse_encode_supported_settings(
SupportedSettings self,
@@ -1021,6 +1395,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_transport_info(TransportInfo self, SseSerializer serializer);
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer);
@@ -1048,6 +1425,108 @@ class RustLibWire implements BaseWire {
RustLibWire(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest',
);
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest',
);
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequestPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle',
);
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle',
);
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandlePtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
ptr,
);
}
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt',
);
late final _rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt =
_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
ffi.Pointer<ffi.Void> ptr,
) {
return _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
ptr,
);
}
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Void>)>>(
'frbgen_elcaju_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt',
);
late final _rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt =
_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMeltPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>)>();
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
ffi.Pointer<ffi.Void> ptr,
+455
View File
@@ -9,6 +9,7 @@
import 'api/error.dart';
import 'api/keys.dart';
import 'api/mint_info.dart';
import 'api/payment_request.dart';
import 'api/token.dart';
import 'api/wallet.dart';
import 'dart:async';
@@ -24,6 +25,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
required super.portManager,
});
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_CreatedPaymentRequestPtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_NostrListenerHandlePtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_PreparedMeltPtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt;
CrossPlatformFinalizerArg
get rust_arc_decrement_strong_count_PreparedSendPtr => wire
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend;
@@ -43,6 +56,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnyhowException dco_decode_AnyhowException(dynamic raw);
@protected
CreatedPaymentRequest
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedMelt
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -67,6 +98,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
CreatedPaymentRequest
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
PreparedMelt
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -85,6 +128,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
dynamic raw,
);
@protected
CreatedPaymentRequest
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedMelt
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
dynamic raw,
);
@protected
PreparedSend
dco_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -112,6 +173,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Map<String, String> dco_decode_Map_String_String_None(dynamic raw);
@protected
CreatedPaymentRequest
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
dynamic raw,
);
@protected
NostrListenerHandle
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
dynamic raw,
);
@protected
PreparedMelt
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
dynamic raw,
);
@protected
PreparedSend
dco_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -139,6 +218,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
RustStreamSink<MintQuote> dco_decode_StreamSink_mint_quote_Sse(dynamic raw);
@protected
RustStreamSink<NostrPaymentEvent>
dco_decode_StreamSink_nostr_payment_event_Sse(dynamic raw);
@protected
RustStreamSink<BigInt> dco_decode_StreamSink_u_64_Sse(dynamic raw);
@@ -151,6 +234,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool dco_decode_box_autoadd_bool(dynamic raw);
@protected
CreateRequestParams dco_decode_box_autoadd_create_request_params(dynamic raw);
@protected
MeltQuote dco_decode_box_autoadd_melt_quote(dynamic raw);
@@ -160,6 +246,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion dco_decode_box_autoadd_mint_version(dynamic raw);
@protected
PersistedRequestData dco_decode_box_autoadd_persisted_request_data(
dynamic raw,
);
@protected
ReceiveOptions dco_decode_box_autoadd_receive_options(dynamic raw);
@@ -183,6 +274,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ContactInfo dco_decode_contact_info(dynamic raw);
@protected
CreateRequestParams dco_decode_create_request_params(dynamic raw);
@protected
Error dco_decode_error(dynamic raw);
@@ -219,6 +313,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<Transaction> dco_decode_list_transaction(dynamic raw);
@protected
List<TransportInfo> dco_decode_list_transport_info(dynamic raw);
@protected
MeltMethodSettings dco_decode_melt_method_settings(dynamic raw);
@@ -240,6 +337,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion dco_decode_mint_version(dynamic raw);
@protected
NostrPaymentEvent dco_decode_nostr_payment_event(dynamic raw);
@protected
NostrPaymentState dco_decode_nostr_payment_state(dynamic raw);
@protected
Nut04Settings dco_decode_nut_04_settings(dynamic raw);
@@ -290,15 +393,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw);
@protected
PaymentRequestInfo dco_decode_payment_request_info(dynamic raw);
@protected
PersistedRequestData dco_decode_persisted_request_data(dynamic raw);
@protected
ReceiveOptions dco_decode_receive_options(dynamic raw);
@protected
ReclaimResult dco_decode_reclaim_result(dynamic raw);
@protected
(String, String) dco_decode_record_string_string(dynamic raw);
@protected
SendOptions dco_decode_send_options(dynamic raw);
@protected
SendResult dco_decode_send_result(dynamic raw);
@protected
SupportedSettings dco_decode_supported_settings(dynamic raw);
@@ -314,6 +429,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
TransactionStatus dco_decode_transaction_status(dynamic raw);
@protected
TransportInfo dco_decode_transport_info(dynamic raw);
@protected
BigInt dco_decode_u_64(dynamic raw);
@@ -329,6 +447,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer);
@protected
CreatedPaymentRequest
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedMelt
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -353,6 +489,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
PreparedMelt
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -371,6 +519,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedMelt
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -400,6 +566,24 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
CreatedPaymentRequest
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
SseDeserializer deserializer,
);
@protected
NostrListenerHandle
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
SseDeserializer deserializer,
);
@protected
PreparedMelt
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
SseDeserializer deserializer,
);
@protected
PreparedSend
sse_decode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -429,6 +613,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseDeserializer deserializer,
);
@protected
RustStreamSink<NostrPaymentEvent>
sse_decode_StreamSink_nostr_payment_event_Sse(SseDeserializer deserializer);
@protected
RustStreamSink<BigInt> sse_decode_StreamSink_u_64_Sse(
SseDeserializer deserializer,
@@ -443,6 +631,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
bool sse_decode_box_autoadd_bool(SseDeserializer deserializer);
@protected
CreateRequestParams sse_decode_box_autoadd_create_request_params(
SseDeserializer deserializer,
);
@protected
MeltQuote sse_decode_box_autoadd_melt_quote(SseDeserializer deserializer);
@@ -452,6 +645,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion sse_decode_box_autoadd_mint_version(SseDeserializer deserializer);
@protected
PersistedRequestData sse_decode_box_autoadd_persisted_request_data(
SseDeserializer deserializer,
);
@protected
ReceiveOptions sse_decode_box_autoadd_receive_options(
SseDeserializer deserializer,
@@ -477,6 +675,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ContactInfo sse_decode_contact_info(SseDeserializer deserializer);
@protected
CreateRequestParams sse_decode_create_request_params(
SseDeserializer deserializer,
);
@protected
Error sse_decode_error(SseDeserializer deserializer);
@@ -519,6 +722,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
List<Transaction> sse_decode_list_transaction(SseDeserializer deserializer);
@protected
List<TransportInfo> sse_decode_list_transport_info(
SseDeserializer deserializer,
);
@protected
MeltMethodSettings sse_decode_melt_method_settings(
SseDeserializer deserializer,
@@ -544,6 +752,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
MintVersion sse_decode_mint_version(SseDeserializer deserializer);
@protected
NostrPaymentEvent sse_decode_nostr_payment_event(
SseDeserializer deserializer,
);
@protected
NostrPaymentState sse_decode_nostr_payment_state(
SseDeserializer deserializer,
);
@protected
Nut04Settings sse_decode_nut_04_settings(SseDeserializer deserializer);
@@ -602,9 +820,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer);
@protected
PaymentRequestInfo sse_decode_payment_request_info(
SseDeserializer deserializer,
);
@protected
PersistedRequestData sse_decode_persisted_request_data(
SseDeserializer deserializer,
);
@protected
ReceiveOptions sse_decode_receive_options(SseDeserializer deserializer);
@protected
ReclaimResult sse_decode_reclaim_result(SseDeserializer deserializer);
@protected
(String, String) sse_decode_record_string_string(
SseDeserializer deserializer,
@@ -613,6 +844,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
SendOptions sse_decode_send_options(SseDeserializer deserializer);
@protected
SendResult sse_decode_send_result(SseDeserializer deserializer);
@protected
SupportedSettings sse_decode_supported_settings(SseDeserializer deserializer);
@@ -630,6 +864,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
TransactionStatus sse_decode_transaction_status(SseDeserializer deserializer);
@protected
TransportInfo sse_decode_transport_info(SseDeserializer deserializer);
@protected
BigInt sse_decode_u_64(SseDeserializer deserializer);
@@ -648,6 +885,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
PreparedMelt self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -676,6 +934,20 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
PreparedMelt self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -697,6 +969,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
PreparedMelt self,
SseSerializer serializer,
);
@protected
void
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -731,6 +1024,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
CreatedPaymentRequest self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
NostrListenerHandle self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
PreparedMelt self,
SseSerializer serializer,
);
@protected
void
sse_encode_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
@@ -765,6 +1079,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_nostr_payment_event_Sse(
RustStreamSink<NostrPaymentEvent> self,
SseSerializer serializer,
);
@protected
void sse_encode_StreamSink_u_64_Sse(
RustStreamSink<BigInt> self,
@@ -780,6 +1100,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_box_autoadd_bool(bool self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_create_request_params(
CreateRequestParams self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_melt_quote(
MeltQuote self,
@@ -798,6 +1124,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_persisted_request_data(
PersistedRequestData self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_receive_options(
ReceiveOptions self,
@@ -828,6 +1160,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_contact_info(ContactInfo self, SseSerializer serializer);
@protected
void sse_encode_create_request_params(
CreateRequestParams self,
SseSerializer serializer,
);
@protected
void sse_encode_error(Error self, SseSerializer serializer);
@@ -885,6 +1223,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_list_transport_info(
List<TransportInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_melt_method_settings(
MeltMethodSettings self,
@@ -915,6 +1259,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_mint_version(MintVersion self, SseSerializer serializer);
@protected
void sse_encode_nostr_payment_event(
NostrPaymentEvent self,
SseSerializer serializer,
);
@protected
void sse_encode_nostr_payment_state(
NostrPaymentState self,
SseSerializer serializer,
);
@protected
void sse_encode_nut_04_settings(Nut04Settings self, SseSerializer serializer);
@@ -984,12 +1340,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_payment_request_info(
PaymentRequestInfo self,
SseSerializer serializer,
);
@protected
void sse_encode_persisted_request_data(
PersistedRequestData self,
SseSerializer serializer,
);
@protected
void sse_encode_receive_options(
ReceiveOptions self,
SseSerializer serializer,
);
@protected
void sse_encode_reclaim_result(ReclaimResult self, SseSerializer serializer);
@protected
void sse_encode_record_string_string(
(String, String) self,
@@ -999,6 +1370,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_send_options(SendOptions self, SseSerializer serializer);
@protected
void sse_encode_send_result(SendResult self, SseSerializer serializer);
@protected
void sse_encode_supported_settings(
SupportedSettings self,
@@ -1023,6 +1397,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_transport_info(TransportInfo self, SseSerializer serializer);
@protected
void sse_encode_u_64(BigInt self, SseSerializer serializer);
@@ -1041,6 +1418,54 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
class RustLibWire implements BaseWire {
RustLibWire.fromExternalLibrary(ExternalLibrary lib);
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
int ptr,
) => wasmModule
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ptr,
);
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
int ptr,
) => wasmModule
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
ptr,
);
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
int ptr,
) => wasmModule
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ptr,
);
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
int ptr,
) => wasmModule
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
ptr,
);
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
int ptr,
) => wasmModule
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
ptr,
);
void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
int ptr,
) => wasmModule
.rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
ptr,
);
void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
int ptr,
@@ -1112,6 +1537,36 @@ external RustLibWasmModule get wasmModule;
@JS()
@anonymous
extension type RustLibWasmModule._(JSObject _) implements JSObject {
external void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
int ptr,
);
external void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerCreatedPaymentRequest(
int ptr,
);
external void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
int ptr,
);
external void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerNostrListenerHandle(
int ptr,
);
external void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
int ptr,
);
external void
rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedMelt(
int ptr,
);
external void
rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerPreparedSend(
int ptr,
+1 -1
View File
@@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
set(BINARY_NAME "elcaju")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.elcaju")
set(APPLICATION_ID "me.elcaju")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
+3 -3
View File
@@ -385,7 +385,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/elcaju.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/elcaju";
@@ -399,7 +399,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/elcaju.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/elcaju";
@@ -413,7 +413,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/elcaju.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/elcaju";
+2 -2
View File
@@ -8,7 +8,7 @@
PRODUCT_NAME = elcaju
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved.
PRODUCT_COPYRIGHT = Copyright © 2026 ElCaju. All rights reserved.
+1 -1
View File
@@ -1,7 +1,7 @@
name: elcaju
description: "ElCaju - Tu wallet de ecash privado"
publish_to: 'none'
version: 0.1.0+2
version: 0.2.0+3
environment:
sdk: ^3.11.0
+393 -17
View File
@@ -17,6 +17,27 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -184,12 +205,49 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "async-utility"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34a3b57207a7a1007832416c3e4862378c8451b4e8e093e436f48c2d3d2c151"
dependencies = [
"futures-util",
"gloo-timers",
"tokio",
"wasm-bindgen-futures",
]
[[package]]
name = "async-wsocket"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069"
dependencies = [
"async-utility",
"futures",
"futures-util",
"js-sys",
"tokio",
"tokio-rustls",
"tokio-socks",
"tokio-tungstenite",
"url",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "atomic-destructor"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4"
[[package]]
name = "atomic-waker"
version = "1.1.2"
@@ -239,6 +297,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bc-ur"
version = "0.12.0"
@@ -303,6 +367,17 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953"
[[package]]
name = "bitcoin-payment-instructions"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c300c948b2ff78c965ea3a613372352125448a22f1acf49e95e3878149824091"
dependencies = [
"bitcoin",
"lightning",
"lightning-invoice",
]
[[package]]
name = "bitcoin-private"
version = "0.1.0"
@@ -354,6 +429,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "brotli"
version = "8.0.2"
@@ -416,15 +500,16 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cashu"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d612261d61069314bb0e39adbd63d0fd3bd22084f332e85c98e615c15f24e5a"
checksum = "26b4ad04070fb67914feb2641e7b54001000f1f7f67a7c39a08f32edf01fd3ae"
dependencies = [
"bitcoin",
"cbor-diag",
"ciborium",
"lightning",
"lightning-invoice",
"nostr-sdk",
"once_cell",
"serde",
"serde_json",
@@ -433,12 +518,22 @@ dependencies = [
"strum_macros",
"thiserror 2.0.18",
"tracing",
"unicode-normalization",
"url",
"uuid",
"web-time",
"zeroize",
]
[[package]]
name = "cbc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
dependencies = [
"cipher",
]
[[package]]
name = "cbor-diag"
version = "0.1.12"
@@ -472,14 +567,15 @@ dependencies = [
[[package]]
name = "cdk"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8955437beea95ee6849c6eab59012760a7589e6eeff4d6e36ebf324c641388d"
checksum = "c6536eb1df7a497cb334edf6bb9eea1fa8e4a9b08c79fe45c40d43413ecb88c8"
dependencies = [
"anyhow",
"arc-swap",
"async-trait",
"bitcoin",
"bitcoin-payment-instructions",
"cbor-diag",
"cdk-common",
"cdk-signatory",
@@ -490,6 +586,7 @@ dependencies = [
"jsonwebtoken",
"lightning",
"lightning-invoice",
"nostr-sdk",
"regex",
"ring",
"rustls",
@@ -498,7 +595,6 @@ dependencies = [
"serde_with",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite",
"tokio-util",
"tracing",
"url",
@@ -509,9 +605,9 @@ dependencies = [
[[package]]
name = "cdk-common"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f5bf771d3780d2321211eb575e1f8341cec50f8a16c4f50b2787807b5090a38"
checksum = "a371d218ffe83dcabd202278251af53744b8fa3e846124ddcd34247c4e38bdc3"
dependencies = [
"anyhow",
"async-trait",
@@ -522,6 +618,7 @@ dependencies = [
"ciborium",
"futures",
"getrandom 0.2.17",
"jsonwebtoken",
"lightning",
"lightning-invoice",
"parking_lot",
@@ -542,23 +639,31 @@ dependencies = [
[[package]]
name = "cdk-http-client"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58f47c4af4a4c862d145863211f5454c3ccfbacaaef9e4913276519a1ac498e"
checksum = "bf194792d45975b360911417d3713e4d3f0142e18b082e2a8c2c88a91630b43d"
dependencies = [
"futures",
"futures-channel",
"js-sys",
"regex",
"reqwest",
"serde",
"serde_json",
"thiserror 2.0.18",
"tokio-tungstenite",
"tracing",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "cdk-signatory"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1dca3b84999ac0e6fd15330d566aff6df6082a349134fd884deef8c9eeb570"
checksum = "4f40745c9402049bbba47ae52e1fc7552d4af8e99572fd73697448f51bbb3a02"
dependencies = [
"anyhow",
"async-trait",
@@ -578,9 +683,9 @@ dependencies = [
[[package]]
name = "cdk-sql-common"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6625a8248a43fbebb92cc57f1b09c453d5de94d62a512b62e9a62b25d0dd5506"
checksum = "36e920cffaa36f396d16727649b05d6dc8d42aecc4108fa955876515149bdb0e"
dependencies = [
"async-trait",
"bitcoin",
@@ -597,9 +702,9 @@ dependencies = [
[[package]]
name = "cdk-sqlite"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9334877fe3f9fb25f6819c0876af1b0690f47aa2b1e548a20ac904740cbd574c"
checksum = "c706b86b76d89bbbfad7e106328c14f1d99440ee5829840510e50922f48fdc51"
dependencies = [
"async-trait",
"bitcoin",
@@ -628,6 +733,30 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]]
name = "chrono"
version = "0.4.44"
@@ -669,6 +798,17 @@ dependencies = [
"half",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
]
[[package]]
name = "clap"
version = "4.6.0"
@@ -807,6 +947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"typenum",
]
@@ -915,6 +1056,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@@ -933,6 +1075,9 @@ name = "dnssec-prover"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec4f825369fc7134da70ca4040fddc8e03b80a46d249ae38d9c1c39b7b4476bf"
dependencies = [
"bitcoin_hashes 0.14.1",
]
[[package]]
name = "dyn-clone"
@@ -940,6 +1085,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "elcaju_core"
version = "0.1.0"
@@ -953,6 +1104,7 @@ dependencies = [
"flutter_rust_bridge",
"getrandom 0.3.4",
"log",
"nostr-sdk",
"oslog",
"reqwest",
"serde_json",
@@ -1313,6 +1465,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.12"
@@ -1397,7 +1558,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
"webpki-roots 1.0.6",
]
[[package]]
@@ -1584,6 +1745,28 @@ dependencies = [
"serde_core",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -1754,6 +1937,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -1832,6 +2021,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "negentropy"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
[[package]]
name = "nom"
version = "7.1.3"
@@ -1842,6 +2037,84 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nostr"
version = "0.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1"
dependencies = [
"aes",
"base64 0.22.1",
"bech32",
"bip39",
"bitcoin_hashes 0.14.1",
"cbc",
"chacha20",
"chacha20poly1305",
"getrandom 0.2.17",
"hex",
"instant",
"scrypt",
"secp256k1",
"serde",
"serde_json",
"unicode-normalization",
"url",
]
[[package]]
name = "nostr-database"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1"
dependencies = [
"lru",
"nostr",
"tokio",
]
[[package]]
name = "nostr-gossip"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade30de16869618919c6b5efc8258f47b654a98b51541eb77f85e8ec5e3c83a6"
dependencies = [
"nostr",
]
[[package]]
name = "nostr-relay-pool"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b"
dependencies = [
"async-utility",
"async-wsocket",
"atomic-destructor",
"hex",
"lru",
"negentropy",
"nostr",
"nostr-database",
"tokio",
"tracing",
]
[[package]]
name = "nostr-sdk"
version = "0.44.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "471732576710e779b64f04c55e3f8b5292f865fea228436daf19694f0bf70393"
dependencies = [
"async-utility",
"nostr",
"nostr-database",
"nostr-gossip",
"nostr-relay-pool",
"tokio",
"tracing",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -1927,6 +2200,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl-probe"
version = "0.2.1"
@@ -1967,12 +2246,33 @@ dependencies = [
"windows-link",
]
[[package]]
name = "password-hash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "pem"
version = "3.0.6"
@@ -2069,6 +2369,17 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -2365,7 +2676,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
"webpki-roots 1.0.6",
]
[[package]]
@@ -2467,6 +2778,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "schannel"
version = "0.1.29"
@@ -2506,6 +2826,18 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"password-hash",
"pbkdf2",
"salsa20",
"sha2",
]
[[package]]
name = "secp256k1"
version = "0.29.1"
@@ -2659,6 +2991,17 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@@ -2960,6 +3303,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-socks"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
dependencies = [
"either",
"futures-util",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.18"
@@ -2985,6 +3340,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots 0.26.11",
]
[[package]]
@@ -3184,6 +3540,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -3213,6 +3579,7 @@ dependencies = [
"idna",
"percent-encoding",
"serde",
"serde_derive",
]
[[package]]
@@ -3405,6 +3772,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.6",
]
[[package]]
name = "webpki-roots"
version = "1.0.6"
+6 -3
View File
@@ -8,9 +8,9 @@ crate-type = ["cdylib", "staticlib"]
[dependencies]
# Cashu — acceso directo, sin intermediarios
cdk = { version = "0.15.1", default-features = false, features = ["wallet"] }
cdk-common = { version = "0.15.1", default-features = false }
cdk-sqlite = { version = "0.15.1", default-features = false, features = ["wallet"] }
cdk = { version = "0.16.0", default-features = false, features = ["wallet", "nostr"] }
cdk-common = { version = "0.16.0", default-features = false, features = ["nostr"] }
cdk-sqlite = { version = "0.16.0", default-features = false, features = ["wallet"] }
# Bridge Flutter <-> Rust
flutter_rust_bridge = { version = "=2.11.1", default-features = false, features = [
@@ -31,6 +31,9 @@ bip39 = { version = "2.1", default-features = false, features = ["std"] }
# Entropy
getrandom = { version = "0.3", default-features = false, features = ["std"] }
# Nostr keys (same version as CDK 0.16.0 — CDK does not re-export Keys)
nostr-sdk = { version = "0.44.1", default-features = false, features = ["nip04", "nip44", "nip59"] }
# Async runtime
tokio = { version = "1", default-features = false }
+1
View File
@@ -1,5 +1,6 @@
pub mod error;
pub mod keys;
pub mod mint_info;
pub mod payment_request;
pub mod token;
pub mod wallet;
+581
View File
@@ -0,0 +1,581 @@
use std::str::FromStr;
use cdk::{
amount::Amount,
mint_url::MintUrl,
nuts::{
CurrencyUnit, PaymentRequest as CdkPaymentRequest, Transport, TransportType,
},
wallet::ReceiveOptions as CdkReceiveOptions,
};
use flutter_rust_bridge::frb;
use nostr_sdk::{
nips::nip19::{Nip19Profile, ToBech32},
Keys as NostrKeys, PublicKey, RelayUrl,
};
use crate::frb_generated::StreamSink;
use super::{error::Error, wallet::Wallet};
// ========================================================================
// Payment Request parsing (NUT-18 creqA + NUT-26 creqB)
// ========================================================================
/// Parsed payment request info exposed to Flutter.
/// CDK's FromStr auto-detects creqA (CBOR) vs creqB (Bech32m).
pub struct PaymentRequestInfo {
/// Raw encoded string (creqA... or CREQB1...)
pub raw: String,
/// Payment id
pub payment_id: Option<String>,
/// Requested amount (in base unit)
pub amount: Option<u64>,
/// Currency unit ("sat", "usd", etc.)
pub unit: Option<String>,
/// Whether this is a single-use request
pub single_use: Option<bool>,
/// Accepted mints (empty = any mint)
pub mints: Vec<String>,
/// Human-readable description
pub description: Option<String>,
/// Available transports
pub transports: Vec<TransportInfo>,
/// Whether NUT-10 spending conditions are required
pub has_nut10: bool,
}
/// Transport info exposed to Flutter
pub struct TransportInfo {
/// "nostr" or "post"
pub transport_type: String,
/// Target (nprofile or URL)
pub target: String,
}
impl PaymentRequestInfo {
/// Parse a payment request string (creqA, CREQB1, or bitcoin:?creq=).
/// Supports NUT-18 (CBOR+base64) and NUT-26 (Bech32m) automatically.
#[frb(sync)]
pub fn parse(encoded: String) -> Result<PaymentRequestInfo, Error> {
parse_payment_request_inner(encoded)
}
}
/// Inner parsing logic, outside of frb macro scope to avoid iterator issues.
fn parse_payment_request_inner(encoded: String) -> Result<PaymentRequestInfo, Error> {
let creq_str = extract_creq_from_uri(&encoded).unwrap_or(encoded);
let pr = CdkPaymentRequest::from_str(&creq_str)
.map_err(|e| Error::Cdk(format!("Invalid payment request: {e}")))?;
let mints: Vec<String> = pr.mints.iter().map(|m| m.to_string()).collect();
let mut transports = Vec::new();
for t in &pr.transports {
transports.push(TransportInfo {
transport_type: match t._type {
TransportType::Nostr => "nostr".into(),
TransportType::HttpPost => "post".into(),
},
target: t.target.clone(),
});
}
let has_nut10 = pr.nut10.is_some();
Ok(PaymentRequestInfo {
raw: creq_str,
payment_id: pr.payment_id,
amount: pr.amount.map(|a| a.into()),
unit: pr.unit.map(|u| format!("{}", u)),
single_use: pr.single_use,
mints,
description: pr.description,
transports,
has_nut10,
})
}
/// Extract creq parameter from a BIP-321 bitcoin: URI.
/// e.g. "bitcoin:?creq=CREQB1...&lightning=lnbc..." → "CREQB1..."
/// Handles percent-encoding and case-insensitive key matching.
fn extract_creq_from_uri(input: &str) -> Option<String> {
if !input
.get(..8)
.is_some_and(|scheme| scheme.eq_ignore_ascii_case("bitcoin:"))
{
return None;
}
let query = input.split_once('?')?.1;
for param in query.split('&') {
let (key, value) = param.split_once('=')?;
if key.eq_ignore_ascii_case("creq") {
return Some(percent_decode(value));
}
}
None
}
/// Simple percent-decoding for URI query values.
fn percent_decode(value: &str) -> String {
let bytes = value.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let Ok(byte) = u8::from_str_radix(
std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""),
16,
) {
out.push(byte);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8(out).unwrap_or_else(|_| value.to_string())
}
// ========================================================================
// Payment execution (payer side)
// ========================================================================
impl Wallet {
/// Pay a NUT-18 payment request.
/// Uses CDK's pay_request() which handles:
/// - NUT-10 spending conditions
/// - Transport selection (Nostr preferred, HTTP POST fallback)
/// - Token preparation and delivery
pub async fn pay_payment_request(
&self,
encoded: String,
custom_amount: Option<u64>,
) -> Result<(), Error> {
let creq_str = extract_creq_from_uri(&encoded).unwrap_or(encoded);
let pr = CdkPaymentRequest::from_str(&creq_str)
.map_err(|e| Error::Cdk(format!("Invalid payment request: {e}")))?;
self.inner
.pay_request(pr, custom_amount.map(Amount::from))
.await?;
self.update_balance_streams().await;
Ok(())
}
}
// ========================================================================
// Payment Request creation (payee/receiver side) — NUT-18/26
// ========================================================================
/// Parameters for creating a payment request.
pub struct CreateRequestParams {
/// Amount to request (in smallest unit, e.g. sats)
pub amount: Option<u64>,
/// Currency unit ("sat", "usd", etc.)
pub unit: String,
/// Human-readable description
pub description: Option<String>,
/// Nostr relay URLs for the transport
pub nostr_relays: Vec<String>,
}
/// Result of creating a payment request.
pub struct CreatedPaymentRequest {
/// NUT-18 encoding: creqA... (CBOR+base64url)
pub creq_a: String,
/// NUT-26 encoding: CREQB1... (Bech32m, uppercase for QR)
pub creq_b: String,
/// Opaque handle holding the ephemeral Nostr keys — pass to wait_for_nostr_payment().
/// The secret key never leaves Rust.
pub listener_handle: NostrListenerHandle,
}
/// Opaque handle that keeps Nostr ephemeral keys in Rust memory.
/// Dart receives this as an opaque reference and passes it back
/// to wait_for_nostr_payment() without ever seeing the secret key.
/// Also carries the expected payment parameters for validation.
#[derive(Clone)]
pub struct NostrListenerHandle {
keys: NostrKeys,
pubkey: PublicKey,
relays: Vec<String>,
/// Expected amount (None = any amount accepted)
expected_amount: Option<Amount>,
/// Expected currency unit
expected_unit: CurrencyUnit,
/// This wallet's mint URL
mint_url: MintUrl,
}
/// Serializable data for persisting a NostrListenerHandle across app restarts.
pub struct PersistedRequestData {
pub secret_hex: String,
pub pubkey_hex: String,
pub relays: Vec<String>,
pub amount: Option<u64>,
pub unit: String,
pub mint_url: String,
}
impl NostrListenerHandle {
/// Export handle data for persistence (e.g. SharedPreferences).
/// The secret key is exposed as hex — the caller is responsible
/// for storing it securely.
#[frb(sync)]
pub fn to_persisted(&self) -> PersistedRequestData {
PersistedRequestData {
secret_hex: self.keys.secret_key().to_secret_hex(),
pubkey_hex: self.pubkey.to_hex(),
relays: self.relays.clone(),
amount: self.expected_amount.map(|a| a.into()),
unit: self.expected_unit.to_string(),
mint_url: self.mint_url.to_string(),
}
}
/// Reconstruct a handle from persisted data (e.g. after app restart).
#[frb(sync)]
pub fn from_persisted(data: PersistedRequestData) -> Result<NostrListenerHandle, Error> {
let secret_key = nostr_sdk::SecretKey::from_hex(&data.secret_hex)
.map_err(|e| Error::Cdk(format!("Invalid persisted secret key: {e}")))?;
let keys = NostrKeys::new(secret_key);
// Derive pubkey from secret — don't trust the persisted copy
let pubkey = keys.public_key;
let unit = CurrencyUnit::from_str(&data.unit)
.unwrap_or(CurrencyUnit::Custom(data.unit));
let mint_url = MintUrl::from_str(&data.mint_url)
.map_err(|e| Error::Cdk(format!("Invalid persisted mint URL: {e}")))?;
Ok(NostrListenerHandle {
keys,
pubkey,
relays: data.relays,
expected_amount: data.amount.map(Amount::from),
expected_unit: unit,
mint_url,
})
}
}
/// State of a Nostr payment listener.
pub enum NostrPaymentState {
/// Connected to relays, waiting for payment
Waiting,
/// Payment received and tokens claimed
Received,
/// Error occurred
Error,
}
/// Event emitted by the Nostr payment listener.
pub struct NostrPaymentEvent {
pub state: NostrPaymentState,
pub amount: Option<u64>,
pub error: Option<String>,
}
impl Wallet {
/// Create a NUT-18 payment request with Nostr transport.
///
/// Builds a PaymentRequest with the wallet's mint URL and unit,
/// generates ephemeral Nostr keys, and returns both creqA and creqB
/// encodings plus the keys needed for the Nostr listener.
pub async fn create_payment_request(
&self,
params: CreateRequestParams,
) -> Result<CreatedPaymentRequest, Error> {
// Generate ephemeral Nostr keys for this request
let keys = NostrKeys::generate();
// Parse relay URLs for nprofile
if params.nostr_relays.is_empty() {
return Err(Error::InvalidInput);
}
let relay_urls: Vec<RelayUrl> = params
.nostr_relays
.iter()
.map(|r| RelayUrl::parse(r))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| Error::Cdk(format!("Invalid relay URL: {e}")))?;
let nprofile = Nip19Profile::new(keys.public_key, relay_urls);
let nprofile_bech32 = nprofile
.to_bech32()
.map_err(|e| Error::Cdk(format!("nprofile encoding failed: {e}")))?;
// Build the Nostr transport
let nostr_transport = Transport {
_type: TransportType::Nostr,
target: nprofile_bech32,
tags: vec![vec!["n".to_string(), "17".to_string()]],
};
// Build the PaymentRequest with this wallet's mint and unit
let mint_url = self.mint_url()?;
if params.unit != self.unit {
return Err(Error::InvalidInput);
}
let unit = CurrencyUnit::from_str(&self.unit)
.unwrap_or(CurrencyUnit::Custom(self.unit.clone()));
let pr = CdkPaymentRequest {
payment_id: None,
amount: params.amount.map(Amount::from),
unit: Some(unit.clone()),
single_use: Some(true),
mints: vec![mint_url.clone()],
description: params.description,
transports: vec![nostr_transport],
nut10: None,
};
// Encode both formats
let creq_a = pr.to_string(); // NUT-18: creqA...
let creq_b = pr
.to_bech32_string()
.map_err(|e| Error::Cdk(format!("Bech32m encoding failed: {e}")))?;
let pubkey = keys.public_key;
Ok(CreatedPaymentRequest {
creq_a,
creq_b,
listener_handle: NostrListenerHandle {
keys,
pubkey,
relays: params.nostr_relays,
expected_amount: params.amount.map(Amount::from),
expected_unit: unit,
mint_url,
},
})
}
/// Wait for an incoming Nostr payment (NIP-17 gift-wrap).
///
/// Takes the opaque NostrListenerHandle returned by create_payment_request().
/// The secret key never leaves Rust memory.
pub async fn wait_for_nostr_payment(
&self,
handle: NostrListenerHandle,
sink: StreamSink<NostrPaymentEvent>,
) -> Result<(), Error> {
let NostrListenerHandle {
keys,
pubkey,
relays,
expected_amount,
expected_unit,
mint_url,
} = handle;
// Fail fast if the handle was created for a different wallet
let current_mint = self.mint_url()?;
let current_unit = CurrencyUnit::from_str(&self.unit)
.unwrap_or(CurrencyUnit::Custom(self.unit.clone()));
if mint_url != current_mint || expected_unit != current_unit {
return Err(Error::InvalidInput);
}
if sink
.add(NostrPaymentEvent {
state: NostrPaymentState::Waiting,
amount: None,
error: None,
})
.is_err()
{
return Ok(());
}
let _self = self.clone();
flutter_rust_bridge::spawn(async move {
match wait_for_nostr_payment_inner(
&_self, keys, pubkey, relays, expected_amount, expected_unit, mint_url, &sink,
)
.await
{
Ok(amount) => {
let _ = sink.add(NostrPaymentEvent {
state: NostrPaymentState::Received,
amount: Some(amount.into()),
error: None,
});
_self.update_balance_streams().await;
}
Err(e) => {
let _ = sink.add(NostrPaymentEvent {
state: NostrPaymentState::Error,
amount: None,
error: Some(e.to_string()),
});
}
}
});
Ok(())
}
}
/// Internal: connect to relays, listen for gift-wrapped payment, receive tokens.
/// Validates incoming payments against expected amount/unit/mint before accepting.
/// Periodically checks if the Dart stream is still alive and disconnects if not.
#[allow(clippy::too_many_arguments)]
async fn wait_for_nostr_payment_inner(
wallet: &Wallet,
keys: NostrKeys,
pubkey: PublicKey,
relays: Vec<String>,
expected_amount: Option<Amount>,
expected_unit: CurrencyUnit,
expected_mint: MintUrl,
sink: &StreamSink<NostrPaymentEvent>,
) -> Result<Amount, Error> {
use cdk::nuts::{nut00::ProofsMethods, PaymentRequestPayload, Token as CdkToken};
use nostr_sdk::{Client, Filter, Kind};
use std::time::Duration;
// Create Nostr client with ephemeral keys
let client = Client::new(keys.clone());
for relay in &relays {
client
.add_read_relay(relay)
.await
.map_err(|e| Error::Cdk(format!("Failed to add relay {relay}: {e}")))?;
}
// Connect with timeout so we fail fast if relays are unreachable
tokio::time::timeout(Duration::from_secs(10), client.connect())
.await
.map_err(|_| Error::Network("Relay connection timed out".to_string()))?;
// Create the broadcast receiver BEFORE subscribing so we capture historical
// events the relay sends back in response to our subscription filter.
// (broadcast::Receiver only sees messages sent after its creation)
let mut notifications = client.notifications();
// Subscribe to NIP-17 gift-wrap events (kind 1059) addressed to our ephemeral pubkey
let filter = Filter::new().pubkey(pubkey).kind(Kind::GiftWrap);
client
.subscribe(filter, None)
.await
.map_err(|e| Error::Cdk(format!("Subscription failed: {e}")))?;
loop {
match tokio::time::timeout(Duration::from_secs(1), notifications.recv()).await {
Ok(Ok(notification)) => {
// Check sink is alive before processing
if sink
.add(NostrPaymentEvent {
state: NostrPaymentState::Waiting,
amount: None,
error: None,
})
.is_err()
{
client.disconnect().await;
return Err(Error::Network("Listener cancelled by client".to_string()));
}
if let nostr_sdk::RelayPoolNotification::Event { event, .. } = notification {
// Try to unwrap NIP-17 gift-wrap
let unwrapped = match client.unwrap_gift_wrap(&event).await {
Ok(rumor) => rumor,
Err(_) => continue,
};
// Parse PaymentRequestPayload from the rumor content
let payload: PaymentRequestPayload =
match serde_json::from_str(&unwrapped.rumor.content) {
Ok(p) => p,
Err(_) => continue,
};
// Validate unit matches
if payload.unit != expected_unit {
log::warn!(
"Ignoring payment: unit mismatch (expected {}, got {})",
expected_unit,
payload.unit
);
continue;
}
// Validate mint matches
if payload.mint != expected_mint {
log::warn!(
"Ignoring payment: mint mismatch (expected {}, got {})",
expected_mint,
payload.mint
);
continue;
}
// Validate amount if a specific amount was requested
if let Some(expected) = expected_amount {
let received_amount = payload.proofs.total_amount().unwrap_or_default();
if received_amount < expected {
log::warn!(
"Ignoring payment: underpayment (expected {}, got {})",
expected,
received_amount
);
continue;
}
}
// Build a token from the validated payload and receive it
let token = CdkToken::new(
payload.mint,
payload.proofs,
payload.memo,
payload.unit,
);
let token_str = token.to_string();
// Continue listening on receive errors (e.g. already-spent proofs)
match wallet
.inner
.receive(&token_str, CdkReceiveOptions::default())
.await
{
Ok(received) => {
client.disconnect().await;
return Ok(received);
}
Err(e) => {
log::warn!("Failed to receive token, continuing: {e}");
continue;
}
}
}
}
Ok(Err(_)) => {
// Notification channel closed
break;
}
Err(_) => {
// Timeout — check if the Dart stream is still alive
if sink
.add(NostrPaymentEvent {
state: NostrPaymentState::Waiting,
amount: None,
error: None,
})
.is_err()
{
// Dart side cancelled the stream, clean up
client.disconnect().await;
return Err(Error::Network("Listener cancelled by client".to_string()));
}
}
}
}
client.disconnect().await;
Err(Error::Network(
"Nostr listener ended before a payment was received".to_string(),
))
}
+25 -1
View File
@@ -4,7 +4,10 @@ use std::{
};
use bc_ur::{prelude::CBOR, MultipartDecoder, MultipartEncoder, UR};
use cdk::nuts::Token as CdkToken;
use cdk::{
mint_url::MintUrl,
nuts::{CurrencyUnit, Proof, Token as CdkToken},
};
use cdk_common::{KeySetInfo, Proofs};
use flutter_rust_bridge::frb;
@@ -92,6 +95,27 @@ impl TryInto<CdkToken> for &Token {
}
}
// === Offline token creation ===
/// Create a token from raw proof data without network access.
/// Uses CDK's CdkToken::new to produce a valid V4 (cashuB) token.
/// `proofs_json` is a JSON array of proofs: [{"id":"...","amount":1,"secret":"...","C":"..."}, ...]
#[frb(sync)]
pub fn create_offline_token(
mint_url: &str,
proofs_json: &str,
memo: Option<String>,
unit: Option<String>,
) -> Result<Token, Error> {
let mint_url = MintUrl::from_str(mint_url)?;
let unit = unit
.map(|u| CurrencyUnit::from_str(&u).unwrap_or(CurrencyUnit::Custom(u)))
.unwrap_or(CurrencyUnit::Sat);
let proofs: Vec<Proof> = serde_json::from_str(proofs_json)?;
let token = CdkToken::new(mint_url, proofs, memo, unit);
Token::try_from(token)
}
// === QR Multiframe (bc-ur) ===
pub struct TokenDecoder {
+334 -93
View File
@@ -12,19 +12,21 @@ use cdk::{
wallet::{
MeltQuote as CdkMeltQuote, MintQuote as CdkMintQuote, PreparedSend as CdkPreparedSend,
ReceiveOptions as CdkReceiveOptions, SendMemo, SendOptions as CdkSendOptions,
Wallet as CdkWallet,
Wallet as CdkWallet, WalletSubscription,
},
};
use cdk_common::{
util::unix_time,
wallet::{
Transaction as CdkTransaction, TransactionDirection as CdkTransactionDirection,
TransactionId,
},
NotificationPayload,
};
use cdk_sqlite::WalletSqliteDatabase;
use flutter_rust_bridge::frb;
use log::info;
use tokio::{sync::broadcast, time::sleep};
use tokio::sync::broadcast;
use uuid::Uuid;
use crate::frb_generated::StreamSink;
@@ -45,7 +47,7 @@ pub struct Wallet {
pub unit: String,
balance_broadcast: broadcast::Sender<u64>,
inner: CdkWallet,
pub(crate) inner: CdkWallet,
seed: [u8; 64],
}
@@ -131,12 +133,12 @@ impl Wallet {
send: PreparedSend,
memo: Option<String>,
include_memo: Option<bool>,
) -> Result<Token, Error> {
) -> Result<SendResult, Error> {
let send_memo = memo.map(|m| SendMemo {
memo: m,
include_memo: include_memo.unwrap_or_default(),
});
let token = self
let cdk_token = self
.inner
.confirm_send(
send.operation_id,
@@ -148,10 +150,35 @@ impl Wallet {
send.cdk_send_fee,
send_memo,
)
.await?
.to_string();
.await?;
// Compute the deterministic transaction ID from the token's proofs
// (SHA-256 of sorted Y values — same as CDK uses internally)
// Best-effort: send is already committed, don't fail on ID computation
let tx_id = match self.inner.get_mint_keysets(cdk::wallet::KeysetFilter::Active).await {
Ok(keysets) => match cdk_token.proofs(&keysets) {
Ok(proofs) => TransactionId::try_from(proofs)
.map(|id| id.to_string())
.map_err(|e| info!("Failed to compute tx ID from proofs: {e}"))
.ok(),
Err(e) => {
info!("Failed to extract proofs from token: {e}");
None
}
},
Err(e) => {
info!("Failed to fetch keysets for tx ID: {e}");
None
}
};
let token_str = cdk_token.to_string();
self.update_balance_streams().await;
Ok(Token::from_str(&token)?)
Ok(SendResult {
token: Token::from_str(&token_str)?,
transaction_id: tx_id,
})
}
pub async fn cancel_send(&self, send: PreparedSend) -> Result<(), Error> {
@@ -179,97 +206,198 @@ impl Wallet {
.inner
.mint_quote(PaymentMethod::BOLT11, Some(amount.into()), description, None)
.await?;
let _ = sink.add(MintQuote::from(quote.clone()));
if sink.add(MintQuote::from(quote.clone())).is_err() {
return Ok(());
}
// Try WebSocket (NUT-17) subscription — if unavailable, HTTP polling still works
let subscription = match self
.inner
.subscribe(WalletSubscription::Bolt11MintQuoteState(vec![quote.id.clone()]))
.await
{
Ok(sub) => Some(sub),
Err(e) => {
info!(
"Mint quote {}: WebSocket unavailable, using HTTP polling only: {e}",
quote.id
);
None
}
};
let _self = self.clone();
flutter_rust_bridge::spawn(async move {
loop {
sleep(Duration::from_secs(3)).await;
info!("Checking mint quote state for {}", quote.id);
match _self.inner.check_mint_quote_status(&quote.id).await {
Ok(state_res) => match state_res.state {
CdkMintQuoteState::Unpaid => {
if state_res.expiry < unix_time() {
let _ = sink.add(MintQuote {
id: quote.id,
request: quote.request,
amount: quote.amount.map(|a| a.into()),
expiry: Some(state_res.expiry),
state: MintQuoteState::Error,
token: None,
error: Some("Quote expired".to_string()),
});
break;
// Timeout: time until quote expires + 30s buffer, or 1 hour if no expiry
let remaining = quote.expiry.saturating_sub(unix_time());
let timeout_dur = if quote.expiry == 0 {
Duration::from_secs(3600)
} else {
Duration::from_secs(remaining + 30)
};
// Clone for the timeout error path (originals are moved into the async block)
let expired_id = quote.id.clone();
let expired_request = quote.request.clone();
let expired_amount = quote.amount;
let expired_expiry = quote.expiry;
// Detect payment via two parallel paths:
// - Path A: WebSocket (NUT-17) — fast when it works (sats on most mints)
// - Path B: HTTP polling every 5s — fallback for mints that don't send
// WebSocket notifications for all units (e.g. Nutshell 0.20.0 + USD)
// First one to detect payment wins.
let quote_id_for_poll = quote.id.clone();
let poll_wallet = _self.clone();
// Timeout only covers detection, not minting.
// If payment is detected near expiry, mint() must still complete.
enum Detected {
Paid,
Issued,
}
let mut subscription = subscription;
let detected = tokio::time::timeout(timeout_dur, async {
tokio::select! {
// Path A: WebSocket subscription (skipped if unavailable)
result = async {
let Some(ref mut sub) = subscription else {
return std::future::pending::<Detected>().await;
};
while let Some(event) = sub.recv().await {
match event.into_inner() {
NotificationPayload::MintQuoteBolt11Response(info)
if info.state == CdkMintQuoteState::Paid =>
{
info!("Mint quote {} paid via WebSocket", quote.id);
return Detected::Paid;
}
NotificationPayload::MintQuoteBolt11Response(info)
if info.state == CdkMintQuoteState::Issued =>
{
return Detected::Issued;
}
_ => continue,
}
continue;
}
CdkMintQuoteState::Issued => {
break;
std::future::pending::<Detected>().await
} => result,
// Path B: HTTP polling fallback
result = async {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
match poll_wallet.inner.check_mint_quote_status(&quote_id_for_poll).await {
Ok(q) if q.state == CdkMintQuoteState::Paid => {
info!("Mint quote {} paid via HTTP polling", quote_id_for_poll);
return Detected::Paid;
}
Ok(q) if q.state == CdkMintQuoteState::Issued => {
return Detected::Issued;
}
_ => continue,
}
}
CdkMintQuoteState::Paid => {
} => result,
}
})
.await;
// Handle detection result — mint logic runs outside the timeout
match detected {
Err(_) => {
// Timeout: quote expired
let _ = sink.add(MintQuote {
id: expired_id,
request: expired_request,
amount: expired_amount.map(|a| a.into()),
expiry: Some(expired_expiry),
state: MintQuoteState::Error,
token: None,
error: Some("Quote expired".to_string()),
transaction_id: None,
});
}
Ok(Detected::Paid) => {
// Notify Dart: payment detected
let _ = sink.add(MintQuote {
id: quote.id.clone(),
request: quote.request.clone(),
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: CdkMintQuoteState::Paid.into(),
token: None,
error: None,
transaction_id: None,
});
// Mint the ecash tokens (outside timeout)
match _self
.inner
.mint(&quote.id, SplitTarget::None, None)
.await
{
Ok(mint_proofs) => {
let tx_id = match TransactionId::try_from(
mint_proofs.clone(),
) {
Ok(id) => Some(id.to_string()),
Err(e) => {
info!("Failed to compute mint tx ID: {e}");
None
}
};
let mint_amount =
mint_proofs.total_amount().unwrap_or_default();
let _ = sink.add(MintQuote {
id: quote.id.clone(),
request: quote.request.clone(),
id: quote.id,
request: quote.request,
amount: Some(mint_amount.into()),
expiry: Some(quote.expiry),
state: CdkMintQuoteState::Issued.into(),
token: Token::try_from(CdkToken::new(
mint_url,
mint_proofs,
None,
unit,
))
.ok(),
error: None,
transaction_id: tx_id,
});
_self.update_balance_streams().await;
}
Err(e) => {
let _ = sink.add(MintQuote {
id: quote.id,
request: quote.request,
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: CdkMintQuoteState::Paid.into(),
state: MintQuoteState::Error,
token: None,
error: None,
error: Some(e.to_string()),
transaction_id: None,
});
match _self
.inner
.mint(&quote.id, SplitTarget::None, None)
.await
{
Ok(mint_proofs) => {
let mint_amount =
mint_proofs.total_amount().unwrap_or_default();
let _ = sink.add(MintQuote {
id: quote.id,
request: quote.request,
amount: Some(mint_amount.into()),
expiry: Some(quote.expiry),
state: CdkMintQuoteState::Issued.into(),
token: Token::try_from(CdkToken::new(
mint_url,
mint_proofs,
None,
unit,
))
.ok(),
error: None,
});
_self.update_balance_streams().await;
break;
}
Err(e) => {
let _ = sink.add(MintQuote {
id: quote.id,
request: quote.request,
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: MintQuoteState::Error,
token: None,
error: Some(e.to_string()),
});
break;
}
}
}
},
Err(e) => {
let _ = sink.add(MintQuote {
id: quote.id,
request: quote.request,
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: MintQuoteState::Error,
token: None,
error: Some(e.to_string()),
});
break;
}
}
Ok(Detected::Issued) => {
// Already issued (recovered from previous session)
let _ = sink.add(MintQuote {
id: quote.id.clone(),
request: quote.request.clone(),
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: CdkMintQuoteState::Issued.into(),
token: None,
error: None,
transaction_id: None,
});
_self.update_balance_streams().await;
}
}
});
Ok(())
@@ -293,15 +421,52 @@ impl Wallet {
.into())
}
pub async fn melt(&self, quote: MeltQuote) -> Result<u64, Error> {
let melted = self
pub async fn prepare_melt(&self, quote: MeltQuote) -> Result<PreparedMelt, Error> {
let prepared = self
.inner
.prepare_melt(&quote.id, HashMap::new())
.await?
.confirm()
.await?;
Ok(PreparedMelt {
amount: prepared.amount().into(),
fee_reserve: prepared.quote().fee_reserve.into(),
swap_fee: prepared.swap_fee().into(),
input_fee: prepared.input_fee().into(),
operation_id: prepared.operation_id(),
cdk_quote: prepared.quote().clone(),
proofs: prepared.proofs().clone(),
proofs_to_swap: prepared.proofs_to_swap().clone(),
cdk_input_fee: prepared.input_fee(),
cdk_input_fee_without_swap: prepared.input_fee_without_swap(),
})
}
pub async fn confirm_melt(&self, melt: PreparedMelt) -> Result<u64, Error> {
let finalized = self
.inner
.confirm_prepared_melt(
melt.operation_id,
melt.cdk_quote,
melt.proofs,
melt.proofs_to_swap,
melt.cdk_input_fee,
melt.cdk_input_fee_without_swap,
HashMap::new(),
)
.await?;
self.update_balance_streams().await;
Ok(melted.total_amount().into())
Ok(finalized.total_amount().into())
}
pub async fn cancel_melt(&self, melt: PreparedMelt) -> Result<(), Error> {
self.inner
.cancel_prepared_melt(
melt.operation_id,
melt.proofs,
melt.proofs_to_swap,
)
.await?;
self.update_balance_streams().await;
Ok(())
}
// === Transactions ===
@@ -363,11 +528,56 @@ impl Wallet {
Ok(())
}
// === Reclaim orphaned proofs ===
/// Check pending-spent proofs with the mint and revert unspent ones.
/// Returns count and total amount of proofs recovered.
pub async fn reclaim_pending_proofs(&self) -> Result<ReclaimResult, Error> {
let pending = self.inner.get_pending_spent_proofs().await?;
if pending.is_empty() {
return Ok(ReclaimResult { count: 0, amount: 0 });
}
// Build a map of Y → amount for later lookup
let mut amount_by_y: HashMap<PublicKey, u64> = HashMap::new();
for proof in &pending {
if let Ok(y) = proof.y() {
amount_by_y.insert(y, u64::from(proof.amount));
}
}
// check_proofs_spent: queries mint AND removes spent proofs from local DB
let states = self.inner.check_proofs_spent(pending).await?;
// Collect Y values of proofs the mint says are NOT spent
// Only reclaim proofs the mint explicitly reports as Unspent.
// Pending proofs (still being processed) must not be unreserved.
let unspent_ys: Vec<PublicKey> = states
.into_iter()
.filter(|s| s.state == ProofState::Unspent)
.map(|s| s.y)
.collect();
let count = unspent_ys.len() as u64;
let amount: u64 = unspent_ys.iter()
.filter_map(|y| amount_by_y.get(y))
.sum();
if count > 0 {
// Revert from PendingSpent to Unspent
self.inner.unreserve_proofs(unspent_ys).await?;
}
// Always refresh: check_proofs_spent may have removed spent proofs
self.update_balance_streams().await;
Ok(ReclaimResult { count, amount })
}
// === Utility ===
pub async fn is_token_spent(&self, token: Token) -> Result<bool, Error> {
let token: CdkToken = token.try_into()?;
let mint_keysets = self.inner.get_mint_keysets().await?;
let mint_keysets = self.inner.get_mint_keysets(cdk::wallet::KeysetFilter::All).await?;
let proof_states = self
.inner
.check_proofs_spent(token.proofs(&mint_keysets)?)
@@ -383,7 +593,7 @@ impl Wallet {
// === Internal helpers ===
fn mint_url(&self) -> Result<MintUrl, Error> {
pub(crate) fn mint_url(&self) -> Result<MintUrl, Error> {
Ok(MintUrl::from_str(&self.mint_url)?)
}
@@ -391,7 +601,7 @@ impl Wallet {
CurrencyUnit::from_str(&self.unit).unwrap_or(CurrencyUnit::Custom(self.unit.clone()))
}
async fn update_balance_streams(&self) {
pub(crate) async fn update_balance_streams(&self) {
let balance = self
.inner
.total_balance()
@@ -406,6 +616,15 @@ impl Wallet {
// Types
// ========================================================================
/// Result of a confirmed send, carrying both the ecash token and the
/// deterministic transaction ID so Dart can save metadata without a racy
/// listTransactions lookup.
pub struct SendResult {
pub token: Token,
/// Deterministic transaction ID (None if computation failed)
pub transaction_id: Option<String>,
}
pub struct MintQuote {
pub id: String,
pub request: String,
@@ -414,6 +633,8 @@ pub struct MintQuote {
pub state: MintQuoteState,
pub token: Option<Token>,
pub error: Option<String>,
/// Deterministic transaction ID (set when proofs are available, typically on Issued)
pub transaction_id: Option<String>,
}
impl From<CdkMintQuote> for MintQuote {
@@ -426,6 +647,7 @@ impl From<CdkMintQuote> for MintQuote {
state: quote.state.into(),
token: None,
error: None,
transaction_id: None,
}
}
}
@@ -504,6 +726,25 @@ impl<'a> From<CdkPreparedSend<'a>> for PreparedSend {
}
}
pub struct ReclaimResult {
pub count: u64,
pub amount: u64,
}
pub struct PreparedMelt {
pub amount: u64,
pub fee_reserve: u64,
pub swap_fee: u64,
pub input_fee: u64,
operation_id: Uuid,
cdk_quote: CdkMeltQuote,
proofs: cdk::nuts::Proofs,
proofs_to_swap: cdk::nuts::Proofs,
cdk_input_fee: Amount,
cdk_input_fee_without_swap: Amount,
}
#[derive(Default)]
pub struct ReceiveOptions {
pub signing_keys: Option<Vec<String>>,
+2067 -165
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -89,11 +89,11 @@ BEGIN
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "CompanyName", "ElCaju" "\0"
VALUE "FileDescription", "elcaju" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "elcaju" "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 ElCaju. All rights reserved." "\0"
VALUE "OriginalFilename", "elcaju.exe" "\0"
VALUE "ProductName", "elcaju" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"