Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cc92d6732 | ||
|
|
013560d25b | ||
|
|
d19a58df84 | ||
|
|
fd64d2f269 | ||
|
|
426b63456d | ||
|
|
4d2ad39323 | ||
|
|
060c6acd4e | ||
|
|
14da243b89 | ||
|
|
4f91a2be1c | ||
|
|
ab13e8f6c2 | ||
|
|
f431f825c5 | ||
|
|
b56aa5ce62 | ||
|
|
c69de2b41b | ||
|
|
e5285058a8 | ||
|
|
b01b658b93 | ||
|
|
6596aa963a | ||
|
|
ed309d18e8 | ||
|
|
1b30d9836b | ||
|
|
588a127f9a | ||
|
|
362e45e40f | ||
|
|
fae57cc46e | ||
|
|
80ab0fd9e1 | ||
|
|
29895f4079 | ||
|
|
41fa32d1b4 | ||
|
|
63ce12d745 | ||
|
|
7245dbb669 | ||
|
|
e124e0839a | ||
|
|
1a31b47c6b | ||
|
|
a5cd50af13 | ||
|
|
66119f6b56 | ||
|
|
484f73a33b | ||
|
|
67204a12f8 | ||
|
|
b4a62fe9e5 | ||
|
|
638b288b7e | ||
|
|
357cc6827f | ||
|
|
682ec4ba7d | ||
|
|
03cf1d2119 | ||
|
|
2309a2edda | ||
|
|
ac1ac11075 | ||
|
|
aafa9f97eb | ||
|
|
7d3fe5ac58 | ||
|
|
b9c01ce865 | ||
|
|
679aa6be5f | ||
|
|
c44a17efbd | ||
|
|
27a2baefcc | ||
|
|
e23c5748ce | ||
|
|
c9781b2ff2 | ||
|
|
b9485d3296 | ||
|
|
c222fff82e | ||
|
|
f839031340 | ||
|
|
41b8743b29 | ||
|
|
056f4c1286 | ||
|
|
cfed450521 | ||
|
|
438603c97b | ||
|
|
c92c438efe | ||
|
|
7d5efcfb75 | ||
|
|
bb942ef72c | ||
|
|
b28a529ffc | ||
|
|
1ff3a6ec16 | ||
|
|
f1b679d912 | ||
|
|
8b514337a6 | ||
|
|
d7f6803f1f | ||
|
|
d91a40c41d | ||
|
|
0ed49789d0 | ||
|
|
a41b99644c | ||
|
|
286eae3812 | ||
|
|
e1e8057be5 | ||
|
|
a1ce50bcd4 | ||
|
|
e9d245c986 | ||
|
|
9836f8f246 | ||
|
|
88e1483152 | ||
|
|
0e7ff326cf | ||
|
|
188e26784b | ||
|
|
989b710cd8 | ||
|
|
01ea5be578 | ||
|
|
07c0a7be89 | ||
|
|
1f629d45b3 | ||
|
|
fa5fd7d98e | ||
|
|
e4f60010f7 | ||
|
|
5d54cb957c | ||
|
|
b930f3c201 | ||
|
|
830c42da93 | ||
|
|
e28c68a0ae | ||
|
|
f59090cec8 | ||
|
|
2a092fc9f8 | ||
|
|
5fa8583cdf | ||
|
|
9ab78e0ab8 | ||
|
|
d13b5e8782 | ||
|
|
76931f7b1d | ||
|
|
115843ce51 | ||
|
|
713f13a46d | ||
|
|
560d6fc683 | ||
|
|
c0f8b166ea | ||
|
|
6b68367c0d | ||
|
|
d80ed4caa7 | ||
|
|
a627ed39f2 | ||
|
|
5d00213d95 | ||
|
|
2e3bb5dcc3 | ||
|
|
749d44650d | ||
|
|
3ff2d6ef84 | ||
|
|
18c72ebf1d | ||
|
|
f9a70bf1ed | ||
|
|
22434245f2 | ||
|
|
fb76bde5b7 | ||
|
|
365047e73c | ||
|
|
c374c042c7 | ||
|
|
0f7e89a687 | ||
|
|
73582c4a69 | ||
|
|
3e9fea34eb | ||
|
|
348ef1d987 | ||
|
|
fc72bb33b2 | ||
|
|
7a9ce65e55 | ||
|
|
604dd65266 | ||
|
|
42f59e877f | ||
|
|
8b0e0dcd15 | ||
|
|
0ac2a8b3ad | ||
|
|
a97d7d45bc |
@@ -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
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## ElCaju 🥜
|
||||
|
||||
[](https://deepwiki.com/Forte11Cuba/elcaju)
|
||||
[](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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
rust_input: crate::api
|
||||
rust_root: rust/
|
||||
dart_output: lib/src/rust
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:');
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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 => 'クリップボードにコピーしました';
|
||||
}
|
||||
|
||||
@@ -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 => '클립보드에 복사되었습니다';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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 => 'Скопировано в буфер обмена';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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([
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -249,6 +249,7 @@ class _MintScreenState extends State<MintScreen> {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => _ConfirmationModal(
|
||||
amount: _amount,
|
||||
unit: _activeUnit,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Generated
+4213
File diff suppressed because it is too large
Load Diff
@@ -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)'] }
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod error;
|
||||
pub mod keys;
|
||||
pub mod mint_info;
|
||||
pub mod payment_request;
|
||||
pub mod token;
|
||||
pub mod wallet;
|
||||
@@ -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(),
|
||||
))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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("e_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("e.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("e.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
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
target
|
||||
.dart_tool
|
||||
*.iml
|
||||
!pubspec.lock
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Executable
+60
@@ -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
Reference in New Issue
Block a user