Compare commits

...
Author SHA1 Message Date
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
Forte11Cuba e9d245c986 docs: capitalize Git in CI/CD section 2026-03-29 01:28:28 -06:00
Forte11Cuba 9836f8f246 docs: address CodeRabbit review — fix URLs, clarify rustc version, add code block hints 2026-03-29 01:25:06 -06:00
Forte11Cuba 88e1483152 docs: update README to reflect current project state 2026-03-29 01:17:16 -06:00
Forte11andGitHub 0e7ff326cf Merge pull request #74 from Forte11Cuba/feat/flutter-integration-phase2
feat: integrate internal Rust bridge, remove cdk_flutter dependency (Phase 2)
2026-03-28 17:49:07 -06:00
Forte11Cuba 188e26784b fix: update outdated cdk-flutter comment in qr_scanner_widget 2026-03-28 17:47:37 -06:00
Forte11Cuba 989b710cd8 feat: integrate internal Rust bridge, remove cdk_flutter dependency (Phase 2) 2026-03-28 15:04:54 -06:00
Forte11andGitHub 01ea5be578 Merge pull request #73 from Forte11Cuba/feat/rust-api-core
feat: implement Phase 1 Rust API core (keys, token, mint_info, wallet)
2026-03-28 03:59:51 -06:00
Forte11Cuba 07c0a7be89 fix: remove panic paths in TokenDecoder lock handling 2026-03-28 03:45:56 -06:00
Forte11Cuba 1f629d45b3 fix: remove panics in generate_mnemonic and token parsing 2026-03-28 03:30:56 -06:00
Forte11Cuba fa5fd7d98e feat: implement Phase 1 Rust API core (keys, token, mint_info, wallet) 2026-03-28 03:14:40 -06:00
Forte11andGitHub e4f60010f7 Merge pull request #72 from Forte11Cuba/feat/rust-internal-bridge
feat: scaffold internal Rust bridge (elcaju_core)
2026-03-28 02:33:58 -06:00
Forte11Cuba 5d54cb957c fix: correct babash typo from sed double-replacement 2026-03-28 02:33:33 -06:00
Forte11Cuba b930f3c201 fix: use bash consistently across build scripts and podspecs 2026-03-28 02:27:54 -06:00
Forte11Cuba 830c42da93 fix: bash consistency in build_pod.sh 2026-03-28 01:33:37 -06:00
Forte11Cuba e28c68a0ae fix: use bash shebang in build_pod.sh (uses bash-only features) 2026-03-28 01:28:41 -06:00
Forte11Cuba f59090cec8 fix: apply CodeRabbit review feedback 2026-03-28 01:21:12 -06:00
Forte11Cuba 2a092fc9f8 feat: scaffold internal Rust bridge (elcaju_core) 2026-03-28 00:52:48 -06:00
Forte11andGitHub 5fa8583cdf Merge pull request #68 from Forte11Cuba/fix/startup-recovery
fix: add startup recovery for lost proofs and pending operations
2026-03-20 11:37:32 -06:00
Forte11Cuba 9ab78e0ab8 fix: add startup recovery for lost proofs and pending operations 2026-03-20 11:32:16 -06:00
Forte11andGitHub d13b5e8782 Merge pull request #66 from Forte11Cuba/feature/nfc-support
feat: NFC support for Cashu token read/write
2026-03-13 17:34:47 -06:00
Forte11Cuba 76931f7b1d fix: parse URI fragment as key-value pairs instead of substring 2026-03-13 14:09:56 -06:00
Forte11Cuba 115843ce51 fix: guard against infinite loop on zero-data IsoDep response 2026-03-13 12:43:35 -06:00
Forte11Cuba 713f13a46d fix: address CodeRabbit review — await NFC sessions, log errors, validate tokens 2026-03-13 12:30:46 -06:00
Forte11Cuba 560d6fc683 feat: phone-to-phone NFC via HCE with proprietary AID 2026-03-13 12:05:36 -06:00
Forte11Cuba c0f8b166ea feat: NFC support for Cashu token read/write 2026-03-12 10:53:50 -06:00
Forte11andGitHub 6b68367c0d Merge pull request #64 from Forte11Cuba/feature/peanut-emoji-encoding
feat: add peanut emoji encoding for Cashu tokens
2026-03-12 08:33:05 -06:00
Forte11Cuba d80ed4caa7 fix: handle malformed peanut tokens with explicit error message 2026-03-12 08:20:52 -06:00
Forte11Cuba a627ed39f2 fix: guard clipboard callback with mounted check in ReceiveScreen 2026-03-12 08:06:24 -06:00
Forte11Cuba 5d00213d95 fix: peanut decode trimLeft consistency and auto-decode on receive 2026-03-12 07:30:52 -06:00
Forte11Cuba 2e3bb5dcc3 feat: add peanut emoji encoding for Cashu tokens 2026-03-12 06:54:50 -06:00
Forte11andGitHub 749d44650d Merge pull request #63 from Forte11Cuba/cleanup/css-duplicates-and-i18n
fix: auto version from pubspec, remove Cuba Bitcoin badge, LaChispa tappable link
2026-02-26 03:09:30 -06:00
Forte11Cuba 3ff2d6ef84 fix: defensive error handling for version load and LaChispa link 2026-02-26 03:04:58 -06:00
Forte11Cuba 18c72ebf1d fix: auto version from pubspec, remove Cuba Bitcoin badge, LaChispa tappable link 2026-02-26 02:50:40 -06:00
Forte11Cuba f9a70bf1ed ci: add universal APK and stable download URL
Release APK / build (push) Canceled after 0s
2026-02-25 05:27:34 -06:00
Forte11Cuba 22434245f2 bump version to v0.1.0 2026-02-25 04:48:05 -06:00
Forte11Cuba fb76bde5b7 fix: remove stale precompiled .so and ignore jniLibs 2026-02-25 04:46:44 -06:00
Forte11andGitHub 365047e73c Merge pull request #62 from Forte11Cuba/feature/p2pk-qr-scan
feat: add QR scan button for P2PK pubkey input (#59)
2026-02-23 16:31:48 -06:00
Forte11Cuba c374c042c7 fix: add user feedback for invalid QR and camera errors in P2PK scanner 2026-02-23 16:30:30 -06:00
Forte11Cuba 0f7e89a687 feat: add QR scan button for P2PK pubkey input (#59) 2026-02-23 16:21:29 -06:00
Forte11andGitHub 73582c4a69 Merge pull request #61 from Forte11Cuba/fix/cancel-send-on-failure
fix: release reserved proofs when confirmSend fails (#58)
2026-02-23 15:51:19 -06:00
Forte11Cuba 3e9fea34eb fix: guard cancelSend to avoid masking original send error 2026-02-23 15:48:46 -06:00
Forte11Cuba 348ef1d987 fix: release reserved proofs when confirmSend fails (#58) 2026-02-23 15:42:02 -06:00
Forte11andGitHub fc72bb33b2 Merge pull request #57 from Forte11Cuba/feature/enable-p2pk-send
feat: enable P2PK send with CDK 0.15 includeFee support
2026-02-23 14:29:52 -06:00
Forte11Cuba 7a9ce65e55 fix: guard against short pubkey in debug log and async paste disposal 2026-02-23 14:17:22 -06:00
Forte11Cuba 604dd65266 feat: enable P2PK send with CDK 0.15 includeFee support 2026-02-23 13:52:25 -06:00
Forte11andGitHub 42f59e877f Merge pull request #56 from Forte11Cuba/fix/pending-invoice-error-handling
fix: add try/catch to _loadPendingMintInvoice to prevent stuck loading state
2026-02-23 13:20:07 -06:00
Forte11Cuba 8b0e0dcd15 fix: log error in _loadPendingMintInvoice catch block 2026-02-23 13:18:28 -06:00
Forte11Cuba 0ac2a8b3ad fix: add try/catch to _loadPendingMintInvoice to prevent stuck loading state 2026-02-23 13:12:16 -06:00
Forte11andGitHub a97d7d45bc Merge pull request #55 from Forte11Cuba/fix/invoice-placeholder-flash
ui: suppress invoice placeholder flash while loading pending mint invoice
2026-02-21 01:38:33 -06:00
134 changed files with 33033 additions and 817 deletions
+8 -1
View File
@@ -41,15 +41,20 @@ jobs:
- name: Generate localizations
run: flutter gen-l10n
- name: Build release APKs
- name: Build release APKs (split per ABI)
run: flutter build apk --release --split-per-abi
- name: Build universal APK
run: flutter build apk --release
- name: Rename APKs
run: |
cd build/app/outputs/flutter-apk
mv app-arm64-v8a-release.apk elcaju-${{ github.ref_name }}-arm64.apk
mv app-armeabi-v7a-release.apk elcaju-${{ github.ref_name }}-armeabi-v7a.apk
mv app-x86_64-release.apk elcaju-${{ github.ref_name }}-x86_64.apk
mv app-release.apk elcaju-${{ github.ref_name }}-universal.apk
cp elcaju-${{ github.ref_name }}-universal.apk elcaju-universal.apk
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
@@ -58,4 +63,6 @@ jobs:
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-arm64.apk
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-armeabi-v7a.apk
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-x86_64.apk
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-universal.apk
build/app/outputs/flutter-apk/elcaju-universal.apk
generate_release_notes: true
+4
View File
@@ -44,3 +44,7 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# Rust/CargoKit compiled native libraries
android/app/src/main/jniLibs/
rust/target/
+141 -115
View File
@@ -1,6 +1,6 @@
## ElCaju 🥜
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Forte11Cuba/elcaju)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Forte11Cuba/elcaju)
*Ask questions about this project using DeepWiki AI*
<p align="center">
@@ -28,7 +28,7 @@
---
## 📱 About ElCaju
## About ElCaju
**ElCaju** is a [Cashu](https://cashu.space) wallet (ecash on Bitcoin) with Cuban identity, brother of [LaChispa](https://github.com/lachispame/) (Lightning). Designed to offer privacy, offline transactions, and a warm tropical experience.
@@ -41,23 +41,31 @@ Cashu is an ecash protocol that enables Bitcoin transactions with maximum privac
---
## Features
## Features
| Feature | Description |
|---------|-------------|
| 🔐 **Total Privacy** | Fungible ecash tokens, no traceable history |
| 📴 **Works Offline** | Store and share tokens without internet |
| **Lightning Bridge** | Deposit and withdraw sats via Lightning Network |
| 🏦 **Multi-Mint** | Connect to multiple Cashu mints simultaneously |
| 💱 **Multi-Unit** | Support for sat, USD, EUR and more |
| 🔑 **Your Seed, Your Money** | Backup with 12 words (BIP39) |
| 🔒 **Optional PIN** | Protect access to your wallet |
| 🌐 **Bilingual** | Spanish and English |
| 🎉 **Friendly UX** | Visual effects and confetti when receiving funds |
| **Total Privacy** | Fungible ecash tokens, no traceable history |
| **Works Offline** | Store, create, and share tokens without internet |
| **Lightning Bridge** | Deposit and withdraw sats via Lightning Network |
| **Multi-Mint** | Connect to unlimited Cashu mints simultaneously |
| **Multi-Unit** | Support for sat, USD, EUR, and any unit offered by mints |
| **P2PK (NUT-10)** | Lock tokens to a Nostr public key — only the recipient can claim |
| **Token Recovery (NUT-13)** | Restore tokens from any mint using your seed phrase |
| **NFC Transfers** | Share tokens via NFC — includes Host Card Emulation (HCE) |
| **Animated QR** | UR-format multi-frame QR codes for large tokens |
| **Peanut Emoji** | Encode tokens as emoji (compatible with cashu.me) |
| **Transaction History** | Full history with filters: Ecash, Lightning, Pending |
| **Pending Tokens** | Save tokens to claim later (up to 50, 30-day expiry) |
| **BTC Price** | Live price via Yadio API with fiat conversion |
| **Your Seed, Your Money** | Backup with 12 words (BIP39) |
| **Optional PIN** | 4-digit PIN to protect wallet access |
| **11 Languages** | ES, EN, PT, FR, RU, DE, IT, KO, ZH, JA, SW |
| **Friendly UX** | Confetti celebrations when receiving funds |
---
## 🚀 Screenshots
## Screenshots
<p align="center">
<img src="assets/img/Captura1.png" width="180"/>
@@ -69,35 +77,42 @@ Cashu is an ecash protocol that enables Bitcoin transactions with maximum privac
---
## 🔧 Technologies
## Technologies
### Frontend
- **Flutter 3.27+**: Cross-platform framework
- **Dart**: Programming language
- **Dart 3.11+**: Programming language
- **Provider**: State management
- **QR Flutter**: QR code generation
- **QR Flutter + Mobile Scanner**: QR generation and scanning
### Backend & Core
- **[cdk-flutter](https://github.com/cashubtc/cdk_flutter)**: Cashu Development Kit (Rust FFI)
- **elcaju_core**: Internal Rust library via [flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge) v2.11.1 (FFI)
- **[CDK](https://github.com/cashubtc/cdk) 0.15.1**: Cashu Development Kit (wallet, SQLite, HTTP client, signatory)
- **SQLite**: Local persistence via cdk-sqlite
- **Flutter Secure Storage**: Secure storage for seed and PIN
- **Flutter Secure Storage**: Encrypted storage for seed and PIN
### Protocols
- **Cashu**: NUT-00 to NUT-13 (ecash protocol)
- **Cashu**: NUT-00, 04, 05, 06, 07, 09, 10 (P2PK), 13 (restore)
- **Lightning Network**: Deposits and withdrawals via BOLT11
- **BIP39**: Seed phrase generation
- **BIP39 / BIP32**: Seed phrase generation and key derivation
- **NIP-06**: Nostr key derivation from mnemonic for P2PK
### Native
- **NFC Manager + HCE**: Phone-to-phone token transfers
- **Android NDK 27.0**: Multi-architecture native compilation
- **Cargokit**: Automated Rust build integration
---
## 📦 Installation
## Installation
### Prerequisites
- Flutter SDK (>=3.27.0)
- Dart SDK (>=3.6.0)
- Dart SDK (>=3.11.0)
- Android Studio with Android SDK
- Android NDK 26.3+ (install via Android Studio > SDK Manager > SDK Tools)
- Rust toolchain (>=1.85.0)
- Android NDK 27.0+ (install via Android Studio > SDK Manager > SDK Tools)
- Rust toolchain (edition 2024; rustc >= 1.85.0)
### Clone the Repository
@@ -118,7 +133,7 @@ flutter pub get
# Install Rust if not already installed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Update to latest stable (requires >= 1.85.0)
# Update to latest stable
rustup update stable
# Install Android targets
@@ -137,61 +152,39 @@ linker = "gcc"
rustflags = ["-C", "link-arg=-fuse-ld=bfd"]
[target.aarch64-linux-android]
linker = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
linker = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
[target.armv7-linux-androideabi]
linker = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
linker = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
[target.x86_64-linux-android]
linker = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang"
linker = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang"
[env]
CC_armv7-linux-androideabi = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
AR_armv7-linux-androideabi = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_x86_64-linux-android = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang"
AR_x86_64-linux-android = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_aarch64-linux-android = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
AR_aarch64-linux-android = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_armv7-linux-androideabi = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
AR_armv7-linux-androideabi = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_x86_64-linux-android = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang"
AR_x86_64-linux-android = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_aarch64-linux-android = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
AR_aarch64-linux-android = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
```
> **Note**: Replace `/path/to/Android/Sdk` with your actual Android SDK path (usually `~/Android/Sdk` on Linux or `~/Library/Android/sdk` on macOS).
### Compile Native Libraries
The cdk_flutter native libraries must be compiled manually:
```bash
# Navigate to cdk_flutter rust directory
cd ~/.pub-cache/git/cdk_flutter-*/rust
# Build for each architecture
cargo build --release --target aarch64-linux-android
cargo build --release --target armv7-linux-androideabi
cargo build --release --target x86_64-linux-android
# Copy libraries to project
mkdir -p /path/to/elcaju/android/app/src/main/jniLibs/{arm64-v8a,armeabi-v7a,x86_64}
cp target/aarch64-linux-android/release/libcdk_flutter.so \
/path/to/elcaju/android/app/src/main/jniLibs/arm64-v8a/
cp target/armv7-linux-androideabi/release/libcdk_flutter.so \
/path/to/elcaju/android/app/src/main/jniLibs/armeabi-v7a/
cp target/x86_64-linux-android/release/libcdk_flutter.so \
/path/to/elcaju/android/app/src/main/jniLibs/x86_64/
```
### Run in Development
The Rust library compiles automatically via Cargokit during `flutter run`:
```bash
flutter run
```
> **Note**: The first build will take several minutes while Rust compiles. Subsequent builds are incremental.
### Build for Production
```bash
# Android APK (includes all architectures, ~55MB)
# Android APK (includes all architectures)
flutter build apk --release
# Android App Bundle (recommended for Play Store)
@@ -203,100 +196,123 @@ flutter build appbundle --release
For faster builds targeting only modern devices:
```bash
# Build only arm64 library
cargo build --release --target aarch64-linux-android
# Copy to project
cp target/aarch64-linux-android/release/libcdk_flutter.so \
android/app/src/main/jniLibs/arm64-v8a/
# Build APK (~40MB)
flutter build apk --release
flutter build apk --release --target-platform android-arm64
```
---
## 🏗️ Architecture
## Architecture
```
```text
┌─────────────────────────────────────────┐
│ Flutter UI (Dart)
│ Provider + Streams + Intl
│ Flutter UI (Dart 3.11+)
│ Provider + Streams + Intl (11 langs)
├─────────────────────────────────────────┤
WalletProvider / SettingsProvider
(Multi-unit: Map<mintUrl:unit>)
4 Providers: Wallet, Settings,
Price (Yadio), P2PK (NIP-06)
├─────────────────────────────────────────┤
cdk-flutter (FFI)
│ flutter_rust_bridge v2.11.1 │
flutter_rust_bridge v2.11.1 (FFI)
├─────────────────────────────────────────┤
Rust Core (CDK)
Cashu 0.13.4 + SQLite + Bitcoin
elcaju_core (Rust interno)
CDK 0.15.1 + SQLite + BIP39 + Tokio
└─────────────────────────────────────────┘
```
### Project Structure
```
```text
elcaju/
├── lib/
│ ├── main.dart # Entry point
│ ├── main.dart # Entry point (RustLib.init + Providers)
│ ├── core/
│ │ ├── constants/ # Colors, dimensions
│ │ ├── theme/ # App theme
│ │ ── utils/ # Formatters, validators
│ │ ├── constants/ # Colors, dimensions
│ │ ├── models/ # Proof model
│ │ ── services/ # LNURL, NFC, price, proofs
│ │ ├── theme/ # Material 3 dark theme
│ │ └── utils/ # Formatters, parsers, nostr, p2pk, peanut codec
│ ├── data/ # Pending tokens, transaction metadata storage
│ ├── models/ # P2PK key model (npub/nsec)
│ ├── providers/
│ │ ├── wallet_provider.dart # Wallet logic
│ │ ── settings_provider.dart# Preferences and PIN
│ │ ├── wallet_provider.dart # Multi-mint, multi-unit wallet logic
│ │ ── settings_provider.dart # Preferences, PIN, seed
│ │ ├── price_provider.dart # BTC price cache (Yadio API)
│ │ └── p2pk_provider.dart # Nostr key management (NIP-06)
│ ├── screens/
│ │ ├── 1_splash/ # Loading screen
│ │ ├── 2_onboarding/ # Create/restore wallet
│ │ ├── 3_home/ # Main screen
│ │ ├── 4_receive/ # Receive tokens
│ │ ├── 5_send/ # Send tokens
│ │ ├── 6_mint/ # Deposit via Lightning
│ │ ├── 7_melt/ # Withdraw to Lightning
│ │ ── 8_settings/ # Settings
│ │ ├── 1_splash/ # Loading & initialization
│ │ ├── 2_onboarding/ # Create / restore / backup seed
│ │ ├── 3_home/ # Main dashboard
│ │ ├── 4_receive/ # Receive tokens (paste, QR, NFC)
│ │ ├── 5_send/ # Send tokens + offline mode + share
│ │ ├── 6_mint/ # Lightning deposit
│ │ ├── 7_melt/ # Lightning withdrawal
│ │ ── 8_settings/ # Settings, mints, P2PK keys, language
│ │ ├── 9_history/ # Transaction history with filters
│ │ └── 10_scanner/ # QR code scanner
│ ├── src/rust/ # FFI bridge (auto-generated + API bindings)
│ ├── widgets/
│ │ ├── common/ # Buttons, cards, etc.
│ │ ── effects/ # Confetti and animations
└── l10n/ # Translations ES/EN
└── assets/
└── img/ # Logo and assets
│ │ ├── common/ # Buttons, cards, numpad, backgrounds
│ │ ── effects/ # Confetti animation
│ ├── proof/ # Proof selector (offline send)
│ │ └── 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
│ └── src/api/ # wallet, token, keys, mint_info, error
├── rust_builder/ # Cargokit integration (auto Rust compilation)
├── android/ # Android config (NDK 27.0, NFC HCE service)
└── assets/img/ # Logo and screenshots
```
---
## 🔐 Security
## Cashu NUT Support
| NUT | Name | Status |
|-----|------|--------|
| NUT-00 | Cashu Protocol | Supported |
| NUT-04 | Mint (Lightning deposit) | Supported |
| NUT-05 | Melt (Lightning withdrawal) | Supported |
| NUT-06 | Token swap | Supported |
| NUT-07 | Spend conditions (witness) | Supported |
| NUT-09 | Token restore | Supported |
| NUT-10 | P2PK (locked tokens) | Supported |
| NUT-13 | Deterministic secrets (recovery) | Supported |
---
## Security
| Feature | Implementation |
|---------|----------------|
| **Seed Phrase** | Stored with Flutter Secure Storage (native encryption) |
| **PIN** | Secure hash, local verification |
| **Local Data** | SQLite with proofs encrypted by Cashu protocol |
| **Seed Phrase** | BIP39 12 words, stored with Flutter Secure Storage (native encryption) |
| **Key Derivation** | BIP32 + NIP-06 (m/44'/1237'/0'/0/0) for P2PK keys |
| **PIN** | 4-digit PIN with secure hash, local verification |
| **Local Data** | SQLite with proofs managed by CDK |
| **P2PK Keys** | Primary (from seed) + up to 10 imported keys, all encrypted |
| **No Tracking** | We don't collect user data |
| **Open Source** | 100% auditable |
### Important
> ⚠️ **Backup your seed phrase (12 words)**. Without it, you won't be able to recover your funds if you lose your device.
> **Backup your seed phrase (12 words)**. Without it, you won't be able to recover your funds if you lose your device.
---
## 📱 Compatibility
## Compatibility
| Platform | Status | Minimum Version |
|----------|--------|-----------------|
| Android | Supported | API 24 (Android 7.0) |
| 🚧 iOS | Coming soon | iOS 12.0+ |
| 🚧 Web | Coming soon | - |
| Android | Supported | API 24 (Android 7.0) |
| iOS | Coming soon | iOS 11.0+ |
| Web | Coming soon | - |
---
## 🌐 Default Mint
## Default Mint
ElCaju comes preconfigured with the Cuba Bitcoin mint:
```
```text
https://mint.cubabitcoin.org
```
@@ -304,7 +320,16 @@ You can add other Cashu mints from the settings.
---
## 🤝 Contributing
## CI/CD
Automated releases via GitHub Actions (`.github/workflows/release.yml`):
- Triggers on Git tags (`v*`)
- Builds multi-architecture APKs (arm64, armv7, x86_64) + universal APK
- Creates GitHub releases automatically
---
## Contributing
Contributions are welcome!
@@ -324,16 +349,17 @@ Open an [issue](https://github.com/Forte11Cuba/elcaju/issues) with:
---
## 📄 License
## License
This project is under the MIT License - see the [LICENSE](LICENSE) file for more details.
---
## 🙏 Credits
## Credits
- **[Cashu](https://cashu.space)** - Ecash protocol
- **[cdk-flutter](https://github.com/cashubtc/cdk_flutter)** - Cashu Development Kit
- **[CDK](https://github.com/cashubtc/cdk)** - Cashu Development Kit
- **[flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge)** - Rust-Dart FFI
- **[Cashu4Community](https://cashu4community.xyz)** - Global Cashu community
- **[Cuba Bitcoin](https://cubabitcoin.org)** - Cuban Bitcoin community
+1 -1
View File
@@ -21,7 +21,7 @@ android {
defaultConfig {
applicationId "me.elcaju"
minSdk 21 // Required for FlutterSecureStorage (EncryptedSharedPreferences)
minSdkVersion flutter.minSdkVersion // Required for FlutterSecureStorage (EncryptedSharedPreferences)
targetSdk flutter.targetSdkVersion
versionCode flutter.versionCode
versionName flutter.versionName
+16
View File
@@ -7,6 +7,9 @@
<!-- Cámara opcional: la app funciona sin ella (pegando desde portapapeles) -->
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<!-- NFC para compartir tokens Cashu -->
<uses-permission android:name="android.permission.NFC"/>
<uses-feature android:name="android.hardware.nfc" android:required="false"/>
<application
android:label="ElCaju"
@@ -34,6 +37,19 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- HCE service: phone emulates NFC tag for phone-to-phone transfers -->
<service
android:name=".NfcHceService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/hce_nfc" />
</service>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
@@ -1,5 +1,60 @@
package me.elcaju
import android.content.ComponentName
import android.nfc.NfcAdapter
import android.nfc.cardemulation.CardEmulation
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
private val CHANNEL = "me.elcaju/nfc_hce"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"setPayload" -> {
val payload = call.argument<ByteArray>("payload")
NfcHceService.ndefPayload = payload
// Force Android to route the NDEF AID to our service
// instead of manufacturer services (Xiaomi Mi Share, etc.)
try {
val adapter = NfcAdapter.getDefaultAdapter(this)
if (adapter != null) {
val cardEmulation = CardEmulation.getInstance(adapter)
cardEmulation.setPreferredService(
this,
ComponentName(this, NfcHceService::class.java)
)
}
} catch (e: Exception) {
Log.w("MainActivity", "setPreferredService failed (non-critical)", e)
}
result.success(true)
}
"clearPayload" -> {
NfcHceService.ndefPayload = null
// Release preferred service routing
try {
val adapter = NfcAdapter.getDefaultAdapter(this)
if (adapter != null) {
val cardEmulation = CardEmulation.getInstance(adapter)
cardEmulation.unsetPreferredService(this)
}
} catch (e: Exception) {
Log.w("MainActivity", "unsetPreferredService failed (non-critical)", e)
}
result.success(true)
}
else -> result.notImplemented()
}
}
}
}
@@ -0,0 +1,140 @@
package me.elcaju
import android.nfc.cardemulation.HostApduService
import android.os.Bundle
import android.util.Log
/**
* Host Card Emulation service that makes the phone act as an NFC Forum Type 4 tag.
* Another phone in reader mode can tap and read the NDEF message we serve.
*
* The payload is set from Flutter via MainActivity's MethodChannel.
*/
class NfcHceService : HostApduService() {
companion object {
private const val TAG = "NfcHceService"
// Shared payload - set by Flutter via MethodChannel
@Volatile
var ndefPayload: ByteArray? = null
private val OK = byteArrayOf(0x90.toByte(), 0x00.toByte())
private val NOT_FOUND = byteArrayOf(0x6A.toByte(), 0x82.toByte())
// Capability Container (CC) file - fixed structure
// MLe=0xFF (255), MLc=0xFF (255), max NDEF=0x70FF (28671)
// Compatible with Numo
private val CC_FILE = byteArrayOf(
0x00, 0x0F, // CC length (15)
0x20, // Mapping version 2.0
0x00, 0xFF.toByte(), // MLe: max read 255 bytes
0x00, 0xFF.toByte(), // MLc: max write 255 bytes
0x04, 0x06, // NDEF File Control TLV
0xE1.toByte(), 0x04, // NDEF file ID
0x70, 0xFF.toByte(), // Max NDEF size: 28671 bytes
0x00, // Read access: open
0xFF.toByte() // Write access: denied
)
}
private var selectedFile: String = "none"
private var ndefFileCache: ByteArray? = null
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
if (commandApdu.size < 4) return NOT_FOUND
val ins = commandApdu[1]
// SELECT command
if (ins == 0xA4.toByte()) {
return handleSelect(commandApdu)
}
// READ BINARY command
if (ins == 0xB0.toByte()) {
return handleRead(commandApdu)
}
return NOT_FOUND
}
private fun handleSelect(apdu: ByteArray): ByteArray {
// Select ElCaju proprietary AID (F04543414A5500)
// Avoids conflicts with Xiaomi Mi Share / Samsung Beam
if (apdu.size >= 12 && apdu[5] == 0xF0.toByte() && apdu[6] == 0x45.toByte()) {
selectedFile = "app"
Log.d(TAG, "Selected ElCaju Application (proprietary AID)")
return OK
}
// Select NDEF Application (AID: D2760000850101)
if (apdu.size >= 12 && apdu[5] == 0xD2.toByte() && apdu[6] == 0x76.toByte()) {
selectedFile = "app"
Log.d(TAG, "Selected NDEF Application")
return OK
}
// Select by file ID
if (apdu.size >= 7) {
val fileId = ((apdu[5].toInt() and 0xFF) shl 8) or (apdu[6].toInt() and 0xFF)
when (fileId) {
0xE103 -> {
selectedFile = "cc"
Log.d(TAG, "Selected CC file")
return OK
}
0xE104 -> {
selectedFile = "ndef"
// Cache the NDEF file when selected
val payload = ndefPayload
if (payload != null) {
val file = ByteArray(2 + payload.size)
file[0] = (payload.size shr 8).toByte()
file[1] = (payload.size and 0xFF).toByte()
System.arraycopy(payload, 0, file, 2, payload.size)
ndefFileCache = file
Log.d(TAG, "Selected NDEF file (${payload.size} bytes payload)")
} else {
ndefFileCache = null
Log.w(TAG, "Selected NDEF file but no payload set")
}
return OK
}
}
}
return NOT_FOUND
}
private fun handleRead(apdu: ByteArray): ByteArray {
if (apdu.size < 5) return NOT_FOUND
val offset = ((apdu[2].toInt() and 0xFF) shl 8) or (apdu[3].toInt() and 0xFF)
// Le=0x00 means 256 bytes in short APDU encoding
var length = apdu[4].toInt() and 0xFF
if (length == 0) length = 256
val data = when (selectedFile) {
"cc" -> CC_FILE
"ndef" -> ndefFileCache ?: return NOT_FOUND
else -> return NOT_FOUND
}
if (offset >= data.size) {
Log.w(TAG, "Read offset $offset beyond data size ${data.size}")
return NOT_FOUND
}
val end = minOf(offset + length, data.size)
val response = data.copyOfRange(offset, end)
Log.d(TAG, "Read $selectedFile: offset=$offset len=$length returned=${response.size} bytes")
return response + OK
}
override fun onDeactivated(reason: Int) {
Log.d(TAG, "Deactivated (reason=$reason)")
selectedFile = "none"
ndefFileCache = null
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hce_description">ElCaju NFC</string>
</resources>
+12
View File
@@ -0,0 +1,12 @@
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/hce_description"
android:requireDeviceUnlock="false">
<aid-group
android:category="other"
android:description="@string/hce_description">
<!-- ElCaju proprietary AID (avoids Xiaomi/Samsung NFC service conflicts) -->
<aid-filter android:name="F04543414A5500" />
<!-- NDEF Tag Application AID (NFC Forum Type 4) -->
<aid-filter android:name="D2760000850101" />
</aid-group>
</host-apdu-service>
+3
View File
@@ -0,0 +1,3 @@
rust_input: crate::api
rust_root: rust/
dart_output: lib/src/rust
+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
@@ -2,4 +2,3 @@ arb-dir: lib/l10n
template-arb-file: app_es.arb
output-localization-file: app_localizations.dart
output-class: L10n
synthetic-package: false
+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
+485
View File
@@ -0,0 +1,485 @@
import 'dart:io' show Platform;
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:nfc_manager/nfc_manager.dart';
import 'package:nfc_manager/nfc_manager_android.dart';
import 'package:ndef_record/ndef_record.dart';
/// NFC availability state for UI rendering.
enum NfcState {
/// Device does not have NFC hardware.
unsupported,
/// Device has NFC but it is disabled in settings.
disabled,
/// NFC is supported and enabled.
enabled,
}
/// Service for reading and writing Cashu tokens via NFC.
///
/// Writes tokens as NDEF Text Records for maximum compatibility
/// with cashu.me and Numo. Reads both Text and URI records.
class NfcService {
static const _hceChannel = MethodChannel('me.elcaju/nfc_hce');
// ─── HCE (phone-to-phone) ───
/// Start emulating an NFC tag with the given text payload.
/// The phone will appear as an NFC tag to other devices.
static Future<void> startEmulating(String text) async {
final ndefMessage = _buildNdefTextMessage(text);
await _hceChannel.invokeMethod('setPayload', {'payload': ndefMessage});
}
/// Stop emulating an NFC tag.
static Future<void> stopEmulating() async {
await _hceChannel.invokeMethod('clearPayload');
}
/// Build an NDEF message with a Text Record.
/// Uses Short Record (SR) for payloads ≤ 255 bytes,
/// Long Record for > 255 bytes (compatible with Numo).
static Uint8List _buildNdefTextMessage(String text) {
final textBytes = Uint8List.fromList(text.codeUnits);
final languageCode = Uint8List.fromList('en'.codeUnits);
// NDEF Text Record payload: [status byte][lang code][text]
final recordPayload = Uint8List(1 + languageCode.length + textBytes.length);
recordPayload[0] = languageCode.length;
recordPayload.setRange(1, 1 + languageCode.length, languageCode);
recordPayload.setRange(1 + languageCode.length, recordPayload.length, textBytes);
final payloadLength = recordPayload.length;
final isShortRecord = payloadLength <= 255;
if (isShortRecord) {
// Short Record: flags(1) + typeLen(1) + payloadLen(1) + type(1) + payload
final record = Uint8List(4 + payloadLength);
record[0] = 0xD1; // MB|ME|SR|TNF=well-known
record[1] = 1; // type length
record[2] = payloadLength;
record[3] = 0x54; // 'T'
record.setRange(4, 4 + payloadLength, recordPayload);
return record;
} else {
// Long Record: flags(1) + typeLen(1) + payloadLen(4) + type(1) + payload
final record = Uint8List(7 + payloadLength);
record[0] = 0xC1; // MB|ME|TNF=well-known (no SR flag)
record[1] = 1; // type length
record[2] = (payloadLength >> 24) & 0xFF;
record[3] = (payloadLength >> 16) & 0xFF;
record[4] = (payloadLength >> 8) & 0xFF;
record[5] = payloadLength & 0xFF;
record[6] = 0x54; // 'T'
record.setRange(7, 7 + payloadLength, recordPayload);
return record;
}
}
// ─── Tag read/write ───
/// Check NFC state on the device.
static Future<NfcState> checkState() async {
if (!Platform.isAndroid) return NfcState.unsupported;
try {
final availability = await NfcManager.instance.checkAvailability();
return switch (availability) {
NfcAvailability.enabled => NfcState.enabled,
NfcAvailability.disabled => NfcState.disabled,
NfcAvailability.unsupported => NfcState.unsupported,
};
} catch (_) {
return NfcState.unsupported;
}
}
/// Write a Cashu token to an NFC tag as NDEF Text Record.
static Future<void> startWrite({
required String token,
required void Function() onSuccess,
required void Function(String error) onError,
}) async {
try {
await NfcManager.instance.startSession(
pollingOptions: {NfcPollingOption.iso14443},
onDiscovered: (NfcTag tag) async {
final ndef = NdefAndroid.from(tag);
if (ndef == null || !ndef.isWritable) {
onError('Tag is not writable');
await NfcManager.instance.stopSession();
return;
}
// Build NDEF Text Record: [status byte][language code][text]
final textBytes = Uint8List.fromList(token.codeUnits);
final languageCode = Uint8List.fromList('en'.codeUnits);
final payload = Uint8List(1 + languageCode.length + textBytes.length);
payload[0] = languageCode.length; // status byte (UTF-8, no length)
payload.setRange(1, 1 + languageCode.length, languageCode);
payload.setRange(1 + languageCode.length, payload.length, textBytes);
// Check size
if (payload.length + 7 > ndef.maxSize) {
onError('Token too large for this NFC tag '
'(${payload.length + 7}B > ${ndef.maxSize}B)');
await NfcManager.instance.stopSession();
return;
}
try {
final message = NdefMessage(records: [
NdefRecord(
typeNameFormat: TypeNameFormat.wellKnown,
type: Uint8List.fromList([0x54]), // 'T' = Text Record
identifier: Uint8List(0),
payload: payload,
),
]);
await ndef.writeNdefMessage(message);
onSuccess();
await NfcManager.instance.stopSession();
} catch (e) {
onError(e.toString());
await NfcManager.instance.stopSession();
}
},
);
} catch (e) {
onError(e.toString());
}
}
/// Stop any active NFC session.
static Future<void> stopWrite() async {
await NfcManager.instance.stopSession();
}
/// Start reading NFC tags for Cashu tokens.
/// Tries IsoDep APDU first (phone-to-phone HCE, bypasses manufacturer
/// NFC services), falls back to NDEF (physical tags).
static Future<void> startRead({
required void Function(String token) onTokenRead,
required void Function(String error) onError,
}) async {
try {
await NfcManager.instance.startSession(
pollingOptions: {NfcPollingOption.iso14443},
onDiscovered: (NfcTag tag) async {
try {
final diagnostics = <String>[];
// 1. Try IsoDep first (HCE phone-to-phone)
final isoDep = IsoDepAndroid.from(tag);
if (isoDep != null) {
final (token, isoInfo) = await _readViaIsoDep(isoDep);
if (token != null) {
onTokenRead(token);
await NfcManager.instance.stopSession();
return;
}
diagnostics.add('IsoDep: $isoInfo');
} else {
diagnostics.add('IsoDep: not available');
}
// 2. Fallback: NDEF (physical tags)
final ndef = NdefAndroid.from(tag);
if (ndef != null) {
final message = ndef.cachedNdefMessage ?? await ndef.getNdefMessage();
if (message != null) {
final token = _extractToken(message);
if (token != null) {
onTokenRead(token);
await NfcManager.instance.stopSession();
return;
}
diagnostics.add('NDEF: ${message.records.length} records, no Cashu token');
} else {
diagnostics.add('NDEF: no message');
}
} else {
diagnostics.add('NDEF: not available');
}
onError('No Cashu token found [${diagnostics.join('; ')}]');
await NfcManager.instance.stopSession();
} catch (e) {
onError(e.toString());
await NfcManager.instance.stopSession();
}
},
);
} catch (e) {
onError(e.toString());
}
}
/// Read NDEF from HCE via IsoDep APDU commands (like Numo).
/// Bypasses manufacturer NFC services (Xiaomi, Samsung, etc).
/// Returns (token, diagnosticInfo) tuple.
static Future<(String?, String)> _readViaIsoDep(IsoDepAndroid isoDep) async {
String _hex(Uint8List bytes) =>
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
try {
// Set longer timeout (5s) to avoid TagLostException
await isoDep.setTimeout(5000);
// Try ElCaju proprietary AID first (avoids Xiaomi/Samsung conflicts)
final selectElCaju = await isoDep.transceive(Uint8List.fromList([
0x00, 0xA4, 0x04, 0x00, 0x07,
0xF0, 0x45, 0x43, 0x41, 0x4A, 0x55, 0x00,
0x00,
]));
final bool elCajuSelected = _isOk(selectElCaju);
// Fallback: standard NDEF Application AID
if (!elCajuSelected) {
final selectApp = await isoDep.transceive(Uint8List.fromList([
0x00, 0xA4, 0x04, 0x00, 0x07,
0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01,
0x00,
]));
if (!_isOk(selectApp)) {
return (null, 'SELECT APP failed (both AIDs): elcaju=${_hex(selectElCaju)}, ndef=${_hex(selectApp)}');
}
}
// SELECT NDEF file (E104)
final selectNdef = await isoDep.transceive(Uint8List.fromList([
0x00, 0xA4, 0x00, 0x0C, 0x02,
0xE1, 0x04,
]));
if (!_isOk(selectNdef)) {
return (null, 'SELECT NDEF failed: ${_hex(selectNdef)}');
}
// READ NLEN (first 2 bytes)
final nlenResponse = await isoDep.transceive(Uint8List.fromList([
0x00, 0xB0, 0x00, 0x00, 0x02,
]));
if (nlenResponse.length < 4 || !_isOk(nlenResponse)) {
return (null, 'READ NLEN failed: ${_hex(nlenResponse)}');
}
final ndefLen = (nlenResponse[0] << 8) | nlenResponse[1];
if (ndefLen == 0) {
return (null, 'NLEN=0 (no payload set on emitter)');
}
// READ NDEF message in chunks (max 255 bytes per read)
final ndefBytes = BytesBuilder();
var offset = 2; // skip NLEN
var remaining = ndefLen;
while (remaining > 0) {
final chunkSize = remaining > 255 ? 255 : remaining;
final readCmd = Uint8List.fromList([
0x00, 0xB0,
(offset >> 8) & 0xFF,
offset & 0xFF,
chunkSize,
]);
final chunk = await isoDep.transceive(readCmd);
if (chunk.length < 2 || !_isOk(chunk)) {
return (null, 'READ chunk at offset=$offset failed: ${_hex(chunk)}');
}
// Response = data + SW1 SW2 (last 2 bytes are status)
final bytesRead = chunk.length - 2;
if (bytesRead == 0) {
return (null, 'READ chunk at offset=$offset returned no data');
}
ndefBytes.add(chunk.sublist(0, bytesRead));
offset += bytesRead;
remaining -= bytesRead;
}
// Parse NDEF message from raw bytes
final raw = ndefBytes.toBytes();
if (raw.isEmpty) {
return (null, 'read ${ndefLen}B but empty after parsing');
}
final token = _parseNdefAndExtractToken(Uint8List.fromList(raw));
if (token != null) {
return (token, 'OK');
}
return (null, 'NDEF parsed (${raw.length}B) but no Cashu token, hex: ${_hex(Uint8List.fromList(raw.length > 40 ? raw.sublist(0, 40) : raw))}...');
} catch (e) {
final msg = e.toString();
if (msg.contains('TagLostException')) {
return (null, 'connection lost (hold phones together longer)');
}
return (null, 'error: $msg');
}
}
/// Check if APDU response ends with 90 00 (OK).
static bool _isOk(Uint8List response) {
if (response.length < 2) return false;
return response[response.length - 2] == 0x90 &&
response[response.length - 1] == 0x00;
}
/// Parse raw NDEF bytes and extract Cashu token.
static String? _parseNdefAndExtractToken(Uint8List raw) {
if (raw.isEmpty) return null;
var pos = 0;
while (pos < raw.length) {
if (pos + 3 > raw.length) break;
final flags = raw[pos];
final tnf = flags & 0x07;
final isShortRecord = (flags & 0x10) != 0;
final hasIdLength = (flags & 0x08) != 0;
pos++;
final typeLength = raw[pos]; pos++;
int payloadLength;
if (isShortRecord) {
payloadLength = raw[pos]; pos++;
} else {
if (pos + 4 > raw.length) break;
payloadLength = (raw[pos] << 24) | (raw[pos+1] << 16) |
(raw[pos+2] << 8) | raw[pos+3];
pos += 4;
}
int idLength = 0;
if (hasIdLength) {
idLength = raw[pos]; pos++;
}
// Type
if (pos + typeLength > raw.length) break;
final type = raw.sublist(pos, pos + typeLength); pos += typeLength;
// ID (skip)
pos += idLength;
// Payload
if (pos + payloadLength > raw.length) break;
final payload = raw.sublist(pos, pos + payloadLength);
pos += payloadLength;
// Check: Text Record (TNF=0x01, type='T')
if (tnf == 0x01 && typeLength == 1 && type[0] == 0x54) {
if (payload.isNotEmpty) {
final langLen = payload[0] & 0x3F;
if (payload.length > 1 + langLen) {
final text = String.fromCharCodes(payload.sublist(1 + langLen));
if (_isCashuToken(text)) return text;
}
}
}
// Check: URI Record (TNF=0x01, type='U')
if (tnf == 0x01 && typeLength == 1 && type[0] == 0x55) {
if (payload.isNotEmpty) {
const prefixes = ['', 'http://www.', 'https://www.', 'http://', 'https://'];
final prefixIdx = payload[0];
final prefix = prefixIdx < prefixes.length ? prefixes[prefixIdx] : '';
final rest = String.fromCharCodes(payload.sublist(1));
final uri = '$prefix$rest';
final token = _extractTokenFromUri(uri);
if (token != null) return token;
}
}
// If ME (Message End) flag set, stop
if ((flags & 0x40) != 0) break;
}
return null;
}
/// Stop any active NFC read session.
static Future<void> stopRead() async {
await NfcManager.instance.stopSession();
}
/// Extract a Cashu token from an NDEF message.
static String? _extractToken(NdefMessage message) {
for (final record in message.records) {
// Text Record
if (record.typeNameFormat == TypeNameFormat.wellKnown &&
record.type.length == 1 &&
record.type[0] == 0x54) {
final text = _decodeTextRecord(record);
if (text != null && _isCashuToken(text)) {
return text;
}
}
// URI Record
if (record.typeNameFormat == TypeNameFormat.wellKnown &&
record.type.length == 1 &&
record.type[0] == 0x55) {
final uri = _decodeUriRecord(record);
if (uri != null) {
final token = _extractTokenFromUri(uri);
if (token != null) return token;
}
}
}
return null;
}
/// Decode NDEF Text Record payload.
static String? _decodeTextRecord(NdefRecord record) {
if (record.payload.isEmpty) return null;
final languageCodeLength = record.payload[0] & 0x3F;
if (record.payload.length <= 1 + languageCodeLength) return null;
return String.fromCharCodes(
record.payload.sublist(1 + languageCodeLength));
}
/// Decode NDEF URI Record payload.
static String? _decodeUriRecord(NdefRecord record) {
if (record.payload.isEmpty) return null;
const prefixes = [
'', // 0x00
'http://www.', // 0x01
'https://www.', // 0x02
'http://', // 0x03
'https://', // 0x04
];
final prefixIndex = record.payload[0];
final prefix = prefixIndex < prefixes.length ? prefixes[prefixIndex] : '';
final rest = String.fromCharCodes(record.payload.sublist(1));
return '$prefix$rest';
}
/// Extract token from a URL like https://wallet.com/#token=cashuA...
static String? _extractTokenFromUri(String uri) {
try {
final parsed = Uri.parse(uri);
final fragmentParams = Uri.splitQueryString(parsed.fragment);
final fragmentToken = fragmentParams['token'];
if (fragmentToken != null && _isCashuToken(fragmentToken)) {
return fragmentToken;
}
final tokenParam = parsed.queryParameters['token'];
if (tokenParam != null && _isCashuToken(tokenParam)) {
return tokenParam;
}
} catch (_) {}
return null;
}
/// 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('creqb') ||
lower.startsWith('bitcoin:');
}
}
+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();
}
+67 -5
View File
@@ -1,5 +1,7 @@
// Parser para detectar el tipo de dato entrante (QR, clipboard, etc.)
// Soporta: tokens Cashu (A/B), invoices Lightning, URLs de mint, payment requests
// Soporta: tokens Cashu (A/B), invoices Lightning, URLs de mint, payment requests, peanut emoji
import 'peanut_codec.dart';
/// Modo de escaneo
enum ScanMode {
@@ -13,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,
}
@@ -57,6 +59,15 @@ class IncomingDataParser {
/// Detecta el tipo de dato y extrae información relevante
static ParsedData parse(String data) {
final trimmed = data.trim();
// Peanut-encoded token (🥜 + variation selectors)
if (PeanutCodec.isPeanut(trimmed)) {
final decoded = PeanutCodec.decode(trimmed);
if (decoded != null) {
return parse(decoded);
}
}
final lower = trimmed.toLowerCase();
// Token Cashu (cashuA... o cashuB...)
@@ -92,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,
@@ -182,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;
}
}
+62
View File
@@ -0,0 +1,62 @@
/// Peanut encoding/decoding for Cashu tokens.
///
/// Encodes Cashu tokens into invisible Unicode Variation Selectors
/// appended to a 🥜 emoji, compatible with cashu.me's implementation.
///
/// Each character of the base64 token is mapped to a Variation Selector:
/// - charCode 0-15 → VS1-VS16 (U+FE00 to U+FE0F)
/// - charCode 16-255 → VS17-VS256 (U+E0100 to U+E01EF)
class PeanutCodec {
static const String _peanutEmoji = '🥜';
/// Encodes a Cashu token string into peanut format (🥜 + variation selectors).
static String encode(String token) {
final buffer = StringBuffer(_peanutEmoji);
for (int i = 0; i < token.length; i++) {
final byte = token.codeUnitAt(i);
if (byte < 16) {
// VS1-VS16: U+FE00 to U+FE0F
buffer.writeCharCode(0xFE00 + byte);
} else if (byte < 256) {
// VS17-VS256: U+E0100 to U+E01EF
buffer.writeCharCode(0xE0100 + (byte - 16));
}
}
return buffer.toString();
}
/// Decodes a peanut-encoded string back to the original Cashu token.
/// Returns null if the input is not valid peanut format.
static String? decode(String peanut) {
final trimmed = peanut.trimLeft();
if (!trimmed.startsWith(_peanutEmoji)) return null;
final buffer = StringBuffer();
final runes = trimmed.runes.toList();
// Skip the first rune (🥜 emoji)
for (int i = 1; i < runes.length; i++) {
final codePoint = runes[i];
if (codePoint >= 0xFE00 && codePoint <= 0xFE0F) {
// VS1-VS16 → byte 0-15
buffer.writeCharCode(codePoint - 0xFE00);
} else if (codePoint >= 0xE0100 && codePoint <= 0xE01EF) {
// VS17-VS256 → byte 16-255
buffer.writeCharCode(codePoint - 0xE0100 + 16);
}
// Skip any other characters (whitespace, etc.)
}
final result = buffer.toString();
return result.isNotEmpty ? result : null;
}
/// Returns true if the string starts with the 🥜 emoji.
static bool isPeanut(String data) {
return data.trimLeft().startsWith(_peanutEmoji);
}
}
+41 -3
View File
@@ -137,6 +137,20 @@
},
"keepTokenWarning": "Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.",
"tokenCopiedToClipboard": "Token in Zwischenablage kopiert",
"copyAsEmoji": "Als Emoji kopieren",
"emojiCopiedToClipboard": "Token als Emoji kopiert 🥜",
"peanutDecodeError": "Emoji-Token konnte nicht dekodiert werden. Möglicherweise beschädigt.",
"nfcWrite": "Auf NFC-Tag schreiben",
"nfcRead": "NFC-Tag lesen",
"nfcHoldNear": "Gerät an NFC-Tag halten...",
"nfcWriteSuccess": "Token auf NFC-Tag geschrieben",
"nfcWriteError": "NFC-Schreibfehler: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "NFC-Lesefehler: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC ist deaktiviert. Aktiviere es in den Einstellungen.",
"nfcUnsupported": "Dieses Gerät unterstützt kein NFC",
"amountToDeposit": "Einzuzahlender Betrag:",
"descriptionOptional": "Beschreibung (optional):",
@@ -296,7 +310,7 @@
}
},
"close": "Schließen",
"aboutDescription": "Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von La Chispa.",
"aboutDescription": "Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von LaChispa.",
"couldNotOpenLink": "Link konnte nicht geöffnet werden",
"deleteWalletQuestion": "Wallet löschen?",
@@ -488,7 +502,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.",
@@ -521,5 +550,14 @@
"p2pkErrorKeyAlreadyExists": "Dieser Schlüssel existiert bereits",
"p2pkErrorKeyNotFound": "Schlüssel nicht gefunden",
"p2pkErrorCannotDeletePrimary": "Primärschlüssel kann nicht gelöscht werden",
"p2pkSendComingSoon": "Demnächst verfügbar"
"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"
}
+53 -3
View File
@@ -112,6 +112,28 @@
"tokenCashuAnimatedQr": "Cashu Token (animated QR - {fragments} UR fragments)",
"keepTokenWarning": "Keep this token until the recipient claims it. If you lose it, you will lose the funds.",
"tokenCopiedToClipboard": "Token copied to clipboard",
"copyAsEmoji": "Copy as emoji",
"emojiCopiedToClipboard": "Token copied as emoji 🥜",
"peanutDecodeError": "Could not decode emoji token. It may be corrupted.",
"nfcWrite": "Write to NFC tag",
"nfcRead": "Read NFC tag",
"nfcHoldNear": "Hold device near NFC tag...",
"nfcWriteSuccess": "Token written to NFC tag",
"nfcWriteError": "NFC write error: {error}",
"@nfcWriteError": {
"placeholders": {
"error": { "type": "String" }
}
},
"nfcReadError": "NFC read error: {error}",
"@nfcReadError": {
"placeholders": {
"error": { "type": "String" }
}
},
"nfcDisabled": "NFC is disabled. Enable it in Settings.",
"nfcUnsupported": "This device does not support NFC",
"amountToDeposit": "Amount to deposit:",
"descriptionOptional": "Description (optional):",
@@ -234,7 +256,7 @@
"selectLanguage": "Select language",
"languageChanged": "Language changed to {language}",
"close": "Close",
"aboutDescription": "A Cashu wallet with Cuban DNA for the entire world. Brother of La Chispa.",
"aboutDescription": "A Cashu wallet with Cuban DNA for the entire world. Brother of LaChispa.",
"couldNotOpenLink": "Could not open link",
"deleteWalletQuestion": "Delete wallet?",
@@ -368,7 +390,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",
@@ -402,5 +443,14 @@
"p2pkErrorKeyAlreadyExists": "This key already exists",
"p2pkErrorKeyNotFound": "Key not found",
"p2pkErrorCannotDeletePrimary": "Cannot delete primary key",
"p2pkSendComingSoon": "Coming soon"
"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"
}
+53 -3
View File
@@ -137,6 +137,28 @@
},
"keepTokenWarning": "Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.",
"tokenCopiedToClipboard": "Token copiado al portapapeles",
"copyAsEmoji": "Copiar como emoji",
"emojiCopiedToClipboard": "Token copiado como emoji 🥜",
"peanutDecodeError": "No se pudo decodificar el token emoji. Puede estar corrupto.",
"nfcWrite": "Escribir en tag NFC",
"nfcRead": "Leer tag NFC",
"nfcHoldNear": "Acerca el dispositivo al tag NFC...",
"nfcWriteSuccess": "Token escrito en tag NFC",
"nfcWriteError": "Error NFC al escribir: {error}",
"@nfcWriteError": {
"placeholders": {
"error": { "type": "String" }
}
},
"nfcReadError": "Error NFC al leer: {error}",
"@nfcReadError": {
"placeholders": {
"error": { "type": "String" }
}
},
"nfcDisabled": "NFC está desactivado. Actívalo en Ajustes.",
"nfcUnsupported": "Este dispositivo no soporta NFC",
"amountToDeposit": "Monto a depositar:",
"descriptionOptional": "Descripción (opcional):",
@@ -296,7 +318,7 @@
}
},
"close": "Cerrar",
"aboutDescription": "Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de La Chispa.",
"aboutDescription": "Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de LaChispa.",
"couldNotOpenLink": "No se pudo abrir el enlace",
"deleteWalletQuestion": "¿Borrar wallet?",
@@ -488,7 +510,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",
@@ -522,5 +563,14 @@
"p2pkErrorKeyAlreadyExists": "Esta clave ya existe",
"p2pkErrorKeyNotFound": "Clave no encontrada",
"p2pkErrorCannotDeletePrimary": "No se puede eliminar la clave principal",
"p2pkSendComingSoon": "Disponible próximamente"
"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"
}
+41 -3
View File
@@ -137,6 +137,20 @@
},
"keepTokenWarning": "Conservez ce token jusqu'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.",
"tokenCopiedToClipboard": "Token copié dans le presse-papiers",
"copyAsEmoji": "Copier en emoji",
"emojiCopiedToClipboard": "Token copié en emoji 🥜",
"peanutDecodeError": "Impossible de décoder le token emoji. Il est peut-être corrompu.",
"nfcWrite": "Écrire sur tag NFC",
"nfcRead": "Lire tag NFC",
"nfcHoldNear": "Approchez l'appareil du tag NFC...",
"nfcWriteSuccess": "Token écrit sur le tag NFC",
"nfcWriteError": "Erreur NFC écriture : {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Erreur NFC lecture : {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC est désactivé. Activez-le dans les Paramètres.",
"nfcUnsupported": "Cet appareil ne prend pas en charge le NFC",
"amountToDeposit": "Montant à déposer :",
"descriptionOptional": "Description (optionnelle) :",
@@ -296,7 +310,7 @@
}
},
"close": "Fermer",
"aboutDescription": "Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de La Chispa.",
"aboutDescription": "Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de LaChispa.",
"couldNotOpenLink": "Impossible d'ouvrir le lien",
"deleteWalletQuestion": "Supprimer le portefeuille ?",
@@ -488,7 +502,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.",
@@ -521,5 +550,14 @@
"p2pkErrorKeyAlreadyExists": "Cette clé existe déjà",
"p2pkErrorKeyNotFound": "Clé non trouvée",
"p2pkErrorCannotDeletePrimary": "Impossible de supprimer la clé principale",
"p2pkSendComingSoon": "Bientôt disponible"
"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"
}
+41 -3
View File
@@ -137,6 +137,20 @@
},
"keepTokenWarning": "Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.",
"tokenCopiedToClipboard": "Token copiato negli appunti",
"copyAsEmoji": "Copia come emoji",
"emojiCopiedToClipboard": "Token copiato come emoji 🥜",
"peanutDecodeError": "Impossibile decodificare il token emoji. Potrebbe essere corrotto.",
"nfcWrite": "Scrivi su tag NFC",
"nfcRead": "Leggi tag NFC",
"nfcHoldNear": "Avvicina il dispositivo al tag NFC...",
"nfcWriteSuccess": "Token scritto sul tag NFC",
"nfcWriteError": "Errore NFC scrittura: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Errore NFC lettura: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC è disattivato. Attivalo nelle Impostazioni.",
"nfcUnsupported": "Questo dispositivo non supporta NFC",
"amountToDeposit": "Importo da depositare:",
"descriptionOptional": "Descrizione (opzionale):",
@@ -296,7 +310,7 @@
}
},
"close": "Chiudi",
"aboutDescription": "Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di La Chispa.",
"aboutDescription": "Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di LaChispa.",
"couldNotOpenLink": "Impossibile aprire il link",
"deleteWalletQuestion": "Eliminare portafoglio?",
@@ -488,7 +502,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.",
@@ -521,5 +550,14 @@
"p2pkErrorKeyAlreadyExists": "Questa chiave esiste già",
"p2pkErrorKeyNotFound": "Chiave non trovata",
"p2pkErrorCannotDeletePrimary": "Impossibile eliminare la chiave principale",
"p2pkSendComingSoon": "Disponibile prossimamente"
"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"
}
+41 -3
View File
@@ -137,6 +137,20 @@
},
"keepTokenWarning": "受取人が請求するまでこのトークンを保管してください。紛失すると資金を失います。",
"tokenCopiedToClipboard": "トークンをクリップボードにコピーしました",
"copyAsEmoji": "絵文字としてコピー",
"emojiCopiedToClipboard": "トークンを絵文字としてコピーしました 🥜",
"peanutDecodeError": "絵文字トークンをデコードできませんでした。破損している可能性があります。",
"nfcWrite": "NFCタグに書き込む",
"nfcRead": "NFCタグを読み取る",
"nfcHoldNear": "デバイスをNFCタグに近づけてください...",
"nfcWriteSuccess": "トークンをNFCタグに書き込みました",
"nfcWriteError": "NFC書き込みエラー: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "NFC読み取りエラー: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFCが無効です。設定で有効にしてください。",
"nfcUnsupported": "このデバイスはNFCに対応していません",
"amountToDeposit": "入金額:",
"descriptionOptional": "説明(任意):",
@@ -296,7 +310,7 @@
}
},
"close": "閉じる",
"aboutDescription": "キューバのDNAを持つ世界のためのCashuウォレット。La Chispaの兄弟。",
"aboutDescription": "キューバのDNAを持つ世界のためのCashuウォレット。LaChispaの兄弟。",
"couldNotOpenLink": "リンクを開けませんでした",
"deleteWalletQuestion": "ウォレットを削除しますか?",
@@ -488,7 +502,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は実験的機能です。注意してご使用ください。",
@@ -521,5 +550,14 @@
"p2pkErrorKeyAlreadyExists": "この鍵は既に存在します",
"p2pkErrorKeyNotFound": "鍵が見つかりません",
"p2pkErrorCannotDeletePrimary": "プライマリ鍵は削除できません",
"p2pkSendComingSoon": "近日公開"
"request": "リクエスト",
"requestPayment": "支払いをリクエスト",
"requestPaymentDescription": "統合支払いリクエストを生成",
"generateRequest": "リクエストを生成",
"generatingRequest": "生成中...",
"requestPaymentReceived": "支払いを受け取りました",
"requestDescriptionHint": "説明(任意)",
"universal": "ユニバーサル",
"copiedToClipboard": "クリップボードにコピーしました"
}
+41 -3
View File
@@ -137,6 +137,20 @@
},
"keepTokenWarning": "수신자가 청구할 때까지 이 토큰을 보관하세요. 분실하면 자금을 잃게 됩니다.",
"tokenCopiedToClipboard": "토큰이 클립보드에 복사되었습니다",
"copyAsEmoji": "이모지로 복사",
"emojiCopiedToClipboard": "토큰이 이모지로 복사되었습니다 🥜",
"peanutDecodeError": "이모지 토큰을 디코딩할 수 없습니다. 손상되었을 수 있습니다.",
"nfcWrite": "NFC 태그에 쓰기",
"nfcRead": "NFC 태그 읽기",
"nfcHoldNear": "기기를 NFC 태그에 가까이 대세요...",
"nfcWriteSuccess": "토큰이 NFC 태그에 기록되었습니다",
"nfcWriteError": "NFC 쓰기 오류: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "NFC 읽기 오류: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC가 비활성화되어 있습니다. 설정에서 활성화하세요.",
"nfcUnsupported": "이 기기는 NFC를 지원하지 않습니다",
"amountToDeposit": "입금할 금액:",
"descriptionOptional": "설명 (선택사항):",
@@ -296,7 +310,7 @@
}
},
"close": "닫기",
"aboutDescription": "전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. La Chispa의 형제.",
"aboutDescription": "전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. LaChispa의 형제.",
"couldNotOpenLink": "링크를 열 수 없음",
"deleteWalletQuestion": "지갑을 삭제하시겠습니까?",
@@ -488,7 +502,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는 실험적 기능입니다. 주의하여 사용하세요.",
@@ -521,5 +550,14 @@
"p2pkErrorKeyAlreadyExists": "이 키는 이미 존재합니다",
"p2pkErrorKeyNotFound": "키를 찾을 수 없습니다",
"p2pkErrorCannotDeletePrimary": "기본 키는 삭제할 수 없습니다",
"p2pkSendComingSoon": "곧 출시 예정"
"request": "요청",
"requestPayment": "결제 요청",
"requestPaymentDescription": "통합 결제 요청 생성",
"generateRequest": "요청 생성",
"generatingRequest": "생성 중...",
"requestPaymentReceived": "결제가 수신되었습니다",
"requestDescriptionHint": "설명 (선택사항)",
"universal": "유니버설",
"copiedToClipboard": "클립보드에 복사되었습니다"
}
+257 -30
View File
@@ -71,7 +71,8 @@ import 'app_localizations_zh.dart';
/// be consistent with the languages listed in the L10n.supportedLocales
/// property.
abstract class L10n {
L10n(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
L10n(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
@@ -91,12 +92,13 @@ abstract class L10n {
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
@@ -110,7 +112,7 @@ abstract class L10n {
Locale('pt'),
Locale('ru'),
Locale('sw'),
Locale('zh')
Locale('zh'),
];
/// No description provided for @appName.
@@ -707,6 +709,72 @@ abstract class L10n {
/// **'Token copiado al portapapeles'**
String get tokenCopiedToClipboard;
/// No description provided for @copyAsEmoji.
///
/// In es, this message translates to:
/// **'Copiar como emoji'**
String get copyAsEmoji;
/// No description provided for @emojiCopiedToClipboard.
///
/// In es, this message translates to:
/// **'Token copiado como emoji 🥜'**
String get emojiCopiedToClipboard;
/// No description provided for @peanutDecodeError.
///
/// In es, this message translates to:
/// **'No se pudo decodificar el token emoji. Puede estar corrupto.'**
String get peanutDecodeError;
/// No description provided for @nfcWrite.
///
/// In es, this message translates to:
/// **'Escribir en tag NFC'**
String get nfcWrite;
/// No description provided for @nfcRead.
///
/// In es, this message translates to:
/// **'Leer tag NFC'**
String get nfcRead;
/// No description provided for @nfcHoldNear.
///
/// In es, this message translates to:
/// **'Acerca el dispositivo al tag NFC...'**
String get nfcHoldNear;
/// No description provided for @nfcWriteSuccess.
///
/// In es, this message translates to:
/// **'Token escrito en tag NFC'**
String get nfcWriteSuccess;
/// No description provided for @nfcWriteError.
///
/// In es, this message translates to:
/// **'Error NFC al escribir: {error}'**
String nfcWriteError(String error);
/// No description provided for @nfcReadError.
///
/// In es, this message translates to:
/// **'Error NFC al leer: {error}'**
String nfcReadError(String error);
/// No description provided for @nfcDisabled.
///
/// In es, this message translates to:
/// **'NFC está desactivado. Actívalo en Ajustes.'**
String get nfcDisabled;
/// No description provided for @nfcUnsupported.
///
/// In es, this message translates to:
/// **'Este dispositivo no soporta NFC'**
String get nfcUnsupported;
/// No description provided for @amountToDeposit.
///
/// In es, this message translates to:
@@ -1376,7 +1444,7 @@ abstract class L10n {
/// No description provided for @aboutDescription.
///
/// In es, this message translates to:
/// **'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de La Chispa.'**
/// **'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de LaChispa.'**
String get aboutDescription;
/// No description provided for @couldNotOpenLink.
@@ -1721,6 +1789,12 @@ abstract class L10n {
/// **'Todo'**
String get selectAll;
/// No description provided for @deselectAll.
///
/// In es, this message translates to:
/// **'Ninguno'**
String get deselectAll;
/// No description provided for @selectNotesToSend.
///
/// In es, this message translates to:
@@ -1943,11 +2017,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.
///
@@ -2141,11 +2299,59 @@ abstract class L10n {
/// **'No se puede eliminar la clave principal'**
String get p2pkErrorCannotDeletePrimary;
/// No description provided for @p2pkSendComingSoon.
/// No description provided for @request.
///
/// In es, this message translates to:
/// **'Disponible próximamente'**
String get p2pkSendComingSoon;
/// **'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;
}
class _L10nDelegate extends LocalizationsDelegate<L10n> {
@@ -2157,34 +2363,55 @@ class _L10nDelegate extends LocalizationsDelegate<L10n> {
}
@override
bool isSupported(Locale locale) => <String>['de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'pt', 'ru', 'sw', 'zh'].contains(locale.languageCode);
bool isSupported(Locale locale) => <String>[
'de',
'en',
'es',
'fr',
'it',
'ja',
'ko',
'pt',
'ru',
'sw',
'zh',
].contains(locale.languageCode);
@override
bool shouldReload(_L10nDelegate old) => false;
}
L10n lookupL10n(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'de': return L10nDe();
case 'en': return L10nEn();
case 'es': return L10nEs();
case 'fr': return L10nFr();
case 'it': return L10nIt();
case 'ja': return L10nJa();
case 'ko': return L10nKo();
case 'pt': return L10nPt();
case 'ru': return L10nRu();
case 'sw': return L10nSw();
case 'zh': return L10nZh();
case 'de':
return L10nDe();
case 'en':
return L10nEn();
case 'es':
return L10nEs();
case 'fr':
return L10nFr();
case 'it':
return L10nIt();
case 'ja':
return L10nJa();
case 'ko':
return L10nKo();
case 'pt':
return L10nPt();
case 'ru':
return L10nRu();
case 'sw':
return L10nSw();
case 'zh':
return L10nZh();
}
throw FlutterError(
'L10n.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.'
'that was used.',
);
}
+183 -36
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nDe extends L10n {
String get generatingSeed => 'Sichere Generierung deiner Seed-Phrase';
@override
String get createWalletDescription => 'Eine 12-Wort Seed-Phrase wird generiert.\nBewahre sie an einem sicheren Ort auf.';
String get createWalletDescription =>
'Eine 12-Wort Seed-Phrase wird generiert.\nBewahre sie an einem sicheren Ort auf.';
@override
String get generateWallet => 'Wallet generieren';
@@ -69,10 +70,12 @@ class L10nDe extends L10n {
String get walletCreated => 'Wallet erstellt!';
@override
String get walletCreatedDescription => 'Deine Wallet ist bereit. Wir empfehlen, jetzt ein Backup deiner Seed-Phrase zu erstellen.';
String get walletCreatedDescription =>
'Deine Wallet ist bereit. Wir empfehlen, jetzt ein Backup deiner Seed-Phrase zu erstellen.';
@override
String get backupWarning => 'Ohne Backup verlierst du den Zugang zu deinen Mitteln, wenn du das Gerät verlierst.';
String get backupWarning =>
'Ohne Backup verlierst du den Zugang zu deinen Mitteln, wenn du das Gerät verlierst.';
@override
String get backupNow => 'Jetzt sichern';
@@ -87,13 +90,15 @@ class L10nDe extends L10n {
String get seedPhraseTitle => 'Deine Seed-Phrase';
@override
String get seedPhraseDescription => 'Speichere diese 12 Wörter in der richtigen Reihenfolge. Sie sind der einzige Weg, deine Wallet wiederherzustellen.';
String get seedPhraseDescription =>
'Speichere diese 12 Wörter in der richtigen Reihenfolge. Sie sind der einzige Weg, deine Wallet wiederherzustellen.';
@override
String get revealSeedPhrase => 'Seed-Phrase anzeigen';
@override
String get tapToReveal => 'Tippe auf den Button, um\ndeine Seed-Phrase anzuzeigen';
String get tapToReveal =>
'Tippe auf den Button, um\ndeine Seed-Phrase anzuzeigen';
@override
String get copyToClipboard => 'In Zwischenablage kopieren';
@@ -105,7 +110,8 @@ class L10nDe extends L10n {
String get neverShareSeed => 'Teile deine Seed-Phrase niemals mit anderen.';
@override
String get confirmBackup => 'Ich habe meine Seed-Phrase an einem sicheren Ort gespeichert';
String get confirmBackup =>
'Ich habe meine Seed-Phrase an einem sicheren Ort gespeichert';
@override
String get continue_ => 'Weiter';
@@ -117,7 +123,8 @@ class L10nDe extends L10n {
String get enterSeedPhrase => 'Gib deine Seed-Phrase ein';
@override
String get enterSeedDescription => 'Gib die 12 oder 24 Wörter durch Leerzeichen getrennt ein.';
String get enterSeedDescription =>
'Gib die 12 oder 24 Wörter durch Leerzeichen getrennt ein.';
@override
String get seedPlaceholder => 'wort1 wort2 wort3 ...';
@@ -131,7 +138,8 @@ class L10nDe extends L10n {
String get needWords => '(du brauchst 12 oder 24)';
@override
String get restoreScanningMint => 'Mint wird nach vorhandenen Token durchsucht...';
String get restoreScanningMint =>
'Mint wird nach vorhandenen Token durchsucht...';
@override
String restoreError(String error) {
@@ -310,11 +318,51 @@ class L10nDe extends L10n {
}
@override
String get keepTokenWarning => 'Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.';
String get keepTokenWarning =>
'Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.';
@override
String get tokenCopiedToClipboard => 'Token in Zwischenablage kopiert';
@override
String get copyAsEmoji => 'Als Emoji kopieren';
@override
String get emojiCopiedToClipboard => 'Token als Emoji kopiert 🥜';
@override
String get peanutDecodeError =>
'Emoji-Token konnte nicht dekodiert werden. Möglicherweise beschädigt.';
@override
String get nfcWrite => 'Auf NFC-Tag schreiben';
@override
String get nfcRead => 'NFC-Tag lesen';
@override
String get nfcHoldNear => 'Gerät an NFC-Tag halten...';
@override
String get nfcWriteSuccess => 'Token auf NFC-Tag geschrieben';
@override
String nfcWriteError(String error) {
return 'NFC-Schreibfehler: $error';
}
@override
String nfcReadError(String error) {
return 'NFC-Lesefehler: $error';
}
@override
String get nfcDisabled =>
'NFC ist deaktiviert. Aktiviere es in den Einstellungen.';
@override
String get nfcUnsupported => 'Dieses Gerät unterstützt kein NFC';
@override
String get amountToDeposit => 'Einzuzahlender Betrag:';
@@ -450,7 +498,8 @@ class L10nDe extends L10n {
String get noPendingTransactions => 'Keine ausstehenden Transaktionen';
@override
String get allTransactionsCompleted => 'Alle deine Transaktionen sind abgeschlossen';
String get allTransactionsCompleted =>
'Alle deine Transaktionen sind abgeschlossen';
@override
String get noEcashTransactions => 'Keine Ecash Transaktionen';
@@ -462,7 +511,8 @@ class L10nDe extends L10n {
String get noLightningTransactions => 'Keine Lightning Transaktionen';
@override
String get depositOrWithdrawLightning => 'Zahle ein oder hebe ab via Lightning';
String get depositOrWithdrawLightning =>
'Zahle ein oder hebe ab via Lightning';
@override
String get pendingStatus => 'Ausstehend';
@@ -665,7 +715,8 @@ class L10nDe extends L10n {
String get close => 'Schließen';
@override
String get aboutDescription => 'Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von La Chispa.';
String get aboutDescription =>
'Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von LaChispa.';
@override
String get couldNotOpenLink => 'Link konnte nicht geöffnet werden';
@@ -677,7 +728,8 @@ class L10nDe extends L10n {
String get actionIrreversible => 'Diese Aktion ist unwiderruflich';
@override
String get deleteWalletWarning => 'Alle Daten werden gelöscht, einschließlich deiner Seed-Phrase und Tokens. Stelle sicher, dass du ein Backup hast.';
String get deleteWalletWarning =>
'Alle Daten werden gelöscht, einschließlich deiner Seed-Phrase und Tokens. Stelle sicher, dass du ein Backup hast.';
@override
String get typeDeleteToConfirm => 'Gib \"LÖSCHEN\" zur Bestätigung ein:';
@@ -694,19 +746,22 @@ class L10nDe extends L10n {
String get recoverTokensTitle => 'Tokens wiederherstellen';
@override
String get recoverTokensDescription => 'Mints scannen, um Tokens wiederherzustellen, die mit deiner Seed-Phrase verknüpft sind (NUT-13)';
String get recoverTokensDescription =>
'Mints scannen, um Tokens wiederherzustellen, die mit deiner Seed-Phrase verknüpft sind (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Meine aktuelle Seed-Phrase verwenden';
@override
String get scanWithSavedWords => 'Mints mit den gespeicherten 12 Wörtern scannen';
String get scanWithSavedWords =>
'Mints mit den gespeicherten 12 Wörtern scannen';
@override
String get useOtherSeedPhrase => 'Andere Seed-Phrase verwenden';
@override
String get recoverFromOtherWords => 'Tokens von anderen 12 Wörtern wiederherstellen';
String get recoverFromOtherWords =>
'Tokens von anderen 12 Wörtern wiederherstellen';
@override
String get mintsToScan => 'Zu scannende Mints:';
@@ -720,7 +775,8 @@ class L10nDe extends L10n {
String get specificMint => 'Ein bestimmter Mint';
@override
String get enterMnemonicWords => 'Gib die 12 Wörter durch Leerzeichen getrennt ein...';
String get enterMnemonicWords =>
'Gib die 12 Wörter durch Leerzeichen getrennt ein...';
@override
String get scanMints => 'Mints scannen';
@@ -740,7 +796,8 @@ class L10nDe extends L10n {
}
@override
String get scanCompleteNoTokens => 'Scan abgeschlossen. Keine neuen Tokens gefunden.';
String get scanCompleteNoTokens =>
'Scan abgeschlossen. Keine neuen Tokens gefunden.';
@override
String mintsWithError(int count) {
@@ -763,7 +820,8 @@ class L10nDe extends L10n {
}
@override
String get noTokensForMnemonic => 'Keine Tokens mit diesem Mnemonic verknüpft gefunden.';
String get noTokensForMnemonic =>
'Keine Tokens mit diesem Mnemonic verknüpft gefunden.';
@override
String get noConnectedMints => 'Keine verbundenen Mints';
@@ -843,7 +901,8 @@ class L10nDe extends L10n {
String get deleteMintConfirmTitle => 'Mint löschen';
@override
String get deleteMintConfirmMessage => 'Wenn du Guthaben auf diesem Mint hast, geht es verloren. Bist du sicher?';
String get deleteMintConfirmMessage =>
'Wenn du Guthaben auf diesem Mint hast, geht es verloren. Bist du sicher?';
@override
String get delete => 'Löschen';
@@ -854,6 +913,9 @@ class L10nDe extends L10n {
@override
String get selectAll => 'Alle';
@override
String get deselectAll => 'Keine';
@override
String get selectNotesToSend => 'Wähle die zu sendenden Notizen:';
@@ -893,19 +955,24 @@ class L10nDe extends L10n {
String get tokenSavedForLater => 'Token zum späteren Einlösen gespeichert';
@override
String get noConnectionTokenSaved => 'Keine Verbindung. Token zum späteren Einlösen gespeichert.';
String get noConnectionTokenSaved =>
'Keine Verbindung. Token zum späteren Einlösen gespeichert.';
@override
String get unknownMintOffline => 'Dieser Token stammt von einem unbekannten Mint. Verbinde dich mit dem Internet, um ihn hinzuzufügen und den Token einzulösen.';
String get unknownMintOffline =>
'Dieser Token stammt von einem unbekannten Mint. Verbinde dich mit dem Internet, um ihn hinzuzufügen und den Token einzulösen.';
@override
String get noConnectionTryLater => 'Keine Verbindung zum Mint. Versuche es später erneut.';
String get noConnectionTryLater =>
'Keine Verbindung zum Mint. Versuche es später erneut.';
@override
String get saveTokenError => 'Fehler beim Speichern des Tokens. Bitte erneut versuchen.';
String get saveTokenError =>
'Fehler beim Speichern des Tokens. Bitte erneut versuchen.';
@override
String get pendingTokenLimitReached => 'Limit für ausstehende Tokens erreicht (max 50)';
String get pendingTokenLimitReached =>
'Limit für ausstehende Tokens erreicht (max 50)';
@override
String get filterToReceive => 'Zu empfangen';
@@ -982,10 +1049,12 @@ class L10nDe extends L10n {
String get unrecognizedQrCode => 'Nicht erkannter QR-Code';
@override
String get scanCashuTokenHint => 'Scanne einen Cashu Token (cashuA... oder cashuB...)';
String get scanCashuTokenHint =>
'Scanne einen Cashu Token (cashuA... oder cashuB...)';
@override
String get scanLightningInvoiceHint => 'Scanne eine Lightning Rechnung (lnbc...)';
String get scanLightningInvoiceHint =>
'Scanne eine Lightning Rechnung (lnbc...)';
@override
String get addMintQuestion => 'Diesen Mint hinzufügen?';
@@ -994,7 +1063,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';
@@ -1003,10 +1119,12 @@ class L10nDe extends L10n {
String get p2pkSettingsDescription => 'Gesperrtes ecash empfangen';
@override
String get p2pkExperimental => 'P2PK ist experimentell. Mit Vorsicht verwenden.';
String get p2pkExperimental =>
'P2PK ist experimentell. Mit Vorsicht verwenden.';
@override
String get p2pkPendingSendWarning => 'Du hast einen ausstehenden P2PK-Versand. Gehe zum Verlauf und aktualisiere, nachdem der Empfänger den Token eingelöst hat.';
String get p2pkPendingSendWarning =>
'Du hast einen ausstehenden P2PK-Versand. Gehe zum Verlauf und aktualisiere, nachdem der Empfänger den Token eingelöst hat.';
@override
String get p2pkExperimentalShort => 'Experimentell';
@@ -1063,7 +1181,8 @@ class L10nDe extends L10n {
String get p2pkLockedToOther => 'Für anderen Schlüssel gesperrt';
@override
String get p2pkCannotUnlock => 'Du hast nicht den Schlüssel, um diesen Token zu entsperren';
String get p2pkCannotUnlock =>
'Du hast nicht den Schlüssel, um diesen Token zu entsperren';
@override
String get p2pkEnterPrivateKey => 'Privaten Schlüssel eingeben (nsec)';
@@ -1072,13 +1191,15 @@ class L10nDe extends L10n {
String get p2pkDeleteTitle => 'Schlüssel löschen';
@override
String get p2pkDeleteConfirm => 'Diesen Schlüssel löschen? Du kannst keine Token mehr empfangen, die daran gesperrt sind.';
String get p2pkDeleteConfirm =>
'Diesen Schlüssel löschen? Du kannst keine Token mehr empfangen, die daran gesperrt sind.';
@override
String get p2pkRequiresConnection => 'P2PK erfordert Verbindung zum Mint';
@override
String get p2pkErrorMaxKeysReached => 'Maximale Anzahl importierter Schlüssel erreicht (10)';
String get p2pkErrorMaxKeysReached =>
'Maximale Anzahl importierter Schlüssel erreicht (10)';
@override
String get p2pkErrorInvalidNsec => 'Ungültiger nsec';
@@ -1090,8 +1211,34 @@ class L10nDe extends L10n {
String get p2pkErrorKeyNotFound => 'Schlüssel nicht gefunden';
@override
String get p2pkErrorCannotDeletePrimary => 'Primärschlüssel kann nicht gelöscht werden';
String get p2pkErrorCannotDeletePrimary =>
'Primärschlüssel kann nicht gelöscht werden';
@override
String get p2pkSendComingSoon => 'Demnächst verfügbar';
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';
}
+148 -20
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nEn extends L10n {
String get generatingSeed => 'Generating your seed phrase securely';
@override
String get createWalletDescription => 'A 12-word seed phrase will be generated.\nStore it in a safe place.';
String get createWalletDescription =>
'A 12-word seed phrase will be generated.\nStore it in a safe place.';
@override
String get generateWallet => 'Generate wallet';
@@ -69,10 +70,12 @@ class L10nEn extends L10n {
String get walletCreated => 'Wallet created!';
@override
String get walletCreatedDescription => 'Your wallet is ready. We recommend backing up your seed phrase now.';
String get walletCreatedDescription =>
'Your wallet is ready. We recommend backing up your seed phrase now.';
@override
String get backupWarning => 'Without backup, you will lose access to your funds if you lose the device.';
String get backupWarning =>
'Without backup, you will lose access to your funds if you lose the device.';
@override
String get backupNow => 'Backup now';
@@ -87,7 +90,8 @@ class L10nEn extends L10n {
String get seedPhraseTitle => 'Your seed phrase';
@override
String get seedPhraseDescription => 'Save these 12 words in order. They are the only way to recover your wallet.';
String get seedPhraseDescription =>
'Save these 12 words in order. They are the only way to recover your wallet.';
@override
String get revealSeedPhrase => 'Reveal seed phrase';
@@ -117,7 +121,8 @@ class L10nEn extends L10n {
String get enterSeedPhrase => 'Enter your seed phrase';
@override
String get enterSeedDescription => 'Type the 12 or 24 words separated by spaces.';
String get enterSeedDescription =>
'Type the 12 or 24 words separated by spaces.';
@override
String get seedPlaceholder => 'word1 word2 word3 ...';
@@ -310,11 +315,50 @@ class L10nEn extends L10n {
}
@override
String get keepTokenWarning => 'Keep this token until the recipient claims it. If you lose it, you will lose the funds.';
String get keepTokenWarning =>
'Keep this token until the recipient claims it. If you lose it, you will lose the funds.';
@override
String get tokenCopiedToClipboard => 'Token copied to clipboard';
@override
String get copyAsEmoji => 'Copy as emoji';
@override
String get emojiCopiedToClipboard => 'Token copied as emoji 🥜';
@override
String get peanutDecodeError =>
'Could not decode emoji token. It may be corrupted.';
@override
String get nfcWrite => 'Write to NFC tag';
@override
String get nfcRead => 'Read NFC tag';
@override
String get nfcHoldNear => 'Hold device near NFC tag...';
@override
String get nfcWriteSuccess => 'Token written to NFC tag';
@override
String nfcWriteError(String error) {
return 'NFC write error: $error';
}
@override
String nfcReadError(String error) {
return 'NFC read error: $error';
}
@override
String get nfcDisabled => 'NFC is disabled. Enable it in Settings.';
@override
String get nfcUnsupported => 'This device does not support NFC';
@override
String get amountToDeposit => 'Amount to deposit:';
@@ -665,7 +709,8 @@ class L10nEn extends L10n {
String get close => 'Close';
@override
String get aboutDescription => 'A Cashu wallet with Cuban DNA for the entire world. Brother of La Chispa.';
String get aboutDescription =>
'A Cashu wallet with Cuban DNA for the entire world. Brother of LaChispa.';
@override
String get couldNotOpenLink => 'Could not open link';
@@ -677,7 +722,8 @@ class L10nEn extends L10n {
String get actionIrreversible => 'This action is irreversible';
@override
String get deleteWalletWarning => 'All data will be deleted including your seed phrase and tokens. Make sure you have a backup.';
String get deleteWalletWarning =>
'All data will be deleted including your seed phrase and tokens. Make sure you have a backup.';
@override
String get typeDeleteToConfirm => 'Type \"DELETE\" to confirm:';
@@ -694,7 +740,8 @@ class L10nEn extends L10n {
String get recoverTokensTitle => 'Recover tokens';
@override
String get recoverTokensDescription => 'Scan mints to recover tokens associated with your seed phrase (NUT-13)';
String get recoverTokensDescription =>
'Scan mints to recover tokens associated with your seed phrase (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Use my current seed phrase';
@@ -763,7 +810,8 @@ class L10nEn extends L10n {
}
@override
String get noTokensForMnemonic => 'No tokens found associated with that mnemonic.';
String get noTokensForMnemonic =>
'No tokens found associated with that mnemonic.';
@override
String get noConnectedMints => 'No connected mints';
@@ -843,7 +891,8 @@ class L10nEn extends L10n {
String get deleteMintConfirmTitle => 'Delete mint';
@override
String get deleteMintConfirmMessage => 'If you have balance in this mint, it will be lost. Are you sure?';
String get deleteMintConfirmMessage =>
'If you have balance in this mint, it will be lost. Are you sure?';
@override
String get delete => 'Delete';
@@ -854,6 +903,9 @@ class L10nEn extends L10n {
@override
String get selectAll => 'All';
@override
String get deselectAll => 'None';
@override
String get selectNotesToSend => 'Select the notes you want to send:';
@@ -893,10 +945,12 @@ class L10nEn extends L10n {
String get tokenSavedForLater => 'Token saved to claim later';
@override
String get noConnectionTokenSaved => 'No connection. Token saved to claim later.';
String get noConnectionTokenSaved =>
'No connection. Token saved to claim later.';
@override
String get unknownMintOffline => 'This token is from an unknown mint. Connect to the internet to add it and claim the token.';
String get unknownMintOffline =>
'This token is from an unknown mint. Connect to the internet to add it and claim the token.';
@override
String get noConnectionTryLater => 'No connection to mint. Try again later.';
@@ -905,7 +959,8 @@ class L10nEn extends L10n {
String get saveTokenError => 'Error saving token. Please try again.';
@override
String get pendingTokenLimitReached => 'Pending tokens limit reached (max 50)';
String get pendingTokenLimitReached =>
'Pending tokens limit reached (max 50)';
@override
String get filterToReceive => 'To receive';
@@ -982,7 +1037,8 @@ class L10nEn extends L10n {
String get unrecognizedQrCode => 'Unrecognized QR code';
@override
String get scanCashuTokenHint => 'Scan a Cashu token (cashuA... or cashuB...)';
String get scanCashuTokenHint =>
'Scan a Cashu token (cashuA... or cashuB...)';
@override
String get scanLightningInvoiceHint => 'Scan a Lightning invoice (lnbc...)';
@@ -994,7 +1050,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';
@@ -1006,7 +1108,8 @@ class L10nEn extends L10n {
String get p2pkExperimental => 'P2PK is experimental. Use with caution.';
@override
String get p2pkPendingSendWarning => 'You have a pending P2PK send. Go to history and refresh after the recipient claims the token.';
String get p2pkPendingSendWarning =>
'You have a pending P2PK send. Go to history and refresh after the recipient claims the token.';
@override
String get p2pkExperimentalShort => 'Experimental';
@@ -1072,7 +1175,8 @@ class L10nEn extends L10n {
String get p2pkDeleteTitle => 'Delete key';
@override
String get p2pkDeleteConfirm => 'Delete this key? You won\'t be able to receive tokens locked to it.';
String get p2pkDeleteConfirm =>
'Delete this key? You won\'t be able to receive tokens locked to it.';
@override
String get p2pkRequiresConnection => 'P2PK requires connection to the mint';
@@ -1093,5 +1197,29 @@ class L10nEn extends L10n {
String get p2pkErrorCannotDeletePrimary => 'Cannot delete primary key';
@override
String get p2pkSendComingSoon => 'Coming soon';
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';
}
+164 -28
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nEs extends L10n {
String get generatingSeed => 'Generando tu frase semilla de forma segura';
@override
String get createWalletDescription => 'Se generará una frase semilla de 12 palabras.\nGuárdala en un lugar seguro.';
String get createWalletDescription =>
'Se generará una frase semilla de 12 palabras.\nGuárdala en un lugar seguro.';
@override
String get generateWallet => 'Generar wallet';
@@ -69,10 +70,12 @@ class L10nEs extends L10n {
String get walletCreated => '¡Wallet creada!';
@override
String get walletCreatedDescription => 'Tu wallet está lista. Te recomendamos hacer backup de tu frase semilla ahora.';
String get walletCreatedDescription =>
'Tu wallet está lista. Te recomendamos hacer backup de tu frase semilla ahora.';
@override
String get backupWarning => 'Sin backup, perderás acceso a tus fondos si pierdes el dispositivo.';
String get backupWarning =>
'Sin backup, perderás acceso a tus fondos si pierdes el dispositivo.';
@override
String get backupNow => 'Hacer backup ahora';
@@ -87,7 +90,8 @@ class L10nEs extends L10n {
String get seedPhraseTitle => 'Tu frase semilla';
@override
String get seedPhraseDescription => 'Guarda estas 12 palabras en orden. Son la única forma de recuperar tu wallet.';
String get seedPhraseDescription =>
'Guarda estas 12 palabras en orden. Son la única forma de recuperar tu wallet.';
@override
String get revealSeedPhrase => 'Revelar frase semilla';
@@ -117,7 +121,8 @@ class L10nEs extends L10n {
String get enterSeedPhrase => 'Ingresa tu frase semilla';
@override
String get enterSeedDescription => 'Escribe las 12 o 24 palabras separadas por espacios.';
String get enterSeedDescription =>
'Escribe las 12 o 24 palabras separadas por espacios.';
@override
String get seedPlaceholder => 'palabra1 palabra2 palabra3 ...';
@@ -310,11 +315,50 @@ class L10nEs extends L10n {
}
@override
String get keepTokenWarning => 'Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.';
String get keepTokenWarning =>
'Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.';
@override
String get tokenCopiedToClipboard => 'Token copiado al portapapeles';
@override
String get copyAsEmoji => 'Copiar como emoji';
@override
String get emojiCopiedToClipboard => 'Token copiado como emoji 🥜';
@override
String get peanutDecodeError =>
'No se pudo decodificar el token emoji. Puede estar corrupto.';
@override
String get nfcWrite => 'Escribir en tag NFC';
@override
String get nfcRead => 'Leer tag NFC';
@override
String get nfcHoldNear => 'Acerca el dispositivo al tag NFC...';
@override
String get nfcWriteSuccess => 'Token escrito en tag NFC';
@override
String nfcWriteError(String error) {
return 'Error NFC al escribir: $error';
}
@override
String nfcReadError(String error) {
return 'Error NFC al leer: $error';
}
@override
String get nfcDisabled => 'NFC está desactivado. Actívalo en Ajustes.';
@override
String get nfcUnsupported => 'Este dispositivo no soporta NFC';
@override
String get amountToDeposit => 'Monto a depositar:';
@@ -450,7 +494,8 @@ class L10nEs extends L10n {
String get noPendingTransactions => 'Sin transacciones pendientes';
@override
String get allTransactionsCompleted => 'Todas tus transacciones están completadas';
String get allTransactionsCompleted =>
'Todas tus transacciones están completadas';
@override
String get noEcashTransactions => 'Sin transacciones Ecash';
@@ -665,7 +710,8 @@ class L10nEs extends L10n {
String get close => 'Cerrar';
@override
String get aboutDescription => 'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de La Chispa.';
String get aboutDescription =>
'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de LaChispa.';
@override
String get couldNotOpenLink => 'No se pudo abrir el enlace';
@@ -677,7 +723,8 @@ class L10nEs extends L10n {
String get actionIrreversible => 'Esta acción es irreversible';
@override
String get deleteWalletWarning => 'Se eliminarán todos los datos incluyendo tu seed phrase y tokens. Asegúrate de tener un backup.';
String get deleteWalletWarning =>
'Se eliminarán todos los datos incluyendo tu seed phrase y tokens. Asegúrate de tener un backup.';
@override
String get typeDeleteToConfirm => 'Escribe \"BORRAR\" para confirmar:';
@@ -694,13 +741,15 @@ class L10nEs extends L10n {
String get recoverTokensTitle => 'Recuperar tokens';
@override
String get recoverTokensDescription => 'Escanea los mints para recuperar tokens asociados a tu seed phrase (NUT-13)';
String get recoverTokensDescription =>
'Escanea los mints para recuperar tokens asociados a tu seed phrase (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Usar mi seed phrase actual';
@override
String get scanWithSavedWords => 'Escanear mints con las 12 palabras guardadas';
String get scanWithSavedWords =>
'Escanear mints con las 12 palabras guardadas';
@override
String get useOtherSeedPhrase => 'Usar otra seed phrase';
@@ -720,7 +769,8 @@ class L10nEs extends L10n {
String get specificMint => 'Un mint específico';
@override
String get enterMnemonicWords => 'Ingresa las 12 palabras separadas por espacios...';
String get enterMnemonicWords =>
'Ingresa las 12 palabras separadas por espacios...';
@override
String get scanMints => 'Escanear mints';
@@ -740,7 +790,8 @@ class L10nEs extends L10n {
}
@override
String get scanCompleteNoTokens => 'Escaneo completado. No se encontraron tokens nuevos.';
String get scanCompleteNoTokens =>
'Escaneo completado. No se encontraron tokens nuevos.';
@override
String mintsWithError(int count) {
@@ -763,7 +814,8 @@ class L10nEs extends L10n {
}
@override
String get noTokensForMnemonic => 'No se encontraron tokens asociados a ese mnemonic.';
String get noTokensForMnemonic =>
'No se encontraron tokens asociados a ese mnemonic.';
@override
String get noConnectedMints => 'No hay mints conectados';
@@ -843,7 +895,8 @@ class L10nEs extends L10n {
String get deleteMintConfirmTitle => 'Eliminar mint';
@override
String get deleteMintConfirmMessage => 'Si tienes balance en este mint, se perderá. ¿Estás seguro?';
String get deleteMintConfirmMessage =>
'Si tienes balance en este mint, se perderá. ¿Estás seguro?';
@override
String get delete => 'Eliminar';
@@ -854,6 +907,9 @@ class L10nEs extends L10n {
@override
String get selectAll => 'Todo';
@override
String get deselectAll => 'Ninguno';
@override
String get selectNotesToSend => 'Selecciona las notas que deseas enviar:';
@@ -893,10 +949,12 @@ class L10nEs extends L10n {
String get tokenSavedForLater => 'Token guardado para reclamar después';
@override
String get noConnectionTokenSaved => 'Sin conexión. Token guardado para reclamar después.';
String get noConnectionTokenSaved =>
'Sin conexión. Token guardado para reclamar después.';
@override
String get unknownMintOffline => 'Este token es de un mint desconocido. Conéctate a internet para agregarlo y reclamar el token.';
String get unknownMintOffline =>
'Este token es de un mint desconocido. Conéctate a internet para agregarlo y reclamar el token.';
@override
String get noConnectionTryLater => 'Sin conexión al mint. Intenta más tarde.';
@@ -905,7 +963,8 @@ class L10nEs extends L10n {
String get saveTokenError => 'Error al guardar el token. Intenta de nuevo.';
@override
String get pendingTokenLimitReached => 'Límite de tokens pendientes alcanzado (max 50)';
String get pendingTokenLimitReached =>
'Límite de tokens pendientes alcanzado (max 50)';
@override
String get filterToReceive => 'Para recibir';
@@ -982,10 +1041,12 @@ class L10nEs extends L10n {
String get unrecognizedQrCode => 'Código QR no reconocido';
@override
String get scanCashuTokenHint => 'Escanea un token Cashu (cashuA... o cashuB...)';
String get scanCashuTokenHint =>
'Escanea un token Cashu (cashuA... o cashuB...)';
@override
String get scanLightningInvoiceHint => 'Escanea un invoice Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Escanea un invoice Lightning (lnbc...)';
@override
String get addMintQuestion => '¿Agregar este mint?';
@@ -994,7 +1055,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';
@@ -1006,7 +1113,8 @@ class L10nEs extends L10n {
String get p2pkExperimental => 'P2PK es experimental. Úsala con precaución.';
@override
String get p2pkPendingSendWarning => 'Tienes un envío P2PK pendiente. Ve al historial y presiona actualizar después de que el destinatario reclame el token.';
String get p2pkPendingSendWarning =>
'Tienes un envío P2PK pendiente. Ve al historial y presiona actualizar después de que el destinatario reclame el token.';
@override
String get p2pkExperimentalShort => 'Experimental';
@@ -1063,7 +1171,8 @@ class L10nEs extends L10n {
String get p2pkLockedToOther => 'Bloqueado para otra clave';
@override
String get p2pkCannotUnlock => 'No tienes la clave para desbloquear este token';
String get p2pkCannotUnlock =>
'No tienes la clave para desbloquear este token';
@override
String get p2pkEnterPrivateKey => 'Ingresa la clave privada (nsec)';
@@ -1072,13 +1181,15 @@ class L10nEs extends L10n {
String get p2pkDeleteTitle => 'Eliminar clave';
@override
String get p2pkDeleteConfirm => '¿Eliminar esta clave? No podrás recibir tokens bloqueados a ella.';
String get p2pkDeleteConfirm =>
'¿Eliminar esta clave? No podrás recibir tokens bloqueados a ella.';
@override
String get p2pkRequiresConnection => 'P2PK requiere conexión al mint';
@override
String get p2pkErrorMaxKeysReached => 'Máximo de claves importadas alcanzado (10)';
String get p2pkErrorMaxKeysReached =>
'Máximo de claves importadas alcanzado (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec inválido';
@@ -1090,8 +1201,33 @@ class L10nEs extends L10n {
String get p2pkErrorKeyNotFound => 'Clave no encontrada';
@override
String get p2pkErrorCannotDeletePrimary => 'No se puede eliminar la clave principal';
String get p2pkErrorCannotDeletePrimary =>
'No se puede eliminar la clave principal';
@override
String get p2pkSendComingSoon => 'Disponible próximamente';
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';
}
+193 -41
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -57,10 +57,12 @@ class L10nFr extends L10n {
String get creatingWallet => 'Création de votre portefeuille...';
@override
String get generatingSeed => 'Génération sécurisée de votre phrase de récupération';
String get generatingSeed =>
'Génération sécurisée de votre phrase de récupération';
@override
String get createWalletDescription => 'Une phrase de récupération de 12 mots sera générée.\nConservez-la dans un endroit sûr.';
String get createWalletDescription =>
'Une phrase de récupération de 12 mots sera générée.\nConservez-la dans un endroit sûr.';
@override
String get generateWallet => 'Générer le portefeuille';
@@ -69,10 +71,12 @@ class L10nFr extends L10n {
String get walletCreated => 'Portefeuille créé !';
@override
String get walletCreatedDescription => 'Votre portefeuille est prêt. Nous vous recommandons de sauvegarder votre phrase de récupération maintenant.';
String get walletCreatedDescription =>
'Votre portefeuille est prêt. Nous vous recommandons de sauvegarder votre phrase de récupération maintenant.';
@override
String get backupWarning => 'Sans sauvegarde, vous perdrez l\'accès à vos fonds si vous perdez l\'appareil.';
String get backupWarning =>
'Sans sauvegarde, vous perdrez l\'accès à vos fonds si vous perdez l\'appareil.';
@override
String get backupNow => 'Sauvegarder maintenant';
@@ -87,13 +91,15 @@ class L10nFr extends L10n {
String get seedPhraseTitle => 'Votre phrase de récupération';
@override
String get seedPhraseDescription => 'Conservez ces 12 mots dans l\'ordre. C\'est le seul moyen de récupérer votre portefeuille.';
String get seedPhraseDescription =>
'Conservez ces 12 mots dans l\'ordre. C\'est le seul moyen de récupérer votre portefeuille.';
@override
String get revealSeedPhrase => 'Révéler la phrase de récupération';
@override
String get tapToReveal => 'Appuyez sur le bouton pour révéler\nvotre phrase de récupération';
String get tapToReveal =>
'Appuyez sur le bouton pour révéler\nvotre phrase de récupération';
@override
String get copyToClipboard => 'Copier dans le presse-papiers';
@@ -102,10 +108,12 @@ class L10nFr extends L10n {
String get seedCopied => 'Phrase copiée dans le presse-papiers';
@override
String get neverShareSeed => 'Ne partagez jamais votre phrase de récupération avec personne.';
String get neverShareSeed =>
'Ne partagez jamais votre phrase de récupération avec personne.';
@override
String get confirmBackup => 'J\'ai sauvegardé ma phrase de récupération dans un endroit sûr';
String get confirmBackup =>
'J\'ai sauvegardé ma phrase de récupération dans un endroit sûr';
@override
String get continue_ => 'Continuer';
@@ -117,7 +125,8 @@ class L10nFr extends L10n {
String get enterSeedPhrase => 'Entrez votre phrase de récupération';
@override
String get enterSeedDescription => 'Tapez les 12 ou 24 mots séparés par des espaces.';
String get enterSeedDescription =>
'Tapez les 12 ou 24 mots séparés par des espaces.';
@override
String get seedPlaceholder => 'mot1 mot2 mot3 ...';
@@ -310,11 +319,51 @@ class L10nFr extends L10n {
}
@override
String get keepTokenWarning => 'Conservez ce token jusqu\'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.';
String get keepTokenWarning =>
'Conservez ce token jusqu\'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.';
@override
String get tokenCopiedToClipboard => 'Token copié dans le presse-papiers';
@override
String get copyAsEmoji => 'Copier en emoji';
@override
String get emojiCopiedToClipboard => 'Token copié en emoji 🥜';
@override
String get peanutDecodeError =>
'Impossible de décoder le token emoji. Il est peut-être corrompu.';
@override
String get nfcWrite => 'Écrire sur tag NFC';
@override
String get nfcRead => 'Lire tag NFC';
@override
String get nfcHoldNear => 'Approchez l\'appareil du tag NFC...';
@override
String get nfcWriteSuccess => 'Token écrit sur le tag NFC';
@override
String nfcWriteError(String error) {
return 'Erreur NFC écriture : $error';
}
@override
String nfcReadError(String error) {
return 'Erreur NFC lecture : $error';
}
@override
String get nfcDisabled =>
'NFC est désactivé. Activez-le dans les Paramètres.';
@override
String get nfcUnsupported => 'Cet appareil ne prend pas en charge le NFC';
@override
String get amountToDeposit => 'Montant à déposer :';
@@ -364,7 +413,8 @@ class L10nFr extends L10n {
String get description => 'Description :';
@override
String get invoiceCopiedToClipboard => 'Facture copiée dans le presse-papiers';
String get invoiceCopiedToClipboard =>
'Facture copiée dans le presse-papiers';
@override
String deposited(String amount, String unit) {
@@ -450,7 +500,8 @@ class L10nFr extends L10n {
String get noPendingTransactions => 'Aucune transaction en attente';
@override
String get allTransactionsCompleted => 'Toutes vos transactions sont terminées';
String get allTransactionsCompleted =>
'Toutes vos transactions sont terminées';
@override
String get noEcashTransactions => 'Aucune transaction Ecash';
@@ -567,7 +618,8 @@ class L10nFr extends L10n {
String get recoverTokens => 'Récupérer les tokens';
@override
String get scanMintsWithSeed => 'Scanner les mints avec la phrase de récupération';
String get scanMintsWithSeed =>
'Scanner les mints avec la phrase de récupération';
@override
String get appearanceSection => 'LANGUE';
@@ -665,7 +717,8 @@ class L10nFr extends L10n {
String get close => 'Fermer';
@override
String get aboutDescription => 'Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de La Chispa.';
String get aboutDescription =>
'Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de LaChispa.';
@override
String get couldNotOpenLink => 'Impossible d\'ouvrir le lien';
@@ -677,7 +730,8 @@ class L10nFr extends L10n {
String get actionIrreversible => 'Cette action est irréversible';
@override
String get deleteWalletWarning => 'Toutes les données seront supprimées, y compris votre phrase de récupération et vos tokens. Assurez-vous d\'avoir une sauvegarde.';
String get deleteWalletWarning =>
'Toutes les données seront supprimées, y compris votre phrase de récupération et vos tokens. Assurez-vous d\'avoir une sauvegarde.';
@override
String get typeDeleteToConfirm => 'Tapez \"SUPPRIMER\" pour confirmer :';
@@ -694,13 +748,16 @@ class L10nFr extends L10n {
String get recoverTokensTitle => 'Récupérer les tokens';
@override
String get recoverTokensDescription => 'Scanner les mints pour récupérer les tokens associés à votre phrase de récupération (NUT-13)';
String get recoverTokensDescription =>
'Scanner les mints pour récupérer les tokens associés à votre phrase de récupération (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Utiliser ma phrase de récupération actuelle';
String get useCurrentSeedPhrase =>
'Utiliser ma phrase de récupération actuelle';
@override
String get scanWithSavedWords => 'Scanner les mints avec les 12 mots sauvegardés';
String get scanWithSavedWords =>
'Scanner les mints avec les 12 mots sauvegardés';
@override
String get useOtherSeedPhrase => 'Utiliser une autre phrase de récupération';
@@ -720,7 +777,8 @@ class L10nFr extends L10n {
String get specificMint => 'Un mint spécifique';
@override
String get enterMnemonicWords => 'Entrez les 12 mots séparés par des espaces...';
String get enterMnemonicWords =>
'Entrez les 12 mots séparés par des espaces...';
@override
String get scanMints => 'Scanner les mints';
@@ -740,7 +798,8 @@ class L10nFr extends L10n {
}
@override
String get scanCompleteNoTokens => 'Scan terminé. Aucun nouveau token trouvé.';
String get scanCompleteNoTokens =>
'Scan terminé. Aucun nouveau token trouvé.';
@override
String mintsWithError(int count) {
@@ -763,7 +822,8 @@ class L10nFr extends L10n {
}
@override
String get noTokensForMnemonic => 'Aucun token trouvé associé à ce mnémonique.';
String get noTokensForMnemonic =>
'Aucun token trouvé associé à ce mnémonique.';
@override
String get noConnectedMints => 'Aucun mint connecté';
@@ -843,7 +903,8 @@ class L10nFr extends L10n {
String get deleteMintConfirmTitle => 'Supprimer le mint';
@override
String get deleteMintConfirmMessage => 'Si vous avez un solde sur ce mint, il sera perdu. Êtes-vous sûr ?';
String get deleteMintConfirmMessage =>
'Si vous avez un solde sur ce mint, il sera perdu. Êtes-vous sûr ?';
@override
String get delete => 'Supprimer';
@@ -854,6 +915,9 @@ class L10nFr extends L10n {
@override
String get selectAll => 'Tout';
@override
String get deselectAll => 'Aucun';
@override
String get selectNotesToSend => 'Sélectionnez les notes à envoyer :';
@@ -893,19 +957,24 @@ class L10nFr extends L10n {
String get tokenSavedForLater => 'Token sauvegardé pour réclamer plus tard';
@override
String get noConnectionTokenSaved => 'Pas de connexion. Token sauvegardé pour réclamer plus tard.';
String get noConnectionTokenSaved =>
'Pas de connexion. Token sauvegardé pour réclamer plus tard.';
@override
String get unknownMintOffline => 'Ce token provient d\'un mint inconnu. Connectez-vous à Internet pour l\'ajouter et réclamer le token.';
String get unknownMintOffline =>
'Ce token provient d\'un mint inconnu. Connectez-vous à Internet pour l\'ajouter et réclamer le token.';
@override
String get noConnectionTryLater => 'Pas de connexion au mint. Réessayez plus tard.';
String get noConnectionTryLater =>
'Pas de connexion au mint. Réessayez plus tard.';
@override
String get saveTokenError => 'Erreur lors de la sauvegarde du token. Veuillez réessayer.';
String get saveTokenError =>
'Erreur lors de la sauvegarde du token. Veuillez réessayer.';
@override
String get pendingTokenLimitReached => 'Limite de tokens en attente atteinte (max 50)';
String get pendingTokenLimitReached =>
'Limite de tokens en attente atteinte (max 50)';
@override
String get filterToReceive => 'À recevoir';
@@ -914,7 +983,8 @@ class L10nFr extends L10n {
String get noPendingTokens => 'Aucun token en attente';
@override
String get noPendingTokensHint => 'Sauvegardez des tokens pour les réclamer plus tard';
String get noPendingTokensHint =>
'Sauvegardez des tokens pour les réclamer plus tard';
@override
String get pendingBadge => 'EN ATTENTE';
@@ -973,19 +1043,23 @@ class L10nFr extends L10n {
String get pointCameraAtQr => 'Pointez la caméra vers le code QR';
@override
String get pointCameraAtCashuQr => 'Pointez la caméra vers le QR du token Cashu';
String get pointCameraAtCashuQr =>
'Pointez la caméra vers le QR du token Cashu';
@override
String get pointCameraAtInvoiceQr => 'Pointez la caméra vers le QR de la facture';
String get pointCameraAtInvoiceQr =>
'Pointez la caméra vers le QR de la facture';
@override
String get unrecognizedQrCode => 'Code QR non reconnu';
@override
String get scanCashuTokenHint => 'Scannez un token Cashu (cashuA... ou cashuB...)';
String get scanCashuTokenHint =>
'Scannez un token Cashu (cashuA... ou cashuB...)';
@override
String get scanLightningInvoiceHint => 'Scannez une facture Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Scannez une facture Lightning (lnbc...)';
@override
String get addMintQuestion => 'Ajouter ce mint ?';
@@ -994,7 +1068,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';
@@ -1003,10 +1124,12 @@ class L10nFr extends L10n {
String get p2pkSettingsDescription => 'Recevoir ecash verrouillé';
@override
String get p2pkExperimental => 'P2PK est expérimental. Utiliser avec prudence.';
String get p2pkExperimental =>
'P2PK est expérimental. Utiliser avec prudence.';
@override
String get p2pkPendingSendWarning => 'Vous avez un envoi P2PK en attente. Allez dans l\'historique et actualisez après que le destinataire a réclamé le jeton.';
String get p2pkPendingSendWarning =>
'Vous avez un envoi P2PK en attente. Allez dans l\'historique et actualisez après que le destinataire a réclamé le jeton.';
@override
String get p2pkExperimentalShort => 'Expérimental';
@@ -1063,7 +1186,8 @@ class L10nFr extends L10n {
String get p2pkLockedToOther => 'Verrouillé pour une autre clé';
@override
String get p2pkCannotUnlock => 'Vous n\'avez pas la clé pour déverrouiller ce token';
String get p2pkCannotUnlock =>
'Vous n\'avez pas la clé pour déverrouiller ce token';
@override
String get p2pkEnterPrivateKey => 'Entrer la clé privée (nsec)';
@@ -1072,13 +1196,15 @@ class L10nFr extends L10n {
String get p2pkDeleteTitle => 'Supprimer la clé';
@override
String get p2pkDeleteConfirm => 'Supprimer cette clé ? Vous ne pourrez plus recevoir de tokens verrouillés dessus.';
String get p2pkDeleteConfirm =>
'Supprimer cette clé ? Vous ne pourrez plus recevoir de tokens verrouillés dessus.';
@override
String get p2pkRequiresConnection => 'P2PK nécessite une connexion au mint';
@override
String get p2pkErrorMaxKeysReached => 'Nombre maximum de clés importées atteint (10)';
String get p2pkErrorMaxKeysReached =>
'Nombre maximum de clés importées atteint (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec invalide';
@@ -1090,8 +1216,34 @@ class L10nFr extends L10n {
String get p2pkErrorKeyNotFound => 'Clé non trouvée';
@override
String get p2pkErrorCannotDeletePrimary => 'Impossible de supprimer la clé principale';
String get p2pkErrorCannotDeletePrimary =>
'Impossible de supprimer la clé principale';
@override
String get p2pkSendComingSoon => 'Bientôt disponible';
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';
}
+170 -30
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nIt extends L10n {
String get generatingSeed => 'Generazione sicura della tua frase seed';
@override
String get createWalletDescription => 'Verrà generata una frase seed di 12 parole.\nConservala in un luogo sicuro.';
String get createWalletDescription =>
'Verrà generata una frase seed di 12 parole.\nConservala in un luogo sicuro.';
@override
String get generateWallet => 'Genera portafoglio';
@@ -69,10 +70,12 @@ class L10nIt extends L10n {
String get walletCreated => 'Portafoglio creato!';
@override
String get walletCreatedDescription => 'Il tuo portafoglio è pronto. Ti consigliamo di fare il backup della frase seed ora.';
String get walletCreatedDescription =>
'Il tuo portafoglio è pronto. Ti consigliamo di fare il backup della frase seed ora.';
@override
String get backupWarning => 'Senza backup, perderai l\'accesso ai tuoi fondi se perdi il dispositivo.';
String get backupWarning =>
'Senza backup, perderai l\'accesso ai tuoi fondi se perdi il dispositivo.';
@override
String get backupNow => 'Backup adesso';
@@ -87,7 +90,8 @@ class L10nIt extends L10n {
String get seedPhraseTitle => 'La tua frase seed';
@override
String get seedPhraseDescription => 'Conserva queste 12 parole in ordine. Sono l\'unico modo per recuperare il tuo portafoglio.';
String get seedPhraseDescription =>
'Conserva queste 12 parole in ordine. Sono l\'unico modo per recuperare il tuo portafoglio.';
@override
String get revealSeedPhrase => 'Mostra frase seed';
@@ -102,7 +106,8 @@ class L10nIt extends L10n {
String get seedCopied => 'Frase copiata negli appunti';
@override
String get neverShareSeed => 'Non condividere mai la tua frase seed con nessuno.';
String get neverShareSeed =>
'Non condividere mai la tua frase seed con nessuno.';
@override
String get confirmBackup => 'Ho salvato la mia frase seed in un luogo sicuro';
@@ -117,7 +122,8 @@ class L10nIt extends L10n {
String get enterSeedPhrase => 'Inserisci la tua frase seed';
@override
String get enterSeedDescription => 'Scrivi le 12 o 24 parole separate da spazi.';
String get enterSeedDescription =>
'Scrivi le 12 o 24 parole separate da spazi.';
@override
String get seedPlaceholder => 'parola1 parola2 parola3 ...';
@@ -310,11 +316,50 @@ class L10nIt extends L10n {
}
@override
String get keepTokenWarning => 'Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.';
String get keepTokenWarning =>
'Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.';
@override
String get tokenCopiedToClipboard => 'Token copiato negli appunti';
@override
String get copyAsEmoji => 'Copia come emoji';
@override
String get emojiCopiedToClipboard => 'Token copiato come emoji 🥜';
@override
String get peanutDecodeError =>
'Impossibile decodificare il token emoji. Potrebbe essere corrotto.';
@override
String get nfcWrite => 'Scrivi su tag NFC';
@override
String get nfcRead => 'Leggi tag NFC';
@override
String get nfcHoldNear => 'Avvicina il dispositivo al tag NFC...';
@override
String get nfcWriteSuccess => 'Token scritto sul tag NFC';
@override
String nfcWriteError(String error) {
return 'Errore NFC scrittura: $error';
}
@override
String nfcReadError(String error) {
return 'Errore NFC lettura: $error';
}
@override
String get nfcDisabled => 'NFC è disattivato. Attivalo nelle Impostazioni.';
@override
String get nfcUnsupported => 'Questo dispositivo non supporta NFC';
@override
String get amountToDeposit => 'Importo da depositare:';
@@ -450,7 +495,8 @@ class L10nIt extends L10n {
String get noPendingTransactions => 'Nessuna transazione in attesa';
@override
String get allTransactionsCompleted => 'Tutte le tue transazioni sono completate';
String get allTransactionsCompleted =>
'Tutte le tue transazioni sono completate';
@override
String get noEcashTransactions => 'Nessuna transazione Ecash';
@@ -665,7 +711,8 @@ class L10nIt extends L10n {
String get close => 'Chiudi';
@override
String get aboutDescription => 'Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di La Chispa.';
String get aboutDescription =>
'Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di LaChispa.';
@override
String get couldNotOpenLink => 'Impossibile aprire il link';
@@ -677,7 +724,8 @@ class L10nIt extends L10n {
String get actionIrreversible => 'Questa azione è irreversibile';
@override
String get deleteWalletWarning => 'Tutti i dati verranno eliminati, inclusa la frase seed e i token. Assicurati di avere un backup.';
String get deleteWalletWarning =>
'Tutti i dati verranno eliminati, inclusa la frase seed e i token. Assicurati di avere un backup.';
@override
String get typeDeleteToConfirm => 'Scrivi \"ELIMINA\" per confermare:';
@@ -694,7 +742,8 @@ class L10nIt extends L10n {
String get recoverTokensTitle => 'Recupera token';
@override
String get recoverTokensDescription => 'Scansiona i mint per recuperare i token associati alla tua frase seed (NUT-13)';
String get recoverTokensDescription =>
'Scansiona i mint per recuperare i token associati alla tua frase seed (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Usa la mia frase seed attuale';
@@ -720,7 +769,8 @@ class L10nIt extends L10n {
String get specificMint => 'Un mint specifico';
@override
String get enterMnemonicWords => 'Inserisci le 12 parole separate da spazi...';
String get enterMnemonicWords =>
'Inserisci le 12 parole separate da spazi...';
@override
String get scanMints => 'Scansiona mint';
@@ -740,7 +790,8 @@ class L10nIt extends L10n {
}
@override
String get scanCompleteNoTokens => 'Scansione completata. Nessun nuovo token trovato.';
String get scanCompleteNoTokens =>
'Scansione completata. Nessun nuovo token trovato.';
@override
String mintsWithError(int count) {
@@ -763,7 +814,8 @@ class L10nIt extends L10n {
}
@override
String get noTokensForMnemonic => 'Nessun token trovato associato a questo mnemonic.';
String get noTokensForMnemonic =>
'Nessun token trovato associato a questo mnemonic.';
@override
String get noConnectedMints => 'Nessun mint connesso';
@@ -843,7 +895,8 @@ class L10nIt extends L10n {
String get deleteMintConfirmTitle => 'Elimina mint';
@override
String get deleteMintConfirmMessage => 'Se hai saldo su questo mint, verrà perso. Sei sicuro?';
String get deleteMintConfirmMessage =>
'Se hai saldo su questo mint, verrà perso. Sei sicuro?';
@override
String get delete => 'Elimina';
@@ -854,6 +907,9 @@ class L10nIt extends L10n {
@override
String get selectAll => 'Tutto';
@override
String get deselectAll => 'Nessuno';
@override
String get selectNotesToSend => 'Seleziona le note da inviare:';
@@ -893,19 +949,23 @@ class L10nIt extends L10n {
String get tokenSavedForLater => 'Token salvato per riscattarlo dopo';
@override
String get noConnectionTokenSaved => 'Nessuna connessione. Token salvato per riscattarlo dopo.';
String get noConnectionTokenSaved =>
'Nessuna connessione. Token salvato per riscattarlo dopo.';
@override
String get unknownMintOffline => 'Questo token proviene da un mint sconosciuto. Connettiti a internet per aggiungerlo e riscattare il token.';
String get unknownMintOffline =>
'Questo token proviene da un mint sconosciuto. Connettiti a internet per aggiungerlo e riscattare il token.';
@override
String get noConnectionTryLater => 'Nessuna connessione al mint. Riprova più tardi.';
String get noConnectionTryLater =>
'Nessuna connessione al mint. Riprova più tardi.';
@override
String get saveTokenError => 'Errore nel salvare il token. Riprova.';
@override
String get pendingTokenLimitReached => 'Limite token in attesa raggiunto (max 50)';
String get pendingTokenLimitReached =>
'Limite token in attesa raggiunto (max 50)';
@override
String get filterToReceive => 'Da ricevere';
@@ -973,19 +1033,23 @@ class L10nIt extends L10n {
String get pointCameraAtQr => 'Punta la fotocamera sul codice QR';
@override
String get pointCameraAtCashuQr => 'Punta la fotocamera sul QR del token Cashu';
String get pointCameraAtCashuQr =>
'Punta la fotocamera sul QR del token Cashu';
@override
String get pointCameraAtInvoiceQr => 'Punta la fotocamera sul QR della fattura';
String get pointCameraAtInvoiceQr =>
'Punta la fotocamera sul QR della fattura';
@override
String get unrecognizedQrCode => 'Codice QR non riconosciuto';
@override
String get scanCashuTokenHint => 'Scansiona un token Cashu (cashuA... o cashuB...)';
String get scanCashuTokenHint =>
'Scansiona un token Cashu (cashuA... o cashuB...)';
@override
String get scanLightningInvoiceHint => 'Scansiona una fattura Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Scansiona una fattura Lightning (lnbc...)';
@override
String get addMintQuestion => 'Aggiungere questo mint?';
@@ -994,7 +1058,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';
@@ -1006,7 +1117,8 @@ class L10nIt extends L10n {
String get p2pkExperimental => 'P2PK è sperimentale. Usare con cautela.';
@override
String get p2pkPendingSendWarning => 'Hai un invio P2PK in sospeso. Vai alla cronologia e aggiorna dopo che il destinatario ha riscattato il token.';
String get p2pkPendingSendWarning =>
'Hai un invio P2PK in sospeso. Vai alla cronologia e aggiorna dopo che il destinatario ha riscattato il token.';
@override
String get p2pkExperimentalShort => 'Sperimentale';
@@ -1072,13 +1184,15 @@ class L10nIt extends L10n {
String get p2pkDeleteTitle => 'Elimina chiave';
@override
String get p2pkDeleteConfirm => 'Eliminare questa chiave? Non potrai ricevere token bloccati ad essa.';
String get p2pkDeleteConfirm =>
'Eliminare questa chiave? Non potrai ricevere token bloccati ad essa.';
@override
String get p2pkRequiresConnection => 'P2PK richiede connessione al mint';
@override
String get p2pkErrorMaxKeysReached => 'Numero massimo di chiavi importate raggiunto (10)';
String get p2pkErrorMaxKeysReached =>
'Numero massimo di chiavi importate raggiunto (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec non valido';
@@ -1090,8 +1204,34 @@ class L10nIt extends L10n {
String get p2pkErrorKeyNotFound => 'Chiave non trovata';
@override
String get p2pkErrorCannotDeletePrimary => 'Impossibile eliminare la chiave principale';
String get p2pkErrorCannotDeletePrimary =>
'Impossibile eliminare la chiave principale';
@override
String get p2pkSendComingSoon => 'Disponibile prossimamente';
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';
}
+122 -9
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -69,7 +69,8 @@ class L10nJa extends L10n {
String get walletCreated => 'ウォレット作成完了!';
@override
String get walletCreatedDescription => 'ウォレットの準備ができました。今すぐシードフレーズをバックアップすることをお勧めします。';
String get walletCreatedDescription =>
'ウォレットの準備ができました。今すぐシードフレーズをバックアップすることをお勧めします。';
@override
String get backupWarning => 'バックアップがないと、デバイスを紛失した場合に資金にアクセスできなくなります。';
@@ -315,6 +316,43 @@ class L10nJa extends L10n {
@override
String get tokenCopiedToClipboard => 'トークンをクリップボードにコピーしました';
@override
String get copyAsEmoji => '絵文字としてコピー';
@override
String get emojiCopiedToClipboard => 'トークンを絵文字としてコピーしました 🥜';
@override
String get peanutDecodeError => '絵文字トークンをデコードできませんでした。破損している可能性があります。';
@override
String get nfcWrite => 'NFCタグに書き込む';
@override
String get nfcRead => 'NFCタグを読み取る';
@override
String get nfcHoldNear => 'デバイスをNFCタグに近づけてください...';
@override
String get nfcWriteSuccess => 'トークンをNFCタグに書き込みました';
@override
String nfcWriteError(String error) {
return 'NFC書き込みエラー: $error';
}
@override
String nfcReadError(String error) {
return 'NFC読み取りエラー: $error';
}
@override
String get nfcDisabled => 'NFCが無効です。設定で有効にしてください。';
@override
String get nfcUnsupported => 'このデバイスはNFCに対応していません';
@override
String get amountToDeposit => '入金額:';
@@ -665,7 +703,7 @@ class L10nJa extends L10n {
String get close => '閉じる';
@override
String get aboutDescription => 'キューバのDNAを持つ世界のためのCashuウォレット。La Chispaの兄弟。';
String get aboutDescription => 'キューバのDNAを持つ世界のためのCashuウォレット。LaChispaの兄弟。';
@override
String get couldNotOpenLink => 'リンクを開けませんでした';
@@ -677,7 +715,8 @@ class L10nJa extends L10n {
String get actionIrreversible => 'この操作は取り消せません';
@override
String get deleteWalletWarning => 'シードフレーズとトークンを含むすべてのデータが削除されます。バックアップがあることを確認してください。';
String get deleteWalletWarning =>
'シードフレーズとトークンを含むすべてのデータが削除されます。バックアップがあることを確認してください。';
@override
String get typeDeleteToConfirm => '確認のため「DELETE」と入力:';
@@ -694,7 +733,8 @@ class L10nJa extends L10n {
String get recoverTokensTitle => 'トークンを復元';
@override
String get recoverTokensDescription => 'シードフレーズに関連付けられたトークンを復元するためにMintをスキャン(NUT-13)';
String get recoverTokensDescription =>
'シードフレーズに関連付けられたトークンを復元するためにMintをスキャン(NUT-13)';
@override
String get useCurrentSeedPhrase => '現在のシードフレーズを使用';
@@ -854,6 +894,9 @@ class L10nJa extends L10n {
@override
String get selectAll => 'すべて';
@override
String get deselectAll => 'なし';
@override
String get selectNotesToSend => '送信するノートを選択:';
@@ -896,7 +939,8 @@ class L10nJa extends L10n {
String get noConnectionTokenSaved => '接続なし。トークンを保存しました。後で請求できます。';
@override
String get unknownMintOffline => 'このトークンは不明なMintからのものです。インターネットに接続してMintを追加し、トークンを請求してください。';
String get unknownMintOffline =>
'このトークンは不明なMintからのものです。インターネットに接続してMintを追加し、トークンを請求してください。';
@override
String get noConnectionTryLater => 'Mintに接続できません。後でもう一度お試しください。';
@@ -994,7 +1038,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鍵';
@@ -1006,7 +1094,8 @@ class L10nJa extends L10n {
String get p2pkExperimental => 'P2PKは実験的機能です。注意してご使用ください。';
@override
String get p2pkPendingSendWarning => '保留中のP2PK送信があります。受取人がトークンを受け取った後、履歴で更新してください。';
String get p2pkPendingSendWarning =>
'保留中のP2PK送信があります。受取人がトークンを受け取った後、履歴で更新してください。';
@override
String get p2pkExperimentalShort => '実験的';
@@ -1093,5 +1182,29 @@ class L10nJa extends L10n {
String get p2pkErrorCannotDeletePrimary => 'プライマリ鍵は削除できません';
@override
String get p2pkSendComingSoon => '近日公開';
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 => 'クリップボードにコピーしました';
}
+126 -11
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nKo extends L10n {
String get generatingSeed => '시드 문구를 안전하게 생성 중';
@override
String get createWalletDescription => '12개의 단어로 된 시드 문구가 생성됩니다.\n안전한 곳에 보관하세요.';
String get createWalletDescription =>
'12개의 단어로 된 시드 문구가 생성됩니다.\n안전한 곳에 보관하세요.';
@override
String get generateWallet => '지갑 생성';
@@ -69,7 +70,8 @@ class L10nKo extends L10n {
String get walletCreated => '지갑이 생성되었습니다!';
@override
String get walletCreatedDescription => '지갑이 준비되었습니다. 지금 시드 문구를 백업하는 것을 권장합니다.';
String get walletCreatedDescription =>
'지갑이 준비되었습니다. 지금 시드 문구를 백업하는 것을 권장합니다.';
@override
String get backupWarning => '백업 없이는 기기를 분실하면 자금에 접근할 수 없습니다.';
@@ -87,7 +89,8 @@ class L10nKo extends L10n {
String get seedPhraseTitle => '시드 문구';
@override
String get seedPhraseDescription => '이 12개의 단어를 순서대로 저장하세요. 지갑을 복구하는 유일한 방법입니다.';
String get seedPhraseDescription =>
'이 12개의 단어를 순서대로 저장하세요. 지갑을 복구하는 유일한 방법입니다.';
@override
String get revealSeedPhrase => '시드 문구 보기';
@@ -315,6 +318,43 @@ class L10nKo extends L10n {
@override
String get tokenCopiedToClipboard => '토큰이 클립보드에 복사되었습니다';
@override
String get copyAsEmoji => '이모지로 복사';
@override
String get emojiCopiedToClipboard => '토큰이 이모지로 복사되었습니다 🥜';
@override
String get peanutDecodeError => '이모지 토큰을 디코딩할 수 없습니다. 손상되었을 수 있습니다.';
@override
String get nfcWrite => 'NFC 태그에 쓰기';
@override
String get nfcRead => 'NFC 태그 읽기';
@override
String get nfcHoldNear => '기기를 NFC 태그에 가까이 대세요...';
@override
String get nfcWriteSuccess => '토큰이 NFC 태그에 기록되었습니다';
@override
String nfcWriteError(String error) {
return 'NFC 쓰기 오류: $error';
}
@override
String nfcReadError(String error) {
return 'NFC 읽기 오류: $error';
}
@override
String get nfcDisabled => 'NFC가 비활성화되어 있습니다. 설정에서 활성화하세요.';
@override
String get nfcUnsupported => '이 기기는 NFC를 지원하지 않습니다';
@override
String get amountToDeposit => '입금할 금액:';
@@ -665,7 +705,7 @@ class L10nKo extends L10n {
String get close => '닫기';
@override
String get aboutDescription => '전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. La Chispa의 형제.';
String get aboutDescription => '전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. LaChispa의 형제.';
@override
String get couldNotOpenLink => '링크를 열 수 없음';
@@ -677,7 +717,8 @@ class L10nKo extends L10n {
String get actionIrreversible => '이 작업은 되돌릴 수 없습니다';
@override
String get deleteWalletWarning => '시드 문구와 토큰을 포함한 모든 데이터가 삭제됩니다. 백업이 있는지 확인하세요.';
String get deleteWalletWarning =>
'시드 문구와 토큰을 포함한 모든 데이터가 삭제됩니다. 백업이 있는지 확인하세요.';
@override
String get typeDeleteToConfirm => '확인하려면 \"삭제\"를 입력하세요:';
@@ -694,7 +735,8 @@ class L10nKo extends L10n {
String get recoverTokensTitle => '토큰 복구';
@override
String get recoverTokensDescription => '시드 문구와 연결된 토큰을 복구하기 위해 mint 스캔 (NUT-13)';
String get recoverTokensDescription =>
'시드 문구와 연결된 토큰을 복구하기 위해 mint 스캔 (NUT-13)';
@override
String get useCurrentSeedPhrase => '현재 시드 문구 사용';
@@ -854,6 +896,9 @@ class L10nKo extends L10n {
@override
String get selectAll => '전체';
@override
String get deselectAll => '없음';
@override
String get selectNotesToSend => '보낼 노트를 선택하세요:';
@@ -896,7 +941,8 @@ class L10nKo extends L10n {
String get noConnectionTokenSaved => '연결 없음. 나중에 청구하기 위해 토큰이 저장되었습니다.';
@override
String get unknownMintOffline => '이 토큰은 알 수 없는 mint의 것입니다. 인터넷에 연결하여 추가하고 토큰을 청구하세요.';
String get unknownMintOffline =>
'이 토큰은 알 수 없는 mint의 것입니다. 인터넷에 연결하여 추가하고 토큰을 청구하세요.';
@override
String get noConnectionTryLater => 'Mint에 연결할 수 없음. 나중에 다시 시도하세요.';
@@ -994,7 +1040,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 키';
@@ -1006,7 +1096,8 @@ class L10nKo extends L10n {
String get p2pkExperimental => 'P2PK는 실험적 기능입니다. 주의하여 사용하세요.';
@override
String get p2pkPendingSendWarning => '대기 중인 P2PK 전송이 있습니다. 수신자가 토큰을 수령한 후 기록에서 새로고침하세요.';
String get p2pkPendingSendWarning =>
'대기 중인 P2PK 전송이 있습니다. 수신자가 토큰을 수령한 후 기록에서 새로고침하세요.';
@override
String get p2pkExperimentalShort => '실험적';
@@ -1093,5 +1184,29 @@ class L10nKo extends L10n {
String get p2pkErrorCannotDeletePrimary => '기본 키는 삭제할 수 없습니다';
@override
String get p2pkSendComingSoon => '곧 출시 예정';
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 => '클립보드에 복사되었습니다';
}
+170 -30
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nPt extends L10n {
String get generatingSeed => 'Gerando sua frase semente de forma segura';
@override
String get createWalletDescription => 'Uma frase semente de 12 palavras será gerada.\nGuarde-a em um lugar seguro.';
String get createWalletDescription =>
'Uma frase semente de 12 palavras será gerada.\nGuarde-a em um lugar seguro.';
@override
String get generateWallet => 'Gerar wallet';
@@ -69,10 +70,12 @@ class L10nPt extends L10n {
String get walletCreated => 'Wallet criada!';
@override
String get walletCreatedDescription => 'Sua wallet está pronta. Recomendamos fazer backup da sua frase semente agora.';
String get walletCreatedDescription =>
'Sua wallet está pronta. Recomendamos fazer backup da sua frase semente agora.';
@override
String get backupWarning => 'Sem backup, você perderá acesso aos seus fundos se perder o dispositivo.';
String get backupWarning =>
'Sem backup, você perderá acesso aos seus fundos se perder o dispositivo.';
@override
String get backupNow => 'Fazer backup agora';
@@ -87,7 +90,8 @@ class L10nPt extends L10n {
String get seedPhraseTitle => 'Sua frase semente';
@override
String get seedPhraseDescription => 'Guarde estas 12 palavras em ordem. Elas são a única forma de recuperar sua wallet.';
String get seedPhraseDescription =>
'Guarde estas 12 palavras em ordem. Elas são a única forma de recuperar sua wallet.';
@override
String get revealSeedPhrase => 'Revelar frase semente';
@@ -102,7 +106,8 @@ class L10nPt extends L10n {
String get seedCopied => 'Frase copiada para área de transferência';
@override
String get neverShareSeed => 'Nunca compartilhe sua frase semente com ninguém.';
String get neverShareSeed =>
'Nunca compartilhe sua frase semente com ninguém.';
@override
String get confirmBackup => 'Guardei minha frase semente em um lugar seguro';
@@ -117,7 +122,8 @@ class L10nPt extends L10n {
String get enterSeedPhrase => 'Digite sua frase semente';
@override
String get enterSeedDescription => 'Digite as 12 ou 24 palavras separadas por espaços.';
String get enterSeedDescription =>
'Digite as 12 ou 24 palavras separadas por espaços.';
@override
String get seedPlaceholder => 'palavra1 palavra2 palavra3 ...';
@@ -310,10 +316,50 @@ class L10nPt extends L10n {
}
@override
String get keepTokenWarning => 'Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.';
String get keepTokenWarning =>
'Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.';
@override
String get tokenCopiedToClipboard => 'Token copiado para área de transferência';
String get tokenCopiedToClipboard =>
'Token copiado para área de transferência';
@override
String get copyAsEmoji => 'Copiar como emoji';
@override
String get emojiCopiedToClipboard => 'Token copiado como emoji 🥜';
@override
String get peanutDecodeError =>
'Não foi possível decodificar o token emoji. Pode estar corrompido.';
@override
String get nfcWrite => 'Escrever em tag NFC';
@override
String get nfcRead => 'Ler tag NFC';
@override
String get nfcHoldNear => 'Aproxime o dispositivo da tag NFC...';
@override
String get nfcWriteSuccess => 'Token escrito na tag NFC';
@override
String nfcWriteError(String error) {
return 'Erro NFC ao escrever: $error';
}
@override
String nfcReadError(String error) {
return 'Erro NFC ao ler: $error';
}
@override
String get nfcDisabled => 'NFC está desativado. Ative nas Configurações.';
@override
String get nfcUnsupported => 'Este dispositivo não suporta NFC';
@override
String get amountToDeposit => 'Valor a depositar:';
@@ -364,7 +410,8 @@ class L10nPt extends L10n {
String get description => 'Descrição:';
@override
String get invoiceCopiedToClipboard => 'Invoice copiado para área de transferência';
String get invoiceCopiedToClipboard =>
'Invoice copiado para área de transferência';
@override
String deposited(String amount, String unit) {
@@ -450,7 +497,8 @@ class L10nPt extends L10n {
String get noPendingTransactions => 'Sem transações pendentes';
@override
String get allTransactionsCompleted => 'Todas as suas transações estão completas';
String get allTransactionsCompleted =>
'Todas as suas transações estão completas';
@override
String get noEcashTransactions => 'Sem transações Ecash';
@@ -665,7 +713,8 @@ class L10nPt extends L10n {
String get close => 'Fechar';
@override
String get aboutDescription => 'Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de La Chispa.';
String get aboutDescription =>
'Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de LaChispa.';
@override
String get couldNotOpenLink => 'Não foi possível abrir o link';
@@ -677,7 +726,8 @@ class L10nPt extends L10n {
String get actionIrreversible => 'Esta ação é irreversível';
@override
String get deleteWalletWarning => 'Todos os dados serão excluídos, incluindo sua frase semente e tokens. Certifique-se de ter um backup.';
String get deleteWalletWarning =>
'Todos os dados serão excluídos, incluindo sua frase semente e tokens. Certifique-se de ter um backup.';
@override
String get typeDeleteToConfirm => 'Digite \"APAGAR\" para confirmar:';
@@ -694,7 +744,8 @@ class L10nPt extends L10n {
String get recoverTokensTitle => 'Recuperar tokens';
@override
String get recoverTokensDescription => 'Escanear mints para recuperar tokens associados à sua frase semente (NUT-13)';
String get recoverTokensDescription =>
'Escanear mints para recuperar tokens associados à sua frase semente (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Usar minha frase semente atual';
@@ -720,7 +771,8 @@ class L10nPt extends L10n {
String get specificMint => 'Um mint específico';
@override
String get enterMnemonicWords => 'Digite as 12 palavras separadas por espaços...';
String get enterMnemonicWords =>
'Digite as 12 palavras separadas por espaços...';
@override
String get scanMints => 'Escanear mints';
@@ -740,7 +792,8 @@ class L10nPt extends L10n {
}
@override
String get scanCompleteNoTokens => 'Escaneamento completo. Nenhum token novo encontrado.';
String get scanCompleteNoTokens =>
'Escaneamento completo. Nenhum token novo encontrado.';
@override
String mintsWithError(int count) {
@@ -763,7 +816,8 @@ class L10nPt extends L10n {
}
@override
String get noTokensForMnemonic => 'Nenhum token encontrado associado a esse mnemônico.';
String get noTokensForMnemonic =>
'Nenhum token encontrado associado a esse mnemônico.';
@override
String get noConnectedMints => 'Nenhum mint conectado';
@@ -843,7 +897,8 @@ class L10nPt extends L10n {
String get deleteMintConfirmTitle => 'Excluir mint';
@override
String get deleteMintConfirmMessage => 'Se você tiver saldo neste mint, ele será perdido. Tem certeza?';
String get deleteMintConfirmMessage =>
'Se você tiver saldo neste mint, ele será perdido. Tem certeza?';
@override
String get delete => 'Excluir';
@@ -854,6 +909,9 @@ class L10nPt extends L10n {
@override
String get selectAll => 'Tudo';
@override
String get deselectAll => 'Nenhum';
@override
String get selectNotesToSend => 'Selecione as notas que deseja enviar:';
@@ -893,10 +951,12 @@ class L10nPt extends L10n {
String get tokenSavedForLater => 'Token salvo para resgatar depois';
@override
String get noConnectionTokenSaved => 'Sem conexão. Token salvo para resgatar depois.';
String get noConnectionTokenSaved =>
'Sem conexão. Token salvo para resgatar depois.';
@override
String get unknownMintOffline => 'Este token é de um mint desconhecido. Conecte-se à internet para adicioná-lo e resgatar o token.';
String get unknownMintOffline =>
'Este token é de um mint desconhecido. Conecte-se à internet para adicioná-lo e resgatar o token.';
@override
String get noConnectionTryLater => 'Sem conexão ao mint. Tente mais tarde.';
@@ -905,7 +965,8 @@ class L10nPt extends L10n {
String get saveTokenError => 'Erro ao salvar o token. Tente novamente.';
@override
String get pendingTokenLimitReached => 'Limite de tokens pendentes atingido (máx 50)';
String get pendingTokenLimitReached =>
'Limite de tokens pendentes atingido (máx 50)';
@override
String get filterToReceive => 'Para receber';
@@ -982,10 +1043,12 @@ class L10nPt extends L10n {
String get unrecognizedQrCode => 'Código QR não reconhecido';
@override
String get scanCashuTokenHint => 'Escaneie um token Cashu (cashuA... ou cashuB...)';
String get scanCashuTokenHint =>
'Escaneie um token Cashu (cashuA... ou cashuB...)';
@override
String get scanLightningInvoiceHint => 'Escaneie um invoice Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Escaneie um invoice Lightning (lnbc...)';
@override
String get addMintQuestion => 'Adicionar este mint?';
@@ -994,7 +1057,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';
@@ -1006,7 +1116,8 @@ class L10nPt extends L10n {
String get p2pkExperimental => 'P2PK é experimental. Use com cautela.';
@override
String get p2pkPendingSendWarning => 'Você tem um envio P2PK pendente. Vá ao histórico e atualize após o destinatário resgatar o token.';
String get p2pkPendingSendWarning =>
'Você tem um envio P2PK pendente. Vá ao histórico e atualize após o destinatário resgatar o token.';
@override
String get p2pkExperimentalShort => 'Experimental';
@@ -1063,7 +1174,8 @@ class L10nPt extends L10n {
String get p2pkLockedToOther => 'Bloqueado para outra chave';
@override
String get p2pkCannotUnlock => 'Você não tem a chave para desbloquear este token';
String get p2pkCannotUnlock =>
'Você não tem a chave para desbloquear este token';
@override
String get p2pkEnterPrivateKey => 'Digite a chave privada (nsec)';
@@ -1072,13 +1184,15 @@ class L10nPt extends L10n {
String get p2pkDeleteTitle => 'Excluir chave';
@override
String get p2pkDeleteConfirm => 'Excluir esta chave? Você não poderá receber tokens bloqueados para ela.';
String get p2pkDeleteConfirm =>
'Excluir esta chave? Você não poderá receber tokens bloqueados para ela.';
@override
String get p2pkRequiresConnection => 'P2PK requer conexão com o mint';
@override
String get p2pkErrorMaxKeysReached => 'Número máximo de chaves importadas atingido (10)';
String get p2pkErrorMaxKeysReached =>
'Número máximo de chaves importadas atingido (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec inválido';
@@ -1090,8 +1204,34 @@ class L10nPt extends L10n {
String get p2pkErrorKeyNotFound => 'Chave não encontrada';
@override
String get p2pkErrorCannotDeletePrimary => 'Não é possível excluir a chave principal';
String get p2pkErrorCannotDeletePrimary =>
'Não é possível excluir a chave principal';
@override
String get p2pkSendComingSoon => 'Em breve disponível';
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';
}
+163 -27
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -33,7 +33,8 @@ class L10nRu extends L10n {
String get loadingMessage6 => 'Go full Calle...';
@override
String get loadingMessage7 => 'Cashu + Bitchat = Конфиденциальность + Свобода';
String get loadingMessage7 =>
'Cashu + Bitchat = Конфиденциальность + Свобода';
@override
String get aboutTagline => 'Конфиденциальность без границ.';
@@ -60,7 +61,8 @@ class L10nRu extends L10n {
String get generatingSeed => 'Безопасная генерация вашей сид-фразы';
@override
String get createWalletDescription => 'Будет сгенерирована сид-фраза из 12 слов.\nСохраните её в безопасном месте.';
String get createWalletDescription =>
'Будет сгенерирована сид-фраза из 12 слов.\nСохраните её в безопасном месте.';
@override
String get generateWallet => 'Создать кошелёк';
@@ -69,10 +71,12 @@ class L10nRu extends L10n {
String get walletCreated => 'Кошелёк создан!';
@override
String get walletCreatedDescription => 'Ваш кошелёк готов. Рекомендуем сделать резервную копию сид-фразы сейчас.';
String get walletCreatedDescription =>
'Ваш кошелёк готов. Рекомендуем сделать резервную копию сид-фразы сейчас.';
@override
String get backupWarning => 'Без резервной копии вы потеряете доступ к средствам при потере устройства.';
String get backupWarning =>
'Без резервной копии вы потеряете доступ к средствам при потере устройства.';
@override
String get backupNow => 'Сделать резервную копию';
@@ -87,7 +91,8 @@ class L10nRu extends L10n {
String get seedPhraseTitle => 'Ваша сид-фраза';
@override
String get seedPhraseDescription => 'Сохраните эти 12 слов по порядку. Это единственный способ восстановить кошелёк.';
String get seedPhraseDescription =>
'Сохраните эти 12 слов по порядку. Это единственный способ восстановить кошелёк.';
@override
String get revealSeedPhrase => 'Показать сид-фразу';
@@ -310,11 +315,50 @@ class L10nRu extends L10n {
}
@override
String get keepTokenWarning => 'Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.';
String get keepTokenWarning =>
'Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.';
@override
String get tokenCopiedToClipboard => 'Токен скопирован в буфер обмена';
@override
String get copyAsEmoji => 'Скопировать как эмодзи';
@override
String get emojiCopiedToClipboard => 'Токен скопирован как эмодзи 🥜';
@override
String get peanutDecodeError =>
'Не удалось декодировать эмодзи-токен. Возможно, он повреждён.';
@override
String get nfcWrite => 'Записать на NFC-метку';
@override
String get nfcRead => 'Читать NFC-метку';
@override
String get nfcHoldNear => 'Поднесите устройство к NFC-метке...';
@override
String get nfcWriteSuccess => 'Токен записан на NFC-метку';
@override
String nfcWriteError(String error) {
return 'Ошибка записи NFC: $error';
}
@override
String nfcReadError(String error) {
return 'Ошибка чтения NFC: $error';
}
@override
String get nfcDisabled => 'NFC отключён. Включите в Настройках.';
@override
String get nfcUnsupported => 'Это устройство не поддерживает NFC';
@override
String get amountToDeposit => 'Сумма для пополнения:';
@@ -384,7 +428,8 @@ class L10nRu extends L10n {
String get invalidInvoice => 'Недействительный счёт';
@override
String get invalidInvoiceMalformed => 'Недействительный или повреждённый счёт';
String get invalidInvoiceMalformed =>
'Недействительный или повреждённый счёт';
@override
String get feeReserved => 'Зарезервированная комиссия:';
@@ -462,7 +507,8 @@ class L10nRu extends L10n {
String get noLightningTransactions => 'Нет Lightning транзакций';
@override
String get depositOrWithdrawLightning => 'Пополните или выведите через Lightning';
String get depositOrWithdrawLightning =>
'Пополните или выведите через Lightning';
@override
String get pendingStatus => 'Ожидание';
@@ -665,7 +711,8 @@ class L10nRu extends L10n {
String get close => 'Закрыть';
@override
String get aboutDescription => 'Cashu кошелёк с кубинской ДНК для всего мира. Брат La Chispa.';
String get aboutDescription =>
'Cashu кошелёк с кубинской ДНК для всего мира. Брат LaChispa.';
@override
String get couldNotOpenLink => 'Не удалось открыть ссылку';
@@ -677,7 +724,8 @@ class L10nRu extends L10n {
String get actionIrreversible => 'Это действие необратимо';
@override
String get deleteWalletWarning => 'Все данные будут удалены, включая сид-фразу и токены. Убедитесь, что у вас есть резервная копия.';
String get deleteWalletWarning =>
'Все данные будут удалены, включая сид-фразу и токены. Убедитесь, что у вас есть резервная копия.';
@override
String get typeDeleteToConfirm => 'Введите \"УДАЛИТЬ\" для подтверждения:';
@@ -694,7 +742,8 @@ class L10nRu extends L10n {
String get recoverTokensTitle => 'Восстановить токены';
@override
String get recoverTokensDescription => 'Сканировать mint для восстановления токенов, связанных с вашей сид-фразой (NUT-13)';
String get recoverTokensDescription =>
'Сканировать mint для восстановления токенов, связанных с вашей сид-фразой (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Использовать текущую сид-фразу';
@@ -729,7 +778,8 @@ class L10nRu extends L10n {
String get selectMintToScan => 'Выберите mint для сканирования';
@override
String get mnemonicMustHaveWords => 'Мнемоника должна содержать 12 или 24 слова';
String get mnemonicMustHaveWords =>
'Мнемоника должна содержать 12 или 24 слова';
@override
String get noConnectedMintsToScan => 'Нет подключённых mint для сканирования';
@@ -740,7 +790,8 @@ class L10nRu extends L10n {
}
@override
String get scanCompleteNoTokens => 'Сканирование завершено. Новых токенов не найдено.';
String get scanCompleteNoTokens =>
'Сканирование завершено. Новых токенов не найдено.';
@override
String mintsWithError(int count) {
@@ -763,7 +814,8 @@ class L10nRu extends L10n {
}
@override
String get noTokensForMnemonic => 'Токены, связанные с этой мнемоникой, не найдены.';
String get noTokensForMnemonic =>
'Токены, связанные с этой мнемоникой, не найдены.';
@override
String get noConnectedMints => 'Нет подключённых mint';
@@ -843,7 +895,8 @@ class L10nRu extends L10n {
String get deleteMintConfirmTitle => 'Удалить mint';
@override
String get deleteMintConfirmMessage => 'Если у вас есть баланс на этом mint, он будет потерян. Вы уверены?';
String get deleteMintConfirmMessage =>
'Если у вас есть баланс на этом mint, он будет потерян. Вы уверены?';
@override
String get delete => 'Удалить';
@@ -854,6 +907,9 @@ class L10nRu extends L10n {
@override
String get selectAll => 'Все';
@override
String get deselectAll => 'Ничего';
@override
String get selectNotesToSend => 'Выберите заметки для отправки:';
@@ -893,10 +949,12 @@ class L10nRu extends L10n {
String get tokenSavedForLater => 'Токен сохранён для получения позже';
@override
String get noConnectionTokenSaved => 'Нет соединения. Токен сохранён для получения позже.';
String get noConnectionTokenSaved =>
'Нет соединения. Токен сохранён для получения позже.';
@override
String get unknownMintOffline => 'Этот токен от неизвестного mint. Подключитесь к интернету, чтобы добавить его и получить токен.';
String get unknownMintOffline =>
'Этот токен от неизвестного mint. Подключитесь к интернету, чтобы добавить его и получить токен.';
@override
String get noConnectionTryLater => 'Нет соединения с mint. Попробуйте позже.';
@@ -905,7 +963,8 @@ class L10nRu extends L10n {
String get saveTokenError => 'Ошибка сохранения токена. Попробуйте снова.';
@override
String get pendingTokenLimitReached => 'Достигнут лимит ожидающих токенов (макс 50)';
String get pendingTokenLimitReached =>
'Достигнут лимит ожидающих токенов (макс 50)';
@override
String get filterToReceive => 'К получению';
@@ -982,7 +1041,8 @@ class L10nRu extends L10n {
String get unrecognizedQrCode => 'Нераспознанный QR-код';
@override
String get scanCashuTokenHint => 'Сканируйте Cashu токен (cashuA... или cashuB...)';
String get scanCashuTokenHint =>
'Сканируйте Cashu токен (cashuA... или cashuB...)';
@override
String get scanLightningInvoiceHint => 'Сканируйте Lightning счёт (lnbc...)';
@@ -994,7 +1054,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';
@@ -1003,10 +1109,12 @@ class L10nRu extends L10n {
String get p2pkSettingsDescription => 'Получить заблокированный ecash';
@override
String get p2pkExperimental => 'P2PK экспериментальный. Используйте с осторожностью.';
String get p2pkExperimental =>
'P2PK экспериментальный. Используйте с осторожностью.';
@override
String get p2pkPendingSendWarning => 'У вас есть ожидающая отправка P2PK. Перейдите в историю и обновите после того, как получатель заберёт токен.';
String get p2pkPendingSendWarning =>
'У вас есть ожидающая отправка P2PK. Перейдите в историю и обновите после того, как получатель заберёт токен.';
@override
String get p2pkExperimentalShort => 'Экспериментальный';
@@ -1063,7 +1171,8 @@ class L10nRu extends L10n {
String get p2pkLockedToOther => 'Заблокировано для другого ключа';
@override
String get p2pkCannotUnlock => 'У вас нет ключа для разблокировки этого токена';
String get p2pkCannotUnlock =>
'У вас нет ключа для разблокировки этого токена';
@override
String get p2pkEnterPrivateKey => 'Введите приватный ключ (nsec)';
@@ -1072,13 +1181,15 @@ class L10nRu extends L10n {
String get p2pkDeleteTitle => 'Удалить ключ';
@override
String get p2pkDeleteConfirm => 'Удалить этот ключ? Вы не сможете получать токены заблокированные на него.';
String get p2pkDeleteConfirm =>
'Удалить этот ключ? Вы не сможете получать токены заблокированные на него.';
@override
String get p2pkRequiresConnection => 'P2PK требует подключения к mint';
@override
String get p2pkErrorMaxKeysReached => 'Достигнуто максимальное количество импортированных ключей (10)';
String get p2pkErrorMaxKeysReached =>
'Достигнуто максимальное количество импортированных ключей (10)';
@override
String get p2pkErrorInvalidNsec => 'Недействительный nsec';
@@ -1093,5 +1204,30 @@ class L10nRu extends L10n {
String get p2pkErrorCannotDeletePrimary => 'Невозможно удалить основной ключ';
@override
String get p2pkSendComingSoon => 'Скоро будет доступно';
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 => 'Скопировано в буфер обмена';
}
+165 -28
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nSw extends L10n {
String get generatingSeed => 'Kutengeneza maneno yako ya mbegu kwa usalama';
@override
String get createWalletDescription => 'Maneno 12 ya mbegu yatatengenezwa.\nYahifadhi mahali salama.';
String get createWalletDescription =>
'Maneno 12 ya mbegu yatatengenezwa.\nYahifadhi mahali salama.';
@override
String get generateWallet => 'Tengeneza pochi';
@@ -69,10 +70,12 @@ class L10nSw extends L10n {
String get walletCreated => 'Pochi imeundwa!';
@override
String get walletCreatedDescription => 'Pochi yako iko tayari. Tunakushauri uhifadhi nakala ya maneno yako ya mbegu sasa.';
String get walletCreatedDescription =>
'Pochi yako iko tayari. Tunakushauri uhifadhi nakala ya maneno yako ya mbegu sasa.';
@override
String get backupWarning => 'Bila nakala, utapoteza uwezo wa kufikia fedha zako ukipoteza kifaa.';
String get backupWarning =>
'Bila nakala, utapoteza uwezo wa kufikia fedha zako ukipoteza kifaa.';
@override
String get backupNow => 'Hifadhi nakala sasa';
@@ -87,7 +90,8 @@ class L10nSw extends L10n {
String get seedPhraseTitle => 'Maneno yako ya mbegu';
@override
String get seedPhraseDescription => 'Hifadhi maneno haya 12 kwa mpangilio. Ndiyo njia pekee ya kurejesha pochi yako.';
String get seedPhraseDescription =>
'Hifadhi maneno haya 12 kwa mpangilio. Ndiyo njia pekee ya kurejesha pochi yako.';
@override
String get revealSeedPhrase => 'Onyesha maneno ya mbegu';
@@ -117,7 +121,8 @@ class L10nSw extends L10n {
String get enterSeedPhrase => 'Ingiza maneno yako ya mbegu';
@override
String get enterSeedDescription => 'Andika maneno 12 au 24 yakitengwa na nafasi.';
String get enterSeedDescription =>
'Andika maneno 12 au 24 yakitengwa na nafasi.';
@override
String get seedPlaceholder => 'neno1 neno2 neno3 ...';
@@ -273,7 +278,8 @@ class L10nSw extends L10n {
String get noActiveMint => 'Hakuna mint inayofanya kazi';
@override
String get offlineModeMessage => 'Hakuna muunganisho. Kutumia hali ya nje ya mtandao...';
String get offlineModeMessage =>
'Hakuna muunganisho. Kutumia hali ya nje ya mtandao...';
@override
String get confirmSend => 'Thibitisha kutuma';
@@ -310,11 +316,50 @@ class L10nSw extends L10n {
}
@override
String get keepTokenWarning => 'Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.';
String get keepTokenWarning =>
'Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.';
@override
String get tokenCopiedToClipboard => 'Tokeni imenakiliwa kwenye ubao';
@override
String get copyAsEmoji => 'Nakili kama emoji';
@override
String get emojiCopiedToClipboard => 'Tokeni imenakiliwa kama emoji 🥜';
@override
String get peanutDecodeError =>
'Imeshindwa kusimbua tokeni ya emoji. Inaweza kuwa imeharibika.';
@override
String get nfcWrite => 'Andika kwenye NFC tag';
@override
String get nfcRead => 'Soma NFC tag';
@override
String get nfcHoldNear => 'Karibia kifaa kwenye NFC tag...';
@override
String get nfcWriteSuccess => 'Tokeni imeandikwa kwenye NFC tag';
@override
String nfcWriteError(String error) {
return 'Hitilafu ya NFC kuandika: $error';
}
@override
String nfcReadError(String error) {
return 'Hitilafu ya NFC kusoma: $error';
}
@override
String get nfcDisabled => 'NFC imezimwa. Iwashe katika Mipangilio.';
@override
String get nfcUnsupported => 'Kifaa hiki hakitumii NFC';
@override
String get amountToDeposit => 'Kiasi cha kuweka:';
@@ -665,7 +710,8 @@ class L10nSw extends L10n {
String get close => 'Funga';
@override
String get aboutDescription => 'Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa La Chispa.';
String get aboutDescription =>
'Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa LaChispa.';
@override
String get couldNotOpenLink => 'Haikuweza kufungua kiungo';
@@ -677,7 +723,8 @@ class L10nSw extends L10n {
String get actionIrreversible => 'Kitendo hiki hakiwezi kutenduliwa';
@override
String get deleteWalletWarning => 'Data zote zitafutwa ikiwa ni pamoja na maneno yako ya mbegu na tokeni. Hakikisha una nakala.';
String get deleteWalletWarning =>
'Data zote zitafutwa ikiwa ni pamoja na maneno yako ya mbegu na tokeni. Hakikisha una nakala.';
@override
String get typeDeleteToConfirm => 'Andika \"FUTA\" kuthibitisha:';
@@ -694,13 +741,15 @@ class L10nSw extends L10n {
String get recoverTokensTitle => 'Rejesha tokeni';
@override
String get recoverTokensDescription => 'Changanua mint kurejesha tokeni zinazohusiana na maneno yako ya mbegu (NUT-13)';
String get recoverTokensDescription =>
'Changanua mint kurejesha tokeni zinazohusiana na maneno yako ya mbegu (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Tumia maneno yangu ya mbegu ya sasa';
@override
String get scanWithSavedWords => 'Changanua mint na maneno 12 yaliyohifadhiwa';
String get scanWithSavedWords =>
'Changanua mint na maneno 12 yaliyohifadhiwa';
@override
String get useOtherSeedPhrase => 'Tumia maneno mengine ya mbegu';
@@ -732,7 +781,8 @@ class L10nSw extends L10n {
String get mnemonicMustHaveWords => 'Mnemonic lazima iwe na maneno 12 au 24';
@override
String get noConnectedMintsToScan => 'Hakuna mint zilizounganishwa za kuchanganua';
String get noConnectedMintsToScan =>
'Hakuna mint zilizounganishwa za kuchanganua';
@override
String recoveredTokens(String tokens, int mints) {
@@ -740,7 +790,8 @@ class L10nSw extends L10n {
}
@override
String get scanCompleteNoTokens => 'Uchanganuzi umekamilika. Hakuna tokeni mpya zilizopatikana.';
String get scanCompleteNoTokens =>
'Uchanganuzi umekamilika. Hakuna tokeni mpya zilizopatikana.';
@override
String mintsWithError(int count) {
@@ -763,7 +814,8 @@ class L10nSw extends L10n {
}
@override
String get noTokensForMnemonic => 'Hakuna tokeni zilizopatikana zinazohusiana na mnemonic hiyo.';
String get noTokensForMnemonic =>
'Hakuna tokeni zilizopatikana zinazohusiana na mnemonic hiyo.';
@override
String get noConnectedMints => 'Hakuna mint zilizounganishwa';
@@ -843,7 +895,8 @@ class L10nSw extends L10n {
String get deleteMintConfirmTitle => 'Futa mint';
@override
String get deleteMintConfirmMessage => 'Ukiwa na salio katika mint hii, itapotea. Una uhakika?';
String get deleteMintConfirmMessage =>
'Ukiwa na salio katika mint hii, itapotea. Una uhakika?';
@override
String get delete => 'Futa';
@@ -854,6 +907,9 @@ class L10nSw extends L10n {
@override
String get selectAll => 'Zote';
@override
String get deselectAll => 'Hakuna';
@override
String get selectNotesToSend => 'Chagua noti unazotaka kutuma:';
@@ -893,19 +949,23 @@ class L10nSw extends L10n {
String get tokenSavedForLater => 'Tokeni imehifadhiwa kudai baadaye';
@override
String get noConnectionTokenSaved => 'Hakuna muunganisho. Tokeni imehifadhiwa kudai baadaye.';
String get noConnectionTokenSaved =>
'Hakuna muunganisho. Tokeni imehifadhiwa kudai baadaye.';
@override
String get unknownMintOffline => 'Tokeni hii ni kutoka mint isiyojulikana. Unganisha na mtandao kuongeza na kudai tokeni.';
String get unknownMintOffline =>
'Tokeni hii ni kutoka mint isiyojulikana. Unganisha na mtandao kuongeza na kudai tokeni.';
@override
String get noConnectionTryLater => 'Hakuna muunganisho na mint. Jaribu baadaye.';
String get noConnectionTryLater =>
'Hakuna muunganisho na mint. Jaribu baadaye.';
@override
String get saveTokenError => 'Hitilafu ya kuhifadhi tokeni. Jaribu tena.';
@override
String get pendingTokenLimitReached => 'Kikomo cha tokeni zinazosubiri kimefikiwa (upeo 50)';
String get pendingTokenLimitReached =>
'Kikomo cha tokeni zinazosubiri kimefikiwa (upeo 50)';
@override
String get filterToReceive => 'Za kupokea';
@@ -973,7 +1033,8 @@ class L10nSw extends L10n {
String get pointCameraAtQr => 'Elekeza kamera kwenye msimbo wa QR';
@override
String get pointCameraAtCashuQr => 'Elekeza kamera kwenye QR ya tokeni ya Cashu';
String get pointCameraAtCashuQr =>
'Elekeza kamera kwenye QR ya tokeni ya Cashu';
@override
String get pointCameraAtInvoiceQr => 'Elekeza kamera kwenye QR ya ankara';
@@ -982,10 +1043,12 @@ class L10nSw extends L10n {
String get unrecognizedQrCode => 'Msimbo wa QR haujatambuliwa';
@override
String get scanCashuTokenHint => 'Changanua tokeni ya Cashu (cashuA... au cashuB...)';
String get scanCashuTokenHint =>
'Changanua tokeni ya Cashu (cashuA... au cashuB...)';
@override
String get scanLightningInvoiceHint => 'Changanua ankara ya Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Changanua ankara ya Lightning (lnbc...)';
@override
String get addMintQuestion => 'Ongeza mint hii?';
@@ -994,7 +1057,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';
@@ -1006,7 +1116,8 @@ class L10nSw extends L10n {
String get p2pkExperimental => 'P2PK ni ya majaribio. Tumia kwa uangalifu.';
@override
String get p2pkPendingSendWarning => 'Una usafirishaji wa P2PK unaosubiri. Nenda kwenye historia na usasishe baada ya mpokeaji kudai tokeni.';
String get p2pkPendingSendWarning =>
'Una usafirishaji wa P2PK unaosubiri. Nenda kwenye historia na usasishe baada ya mpokeaji kudai tokeni.';
@override
String get p2pkExperimentalShort => 'Majaribio';
@@ -1072,13 +1183,15 @@ class L10nSw extends L10n {
String get p2pkDeleteTitle => 'Futa ufunguo';
@override
String get p2pkDeleteConfirm => 'Futa ufunguo huu? Hutaweza kupokea tokeni zilizofungwa kwake.';
String get p2pkDeleteConfirm =>
'Futa ufunguo huu? Hutaweza kupokea tokeni zilizofungwa kwake.';
@override
String get p2pkRequiresConnection => 'P2PK inahitaji muunganisho kwa mint';
@override
String get p2pkErrorMaxKeysReached => 'Idadi ya juu ya funguo zilizoingizwa imefikiwa (10)';
String get p2pkErrorMaxKeysReached =>
'Idadi ya juu ya funguo zilizoingizwa imefikiwa (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec batili';
@@ -1093,5 +1206,29 @@ class L10nSw extends L10n {
String get p2pkErrorCannotDeletePrimary => 'Haiwezekani kufuta ufunguo mkuu';
@override
String get p2pkSendComingSoon => 'Inakuja hivi karibuni';
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';
}
+112 -4
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -315,6 +315,43 @@ class L10nZh extends L10n {
@override
String get tokenCopiedToClipboard => '代币已复制到剪贴板';
@override
String get copyAsEmoji => '复制为表情符号';
@override
String get emojiCopiedToClipboard => '代币已复制为表情符号 🥜';
@override
String get peanutDecodeError => '无法解码表情符号代币。可能已损坏。';
@override
String get nfcWrite => '写入NFC标签';
@override
String get nfcRead => '读取NFC标签';
@override
String get nfcHoldNear => '将设备靠近NFC标签...';
@override
String get nfcWriteSuccess => '代币已写入NFC标签';
@override
String nfcWriteError(String error) {
return 'NFC写入错误:$error';
}
@override
String nfcReadError(String error) {
return 'NFC读取错误:$error';
}
@override
String get nfcDisabled => 'NFC已禁用,请在设置中启用。';
@override
String get nfcUnsupported => '此设备不支持NFC';
@override
String get amountToDeposit => '存入金额:';
@@ -665,7 +702,7 @@ class L10nZh extends L10n {
String get close => '关闭';
@override
String get aboutDescription => '具有古巴基因的 Cashu 钱包,面向全世界。La Chispa 的兄弟项目。';
String get aboutDescription => '具有古巴基因的 Cashu 钱包,面向全世界。LaChispa 的兄弟项目。';
@override
String get couldNotOpenLink => '无法打开链接';
@@ -854,6 +891,9 @@ class L10nZh extends L10n {
@override
String get selectAll => '全部';
@override
String get deselectAll => '';
@override
String get selectNotesToSend => '选择要发送的票据:';
@@ -994,7 +1034,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密钥';
@@ -1093,5 +1177,29 @@ class L10nZh extends L10n {
String get p2pkErrorCannotDeletePrimary => '无法删除主密钥';
@override
String get p2pkSendComingSoon => '即将推出';
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 => '已复制到剪贴板';
}
+41 -3
View File
@@ -137,6 +137,20 @@
},
"keepTokenWarning": "Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.",
"tokenCopiedToClipboard": "Token copiado para área de transferência",
"copyAsEmoji": "Copiar como emoji",
"emojiCopiedToClipboard": "Token copiado como emoji 🥜",
"peanutDecodeError": "Não foi possível decodificar o token emoji. Pode estar corrompido.",
"nfcWrite": "Escrever em tag NFC",
"nfcRead": "Ler tag NFC",
"nfcHoldNear": "Aproxime o dispositivo da tag NFC...",
"nfcWriteSuccess": "Token escrito na tag NFC",
"nfcWriteError": "Erro NFC ao escrever: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Erro NFC ao ler: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC está desativado. Ative nas Configurações.",
"nfcUnsupported": "Este dispositivo não suporta NFC",
"amountToDeposit": "Valor a depositar:",
"descriptionOptional": "Descrição (opcional):",
@@ -296,7 +310,7 @@
}
},
"close": "Fechar",
"aboutDescription": "Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de La Chispa.",
"aboutDescription": "Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de LaChispa.",
"couldNotOpenLink": "Não foi possível abrir o link",
"deleteWalletQuestion": "Apagar wallet?",
@@ -488,7 +502,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.",
@@ -521,5 +550,14 @@
"p2pkErrorKeyAlreadyExists": "Esta chave já existe",
"p2pkErrorKeyNotFound": "Chave não encontrada",
"p2pkErrorCannotDeletePrimary": "Não é possível excluir a chave principal",
"p2pkSendComingSoon": "Em breve disponível"
"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"
}
+41 -3
View File
@@ -137,6 +137,20 @@
},
"keepTokenWarning": "Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.",
"tokenCopiedToClipboard": "Токен скопирован в буфер обмена",
"copyAsEmoji": "Скопировать как эмодзи",
"emojiCopiedToClipboard": "Токен скопирован как эмодзи 🥜",
"peanutDecodeError": "Не удалось декодировать эмодзи-токен. Возможно, он повреждён.",
"nfcWrite": "Записать на NFC-метку",
"nfcRead": "Читать NFC-метку",
"nfcHoldNear": "Поднесите устройство к NFC-метке...",
"nfcWriteSuccess": "Токен записан на NFC-метку",
"nfcWriteError": "Ошибка записи NFC: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Ошибка чтения NFC: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC отключён. Включите в Настройках.",
"nfcUnsupported": "Это устройство не поддерживает NFC",
"amountToDeposit": "Сумма для пополнения:",
"descriptionOptional": "Описание (необязательно):",
@@ -296,7 +310,7 @@
}
},
"close": "Закрыть",
"aboutDescription": "Cashu кошелёк с кубинской ДНК для всего мира. Брат La Chispa.",
"aboutDescription": "Cashu кошелёк с кубинской ДНК для всего мира. Брат LaChispa.",
"couldNotOpenLink": "Не удалось открыть ссылку",
"deleteWalletQuestion": "Удалить кошелёк?",
@@ -488,7 +502,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 экспериментальный. Используйте с осторожностью.",
@@ -521,5 +550,14 @@
"p2pkErrorKeyAlreadyExists": "Этот ключ уже существует",
"p2pkErrorKeyNotFound": "Ключ не найден",
"p2pkErrorCannotDeletePrimary": "Невозможно удалить основной ключ",
"p2pkSendComingSoon": "Скоро будет доступно"
"request": "Запросить",
"requestPayment": "Запросить платёж",
"requestPaymentDescription": "Создать унифицированный запрос на оплату",
"generateRequest": "Создать запрос",
"generatingRequest": "Создание запроса...",
"requestPaymentReceived": "Платёж получен",
"requestDescriptionHint": "Описание (необязательно)",
"universal": "Универсальный",
"copiedToClipboard": "Скопировано в буфер обмена"
}
+41 -3
View File
@@ -137,6 +137,20 @@
},
"keepTokenWarning": "Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.",
"tokenCopiedToClipboard": "Tokeni imenakiliwa kwenye ubao",
"copyAsEmoji": "Nakili kama emoji",
"emojiCopiedToClipboard": "Tokeni imenakiliwa kama emoji 🥜",
"peanutDecodeError": "Imeshindwa kusimbua tokeni ya emoji. Inaweza kuwa imeharibika.",
"nfcWrite": "Andika kwenye NFC tag",
"nfcRead": "Soma NFC tag",
"nfcHoldNear": "Karibia kifaa kwenye NFC tag...",
"nfcWriteSuccess": "Tokeni imeandikwa kwenye NFC tag",
"nfcWriteError": "Hitilafu ya NFC kuandika: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Hitilafu ya NFC kusoma: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC imezimwa. Iwashe katika Mipangilio.",
"nfcUnsupported": "Kifaa hiki hakitumii NFC",
"amountToDeposit": "Kiasi cha kuweka:",
"descriptionOptional": "Maelezo (si lazima):",
@@ -296,7 +310,7 @@
}
},
"close": "Funga",
"aboutDescription": "Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa La Chispa.",
"aboutDescription": "Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa LaChispa.",
"couldNotOpenLink": "Haikuweza kufungua kiungo",
"deleteWalletQuestion": "Futa pochi?",
@@ -488,7 +502,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.",
@@ -521,5 +550,14 @@
"p2pkErrorKeyAlreadyExists": "Ufunguo huu tayari upo",
"p2pkErrorKeyNotFound": "Ufunguo haujapatikana",
"p2pkErrorCannotDeletePrimary": "Haiwezekani kufuta ufunguo mkuu",
"p2pkSendComingSoon": "Inakuja hivi karibuni"
"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"
}
+41 -3
View File
@@ -137,6 +137,20 @@
},
"keepTokenWarning": "请保留此代币直到收款方领取。如果丢失,资金将无法找回。",
"tokenCopiedToClipboard": "代币已复制到剪贴板",
"copyAsEmoji": "复制为表情符号",
"emojiCopiedToClipboard": "代币已复制为表情符号 🥜",
"peanutDecodeError": "无法解码表情符号代币。可能已损坏。",
"nfcWrite": "写入NFC标签",
"nfcRead": "读取NFC标签",
"nfcHoldNear": "将设备靠近NFC标签...",
"nfcWriteSuccess": "代币已写入NFC标签",
"nfcWriteError": "NFC写入错误:{error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "NFC读取错误:{error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC已禁用,请在设置中启用。",
"nfcUnsupported": "此设备不支持NFC",
"amountToDeposit": "存入金额:",
"descriptionOptional": "描述(可选):",
@@ -296,7 +310,7 @@
}
},
"close": "关闭",
"aboutDescription": "具有古巴基因的 Cashu 钱包,面向全世界。La Chispa 的兄弟项目。",
"aboutDescription": "具有古巴基因的 Cashu 钱包,面向全世界。LaChispa 的兄弟项目。",
"couldNotOpenLink": "无法打开链接",
"deleteWalletQuestion": "删除钱包?",
@@ -488,7 +502,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是实验性功能。请谨慎使用。",
@@ -521,5 +550,14 @@
"p2pkErrorKeyAlreadyExists": "此密钥已存在",
"p2pkErrorKeyNotFound": "未找到密钥",
"p2pkErrorCannotDeletePrimary": "无法删除主密钥",
"p2pkSendComingSoon": "即将推出"
"request": "请求",
"requestPayment": "请求付款",
"requestPaymentDescription": "生成统一付款请求",
"generateRequest": "生成请求",
"generatingRequest": "正在生成请求...",
"requestPaymentReceived": "已收到付款",
"requestDescriptionHint": "描述(可选)",
"universal": "通用",
"copiedToClipboard": "已复制到剪贴板"
}
+3 -3
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import 'src/rust/frb_generated.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import 'core/theme/app_theme.dart';
import 'providers/wallet_provider.dart';
@@ -15,8 +15,8 @@ import 'screens/1_splash/splash_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inicializar CDK Flutter (Cashu SDK)
await CdkFlutter.init();
// Inicializar bridge Rust interno (Cashu SDK)
await RustLib.init();
// Configurar orientación y estilo de barra de estado
SystemChrome.setPreferredOrientations([
+1 -1
View File
@@ -11,7 +11,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:bip32/bip32.dart' as bip32;
import 'package:cdk_flutter/cdk_flutter.dart' as cdk;
import '../src/rust/api/keys.dart' as cdk;
import 'package:uuid/uuid.dart';
import '../models/p2pk_key.dart';
+379 -162
View File
@@ -3,15 +3,20 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:cbor/cbor.dart';
import 'package:cdk_flutter/cdk_flutter.dart';
import 'package:http/http.dart' as http;
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
@@ -63,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;
@@ -74,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';
@@ -177,15 +187,11 @@ class WalletProvider extends ChangeNotifier {
// ============================================================
/// Verifica si podemos alcanzar un mint específico.
/// Hace ping HTTP GET a {mintUrl}/v1/info con timeout de 3 segundos.
/// Usa función Rust que hace GET a {mintUrl}/v1/info.
/// Retorna true si responde 200, false en cualquier otro caso.
Future<bool> canReachMint(String mintUrl) async {
try {
final uri = Uri.parse('$mintUrl/v1/info');
final response = await http.get(uri).timeout(
const Duration(seconds: 3),
);
return response.statusCode == 200;
return await pingMint(mintUrl: mintUrl);
} catch (e) {
debugPrint('Ping to mint failed: $e');
return false;
@@ -350,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');
@@ -401,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
// ============================================================
@@ -409,7 +475,7 @@ class WalletProvider extends ChangeNotifier {
/// Usa mintInfo.nuts.nut04 (MintMethodSettings) para obtener las unidades.
Future<List<String>> _detectAndAddMint(String mintUrl) async {
try {
// Obtener info del mint (función global de cdk_flutter)
// Obtener info del mint (función del bridge Rust)
final mintInfo = await getMintInfo(mintUrl: mintUrl);
// Extraer unidades de nut04 (mint methods)
@@ -446,31 +512,14 @@ class WalletProvider extends ChangeNotifier {
}
/// Obtiene los keysets de un mint y los agrega al caché _keysetUnits.
/// Llama a GET {mintUrl}/v1/keysets para obtener el mapping keyset_id → unit.
/// Usa función Rust que llama a GET {mintUrl}/v1/keysets.
Future<void> _fetchKeysets(String mintUrl) async {
try {
final response = await http.get(
Uri.parse('$mintUrl/v1/keysets'),
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final keysets = data['keysets'] as List<dynamic>?;
if (keysets != null) {
for (final ks in keysets) {
final id = ks['id'] as String?;
final unit = ks['unit'] as String?;
if (id != null && unit != null) {
_keysetUnits[id] = unit;
}
}
debugPrint('Keysets cargados para $mintUrl: ${keysets.length}');
}
} else {
debugPrint('Error HTTP ${response.statusCode} al obtener keysets de $mintUrl');
final keysets = await fetchKeysets(mintUrl: mintUrl);
for (final ks in keysets) {
_keysetUnits[ks.id] = ks.unit;
}
debugPrint('Keysets cargados para $mintUrl: ${keysets.length}');
} catch (e) {
debugPrint('Error obteniendo keysets de $mintUrl: $e');
// No lanzamos excepción, el caché simplemente no tendrá estos keysets
@@ -617,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.
@@ -859,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)');
@@ -967,11 +1042,12 @@ class WalletProvider extends ChangeNotifier {
}
/// Prepara un envío P2PK (bloqueado a una clave pública).
/// includeFee: true asegura que se reserven proofs para cubrir el swap fee (CDK 0.15+).
Future<PreparedSend> prepareSendP2pk(BigInt amount, String pubkey) async {
final wallet = await getActiveWallet();
return await wallet.prepareSend(
amount: amount,
opts: SendOptions(pubkey: pubkey),
opts: SendOptions(pubkey: pubkey, includeFee: true),
);
}
@@ -980,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).
@@ -1028,91 +1092,41 @@ class WalletProvider extends ChangeNotifier {
}
/// Método de conveniencia: prepara y confirma en un solo paso.
/// Si confirmSend falla, libera proofs reservados con cancelSend.
Future<String> sendTokens(BigInt amount, String? memo) async {
final wallet = await getActiveWallet();
final balanceBefore = await wallet.balance();
debugPrint('[SEND DEBUG] Normal send - balance=$balanceBefore, amount=$amount');
await KeysetDebug.logCounters('BEFORE normal send');
final prepared = await prepareSend(amount);
debugPrint('[SEND DEBUG] prepareSend OK - fee=${prepared.fee}');
final token = await confirmSend(prepared, memo);
final balanceAfter = await wallet.balance();
debugPrint('[SEND DEBUG] confirmSend OK - balance after=$balanceAfter');
await KeysetDebug.logCounters('AFTER normal send');
return token;
}
/// Verifica si el mint activo soporta P2PK sin el bug de fees.
/// Bug CDK: prepare_send con P2PK (force_swap=true) no reserva proofs
/// para el swap fee. Solo mints con ppk=0 funcionan correctamente.
/// Ver P2PK_SEND_BUG.md para análisis completo.
Future<void> _checkP2pkMintCompatibility() async {
final mintUrl = _activeMintUrl!;
final unit = _activeUnit;
final ppk = await KeysetDebug.getInputFeePpk(mintUrl, unit);
debugPrint('[P2PK] mintUrl=$mintUrl, unit=$unit, ppk=$ppk');
if (ppk > 0) {
throw Exception(
'P2PK no disponible en este mint. '
'El mint $mintUrl cobra input fees (ppk=$ppk) y CDK tiene un bug '
'que impide P2PK sends en mints con fees. '
'Usa un mint con ppk=0 (ej: mint.cubabitcoin.org).',
);
debugPrint('[SEND] prepareSend OK - fee=${prepared.fee}');
try {
final token = await confirmSend(prepared, memo);
debugPrint('[SEND] Send completed');
return token;
} catch (e) {
try {
await cancelSend(prepared);
} catch (cancelErr) {
debugPrint('[SEND] cancelSend failed: $cancelErr');
}
rethrow;
}
}
/// Envía tokens P2PK (bloqueados a una clave pública).
/// Solo funciona en mints con ppk=0 debido a bug en CDK core.
/// Ver P2PK_SEND_BUG.md para detalles.
/// CDK 0.15+ con includeFee: true maneja correctamente mints con ppk>0.
/// Si confirmSend falla, libera proofs reservados con cancelSend.
Future<String> sendTokensP2pk(BigInt amount, String pubkey, String? memo) async {
final wallet = await getActiveWallet();
// Verificar compatibilidad del mint con P2PK
await _checkP2pkMintCompatibility();
// DEBUG: estado antes del P2PK send
final balanceBefore = await wallet.balance();
debugPrint('[P2PK DEBUG] ===== BEFORE P2PK SEND =====');
debugPrint('[P2PK DEBUG] Balance: $balanceBefore');
debugPrint('[P2PK DEBUG] Amount to send: $amount');
debugPrint('[P2PK DEBUG] Pubkey: ${pubkey.length > 16 ? pubkey.substring(0, 16) : pubkey}...');
// DEBUG: counters antes de prepareSend
await KeysetDebug.logCounters('BEFORE P2PK prepareSend');
// Paso 1: prepareSend P2PK
debugPrint('[P2PK DEBUG] Calling prepareSendP2pk...');
debugPrint('[P2PK] Sending $amount to ${pubkey.length > 16 ? pubkey.substring(0, 16) : pubkey}...');
final prepared = await prepareSendP2pk(amount, pubkey);
debugPrint('[P2PK DEBUG] prepareSendP2pk OK - fee=${prepared.fee}');
final balanceAfterPrepare = await wallet.balance();
debugPrint('[P2PK DEBUG] Balance after prepare: $balanceAfterPrepare');
// DEBUG: counters después de prepareSend
await KeysetDebug.logCounters('AFTER P2PK prepareSend');
// Paso 2: confirmSend
debugPrint('[P2PK DEBUG] Calling confirmSend...');
debugPrint('[P2PK] prepareSend OK - fee=${prepared.fee}');
try {
final token = await confirmSend(prepared, memo);
final balanceAfterSend = await wallet.balance();
debugPrint('[P2PK DEBUG] confirmSend OK!');
debugPrint('[P2PK DEBUG] Balance after send: $balanceAfterSend');
// DEBUG: counters después de send exitoso
await KeysetDebug.logCounters('AFTER P2PK send OK');
debugPrint('[P2PK] Send completed');
return token;
} catch (e) {
final balanceAfterError = await wallet.balance();
debugPrint('[P2PK DEBUG] confirmSend FAILED: $e');
debugPrint('[P2PK DEBUG] Balance after error: $balanceAfterError');
// DEBUG: counters después de send fallido
await KeysetDebug.logCounters('AFTER P2PK send FAILED');
try {
await cancelSend(prepared);
} catch (cancelErr) {
debugPrint('[P2PK] cancelSend failed: $cancelErr');
}
rethrow;
}
}
@@ -1130,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)
// ============================================================
@@ -1159,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)
@@ -1172,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;
@@ -1181,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)
@@ -1291,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;
}
}
@@ -1442,17 +1607,52 @@ class WalletProvider extends ChangeNotifier {
/// Llamar en background al iniciar la app.
/// También vincula transacciones incoming sin metadata con invoices pendientes.
Future<void> checkPendingTransactions() async {
for (final wallet in _wallets.values) {
try {
await wallet.checkPendingTransactions();
} catch (e) {
// Silencioso - puede fallar offline
debugPrint('Check pending failed: $e');
// Iterar todos los mints y todas sus unidades, no solo los wallets
// ya instanciados. initialize() solo precarga units.first por mint,
// así que quotes/sagas/melts en otras unidades se perderían.
for (final entry in _mintUnits.entries) {
for (final unit in entry.value) {
try {
final wallet = await getWallet(entry.key, unit);
try {
// Reclamar quotes pagados pero no emitidos (Lightning → proofs)
await wallet.checkAllMintQuotes();
} catch (e) {
debugPrint('Check mint quotes failed: $e');
}
try {
// Recuperar sagas incompletas (swaps/mints/melts interrumpidos)
await wallet.recoverIncompleteSagas();
} catch (e) {
debugPrint('Recover incomplete sagas failed: $e');
}
try {
// Finalizar retiros Lightning que quedaron a medias
await wallet.finalizePendingMelts();
} catch (e) {
debugPrint('Finalize pending melts failed: $e');
}
try {
await wallet.checkPendingTransactions();
} catch (e) {
// Silencioso - puede fallar offline
debugPrint('Check pending failed: $e');
}
} catch (e) {
debugPrint('Error getting wallet ${entry.key}:$unit: $e');
}
}
}
// 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
@@ -1606,7 +1806,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,
@@ -1614,7 +1814,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) {
@@ -1653,6 +1853,8 @@ class WalletProvider extends ChangeNotifier {
_activeUnit = 'sat';
_mnemonic = null;
_db = null;
_cachedBalanceStream = null;
_cachedBalanceKey = null;
// Limpiar metadata de transacciones
await _txMetaStorage.clear();
@@ -1681,6 +1883,8 @@ class WalletProvider extends ChangeNotifier {
_activeUnit = 'sat';
_mnemonic = null;
_db = null;
_cachedBalanceStream = null;
_cachedBalanceKey = null;
notifyListeners();
}
@@ -1737,7 +1941,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');
@@ -1750,8 +1955,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);
@@ -1780,7 +1993,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};
@@ -1794,7 +2008,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
@@ -1845,6 +2060,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',
),
);
}
}
@@ -64,7 +64,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
final p2pkProvider = context.read<P2PKProvider>();
// Inicializar wallet primero (valida el mnemonic internamente).
// Si es inválido, cdk_flutter lanzará una excepción antes de persistir.
// Si es inválido, el bridge Rust lanzará una excepción antes de persistir.
await walletProvider.initialize(mnemonic);
// Solo guardar mnemonic si initialize() pasó sin error
+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()),
);
},
),
+215 -7
View File
@@ -8,12 +8,15 @@ import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../core/utils/incoming_data_parser.dart' hide TokenInfo;
import '../../core/utils/nostr_utils.dart';
import '../../core/utils/peanut_codec.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 '../../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 {
@@ -46,9 +49,14 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
bool _showManualKey = false; // Toggle para mostrar/ocultar nsec
String? _manualKeyError;
// NFC
NfcState _nfcState = NfcState.unsupported;
bool _nfcReading = false;
@override
void initState() {
super.initState();
_checkNfc();
// Pre-cargar token inicial si existe
if (widget.initialToken != null && widget.initialToken!.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -60,11 +68,19 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
@override
void dispose() {
if (_nfcReading) NfcService.stopRead();
_tokenController.dispose();
_manualKeyController.dispose();
super.dispose();
}
Future<void> _checkNfc() async {
final state = await NfcService.checkState();
if (mounted) {
setState(() => _nfcState = state);
}
}
@override
Widget build(BuildContext context) {
return GradientBackground(
@@ -245,6 +261,9 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
// Botón pegar (expandido)
Expanded(child: _buildPasteButton()),
const SizedBox(width: 12),
// Botón NFC (siempre visible)
_buildNfcButton(),
const SizedBox(width: 12),
// Botón escanear QR
_buildScanButton(),
],
@@ -291,6 +310,134 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
);
}
Widget _buildNfcButton() {
final isNfcEnabled = _nfcState == NfcState.enabled;
final isNfcUnsupported = _nfcState == NfcState.unsupported;
return Tooltip(
message: isNfcUnsupported
? L10n.of(context)!.nfcUnsupported
: isNfcEnabled
? L10n.of(context)!.nfcRead
: L10n.of(context)!.nfcDisabled,
child: GestureDetector(
onTap: _toggleNfcRead,
child: Opacity(
opacity: isNfcUnsupported ? 0.3 : 1.0,
child: Container(
padding: const EdgeInsets.all(AppDimensions.paddingSmall + 4),
decoration: BoxDecoration(
color: _nfcReading
? AppColors.primaryAction.withValues(alpha: 0.3)
: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _nfcReading
? AppColors.primaryAction
: Colors.white.withValues(alpha: 0.2),
width: 1,
),
),
child: Icon(
LucideIcons.nfc,
color: _nfcReading
? AppColors.primaryAction
: isNfcEnabled
? AppColors.textSecondary
: AppColors.textSecondary.withValues(alpha: 0.5),
size: 24,
),
),
),
),
);
}
void _toggleNfcRead() async {
// Cancel active session first (before state checks, in case NFC was
// disabled while a session was running)
if (_nfcReading) {
NfcService.stopRead();
setState(() => _nfcReading = false);
return;
}
// Re-check state in case user toggled NFC in settings
final currentState = await NfcService.checkState();
if (mounted) setState(() => _nfcState = currentState);
if (currentState == NfcState.unsupported) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcUnsupported),
backgroundColor: AppColors.textSecondary,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
return;
}
if (currentState == NfcState.disabled) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcDisabled),
backgroundColor: AppColors.warning,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
return;
}
setState(() => _nfcReading = true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcHoldNear),
backgroundColor: AppColors.primaryAction,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
try {
await NfcService.startRead(
onTokenRead: (token) {
if (!mounted) return;
setState(() => _nfcReading = false);
_tokenController.text = token;
_onTokenChanged(token);
},
onError: (error) {
if (!mounted) return;
setState(() => _nfcReading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcReadError(error)),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
);
},
);
} catch (e) {
if (!mounted) return;
setState(() => _nfcReading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcReadError(e.toString())),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
);
}
}
Widget _buildScanButton() {
return GestureDetector(
onTap: _openScanner,
@@ -779,9 +926,29 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
Future<void> _pasteFromClipboard() async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
if (!mounted) return;
if (clipboardData?.text != null) {
_tokenController.text = clipboardData!.text!.trim();
_onTokenChanged(clipboardData.text!.trim());
var text = clipboardData!.text!.trim();
// Auto-decode peanut emoji format
if (PeanutCodec.isPeanut(text)) {
final decoded = PeanutCodec.decode(text);
if (decoded != null) {
text = decoded;
} else {
// Malformed peanut: clear field and show specific error
_tokenController.clear();
setState(() {
_isValidToken = false;
_tokenInfo = null;
_errorMessage = L10n.of(context)!.peanutDecodeError;
});
return;
}
}
_tokenController.text = text;
_onTokenChanged(text);
}
}
@@ -789,6 +956,28 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
final walletProvider = context.read<WalletProvider>();
final p2pkProvider = context.read<P2PKProvider>();
// Auto-decode peanut emoji format
var tokenValue = value.trim();
if (PeanutCodec.isPeanut(tokenValue)) {
final decoded = PeanutCodec.decode(tokenValue);
if (decoded != null) {
tokenValue = decoded;
// Update the text field with the decoded token
_tokenController.text = tokenValue;
_tokenController.selection = TextSelection.collapsed(
offset: tokenValue.length,
);
} else {
// Malformed peanut: show specific error
setState(() {
_isValidToken = false;
_tokenInfo = null;
_errorMessage = L10n.of(context)!.peanutDecodeError;
});
return;
}
}
setState(() {
_errorMessage = null;
_manualKeyError = null;
@@ -799,24 +988,43 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
_lockedToPubkeyHex = null;
_matchingKeyLabel = null;
if (value.isEmpty) {
if (tokenValue.isEmpty) {
_isValidToken = false;
_tokenInfo = null;
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(value.trim());
final tokenInfo = walletProvider.parseToken(tokenValue);
if (tokenInfo != null) {
_isValidToken = true;
_tokenInfo = tokenInfo;
// Detectar P2PK
_isP2PKLocked = p2pkProvider.isTokenLocked(value.trim());
_isP2PKLocked = p2pkProvider.isTokenLocked(tokenValue);
if (_isP2PKLocked) {
_lockedToPubkeyHex = p2pkProvider.extractLockedPubkey(value.trim());
_isLockedToUs = p2pkProvider.isTokenLockedToUs(value.trim());
_lockedToPubkeyHex = p2pkProvider.extractLockedPubkey(tokenValue);
_isLockedToUs = p2pkProvider.isTokenLockedToUs(tokenValue);
// Si es nuestra, buscar el label de la clave
if (_isLockedToUs && _lockedToPubkeyHex != null) {
+28 -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,6 +6,7 @@ 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';
@@ -45,7 +47,7 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
@override
void dispose() {
_memoController.dispose();
_proofService.close();
if (!_isCreating) _proofService.close();
super.dispose();
}
@@ -321,32 +323,40 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
}
Future<void> _createOfflineToken() async {
final selectedProofs = _selectedProofs;
final selectedTotal = _proofService.calculateTotal(selectedProofs);
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 +364,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;
+158 -45
View File
@@ -13,6 +13,7 @@ import '../../widgets/common/primary_button.dart';
import '../../widgets/common/numpad_widget.dart';
import '../../providers/wallet_provider.dart';
import '../../core/utils/nostr_utils.dart';
import '../../widgets/scanner/qr_scanner_widget.dart';
import 'share_token_screen.dart';
import 'offline_send_screen.dart';
@@ -276,57 +277,109 @@ class _SendScreenState extends State<SendScreen> {
Widget _buildP2PKSection() {
final l10n = L10n.of(context)!;
// P2PK Send deshabilitado temporalmente por bug en CDK (cdk-flutter usa CDK 0.13.4).
// Se habilitará cuando cdk-flutter actualice a CDK 0.14.x con el fix de include_fee.
const bool p2pkSendEnabled = false;
return Opacity(
opacity: p2pkSendEnabled ? 1.0 : 0.5,
child: GlassCard(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.paddingMedium,
vertical: AppDimensions.paddingSmall,
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GlassCard(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.paddingMedium,
vertical: AppDimensions.paddingSmall,
),
child: Row(
children: [
Icon(
LucideIcons.lock,
color: _useP2PK ? AppColors.primaryAction : AppColors.textSecondary,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.p2pkLockToKey,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
Text(
l10n.p2pkLockDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
),
),
Switch(
value: _useP2PK,
onChanged: (value) => setState(() => _useP2PK = value),
activeColor: AppColors.primaryAction,
),
],
),
),
child: Row(
children: [
Icon(
LucideIcons.lock,
color: AppColors.textSecondary,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.p2pkLockToKey,
style: TextStyle(
// Campo de pubkey cuando P2PK está activo
if (_useP2PK) ...[
const SizedBox(height: AppDimensions.paddingSmall),
GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _pubkeyController,
onChanged: _validatePubkey,
maxLines: 1,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
decoration: InputDecoration(
hintText: l10n.p2pkReceiverPubkey,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: p2pkSendEnabled ? Colors.white : AppColors.textSecondary,
color: Colors.white.withValues(alpha: 0.3),
),
),
Text(
l10n.p2pkSendComingSoon,
style: TextStyle(
border: InputBorder.none,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(LucideIcons.clipboard, size: 20),
color: AppColors.textSecondary,
onPressed: _pastePubkey,
),
IconButton(
icon: const Icon(LucideIcons.scanLine, size: 20),
color: AppColors.textSecondary,
tooltip: l10n.scanQrCode,
onPressed: _scanPubkey,
),
],
),
errorText: _pubkeyError,
errorStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.7),
color: AppColors.error,
),
),
],
),
),
],
),
Switch(
value: false,
onChanged: null,
activeColor: AppColors.primaryAction,
),
],
),
),
),
],
],
);
}
@@ -344,12 +397,71 @@ class _SendScreenState extends State<SendScreen> {
Future<void> _pastePubkey() async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
if (clipboardData?.text != null) {
_pubkeyController.text = clipboardData!.text!;
_validatePubkey(clipboardData.text!);
if (!mounted) return;
final text = clipboardData?.text?.trim();
if (text != null && text.isNotEmpty) {
_pubkeyController.text = text;
_validatePubkey(text);
}
}
Future<void> _scanPubkey() async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.x, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
L10n.of(context)!.scanQrCode,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
centerTitle: true,
),
body: QrScannerWidget(
onDetect: (data) {
final trimmed = data.trim();
if (NostrUtils.isValidP2PKPubkey(trimmed)) {
Navigator.pop(context, trimmed);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.p2pkInvalidPubkey),
duration: const Duration(seconds: 2),
),
);
}
},
onError: (error) {
debugPrint('[P2PK Scan] $error');
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.cameraPermissionDenied),
backgroundColor: AppColors.error,
),
);
},
),
),
),
);
if (!mounted || result == null) return;
_pubkeyController.text = result;
_validatePubkey(result);
}
Widget _buildErrorMessage() {
return Container(
margin: const EdgeInsets.only(bottom: AppDimensions.paddingMedium),
@@ -469,6 +581,7 @@ class _SendScreenState extends State<SendScreen> {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _ConfirmationModal(
amount: _amount,
unit: _activeUnit,
+173 -4
View File
@@ -4,11 +4,13 @@ import 'package:flutter/services.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:share_plus/share_plus.dart';
import 'package:cdk_flutter/cdk_flutter.dart' as cdk;
import '../../src/rust/api/token.dart' as cdk;
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/peanut_codec.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';
@@ -44,21 +46,39 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
int _currentInterval = _intervalMedium;
String _speedLabel = 'M';
// NFC
NfcState _nfcState = NfcState.unsupported;
bool _nfcWriting = false;
@override
void initState() {
super.initState();
_encodeTokenToUR();
_checkNfc();
}
@override
void dispose() {
_animationTimer?.cancel();
if (_nfcWriting) NfcService.stopEmulating();
super.dispose();
}
Future<void> _checkNfc() async {
final state = await NfcService.checkState();
if (mounted) {
setState(() => _nfcState = state);
}
}
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
@@ -155,9 +175,14 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
const SizedBox(height: AppDimensions.paddingMedium),
// Botones copiar y compartir
// Botones principales: copiar y compartir
_buildActionButtons(context),
const SizedBox(height: AppDimensions.paddingSmall),
// Botones secundarios: bean (peanut) y NFC
_buildSecondaryActions(context),
const SizedBox(height: AppDimensions.paddingLarge),
// Advertencia
@@ -450,6 +475,75 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
);
}
Widget _buildSecondaryActions(BuildContext context) {
final isNfcEnabled = _nfcState == NfcState.enabled;
final isNfcUnsupported = _nfcState == NfcState.unsupported;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Copiar como emoji (peanut encoding)
Tooltip(
message: L10n.of(context)!.copyAsEmoji,
child: GestureDetector(
onTap: () => _copyPeanut(context),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Icon(LucideIcons.bean, color: AppColors.primaryAction, size: 20),
),
),
),
const SizedBox(width: 12),
// NFC - siempre visible con 3 estados
Tooltip(
message: isNfcUnsupported
? L10n.of(context)!.nfcUnsupported
: isNfcEnabled
? L10n.of(context)!.nfcWrite
: L10n.of(context)!.nfcDisabled,
child: GestureDetector(
onTap: () => _writeNfc(context),
child: Opacity(
opacity: isNfcUnsupported ? 0.3 : 1.0,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _nfcWriting
? AppColors.primaryAction.withValues(alpha: 0.3)
: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _nfcWriting
? AppColors.primaryAction
: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Icon(
LucideIcons.nfc,
color: _nfcWriting
? AppColors.primaryAction
: isNfcEnabled
? AppColors.textSecondary
: AppColors.textSecondary.withValues(alpha: 0.5),
size: 20,
),
),
),
),
),
],
);
}
Widget _buildWarning() {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
@@ -492,6 +586,19 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
);
}
void _copyPeanut(BuildContext context) {
final peanut = PeanutCodec.encode(widget.token);
Clipboard.setData(ClipboardData(text: peanut));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.emojiCopiedToClipboard),
backgroundColor: AppColors.success,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
}
void _shareToken(BuildContext context) async {
final memo = widget.memo != null && widget.memo!.isNotEmpty
? '\n"${widget.memo}"'
@@ -508,6 +615,68 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
);
}
void _writeNfc(BuildContext context) async {
// Toggle off: stop emulating
if (_nfcWriting) {
await NfcService.stopEmulating();
setState(() => _nfcWriting = false);
return;
}
// Re-check state in case user toggled NFC in settings
final currentState = await NfcService.checkState();
if (mounted) setState(() => _nfcState = currentState);
if (currentState == NfcState.unsupported) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcUnsupported),
backgroundColor: AppColors.textSecondary,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
return;
}
if (currentState == NfcState.disabled) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcDisabled),
backgroundColor: AppColors.warning,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
return;
}
try {
await NfcService.startEmulating(widget.token);
setState(() => _nfcWriting = true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcHoldNear),
backgroundColor: AppColors.primaryAction,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
} catch (e) {
if (!mounted) return;
setState(() => _nfcWriting = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcWriteError(e.toString())),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
);
}
}
void _goToHome(BuildContext context) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
+3 -4
View File
@@ -4,7 +4,7 @@ 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:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import '../../src/rust/api/wallet.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -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,
+16 -3
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import '../../src/rust/api/wallet.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -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);
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:cdk_flutter/cdk_flutter.dart' show MintInfo, ContactInfo;
import '../../src/rust/api/mint_info.dart' show MintInfo, ContactInfo;
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/utils/formatters.dart';
+2 -2
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:provider/provider.dart';
import 'package:cdk_flutter/cdk_flutter.dart' show MintInfo;
import '../../src/rust/api/mint_info.dart' show MintInfo;
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -623,7 +623,7 @@ class _AddMintDialogState extends State<_AddMintDialog> {
});
try {
// Intentar agregar el mint (cdk_flutter validará la conexión)
// Intentar agregar el mint (el bridge Rust validará la conexión)
await widget.walletProvider.addMint(url);
if (mounted) {
+89 -39
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:elcaju/l10n/app_localizations.dart';
@@ -24,6 +25,25 @@ class SettingsScreen extends StatefulWidget {
}
class _SettingsScreenState extends State<SettingsScreen> {
String _appVersion = '';
@override
void initState() {
super.initState();
_loadVersion();
}
Future<void> _loadVersion() async {
try {
final info = await PackageInfo.fromPlatform();
if (!mounted) return;
setState(() => _appVersion = info.version);
} catch (_) {
if (!mounted) return;
setState(() => _appVersion = '');
}
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
@@ -124,7 +144,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildInfoTile(
icon: LucideIcons.tag,
title: l10n.version,
subtitle: '0.0.1',
subtitle: _appVersion,
),
_buildSettingTile(
icon: LucideIcons.info,
@@ -620,7 +640,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
const SizedBox(height: 4),
Text(
'v0.0.1',
'v$_appVersion',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
@@ -638,43 +658,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
L10n.of(context)!.aboutDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.bitcoin,
color: AppColors.secondaryAction,
size: 16,
),
const SizedBox(width: 8),
Text(
'Cuba Bitcoin',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.secondaryAction,
),
),
],
),
),
_buildAboutDescription(context),
],
),
actions: [
@@ -693,6 +677,72 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
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 (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.couldNotOpenLink),
backgroundColor: AppColors.error,
),
);
}
}
}
/// 4. Abrir GitHub
Future<void> _openGitHub() async {
final url = Uri.parse('https://github.com/Forte11Cuba/elcaju');
+21 -7
View File
@@ -5,8 +5,8 @@ import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:intl/intl.dart';
import 'package:cdk_flutter/cdk_flutter.dart' as cdk;
import 'package:cdk_flutter/cdk_flutter.dart' show Transaction, TransactionDirection, TransactionStatus;
import '../../src/rust/api/token.dart' as cdk;
import '../../src/rust/api/wallet.dart' show Transaction, TransactionDirection, TransactionStatus;
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -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(
@@ -722,10 +731,15 @@ class _TransactionDetailScreenState extends State<_TransactionDetailScreen> {
/// Busca el invoice en pending mint invoices si no hay metadata.
Future<void> _loadPendingMintInvoice() async {
setState(() => _isLoadingPendingInvoice = true);
final invoice = await widget.walletProvider.findPendingMintInvoice(
widget.transaction.mintUrl,
widget.transaction.unit,
);
String? invoice;
try {
invoice = await widget.walletProvider.findPendingMintInvoice(
widget.transaction.mintUrl,
widget.transaction.unit,
);
} catch (e, st) {
debugPrint('findPendingMintInvoice failed: $e\n$st');
}
if (mounted) {
setState(() {
_isLoadingPendingInvoice = false;
+31
View File
@@ -0,0 +1,31 @@
// 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 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'error.freezed.dart';
// 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`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
@freezed
sealed class Error with _$Error implements FrbException {
const Error._();
/// Error del protocolo Cashu (cdk)
const factory Error.cdk(String field0) = Error_Cdk;
/// Error de base de datos (SQLite)
const factory Error.database(String field0) = Error_Database;
/// Input inválido del usuario
const factory Error.invalidInput() = Error_InvalidInput;
/// Error de red (HTTP, conexión)
const factory Error.network(String field0) = Error_Network;
/// Error de UR (QR multiframe)
const factory Error.ur(String field0) = Error_Ur;
}
+863
View File
@@ -0,0 +1,863 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'error.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$Error {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ErrorCopyWith<$Res> {
factory $ErrorCopyWith(Error value, $Res Function(Error) then) =
_$ErrorCopyWithImpl<$Res, Error>;
}
/// @nodoc
class _$ErrorCopyWithImpl<$Res, $Val extends Error>
implements $ErrorCopyWith<$Res> {
_$ErrorCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$Error_CdkImplCopyWith<$Res> {
factory _$$Error_CdkImplCopyWith(
_$Error_CdkImpl value,
$Res Function(_$Error_CdkImpl) then,
) = __$$Error_CdkImplCopyWithImpl<$Res>;
@useResult
$Res call({String field0});
}
/// @nodoc
class __$$Error_CdkImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_CdkImpl>
implements _$$Error_CdkImplCopyWith<$Res> {
__$$Error_CdkImplCopyWithImpl(
_$Error_CdkImpl _value,
$Res Function(_$Error_CdkImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? field0 = null}) {
return _then(
_$Error_CdkImpl(
null == field0
? _value.field0
: field0 // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$Error_CdkImpl extends Error_Cdk {
const _$Error_CdkImpl(this.field0) : super._();
@override
final String field0;
@override
String toString() {
return 'Error.cdk(field0: $field0)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Error_CdkImpl &&
(identical(other.field0, field0) || other.field0 == field0));
}
@override
int get hashCode => Object.hash(runtimeType, field0);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$Error_CdkImplCopyWith<_$Error_CdkImpl> get copyWith =>
__$$Error_CdkImplCopyWithImpl<_$Error_CdkImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return cdk(field0);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return cdk?.call(field0);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (cdk != null) {
return cdk(field0);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return cdk(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return cdk?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (cdk != null) {
return cdk(this);
}
return orElse();
}
}
abstract class Error_Cdk extends Error {
const factory Error_Cdk(final String field0) = _$Error_CdkImpl;
const Error_Cdk._() : super._();
String get field0;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$Error_CdkImplCopyWith<_$Error_CdkImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$Error_DatabaseImplCopyWith<$Res> {
factory _$$Error_DatabaseImplCopyWith(
_$Error_DatabaseImpl value,
$Res Function(_$Error_DatabaseImpl) then,
) = __$$Error_DatabaseImplCopyWithImpl<$Res>;
@useResult
$Res call({String field0});
}
/// @nodoc
class __$$Error_DatabaseImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_DatabaseImpl>
implements _$$Error_DatabaseImplCopyWith<$Res> {
__$$Error_DatabaseImplCopyWithImpl(
_$Error_DatabaseImpl _value,
$Res Function(_$Error_DatabaseImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? field0 = null}) {
return _then(
_$Error_DatabaseImpl(
null == field0
? _value.field0
: field0 // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$Error_DatabaseImpl extends Error_Database {
const _$Error_DatabaseImpl(this.field0) : super._();
@override
final String field0;
@override
String toString() {
return 'Error.database(field0: $field0)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Error_DatabaseImpl &&
(identical(other.field0, field0) || other.field0 == field0));
}
@override
int get hashCode => Object.hash(runtimeType, field0);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$Error_DatabaseImplCopyWith<_$Error_DatabaseImpl> get copyWith =>
__$$Error_DatabaseImplCopyWithImpl<_$Error_DatabaseImpl>(
this,
_$identity,
);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return database(field0);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return database?.call(field0);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (database != null) {
return database(field0);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return database(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return database?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (database != null) {
return database(this);
}
return orElse();
}
}
abstract class Error_Database extends Error {
const factory Error_Database(final String field0) = _$Error_DatabaseImpl;
const Error_Database._() : super._();
String get field0;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$Error_DatabaseImplCopyWith<_$Error_DatabaseImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$Error_InvalidInputImplCopyWith<$Res> {
factory _$$Error_InvalidInputImplCopyWith(
_$Error_InvalidInputImpl value,
$Res Function(_$Error_InvalidInputImpl) then,
) = __$$Error_InvalidInputImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$Error_InvalidInputImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_InvalidInputImpl>
implements _$$Error_InvalidInputImplCopyWith<$Res> {
__$$Error_InvalidInputImplCopyWithImpl(
_$Error_InvalidInputImpl _value,
$Res Function(_$Error_InvalidInputImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$Error_InvalidInputImpl extends Error_InvalidInput {
const _$Error_InvalidInputImpl() : super._();
@override
String toString() {
return 'Error.invalidInput()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$Error_InvalidInputImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return invalidInput();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return invalidInput?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (invalidInput != null) {
return invalidInput();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return invalidInput(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return invalidInput?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (invalidInput != null) {
return invalidInput(this);
}
return orElse();
}
}
abstract class Error_InvalidInput extends Error {
const factory Error_InvalidInput() = _$Error_InvalidInputImpl;
const Error_InvalidInput._() : super._();
}
/// @nodoc
abstract class _$$Error_NetworkImplCopyWith<$Res> {
factory _$$Error_NetworkImplCopyWith(
_$Error_NetworkImpl value,
$Res Function(_$Error_NetworkImpl) then,
) = __$$Error_NetworkImplCopyWithImpl<$Res>;
@useResult
$Res call({String field0});
}
/// @nodoc
class __$$Error_NetworkImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_NetworkImpl>
implements _$$Error_NetworkImplCopyWith<$Res> {
__$$Error_NetworkImplCopyWithImpl(
_$Error_NetworkImpl _value,
$Res Function(_$Error_NetworkImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? field0 = null}) {
return _then(
_$Error_NetworkImpl(
null == field0
? _value.field0
: field0 // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$Error_NetworkImpl extends Error_Network {
const _$Error_NetworkImpl(this.field0) : super._();
@override
final String field0;
@override
String toString() {
return 'Error.network(field0: $field0)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Error_NetworkImpl &&
(identical(other.field0, field0) || other.field0 == field0));
}
@override
int get hashCode => Object.hash(runtimeType, field0);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$Error_NetworkImplCopyWith<_$Error_NetworkImpl> get copyWith =>
__$$Error_NetworkImplCopyWithImpl<_$Error_NetworkImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return network(field0);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return network?.call(field0);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (network != null) {
return network(field0);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return network(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return network?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (network != null) {
return network(this);
}
return orElse();
}
}
abstract class Error_Network extends Error {
const factory Error_Network(final String field0) = _$Error_NetworkImpl;
const Error_Network._() : super._();
String get field0;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$Error_NetworkImplCopyWith<_$Error_NetworkImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$Error_UrImplCopyWith<$Res> {
factory _$$Error_UrImplCopyWith(
_$Error_UrImpl value,
$Res Function(_$Error_UrImpl) then,
) = __$$Error_UrImplCopyWithImpl<$Res>;
@useResult
$Res call({String field0});
}
/// @nodoc
class __$$Error_UrImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_UrImpl>
implements _$$Error_UrImplCopyWith<$Res> {
__$$Error_UrImplCopyWithImpl(
_$Error_UrImpl _value,
$Res Function(_$Error_UrImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? field0 = null}) {
return _then(
_$Error_UrImpl(
null == field0
? _value.field0
: field0 // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$Error_UrImpl extends Error_Ur {
const _$Error_UrImpl(this.field0) : super._();
@override
final String field0;
@override
String toString() {
return 'Error.ur(field0: $field0)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Error_UrImpl &&
(identical(other.field0, field0) || other.field0 == field0));
}
@override
int get hashCode => Object.hash(runtimeType, field0);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$Error_UrImplCopyWith<_$Error_UrImpl> get copyWith =>
__$$Error_UrImplCopyWithImpl<_$Error_UrImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return ur(field0);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return ur?.call(field0);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (ur != null) {
return ur(field0);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return ur(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return ur?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (ur != null) {
return ur(this);
}
return orElse();
}
}
abstract class Error_Ur extends Error {
const factory Error_Ur(final String field0) = _$Error_UrImpl;
const Error_Ur._() : super._();
String get field0;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$Error_UrImplCopyWith<_$Error_UrImpl> get copyWith =>
throw _privateConstructorUsedError;
}
+20
View File
@@ -0,0 +1,20 @@
// 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';
/// Generate a new 12-word BIP39 mnemonic
String generateMnemonic() =>
RustLib.instance.api.crateApiKeysGenerateMnemonic();
/// Convert a mnemonic to a 64-byte seed
Uint8List mnemonicToSeed({required String mnemonic}) =>
RustLib.instance.api.crateApiKeysMnemonicToSeed(mnemonic: mnemonic);
/// Derive the public key (hex) from a secret key (hex)
String getPubKey({required String secret}) =>
RustLib.instance.api.crateApiKeysGetPubKey(secret: secret);
+309
View File
@@ -0,0 +1,309 @@
// 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 function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
/// Fetch full mint info from a mint URL
Future<MintInfo> getMintInfo({required String mintUrl}) =>
RustLib.instance.api.crateApiMintInfoGetMintInfo(mintUrl: mintUrl);
/// Ping a mint to check if it's reachable (replaces Dart HTTP call)
Future<bool> pingMint({required String mintUrl}) =>
RustLib.instance.api.crateApiMintInfoPingMint(mintUrl: mintUrl);
/// Fetch keysets from a mint (replaces Dart HTTP call)
Future<List<KeysetInfo>> fetchKeysets({required String mintUrl}) =>
RustLib.instance.api.crateApiMintInfoFetchKeysets(mintUrl: mintUrl);
class ContactInfo {
final String method;
final String info;
const ContactInfo({required this.method, required this.info});
@override
int get hashCode => method.hashCode ^ info.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ContactInfo &&
runtimeType == other.runtimeType &&
method == other.method &&
info == other.info;
}
/// Keyset info returned by fetch_keysets
class KeysetInfo {
final String id;
final String unit;
final bool active;
const KeysetInfo({
required this.id,
required this.unit,
required this.active,
});
@override
int get hashCode => id.hashCode ^ unit.hashCode ^ active.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is KeysetInfo &&
runtimeType == other.runtimeType &&
id == other.id &&
unit == other.unit &&
active == other.active;
}
class MeltMethodSettings {
final String method;
final String unit;
final BigInt? minAmount;
final BigInt? maxAmount;
const MeltMethodSettings({
required this.method,
required this.unit,
this.minAmount,
this.maxAmount,
});
@override
int get hashCode =>
method.hashCode ^ unit.hashCode ^ minAmount.hashCode ^ maxAmount.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MeltMethodSettings &&
runtimeType == other.runtimeType &&
method == other.method &&
unit == other.unit &&
minAmount == other.minAmount &&
maxAmount == other.maxAmount;
}
class MintInfo {
final String? name;
final String? pubkey;
final MintVersion? version;
final String? description;
final String? descriptionLong;
final List<ContactInfo>? contact;
final Nuts nuts;
final String? iconUrl;
final List<String>? urls;
final String? motd;
final BigInt? time;
final String? tosUrl;
const MintInfo({
this.name,
this.pubkey,
this.version,
this.description,
this.descriptionLong,
this.contact,
required this.nuts,
this.iconUrl,
this.urls,
this.motd,
this.time,
this.tosUrl,
});
@override
int get hashCode =>
name.hashCode ^
pubkey.hashCode ^
version.hashCode ^
description.hashCode ^
descriptionLong.hashCode ^
contact.hashCode ^
nuts.hashCode ^
iconUrl.hashCode ^
urls.hashCode ^
motd.hashCode ^
time.hashCode ^
tosUrl.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MintInfo &&
runtimeType == other.runtimeType &&
name == other.name &&
pubkey == other.pubkey &&
version == other.version &&
description == other.description &&
descriptionLong == other.descriptionLong &&
contact == other.contact &&
nuts == other.nuts &&
iconUrl == other.iconUrl &&
urls == other.urls &&
motd == other.motd &&
time == other.time &&
tosUrl == other.tosUrl;
}
class MintMethodSettings {
final String method;
final String unit;
final BigInt? minAmount;
final BigInt? maxAmount;
const MintMethodSettings({
required this.method,
required this.unit,
this.minAmount,
this.maxAmount,
});
@override
int get hashCode =>
method.hashCode ^ unit.hashCode ^ minAmount.hashCode ^ maxAmount.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MintMethodSettings &&
runtimeType == other.runtimeType &&
method == other.method &&
unit == other.unit &&
minAmount == other.minAmount &&
maxAmount == other.maxAmount;
}
class MintVersion {
final String name;
final String version;
const MintVersion({required this.name, required this.version});
@override
int get hashCode => name.hashCode ^ version.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MintVersion &&
runtimeType == other.runtimeType &&
name == other.name &&
version == other.version;
}
class Nut04Settings {
final List<MintMethodSettings> methods;
final bool disabled;
const Nut04Settings({required this.methods, required this.disabled});
@override
int get hashCode => methods.hashCode ^ disabled.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Nut04Settings &&
runtimeType == other.runtimeType &&
methods == other.methods &&
disabled == other.disabled;
}
class Nut05Settings {
final List<MeltMethodSettings> methods;
final bool disabled;
const Nut05Settings({required this.methods, required this.disabled});
@override
int get hashCode => methods.hashCode ^ disabled.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Nut05Settings &&
runtimeType == other.runtimeType &&
methods == other.methods &&
disabled == other.disabled;
}
class Nuts {
final Nut04Settings nut04;
final Nut05Settings nut05;
final SupportedSettings nut07;
final SupportedSettings nut08;
final SupportedSettings nut09;
final SupportedSettings nut10;
final SupportedSettings nut11;
final SupportedSettings nut12;
final SupportedSettings nut14;
final SupportedSettings nut20;
const Nuts({
required this.nut04,
required this.nut05,
required this.nut07,
required this.nut08,
required this.nut09,
required this.nut10,
required this.nut11,
required this.nut12,
required this.nut14,
required this.nut20,
});
@override
int get hashCode =>
nut04.hashCode ^
nut05.hashCode ^
nut07.hashCode ^
nut08.hashCode ^
nut09.hashCode ^
nut10.hashCode ^
nut11.hashCode ^
nut12.hashCode ^
nut14.hashCode ^
nut20.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Nuts &&
runtimeType == other.runtimeType &&
nut04 == other.nut04 &&
nut05 == other.nut05 &&
nut07 == other.nut07 &&
nut08 == other.nut08 &&
nut09 == other.nut09 &&
nut10 == other.nut10 &&
nut11 == other.nut11 &&
nut12 == other.nut12 &&
nut14 == other.nut14 &&
nut20 == other.nut20;
}
class SupportedSettings {
final bool supported;
const SupportedSettings({required this.supported});
@override
int get hashCode => supported.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SupportedSettings &&
runtimeType == other.runtimeType &&
supported == other.supported;
}
+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;
}
+77
View File
@@ -0,0 +1,77 @@
// 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`: `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,
maxFragmentLength: maxFragmentLength,
);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<TokenDecoder>>
abstract class TokenDecoder implements RustOpaqueInterface {
bool isComplete();
factory TokenDecoder() => RustLib.instance.api.crateApiTokenTokenDecoderNew();
void receive({required String input});
Token? value();
}
class Token {
final String encoded;
final Uint8List? raw;
final BigInt amount;
final String mintUrl;
const Token({
required this.encoded,
this.raw,
required this.amount,
required this.mintUrl,
});
static Token fromRawBytes({required List<int> raw}) =>
RustLib.instance.api.crateApiTokenTokenFromRawBytes(raw: raw);
static Token parse({required String encoded}) =>
RustLib.instance.api.crateApiTokenTokenParse(encoded: encoded);
@override
int get hashCode =>
encoded.hashCode ^ raw.hashCode ^ amount.hashCode ^ mintUrl.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Token &&
runtimeType == other.runtimeType &&
encoded == other.encoded &&
raw == other.raw &&
amount == other.amount &&
mintUrl == other.mintUrl;
}
+353
View File
@@ -0,0 +1,353 @@
// 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 '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<PreparedSend>>
abstract class PreparedSend implements RustOpaqueInterface {
BigInt get amount;
BigInt get fee;
BigInt get sendFee;
BigInt get swapFee;
set amount(BigInt amount);
set fee(BigInt fee);
set sendFee(BigInt sendFee);
set swapFee(BigInt swapFee);
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Wallet>>
abstract class Wallet implements RustOpaqueInterface {
String get mintUrl;
String get unit;
set mintUrl(String mintUrl);
set unit(String unit);
Future<BigInt> balance();
Future<void> cancelSend({required PreparedSend send});
Future<void> checkAllMintQuotes();
Future<void> checkPendingTransactions();
/// 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();
Future<bool> isTokenSpent({required Token token});
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});
factory Wallet({
required String mintUrl,
required String unit,
required String mnemonic,
BigInt? targetProofCount,
required WalletDatabase db,
}) => RustLib.instance.api.crateApiWalletWalletNew(
mintUrl: mintUrl,
unit: unit,
mnemonic: mnemonic,
targetProofCount: targetProofCount,
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<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 the number of proofs recovered.
Future<BigInt> reclaimPendingProofs();
Future<void> recoverIncompleteSagas();
Future<void> restore();
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>>
abstract class WalletDatabase implements RustOpaqueInterface {
String get path;
set path(String path);
static Future<WalletDatabase> newInstance({required String path}) =>
RustLib.instance.api.crateApiWalletWalletDatabaseNewInstance(path: path);
Future<void> removeMint({required String mintUrl});
}
class MeltQuote {
final String id;
final String request;
final BigInt amount;
final BigInt feeReserve;
final BigInt expiry;
const MeltQuote({
required this.id,
required this.request,
required this.amount,
required this.feeReserve,
required this.expiry,
});
@override
int get hashCode =>
id.hashCode ^
request.hashCode ^
amount.hashCode ^
feeReserve.hashCode ^
expiry.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MeltQuote &&
runtimeType == other.runtimeType &&
id == other.id &&
request == other.request &&
amount == other.amount &&
feeReserve == other.feeReserve &&
expiry == other.expiry;
}
class MintQuote {
final String id;
final String request;
final BigInt? amount;
final BigInt? expiry;
final MintQuoteState state;
final Token? token;
final String? error;
/// Deterministic transaction ID (set when state == Issued)
final String? transactionId;
const MintQuote({
required this.id,
required this.request,
this.amount,
this.expiry,
required this.state,
this.token,
this.error,
this.transactionId,
});
@override
int get hashCode =>
id.hashCode ^
request.hashCode ^
amount.hashCode ^
expiry.hashCode ^
state.hashCode ^
token.hashCode ^
error.hashCode ^
transactionId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MintQuote &&
runtimeType == other.runtimeType &&
id == other.id &&
request == other.request &&
amount == other.amount &&
expiry == other.expiry &&
state == other.state &&
token == other.token &&
error == other.error &&
transactionId == other.transactionId;
}
enum MintQuoteState { unpaid, paid, issued, error }
class ReceiveOptions {
final List<String>? signingKeys;
final List<String>? preimages;
const ReceiveOptions({this.signingKeys, this.preimages});
static Future<ReceiveOptions> default_() =>
RustLib.instance.api.crateApiWalletReceiveOptionsDefault();
@override
int get hashCode => signingKeys.hashCode ^ preimages.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ReceiveOptions &&
runtimeType == other.runtimeType &&
signingKeys == other.signingKeys &&
preimages == other.preimages;
}
class SendOptions {
final String? pubkey;
final bool? includeFee;
const SendOptions({this.pubkey, this.includeFee});
static Future<SendOptions> default_() =>
RustLib.instance.api.crateApiWalletSendOptionsDefault();
@override
int get hashCode => pubkey.hashCode ^ includeFee.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SendOptions &&
runtimeType == other.runtimeType &&
pubkey == other.pubkey &&
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;
final TransactionDirection direction;
final BigInt amount;
final BigInt fee;
final String unit;
final List<String> ys;
final BigInt timestamp;
final String? memo;
final Map<String, String> metadata;
final TransactionStatus status;
const Transaction({
required this.id,
required this.mintUrl,
required this.direction,
required this.amount,
required this.fee,
required this.unit,
required this.ys,
required this.timestamp,
this.memo,
required this.metadata,
required this.status,
});
@override
int get hashCode =>
id.hashCode ^
mintUrl.hashCode ^
direction.hashCode ^
amount.hashCode ^
fee.hashCode ^
unit.hashCode ^
ys.hashCode ^
timestamp.hashCode ^
memo.hashCode ^
metadata.hashCode ^
status.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Transaction &&
runtimeType == other.runtimeType &&
id == other.id &&
mintUrl == other.mintUrl &&
direction == other.direction &&
amount == other.amount &&
fee == other.fee &&
unit == other.unit &&
ys == other.ys &&
timestamp == other.timestamp &&
memo == other.memo &&
metadata == other.metadata &&
status == other.status;
}
enum TransactionDirection { incoming, outgoing }
enum TransactionStatus { pending, settled }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import 'package:cdk_flutter/cdk_flutter.dart';
import '../../src/rust/api/token.dart';
import '../../core/constants/colors.dart';
import '../../core/utils/incoming_data_parser.dart';
@@ -36,7 +36,7 @@ class QrScannerWidget extends StatefulWidget {
class _QrScannerWidgetState extends State<QrScannerWidget> {
late MobileScannerController _controller;
// Estado para QR animados (UR multipartes) usando TokenDecoder de cdk-flutter
// Estado para QR animados (UR multipartes) TokenDecoder (Rust bridge)
TokenDecoder? _urDecoder;
bool _isCapturingUr = false;
final Set<String> _urFragmentsSeen = {}; // Trackear fragmentos únicos
+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.
+1 -1
View File
@@ -8,7 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
cdk_flutter
rust_builder
)
set(PLUGIN_BUNDLED_LIBRARIES)
@@ -7,6 +7,7 @@ import Foundation
import flutter_secure_storage_macos
import mobile_scanner
import package_info_plus
import path_provider_foundation
import share_plus
import shared_preferences_foundation
@@ -16,6 +17,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+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.
+322 -44
View File
@@ -1,6 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
source: hosted
version: "85.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
url: "https://pub.dev"
source: hosted
version: "7.7.1"
ansicolor:
dependency: transitive
description:
@@ -65,6 +81,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
build:
dependency: transitive
description:
name: build
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_cli_annotations:
dependency: transitive
description:
@@ -73,6 +97,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.1.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
url: "https://pub.dev"
source: hosted
version: "9.1.2"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af"
url: "https://pub.dev"
source: hosted
version: "8.12.5"
cbor:
dependency: "direct main"
description:
@@ -81,23 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.3.5"
cdk_flutter:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "3e0c77c22c807459d5d355e270ef4b5ad6a490c1"
url: "https://github.com/cashubtc/cdk_flutter.git"
source: git
version: "0.1.0"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -118,18 +189,26 @@ packages:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
collection:
dependency: transitive
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.19.1"
convert:
dependency: transitive
description:
@@ -170,14 +249,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.3"
ffi:
dependency: transitive
description:
@@ -237,7 +324,7 @@ packages:
source: hosted
version: "2.4.4"
flutter_rust_bridge:
dependency: transitive
dependency: "direct main"
description:
name: flutter_rust_bridge
sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e"
@@ -302,14 +389,38 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
url: "https://pub.dev"
source: hosted
version: "2.5.8"
freezed_annotation:
dependency: transitive
dependency: "direct main"
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
@@ -318,6 +429,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.3.0"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hex:
dependency: transitive
description:
@@ -342,6 +461,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
@@ -370,10 +497,18 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
version: "0.20.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
@@ -394,26 +529,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.7"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.8"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -422,6 +557,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.1"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
lucide_icons:
dependency: "direct main"
description:
@@ -434,26 +577,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -470,6 +613,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.2.3"
ndef_record:
dependency: "direct main"
description:
name: ndef_record
sha256: "210ffb12284961cab9e44b99462143316d9a20cd992581170706069ef77d74a6"
url: "https://pub.dev"
source: hosted
version: "1.4.2"
nested:
dependency: transitive
description:
@@ -478,14 +629,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
nfc_manager:
dependency: "direct main"
description:
name: nfc_manager
sha256: "24c78b0e5702da53e7f8794d073624c0bee7cd99924f257cbd11f5d1c5866879"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
url: "https://pub.dev"
source: hosted
version: "9.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
@@ -566,6 +749,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix:
dependency: transitive
description:
@@ -582,6 +773,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
qr:
dependency: transitive
description:
@@ -598,6 +805,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rust_builder:
dependency: "direct main"
description:
path: rust_builder
relative: true
source: path
version: "0.1.0"
share_plus:
dependency: "direct main"
description:
@@ -670,11 +884,35 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
source_span:
dependency: transitive
description:
@@ -743,18 +981,26 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@@ -783,10 +1029,18 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
version: "0.7.10"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:
@@ -879,10 +1133,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
@@ -891,6 +1145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "14.3.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
@@ -899,6 +1161,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
win32:
dependency: transitive
description:
@@ -932,5 +1210,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.27.0"
+14 -6
View File
@@ -1,10 +1,10 @@
name: elcaju
description: "ElCaju - Tu wallet de ecash privado"
publish_to: 'none'
version: 0.0.1+1
version: 0.2.0+3
environment:
sdk: ^3.6.0
sdk: ^3.11.0
dependencies:
flutter:
@@ -15,10 +15,9 @@ dependencies:
cupertino_icons: ^1.0.8
# CDK Flutter (Cashu backend)
cdk_flutter:
git:
url: https://github.com/cashubtc/cdk_flutter.git
# Internal Rust bridge (Cashu backend via cdk)
rust_builder:
path: rust_builder
# State Management
provider: ^6.1.2
@@ -56,8 +55,15 @@ dependencies:
# UUID generator for pending tokens
uuid: ^4.3.3
# NFC for token sharing
nfc_manager: ^4.1.1
ndef_record: ^1.4.2
# BIP32 for NIP-06 key derivation (P2PK)
bip32: ^2.0.0
package_info_plus: ^9.0.0
flutter_rust_bridge: 2.11.1
freezed_annotation: ^2.4.4
dev_dependencies:
flutter_test:
@@ -65,6 +71,8 @@ dev_dependencies:
flutter_lints: ^5.0.0
flutter_launcher_icons: ^0.14.3
flutter_native_splash: ^2.4.4
freezed: ^2.5.8
build_runner: ^2.5.4
# App Icon configuration
flutter_launcher_icons:
+4213
View File
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
[package]
name = "elcaju_core"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "staticlib"]
[dependencies]
# Cashu — acceso directo, sin intermediarios
cdk = { version = "0.15.1", default-features = false, features = ["wallet", "nostr"] }
cdk-common = { version = "0.15.1", default-features = false, features = ["nostr"] }
cdk-sqlite = { version = "0.15.1", default-features = false, features = ["wallet"] }
# Bridge Flutter <-> Rust
flutter_rust_bridge = { version = "=2.11.1", default-features = false, features = [
"anyhow",
"dart-opaque",
"portable-atomic",
"rust-async",
"thread-pool",
"wasm-start",
] }
# QR multiframe (bc-ur)
bc-ur = { version = "0.12.0", default-features = false }
# Mnemonic
bip39 = { version = "2.1", default-features = false, features = ["std"] }
# Entropy
getrandom = { version = "0.3", default-features = false, features = ["std"] }
# Nostr keys (same version as CDK 0.15.1 — CDK does not re-export Keys)
nostr-sdk = { version = "0.44.1", default-features = false, features = ["nip04", "nip44", "nip59"] }
# Async runtime
tokio = { version = "1", default-features = false }
# HTTP (para ping_mint, fetch_keysets)
reqwest = { version = "0.12", default-features = false, features = [
"deflate",
"gzip",
"json",
"rustls-tls",
"rustls-tls-native-roots",
] }
# Serialización
serde_json = { version = "1.0", default-features = false, features = ["std"] }
# UUID (for PreparedSend operation_id)
uuid = { version = "1", default-features = false }
# Logging
log = { version = "0.4", default-features = false }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.15"
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
oslog = "0.2.0"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
+100
View File
@@ -0,0 +1,100 @@
use std::fmt;
/// Errores unificados de elcaju_core
#[derive(Debug)]
pub enum Error {
/// Error del protocolo Cashu (cdk)
Cdk(String),
/// Error de base de datos (SQLite)
Database(String),
/// Input inválido del usuario
InvalidInput,
/// Error de red (HTTP, conexión)
Network(String),
/// Error de UR (QR multiframe)
Ur(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Cdk(msg) => write!(f, "Cashu error: {msg}"),
Error::Database(msg) => write!(f, "Database error: {msg}"),
Error::InvalidInput => write!(f, "Invalid input"),
Error::Network(msg) => write!(f, "Network error: {msg}"),
Error::Ur(msg) => write!(f, "UR error: {msg}"),
}
}
}
impl std::error::Error for Error {}
// --- cdk errors ---
impl From<cdk::error::Error> for Error {
fn from(e: cdk::error::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<cdk::amount::Error> for Error {
fn from(e: cdk::amount::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<cdk::cdk_database::Error> for Error {
fn from(e: cdk::cdk_database::Error) -> Self {
Error::Database(e.to_string())
}
}
impl From<cdk::mint_url::Error> for Error {
fn from(e: cdk::mint_url::Error) -> Self {
Error::Network(e.to_string())
}
}
impl From<cdk::nuts::nut00::Error> for Error {
fn from(e: cdk::nuts::nut00::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<cdk::nuts::nut01::Error> for Error {
fn from(e: cdk::nuts::nut01::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<cdk::nuts::nut11::Error> for Error {
fn from(e: cdk::nuts::nut11::Error) -> Self {
Error::Cdk(e.to_string())
}
}
// --- external errors ---
impl From<bc_ur::Error> for Error {
fn from(e: bc_ur::Error) -> Self {
Error::Ur(e.to_string())
}
}
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
Error::Network(e.to_string())
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(e: std::string::FromUtf8Error) -> Self {
Error::Cdk(e.to_string())
}
}
+31
View File
@@ -0,0 +1,31 @@
use std::str::FromStr;
use bip39::Mnemonic;
use cdk_common::SecretKey;
use flutter_rust_bridge::frb;
use super::error::Error;
/// Generate a new 12-word BIP39 mnemonic
#[frb(sync)]
pub fn generate_mnemonic() -> Result<String, Error> {
let mut entropy = [0u8; 16]; // 128 bits → 12 words
getrandom::fill(&mut entropy).map_err(|_| Error::InvalidInput)?;
let mnemonic = Mnemonic::from_entropy(&entropy).map_err(|_| Error::InvalidInput)?;
Ok(mnemonic.to_string())
}
/// Convert a mnemonic to a 64-byte seed
#[frb(sync)]
pub fn mnemonic_to_seed(mnemonic: String) -> Result<Vec<u8>, Error> {
let mnemonic = Mnemonic::parse(&mnemonic).map_err(|_| Error::InvalidInput)?;
Ok(mnemonic.to_seed("").to_vec())
}
/// Derive the public key (hex) from a secret key (hex)
#[frb(sync)]
pub fn get_pub_key(secret: String) -> Result<String, Error> {
let secret = SecretKey::from_str(&secret)?;
let pub_key = secret.public_key();
Ok(pub_key.to_string())
}
+243
View File
@@ -0,0 +1,243 @@
use std::str::FromStr;
use cdk::{
nuts::{nut04, nut05, nut06},
wallet::{HttpClient, MintConnector},
};
use cdk_common::{
mint_url::MintUrl, ContactInfo as CdkContactInfo, MintInfo as CdkMintInfo,
MintVersion as CdkMintVersion, Nuts as CdkNuts,
};
use super::error::Error;
// === MintInfo structs ===
pub struct MintInfo {
pub name: Option<String>,
pub pubkey: Option<String>,
pub version: Option<MintVersion>,
pub description: Option<String>,
pub description_long: Option<String>,
pub contact: Option<Vec<ContactInfo>>,
pub nuts: Nuts,
pub icon_url: Option<String>,
pub urls: Option<Vec<String>>,
pub motd: Option<String>,
pub time: Option<u64>,
pub tos_url: Option<String>,
}
impl From<CdkMintInfo> for MintInfo {
fn from(value: CdkMintInfo) -> Self {
Self {
name: value.name,
pubkey: value.pubkey.map(|p| p.to_string()),
version: value.version.map(|v| v.into()),
description: value.description,
description_long: value.description_long,
contact: value
.contact
.map(|c| c.into_iter().map(|c| c.into()).collect()),
nuts: value.nuts.into(),
icon_url: value.icon_url,
urls: value.urls,
motd: value.motd,
time: value.time,
tos_url: value.tos_url,
}
}
}
pub struct MintVersion {
pub name: String,
pub version: String,
}
impl From<CdkMintVersion> for MintVersion {
fn from(value: CdkMintVersion) -> Self {
Self {
name: value.name,
version: value.version,
}
}
}
pub struct ContactInfo {
pub method: String,
pub info: String,
}
impl From<CdkContactInfo> for ContactInfo {
fn from(c: CdkContactInfo) -> Self {
Self {
method: c.method,
info: c.info,
}
}
}
// === NUT settings ===
pub struct Nuts {
pub nut04: Nut04Settings,
pub nut05: Nut05Settings,
pub nut07: SupportedSettings,
pub nut08: SupportedSettings,
pub nut09: SupportedSettings,
pub nut10: SupportedSettings,
pub nut11: SupportedSettings,
pub nut12: SupportedSettings,
pub nut14: SupportedSettings,
pub nut20: SupportedSettings,
}
impl From<CdkNuts> for Nuts {
fn from(value: CdkNuts) -> Self {
Self {
nut04: value.nut04.into(),
nut05: value.nut05.into(),
nut07: value.nut07.into(),
nut08: value.nut08.into(),
nut09: value.nut09.into(),
nut10: value.nut10.into(),
nut11: value.nut11.into(),
nut12: value.nut12.into(),
nut14: value.nut14.into(),
nut20: value.nut20.into(),
}
}
}
pub struct SupportedSettings {
pub supported: bool,
}
impl From<nut06::SupportedSettings> for SupportedSettings {
fn from(value: nut06::SupportedSettings) -> Self {
Self {
supported: value.supported,
}
}
}
pub struct Nut04Settings {
pub methods: Vec<MintMethodSettings>,
pub disabled: bool,
}
impl From<nut04::Settings> for Nut04Settings {
fn from(value: nut04::Settings) -> Self {
Self {
methods: value.methods.into_iter().map(|m| m.into()).collect(),
disabled: value.disabled,
}
}
}
pub struct MintMethodSettings {
pub method: String,
pub unit: String,
pub min_amount: Option<u64>,
pub max_amount: Option<u64>,
}
impl From<nut04::MintMethodSettings> for MintMethodSettings {
fn from(value: nut04::MintMethodSettings) -> Self {
Self {
method: value.method.to_string(),
unit: value.unit.to_string(),
min_amount: value.min_amount.map(|v| v.into()),
max_amount: value.max_amount.map(|v| v.into()),
}
}
}
pub struct Nut05Settings {
pub methods: Vec<MeltMethodSettings>,
pub disabled: bool,
}
impl From<nut05::Settings> for Nut05Settings {
fn from(value: nut05::Settings) -> Self {
Self {
methods: value.methods.into_iter().map(|m| m.into()).collect(),
disabled: value.disabled,
}
}
}
pub struct MeltMethodSettings {
pub method: String,
pub unit: String,
pub min_amount: Option<u64>,
pub max_amount: Option<u64>,
}
impl From<nut05::MeltMethodSettings> for MeltMethodSettings {
fn from(value: nut05::MeltMethodSettings) -> Self {
Self {
method: value.method.to_string(),
unit: value.unit.to_string(),
min_amount: value.min_amount.map(|v| v.into()),
max_amount: value.max_amount.map(|v| v.into()),
}
}
}
// === API functions ===
/// Fetch full mint info from a mint URL
pub async fn get_mint_info(mint_url: &str) -> Result<MintInfo, Error> {
let mint_url = MintUrl::from_str(mint_url)?;
let client = HttpClient::new(mint_url, None);
Ok(client.get_mint_info().await?.into())
}
/// Ping a mint to check if it's reachable (replaces Dart HTTP call)
pub async fn ping_mint(mint_url: &str) -> Result<bool, Error> {
let url = format!("{}/v1/info", mint_url.trim_end_matches('/'));
let resp = reqwest::Client::new()
.get(&url)
.timeout(std::time::Duration::from_secs(3))
.send()
.await;
match resp {
Ok(r) => Ok(r.status().is_success()),
Err(_) => Ok(false),
}
}
/// Keyset info returned by fetch_keysets
pub struct KeysetInfo {
pub id: String,
pub unit: String,
pub active: bool,
}
/// Fetch keysets from a mint (replaces Dart HTTP call)
pub async fn fetch_keysets(mint_url: &str) -> Result<Vec<KeysetInfo>, Error> {
let url = format!("{}/v1/keysets", mint_url.trim_end_matches('/'));
let resp = reqwest::Client::new()
.get(&url)
.timeout(std::time::Duration::from_secs(5))
.send()
.await?
.text()
.await?;
let data: serde_json::Value = serde_json::from_str(&resp)?;
let keysets = data["keysets"]
.as_array()
.ok_or(Error::InvalidInput)?
.iter()
.filter_map(|ks| {
Some(KeysetInfo {
id: ks["id"].as_str()?.to_string(),
unit: ks["unit"].as_str()?.to_string(),
active: ks["active"].as_bool().unwrap_or(false),
})
})
.collect();
Ok(keysets)
}
+6
View File
@@ -0,0 +1,6 @@
pub mod error;
pub mod keys;
pub mod mint_info;
pub mod payment_request;
pub mod token;
pub mod wallet;
+590
View File
@@ -0,0 +1,590 @@
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> = match &pr.mints {
Some(list) => {
let mut v = Vec::new();
for m in list {
v.push(format!("{}", m));
}
v
}
None => Vec::new(),
};
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: Some(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: Some(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(),
))
}
+185
View File
@@ -0,0 +1,185 @@
use std::{
str::FromStr,
sync::{Arc, RwLock},
};
use bc_ur::{prelude::CBOR, MultipartDecoder, MultipartEncoder, UR};
use cdk::{
mint_url::MintUrl,
nuts::{CurrencyUnit, Proof, Token as CdkToken},
};
use cdk_common::{KeySetInfo, Proofs};
use flutter_rust_bridge::frb;
use super::error::Error;
pub struct Token {
pub encoded: String,
pub raw: Option<Vec<u8>>,
pub amount: u64,
pub mint_url: String,
}
impl Token {
#[frb(sync)]
pub fn parse(encoded: &str) -> Result<Self, Error> {
let encoded = encoded.strip_prefix("cashu:").unwrap_or(encoded);
let token = CdkToken::from_str(encoded)?;
Token::try_from(token)
}
#[frb(sync)]
pub fn from_raw_bytes(raw: Vec<u8>) -> Result<Self, Error> {
let token = CdkToken::try_from(&raw)?;
Token::try_from(token)
}
pub(crate) fn proofs(&self, mint_keysets: &[KeySetInfo]) -> Result<Proofs, Error> {
let token: CdkToken = self.try_into()?;
Ok(token.proofs(mint_keysets)?)
}
}
impl std::fmt::Display for Token {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.encoded)
}
}
impl FromStr for Token {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
Token::parse(s)
}
}
impl TryFrom<CdkToken> for Token {
type Error = Error;
fn try_from(token: CdkToken) -> Result<Self, Error> {
match token {
CdkToken::TokenV3(token_v3) => Ok(Token {
encoded: token_v3.to_string(),
raw: None,
amount: token_v3.value()?.into(),
mint_url: token_v3
.mint_urls()
.first()
.ok_or(Error::InvalidInput)?
.to_string(),
}),
CdkToken::TokenV4(token_v4) => Ok(Token {
encoded: token_v4.to_string(),
raw: Some(token_v4.to_raw_bytes()?),
amount: token_v4.value()?.into(),
mint_url: token_v4.mint_url.to_string(),
}),
}
}
}
impl TryInto<CdkToken> for Token {
type Error = Error;
fn try_into(self) -> Result<CdkToken, Error> {
Ok(CdkToken::from_str(&self.encoded)?)
}
}
impl TryInto<CdkToken> for &Token {
type Error = Error;
fn try_into(self) -> Result<CdkToken, Error> {
Ok(CdkToken::from_str(&self.encoded)?)
}
}
// === 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 {
decoder: Arc<RwLock<MultipartDecoder>>,
}
impl TokenDecoder {
#[frb(sync)]
pub fn new() -> Self {
Self {
decoder: Arc::new(RwLock::new(MultipartDecoder::default())),
}
}
#[frb(sync)]
pub fn is_complete(&self) -> bool {
self.decoder
.read()
.map(|decoder| decoder.is_complete())
.unwrap_or(false)
}
#[frb(sync)]
pub fn receive(&self, input: String) -> Result<(), Error> {
let mut decoder = self
.decoder
.write()
.map_err(|_| Error::Ur("Token decoder lock poisoned".to_string()))?;
decoder.receive(&input)?;
Ok(())
}
#[frb(sync)]
pub fn value(&self) -> Result<Option<Token>, Error> {
let decoder = self
.decoder
.read()
.map_err(|_| Error::Ur("Token decoder lock poisoned".to_string()))?;
let ur = decoder.message()?;
match ur {
Some(ur) => {
let bytes = ur
.cbor()
.try_byte_string()
.map_err(|e| Error::Ur(format!("Failed to decode CBOR byte string: {}", e)))?;
let utf8_encoded = String::from_utf8(bytes)?;
let token = Token::from_str(&utf8_encoded)?;
Ok(Some(token))
}
None => Ok(None),
}
}
}
#[frb(sync)]
pub fn encode_qr_token(
token: &Token,
max_fragment_length: Option<usize>,
) -> Result<Vec<String>, Error> {
let ur = UR::new("", CBOR::to_byte_string(&token.encoded))?;
let mut encoder = MultipartEncoder::new(&ur, max_fragment_length.unwrap_or(150))?;
let mut parts = Vec::new();
for _ in 0..encoder.parts_count() {
parts.push(encoder.next_part()?);
}
Ok(parts)
}
+822
View File
@@ -0,0 +1,822 @@
use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
use bip39::Mnemonic;
use cdk::{
amount::{Amount, SplitTarget},
cdk_database::WalletDatabase as _,
mint_url::MintUrl,
nuts::{
nut00::ProofsMethods, CurrencyUnit, MintQuoteState as CdkMintQuoteState, PaymentMethod,
PublicKey, SecretKey, SpendingConditions, State as ProofState, Token as CdkToken,
},
wallet::{
MeltQuote as CdkMeltQuote, MintQuote as CdkMintQuote, PreparedSend as CdkPreparedSend,
ReceiveOptions as CdkReceiveOptions, SendMemo, SendOptions as CdkSendOptions,
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;
use uuid::Uuid;
use crate::frb_generated::StreamSink;
use super::{
error::Error,
mint_info::MintInfo,
token::Token,
};
// ========================================================================
// Wallet
// ========================================================================
#[derive(Clone)]
pub struct Wallet {
pub mint_url: String,
pub unit: String,
balance_broadcast: broadcast::Sender<u64>,
pub(crate) inner: CdkWallet,
seed: [u8; 64],
}
impl Wallet {
#[frb(sync)]
pub fn new(
mint_url: String,
unit: String,
mnemonic: String,
target_proof_count: Option<usize>,
db: &WalletDatabase,
) -> Result<Self, Error> {
let mnemonic = Mnemonic::parse(&mnemonic).map_err(|_| Error::InvalidInput)?;
let seed: [u8; 64] = mnemonic.to_seed("").into();
let unit = CurrencyUnit::from_str(&unit).unwrap_or(CurrencyUnit::Custom(unit.to_string()));
Ok(Self {
mint_url: mint_url.clone(),
unit: unit.to_string(),
balance_broadcast: broadcast::channel(1).0,
inner: CdkWallet::new(
&mint_url,
unit,
db.inner.clone(),
seed,
target_proof_count,
)?,
seed,
})
}
// === Balance ===
pub async fn balance(&self) -> Result<u64, Error> {
Ok(self.inner.total_balance().await?.into())
}
pub async fn stream_balance(&self, sink: StreamSink<u64>) -> Result<(), Error> {
let mut receiver = self.balance_broadcast.subscribe();
let _ = sink.add(self.balance().await?);
flutter_rust_bridge::spawn(async move {
loop {
match receiver.recv().await {
Ok(balance) => {
if sink.add(balance).is_err() {
break;
}
}
Err(_) => break,
}
}
});
Ok(())
}
// === Receive ===
pub async fn receive(&self, token: Token, opts: Option<ReceiveOptions>) -> Result<u64, Error> {
let amount = self
.inner
.receive(&token.encoded, opts.unwrap_or_default().try_into()?)
.await?
.into();
self.update_balance_streams().await;
Ok(amount)
}
// === Send ===
pub async fn prepare_send(
&self,
amount: u64,
opts: Option<SendOptions>,
) -> Result<PreparedSend, Error> {
let prepared_send = self
.inner
.prepare_send(amount.into(), opts.unwrap_or_default().try_into()?)
.await?;
Ok(prepared_send.into())
}
pub async fn send(
&self,
send: PreparedSend,
memo: Option<String>,
include_memo: Option<bool>,
) -> Result<SendResult, Error> {
let send_memo = memo.map(|m| SendMemo {
memo: m,
include_memo: include_memo.unwrap_or_default(),
});
let cdk_token = self
.inner
.confirm_send(
send.operation_id,
send.cdk_amount,
send.cdk_options,
send.proofs_to_swap,
send.proofs_to_send,
send.cdk_swap_fee,
send.cdk_send_fee,
send_memo,
)
.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().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(SendResult {
token: Token::from_str(&token_str)?,
transaction_id: tx_id,
})
}
pub async fn cancel_send(&self, send: PreparedSend) -> Result<(), Error> {
self.inner
.cancel_send(
send.operation_id,
send.proofs_to_swap,
send.proofs_to_send,
)
.await?;
Ok(())
}
// === Lightning Deposit (NUT-04) ===
pub async fn mint(
&self,
amount: u64,
description: Option<String>,
sink: StreamSink<MintQuote>,
) -> Result<(), Error> {
let mint_url = self.mint_url()?;
let unit = self.unit();
let quote = self
.inner
.mint_quote(PaymentMethod::BOLT11, Some(amount.into()), description, None)
.await?;
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 {
// 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,
}
}
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,
}
}
} => 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,
request: quote.request,
amount: Some(mint_amount.into()),
expiry: Some(quote.expiry),
state: CdkMintQuoteState::Issued.into(),
token: Token::try_from(CdkToken::new(
mint_url,
mint_proofs,
None,
unit,
))
.ok(),
error: None,
transaction_id: tx_id,
});
_self.update_balance_streams().await;
}
Err(e) => {
let _ = sink.add(MintQuote {
id: quote.id,
request: quote.request,
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: MintQuoteState::Error,
token: None,
error: Some(e.to_string()),
transaction_id: None,
});
}
}
}
Ok(Detected::Issued) => {
// Already issued (recovered from previous session)
let _ = sink.add(MintQuote {
id: quote.id.clone(),
request: quote.request.clone(),
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: CdkMintQuoteState::Issued.into(),
token: None,
error: None,
transaction_id: None,
});
_self.update_balance_streams().await;
}
}
});
Ok(())
}
pub async fn check_all_mint_quotes(&self) -> Result<(), Error> {
let minted = self.inner.mint_unissued_quotes().await?;
if minted > Amount::ZERO {
self.update_balance_streams().await;
}
Ok(())
}
// === Lightning Withdrawal (NUT-05) ===
pub async fn melt_quote(&self, request: String) -> Result<MeltQuote, Error> {
Ok(self
.inner
.melt_quote(PaymentMethod::BOLT11, request, None, None)
.await?
.into())
}
pub async fn melt(&self, quote: MeltQuote) -> Result<u64, Error> {
let melted = self
.inner
.prepare_melt(&quote.id, HashMap::new())
.await?
.confirm()
.await?;
self.update_balance_streams().await;
Ok(melted.total_amount().into())
}
// === Transactions ===
pub async fn list_transactions(
&self,
direction: Option<TransactionDirection>,
) -> Result<Vec<Transaction>, Error> {
let pending_ys = self
.inner
.get_pending_spent_proofs()
.await?
.into_iter()
.map(|p| p.y())
.collect::<Result<Vec<_>, _>>()?;
let cdk_txs = self
.inner
.list_transactions(direction.map(|d| d.into()))
.await?;
let mut txs = Vec::new();
for tx in cdk_txs {
let status = if pending_ys.iter().any(|y| tx.ys.contains(y)) {
TransactionStatus::Pending
} else {
TransactionStatus::Settled
};
let mut transaction: Transaction = tx.into();
transaction.status = status;
txs.push(transaction);
}
Ok(txs)
}
pub async fn check_pending_transactions(&self) -> Result<(), Error> {
self.inner.check_all_pending_proofs().await?;
self.update_balance_streams().await;
Ok(())
}
// === Recovery ===
pub async fn restore(&self) -> Result<(), Error> {
self.inner.restore().await?;
self.update_balance_streams().await;
Ok(())
}
pub async fn recover_incomplete_sagas(&self) -> Result<(), Error> {
self.inner.recover_incomplete_sagas().await?;
self.update_balance_streams().await;
Ok(())
}
pub async fn finalize_pending_melts(&self) -> Result<(), Error> {
self.inner.finalize_pending_melts().await?;
self.update_balance_streams().await;
Ok(())
}
// === Reclaim orphaned proofs ===
/// Check pending-spent proofs with the mint and revert unspent ones.
/// Returns the number of proofs recovered.
pub async fn reclaim_pending_proofs(&self) -> Result<u64, Error> {
let pending = self.inner.get_pending_spent_proofs().await?;
if pending.is_empty() {
return Ok(0);
}
// 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;
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(count)
}
// === 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 proof_states = self
.inner
.check_proofs_spent(token.proofs(&mint_keysets)?)
.await?;
Ok(proof_states
.iter()
.any(|state| state.state == ProofState::Spent))
}
pub async fn get_mint(&self) -> Result<Option<MintInfo>, Error> {
Ok(self.inner.fetch_mint_info().await?.map(|info| info.into()))
}
// === Internal helpers ===
pub(crate) fn mint_url(&self) -> Result<MintUrl, Error> {
Ok(MintUrl::from_str(&self.mint_url)?)
}
fn unit(&self) -> CurrencyUnit {
CurrencyUnit::from_str(&self.unit).unwrap_or(CurrencyUnit::Custom(self.unit.clone()))
}
pub(crate) async fn update_balance_streams(&self) {
let balance = self
.inner
.total_balance()
.await
.unwrap_or(Amount::ZERO)
.into();
let _ = self.balance_broadcast.send(balance);
}
}
// ========================================================================
// 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,
pub amount: Option<u64>,
pub expiry: Option<u64>,
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 {
fn from(quote: CdkMintQuote) -> Self {
Self {
id: quote.id,
request: quote.request,
amount: quote.amount.map(|a| a.into()),
expiry: Some(quote.expiry),
state: quote.state.into(),
token: None,
error: None,
transaction_id: None,
}
}
}
pub enum MintQuoteState {
Unpaid,
Paid,
Issued,
Error,
}
impl From<CdkMintQuoteState> for MintQuoteState {
fn from(state: CdkMintQuoteState) -> Self {
match state {
CdkMintQuoteState::Unpaid => Self::Unpaid,
CdkMintQuoteState::Paid => Self::Paid,
CdkMintQuoteState::Issued => Self::Issued,
}
}
}
pub struct MeltQuote {
pub id: String,
pub request: String,
pub amount: u64,
pub fee_reserve: u64,
pub expiry: u64,
}
impl From<CdkMeltQuote> for MeltQuote {
fn from(quote: CdkMeltQuote) -> Self {
Self {
id: quote.id,
request: quote.request,
amount: quote.amount.into(),
fee_reserve: quote.fee_reserve.into(),
expiry: quote.expiry,
}
}
}
pub struct PreparedSend {
pub amount: u64,
pub swap_fee: u64,
pub send_fee: u64,
pub fee: u64,
operation_id: Uuid,
cdk_amount: Amount,
cdk_options: CdkSendOptions,
proofs_to_swap: cdk::nuts::Proofs,
proofs_to_send: cdk::nuts::Proofs,
cdk_swap_fee: Amount,
cdk_send_fee: Amount,
}
impl<'a> From<CdkPreparedSend<'a>> for PreparedSend {
fn from(ps: CdkPreparedSend<'a>) -> Self {
let amount = ps.amount();
let swap_fee = ps.swap_fee();
let send_fee = ps.send_fee();
let fee = ps.fee();
Self {
amount: amount.into(),
swap_fee: swap_fee.into(),
send_fee: send_fee.into(),
fee: fee.into(),
operation_id: ps.operation_id(),
cdk_amount: amount,
cdk_options: ps.options().clone(),
proofs_to_swap: ps.proofs_to_swap().to_vec(),
proofs_to_send: ps.proofs_to_send().to_vec(),
cdk_swap_fee: swap_fee,
cdk_send_fee: send_fee,
}
}
}
#[derive(Default)]
pub struct ReceiveOptions {
pub signing_keys: Option<Vec<String>>,
pub preimages: Option<Vec<String>>,
}
impl TryInto<CdkReceiveOptions> for ReceiveOptions {
type Error = Error;
fn try_into(self) -> Result<CdkReceiveOptions, Self::Error> {
let p2pk_signing_keys = self
.signing_keys
.unwrap_or_default()
.into_iter()
.map(|s| SecretKey::from_str(&s))
.collect::<Result<Vec<_>, _>>()?;
Ok(CdkReceiveOptions {
p2pk_signing_keys,
preimages: self.preimages.unwrap_or_default(),
..Default::default()
})
}
}
#[derive(Default)]
pub struct SendOptions {
pub pubkey: Option<String>,
pub include_fee: Option<bool>,
}
impl TryInto<CdkSendOptions> for SendOptions {
type Error = Error;
fn try_into(self) -> Result<CdkSendOptions, Self::Error> {
let pubkey = self.pubkey.map(|s| PublicKey::from_str(&s)).transpose()?;
Ok(CdkSendOptions {
conditions: pubkey.map(|pubkey| SpendingConditions::new_p2pk(pubkey, None)),
include_fee: self.include_fee.unwrap_or_default(),
..Default::default()
})
}
}
#[derive(PartialEq, Eq)]
pub struct Transaction {
pub id: String,
pub mint_url: String,
pub direction: TransactionDirection,
pub amount: u64,
pub fee: u64,
pub unit: String,
pub ys: Vec<String>,
pub timestamp: u64,
pub memo: Option<String>,
pub metadata: HashMap<String, String>,
pub status: TransactionStatus,
}
impl From<CdkTransaction> for Transaction {
fn from(tx: CdkTransaction) -> Self {
Self {
id: tx.id().to_string(),
mint_url: tx.mint_url.to_string(),
direction: tx.direction.into(),
amount: tx.amount.into(),
fee: tx.fee.into(),
unit: tx.unit.to_string(),
ys: tx.ys.iter().map(|y| y.to_string()).collect(),
timestamp: tx.timestamp,
memo: tx.memo,
metadata: tx.metadata,
status: TransactionStatus::Settled,
}
}
}
impl PartialOrd for Transaction {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Transaction {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.timestamp.cmp(&other.timestamp).reverse()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TransactionDirection {
Incoming,
Outgoing,
}
impl From<CdkTransactionDirection> for TransactionDirection {
fn from(direction: CdkTransactionDirection) -> Self {
match direction {
CdkTransactionDirection::Incoming => Self::Incoming,
CdkTransactionDirection::Outgoing => Self::Outgoing,
}
}
}
impl Into<CdkTransactionDirection> for TransactionDirection {
fn into(self) -> CdkTransactionDirection {
match self {
Self::Incoming => CdkTransactionDirection::Incoming,
Self::Outgoing => CdkTransactionDirection::Outgoing,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransactionStatus {
Pending,
Settled,
}
// ========================================================================
// WalletDatabase
// ========================================================================
#[derive(Clone)]
pub struct WalletDatabase {
pub path: String,
inner: Arc<WalletSqliteDatabase>,
}
impl WalletDatabase {
pub async fn new_instance(path: String) -> Result<Self, Error> {
let inner = WalletSqliteDatabase::new(path.as_str()).await?;
Ok(Self {
inner: Arc::new(inner),
path,
})
}
pub async fn remove_mint(&self, mint_url: &str) -> Result<(), Error> {
let mint_url = MintUrl::from_str(mint_url)?;
self.inner.remove_mint(mint_url).await?;
Ok(())
}
}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
mod frb_generated; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */
pub mod api;
+49
View File
@@ -0,0 +1,49 @@
// Android NDK build integration for elcaju_core Rust crate.
group 'com.elcaju.rust_builder'
version '1.0'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
android {
if (project.android.hasProperty("namespace")) {
namespace 'com.elcaju.rust_builder'
}
compileSdkVersion 33
ndkVersion "27.0.12077973"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
minSdkVersion 19
}
}
apply from: "../cargokit/gradle/plugin.gradle"
cargokit {
manifestDir = "../../rust"
libname = "elcaju_core"
}
+4
View File
@@ -0,0 +1,4 @@
target
.dart_tool
*.iml
!pubspec.lock
+42
View File
@@ -0,0 +1,42 @@
/// This is copied from Cargokit (which is the official way to use it currently)
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
Copyright 2022 Matej Knopp
================================================================================
MIT LICENSE
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================================
APACHE LICENSE, VERSION 2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+11
View File
@@ -0,0 +1,11 @@
/// This is copied from Cargokit (which is the official way to use it currently)
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
Experimental repository to provide glue for seamlessly integrating cargo build
with flutter plugins and packages.
See https://matejknopp.com/post/flutter_plugin_in_rust_with_no_prebuilt_binaries/
for a tutorial on how to use Cargokit.
Example plugin available at https://github.com/irondash/hello_rust_ffi_plugin.
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
set -e
BASEDIR=$(dirname "$0")
# Workaround for https://github.com/dart-lang/pub/issues/4010
BASEDIR=$(cd "$BASEDIR" ; pwd -P)
# Remove XCode SDK from path. Otherwise this breaks tool compilation when building iOS project
NEW_PATH=`echo $PATH | tr ":" "\n" | grep -v "Contents/Developer/" | tr "\n" ":"`
export PATH=${NEW_PATH%?} # remove trailing :
if [ -n "$CARGOKIT_DEBUG" ]; then
env | grep -E '^(CARGOKIT|FLUTTER|PLATFORM_NAME|ARCHS|CONFIGURATION|PODS_)='
fi
# Platform name (macosx, iphoneos, iphonesimulator)
export CARGOKIT_DARWIN_PLATFORM_NAME=$PLATFORM_NAME
# Active architectures (arm64, armv7, x86_64), space separated.
export CARGOKIT_DARWIN_ARCHS=$ARCHS
# Current build configuration (Debug, Release)
export CARGOKIT_CONFIGURATION=$CONFIGURATION
# Path to directory containing Cargo.toml.
export CARGOKIT_MANIFEST_DIR=$PODS_TARGET_SRCROOT/$1
# Temporary directory for build artifacts.
export CARGOKIT_TARGET_TEMP_DIR=$TARGET_TEMP_DIR
# Output directory for final artifacts.
export CARGOKIT_OUTPUT_DIR=$PODS_CONFIGURATION_BUILD_DIR/$PRODUCT_NAME
# Directory to store built tool artifacts.
export CARGOKIT_TOOL_TEMP_DIR=$TARGET_TEMP_DIR/build_tool
# Directory inside root project. Not necessarily the top level directory of root project.
export CARGOKIT_ROOT_PROJECT_DIR=$SRCROOT
FLUTTER_EXPORT_BUILD_ENVIRONMENT=(
"$PODS_ROOT/../Flutter/ephemeral/flutter_export_environment.sh" # macOS
"$PODS_ROOT/../Flutter/flutter_export_environment.sh" # iOS
)
for path in "${FLUTTER_EXPORT_BUILD_ENVIRONMENT[@]}"
do
if [[ -f "$path" ]]; then
source "$path"
fi
done
bash "$BASEDIR/run_build_tool.sh" build-pod "$@"
# Make a symlink from built framework to phony file, which will be used as input to
# build script. This should force rebuild (podspec currently doesn't support alwaysOutOfDate
# attribute on custom build phase)
ln -fs "$OBJROOT/XCBuildData/build.db" "${BUILT_PRODUCTS_DIR}/cargokit_phony"
ln -fs "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/cargokit_phony_out"
@@ -0,0 +1,5 @@
/// This is copied from Cargokit (which is the official way to use it currently)
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
A sample command-line application with an entrypoint in `bin/`, library code
in `lib/`, and example unit test in `test/`.
@@ -0,0 +1,34 @@
# This is copied from Cargokit (which is the official way to use it currently)
# Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
linter:
rules:
- prefer_relative_imports
- directives_ordering
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,8 @@
/// This is copied from Cargokit (which is the official way to use it currently)
/// Details: https://fzyzcjy.github.io/flutter_rust_bridge/manual/integrate/builtin
import 'package:build_tool/build_tool.dart' as build_tool;
void main(List<String> arguments) {
build_tool.runMain(arguments);
}

Some files were not shown because too many files have changed in this diff Show More