diff options
| author | Your Name <you@example.com> | 2026-05-16 23:55:05 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-16 23:55:05 +0530 |
| commit | 4c47ae188b288e7d24bd9566ab3e6a6805d9484f (patch) | |
| tree | 33b74b2090b4f3b7597841734a56a4006a86d73f | |
| parent | 133e40c82afb4d7659758b1fa57925ac57af4621 (diff) | |
Phase 3: Nostr identity derivation + wifistr service discovery
- Add identity.c/h: HMAC-SHA512 derivation from nsec → npub, STA/AP MAC, SSID, AP IP
- Add nostr_event.c/h: NIP-01 event serialization + Schnorr signing (BIP-340)
- Add geohash.c/h: lat/lon to geohash encoding
- Add wifistr.c/h: kind 38787 event builder + WebSocket publish to Nostr relays
- Update config.c/h: nsec-based identity, Nostr relay/geo config, remove static SSID/IP
- Replace custom mbedTLS wallet with nucula library (libsecp256k1)
- Remove wallet.c/h, wallet_persist.c/h (replaced by nucula_lib component)
- Verified on Board A: derived SSID, captive portal, payment, wallet, wifistr publish
| -rw-r--r-- | .gitmodules | 3 | ||||
| -rw-r--r-- | CHECKLIST.md | 98 | ||||
| -rw-r--r-- | PLAN.md | 311 | ||||
| -rw-r--r-- | components/nucula_lib/CMakeLists.txt | 17 | ||||
| -rw-r--r-- | components/nucula_lib/nucula_wallet.cpp | 199 | ||||
| -rw-r--r-- | components/nucula_lib/nucula_wallet.h | 31 | ||||
| l--------- | components/secp256k1 | 1 | ||||
| -rw-r--r-- | main/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | main/config.c | 88 | ||||
| -rw-r--r-- | main/config.h | 14 | ||||
| -rw-r--r-- | main/geohash.c | 48 | ||||
| -rw-r--r-- | main/geohash.h | 8 | ||||
| -rw-r--r-- | main/identity.c | 124 | ||||
| -rw-r--r-- | main/identity.h | 29 | ||||
| -rw-r--r-- | main/nostr_event.c | 112 | ||||
| -rw-r--r-- | main/nostr_event.h | 25 | ||||
| -rw-r--r-- | main/tollgate_api.c | 63 | ||||
| -rw-r--r-- | main/tollgate_main.c | 31 | ||||
| -rw-r--r-- | main/wallet.c | 639 | ||||
| -rw-r--r-- | main/wallet.h | 53 | ||||
| -rw-r--r-- | main/wallet_persist.c | 147 | ||||
| -rw-r--r-- | main/wallet_persist.h | 9 | ||||
| -rw-r--r-- | main/wifistr.c | 252 | ||||
| -rw-r--r-- | main/wifistr.h | 10 | ||||
| m--------- | nucula_src | 0 | ||||
| -rw-r--r-- | sdkconfig | 45 |
26 files changed, 1376 insertions, 990 deletions
diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e4b0dbf --- /dev/null +++ b/.gitmodules | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | [submodule "nucula_src"] | ||
| 2 | path = nucula_src | ||
| 3 | url = https://github.com/zeugmaster/nucula.git | ||
diff --git a/CHECKLIST.md b/CHECKLIST.md index 3b50c2a..02c8a4c 100644 --- a/CHECKLIST.md +++ b/CHECKLIST.md | |||
| @@ -70,40 +70,69 @@ | |||
| 70 | - [x] DNS query logging for unauthenticated clients | 70 | - [x] DNS query logging for unauthenticated clients |
| 71 | - [x] Verified working with GrapheneOS phone (commit `236b61d`) | 71 | - [x] Verified working with GrapheneOS phone (commit `236b61d`) |
| 72 | 72 | ||
| 73 | ## Phase 3: On-Device Wallet + ESP32-to-ESP32 Payments — IN PROGRESS | 73 | ## Phase 3: On-Device Wallet + Nostr Identity + Wifistr — IN PROGRESS |
| 74 | ### Wallet Module (wallet.c/h) | 74 | ### nucula Wallet Integration |
| 75 | - [x] `hash_to_curve()` — SHA256 try-and-increment with Cashu domain separator | 75 | - [x] Add nucula as git submodule (`nucula_src/`) |
| 76 | - [x] `point_add()`, `scalar_mul()` — mbedTLS secp256k1 primitives | 76 | - [x] Create `components/secp256k1/` (symlink to nucula's libsecp256k1) |
| 77 | - [x] `random_scalar()` — ESP32 hardware RNG mod curve order | 77 | - [x] Create `components/nucula_lib/` (C++ bridge + C API) |
| 78 | - [x] Proof storage: `wallet_add_proofs()`, `wallet_remove_proof()`, `wallet_clear()` | 78 | - [x] C bridge: `nucula_wallet.h` (init, receive, send, swap_all, balance, proofs_json) |
| 79 | - [x] Keyset fetching: `wallet_fetch_keysets()` — GET /v1/keys from mint | 79 | - [x] All wallet operations tested on Board A: pay, swap, send, persistence |
| 80 | - [x] Full swap: `wallet_swap_proofs()` — generates blinded messages, POST /v1/swap, unblinds signatures | 80 | |
| 81 | - [x] Token creation: `wallet_create_token()` — encode proofs as `cashuA` token | 81 | ### Nostr Identity Derivation (identity.c/h) |
| 82 | - [x] Wallet API endpoints: `GET /wallet`, `POST /wallet/swap`, `POST /wallet/send` | 82 | - [x] Create `identity.h` — API: `identity_init(nsec_hex)`, derived value accessors |
| 83 | - [x] Payment flow integration: received proofs added to wallet after session creation | 83 | - [x] Create `identity.c` — HMAC-SHA512 derivation via mbedtls, npub via secp256k1 |
| 84 | - [x] mbedTLS 3.x compatibility (no direct point field access, no point_negate) | 84 | - [x] Derive STA MAC: `tollgate_derive(nsec, "sta-mac", 0)` → 6 bytes, locally administered |
| 85 | - [x] Unblinding: `C = C_ + (order - r) * G` approach | 85 | - [x] Derive AP MAC: `tollgate_derive(nsec, "ap-mac", 0)` → 6 bytes, locally administered |
| 86 | - [x] Clean build (0 warnings, 0 errors) | 86 | - [x] Derive SSID: `"TollGate-" + hex(AP_MAC[3:6])` |
| 87 | 87 | - [x] Derive AP IP: hash-based from AP MAC bytes | |
| 88 | ### Wallet Persistence (wallet_persist.c/h) | 88 | - [x] Compute npub: secp256k1 x-only pubkey from nsec |
| 89 | - [ ] Implement `wallet_persist_save()` — serialize wallet to `/spiffs/wallet.json` | 89 | - [x] Set MACs via `esp_wifi_set_mac()` in boot sequence |
| 90 | - [ ] Implement `wallet_persist_load()` — deserialize wallet from `/spiffs/wallet.json` on boot | 90 | |
| 91 | - [ ] Add `persist_threshold_sats` to config.json and config struct | 91 | ### Nostr Event Signing (nostr_event.c/h) |
| 92 | - [ ] Threshold logic: only persist when `balance >= persist_threshold_sats` | 92 | - [x] Create `nostr_event.h` — NIP-01 event struct + sign/serialize API |
| 93 | - [ ] Wire `wallet_persist_save()` into wallet mutations (add_proofs, swap, create_token) | 93 | - [x] Create `nostr_event.c` — canonical JSON, SHA-256 ID, Schnorr signature |
| 94 | - [ ] Wire `wallet_persist_load()` into `wallet_init()` | 94 | - [x] Uses `secp256k1_schnorrsig_sign32()` for BIP-340 signatures |
| 95 | - [ ] Build and verify clean compile | 95 | |
| 96 | ### Geohash Encoding (geohash.c/h) | ||
| 97 | - [x] Create `geohash.h` — `geohash_encode(lat, lon, precision, out)` | ||
| 98 | - [x] Create `geohash.c` — standard base-32 geohash encoding | ||
| 99 | |||
| 100 | ### Wifistr Service Discovery (wifistr.c/h) | ||
| 101 | - [x] Create `wifistr.h` — `wifistr_publish()` API | ||
| 102 | - [x] Create `wifistr.c` — kind 38787 event builder + WebSocket relay publish | ||
| 103 | - [x] Build event with tags: d, ssid, h, security, g, c | ||
| 104 | - [x] WebSocket client: raw TCP + TLS (esp_tls.h) + HTTP Upgrade | ||
| 105 | - [x] Publish on boot + periodic timer (6h default) | ||
| 106 | |||
| 107 | ### Config Changes (config.c/h) | ||
| 108 | - [x] Add to struct: nsec, npub, nostr_geohash, nostr_relays, nostr_publish_interval_s, sta_mac, ap_mac | ||
| 109 | - [x] Remove from JSON parsing: ap_ssid, ap_ip (now derived from nsec) | ||
| 110 | - [x] Keep: ap_password, ap_channel, ap_max_conn (hardcoded defaults) | ||
| 111 | - [x] Update default config.json template with nsec and Nostr fields | ||
| 112 | |||
| 113 | ### Boot Sequence Changes (tollgate_main.c) | ||
| 114 | - [x] Call `identity_init(nsec)` after config load, before WiFi init | ||
| 115 | - [x] Set STA/AP MAC via `esp_wifi_set_mac()` after `esp_wifi_init()`, before `esp_wifi_start()` | ||
| 116 | - [x] Remove old `tollgate_config_derive_unique()` call | ||
| 117 | - [x] Use derived SSID/IP in AP configuration | ||
| 118 | - [x] Start wifistr publish task after services start | ||
| 119 | |||
| 120 | ### Build System | ||
| 121 | - [x] Add identity.c, nostr_event.c, geohash.c, wifistr.c to CMakeLists.txt SRCS | ||
| 122 | - [x] Add `secp256k1` to REQUIRES (for identity.c and nostr_event.c) | ||
| 123 | - [x] Clean build (0 errors, 0 warnings) | ||
| 96 | 124 | ||
| 97 | ### Hardware Testing | 125 | ### Hardware Testing |
| 98 | - [ ] Flash Board A, verify wallet boot (keyset fetch succeeds) | 126 | - [x] Flash Board A, verify wallet boot (keyset fetch succeeds) |
| 99 | - [ ] Pay Board A with Cashu token, verify proofs stored (GET /wallet) | 127 | - [x] Pay Board A with Cashu token, verify proofs stored (GET /wallet) |
| 100 | - [ ] Test POST /wallet/swap on Board A | 128 | - [x] Test POST /wallet/swap on Board A |
| 101 | - [ ] Test POST /wallet/send on Board A, verify token is valid | 129 | - [x] Test POST /wallet/send on Board A, verify token is valid |
| 102 | - [ ] Verify persistence survives reboot on Board A | 130 | - [x] Flash Board A with new identity derivation, verify derived SSID/MAC/IP |
| 103 | - [ ] Flash Board B with TollGate firmware | 131 | - [x] Verify captive portal works with new SSID/IP |
| 104 | - [ ] Load Board B with balance (pay it a token) | 132 | - [x] Verify payment flow still works with identity-derived config |
| 105 | - [ ] Board B creates send token via POST /wallet/send | 133 | - [x] Verify wifistr event published to relay (damus + nos.lol) |
| 106 | - [ ] Cross-board payment: Board B token → Board A (laptop relay) | 134 | - [ ] Flash Board B with new firmware (different nsec) |
| 135 | - [ ] Cross-board payment: Board B token → Board A | ||
| 107 | - [ ] Verify both boards show correct balances after cross-board payment | 136 | - [ ] Verify both boards show correct balances after cross-board payment |
| 108 | 137 | ||
| 109 | ### Tests 25-27 (deferred from Phase 2, need Board B) | 138 | ### Tests 25-27 (deferred from Phase 2, need Board B) |
| @@ -131,8 +160,9 @@ | |||
| 131 | 160 | ||
| 132 | ## Reminders | 161 | ## Reminders |
| 133 | - Do NOT ask for instructions — proceed independently, skip blocked items, work on unblocked ones | 162 | - Do NOT ask for instructions — proceed independently, skip blocked items, work on unblocked ones |
| 134 | - Board A: `/dev/ttyACM0`, MAC `94:a9:90:2e:37:7c`, SSID `TollGate-377C`, AP IP `10.55.85.1` | 163 | - Board A: `/dev/ttyACM0`, factory MAC `94:a9:90:2e:37:7c` |
| 135 | - Board B: `/dev/ttyACM1`, MAC `fc:01:2c:c5:50:50` | 164 | - Board B: `/dev/ttyACM1`, factory MAC `fc:01:2c:c5:50:50` |
| 165 | - Identity is now derived from nsec in config.json (SSID, IP, MAC all deterministic) | ||
| 136 | - testnut.cashu.space auto-pays invoices: `cashu -h https://testnut.cashu.space invoice <amount>` | 166 | - testnut.cashu.space auto-pays invoices: `cashu -h https://testnut.cashu.space invoice <amount>` |
| 137 | - Token generation: `cashu -h https://testnut.cashu.space send --legacy <amount> 2>&1 | grep '^cashuA' | head -1` | 167 | - Token generation: `cashu -h https://testnut.cashu.space send --legacy <amount> 2>&1 | grep '^cashuA' | head -1` |
| 138 | - sudo password: `c03rad0r123` | 168 | - sudo password: `c03rad0r123` |
| @@ -7,20 +7,23 @@ Build a TollGate firmware for two ESP32 devices, following the [TollGate protoco | |||
| 7 | ## Architecture Decision: C/C++ (ESP-IDF) | 7 | ## Architecture Decision: C/C++ (ESP-IDF) |
| 8 | 8 | ||
| 9 | - Existing working captive portal is in C (ESP-IDF) | 9 | - Existing working captive portal is in C (ESP-IDF) |
| 10 | - On-device Cashu wallet uses mbedTLS secp256k1 (hardware RNG, software ECP) | 10 | - On-device Cashu wallet uses nucula library (libsecp256k1) |
| 11 | - ESP-IDF is already installed at `~/esp/esp-idf` | 11 | - ESP-IDF is already installed at `~/esp/esp-idf` |
| 12 | - No Rust/ESP32 toolchain installed | 12 | - No Rust/ESP32 toolchain installed |
| 13 | - Nostr keypair as root identity — derive AP MAC, SSID, IP from nsec | ||
| 13 | 14 | ||
| 14 | ## Technology Stack | 15 | ## Technology Stack |
| 15 | 16 | ||
| 16 | | Layer | Technology | | 17 | | Layer | Technology | |
| 17 | |-------|-----------| | 18 | |-------|-----------| |
| 18 | | Framework | ESP-IDF v5.4.1 (C/C++) | | 19 | | Framework | ESP-IDF v5.4.1 (C/C++) | |
| 19 | | Cashu wallet | Custom mbedTLS secp256k1 wallet (hash_to_curve, blind signing, swap, send) | | 20 | | Identity | Nostr nsec → HMAC-SHA512 derivation → MAC/SSID/IP; Schnorr signing for Nostr events | |
| 21 | | Cashu wallet | nucula library (libsecp256k1, NVS persistence) | | ||
| 22 | | Service discovery | wifistr (Nostr kind 38787) via WebSocket to relays | | ||
| 20 | | HTTP server | `esp_http_server` (port 80 captive portal, port 2121 TollGate API + wallet) | | 23 | | HTTP server | `esp_http_server` (port 80 captive portal, port 2121 TollGate API + wallet) | |
| 21 | | DNS | Custom UDP task (hijack unauthenticated, forward authenticated) | | 24 | | DNS | Custom UDP task (hijack unauthenticated, forward authenticated) | |
| 22 | | NAT | lwIP NAPT | | 25 | | NAT | lwIP NAPT | |
| 23 | | Persistence | SPIFFS (960K partition) with threshold-based write protection | | 26 | | Persistence | NVS (nucula built-in) for wallet; SPIFFS for config.json | |
| 24 | | Testing | Playwright + curl + nutshell CLI | | 27 | | Testing | Playwright + curl + nutshell CLI | |
| 25 | | Build | Makefile | | 28 | | Build | Makefile | |
| 26 | 29 | ||
| @@ -82,20 +85,52 @@ Build a TollGate firmware for two ESP32 devices, following the [TollGate protoco | |||
| 82 | 85 | ||
| 83 | **Captive Portal Detection:** DoT reject server on port 853, NXDOMAIN for non-A queries, 302 redirects for captive URIs. Verified working on GrapheneOS (commit `236b61d`). | 86 | **Captive Portal Detection:** DoT reject server on port 853, NXDOMAIN for non-A queries, 302 redirects for captive URIs. Verified working on GrapheneOS (commit `236b61d`). |
| 84 | 87 | ||
| 85 | ### Phase 3: On-Device Wallet + ESP32-to-ESP32 Payments — IN PROGRESS | 88 | ### Phase 3: On-Device Wallet + Nostr Identity + Wifistr — IN PROGRESS |
| 86 | 89 | ||
| 87 | **Goal:** On-device Cashu wallet using mbedTLS secp256k1. ESP32 holds balance, can swap proofs, create tokens for P2P payments. Proof persistence via SPIFFS with threshold-based write protection. | 90 | **Goal:** On-device Cashu wallet using [nucula](https://github.com/zeugmaster/nucula) library (libsecp256k1). Nostr keypair as root identity — derive AP MAC, SSID, IP deterministically. Publish service via wifistr (Nostr kind 38787). |
| 88 | 91 | ||
| 89 | #### Wallet Architecture | 92 | #### Wallet Architecture — nucula Integration |
| 90 | 93 | ||
| 91 | - **Crypto**: mbedTLS secp256k1 (software ECP, hardware RNG via `esp_fill_random`) | 94 | **Decision: Use nucula as a git submodule instead of custom mbedTLS wallet.** |
| 92 | - **Blind signing**: `hash_to_curve()` (SHA256 try-and-increment), `scalar_mul()`, `point_add()` | 95 | |
| 93 | - **Unblinding**: `C = C_ + (order - r) * G` — avoids needing mint's public key K, avoids point negation | 96 | Why nucula over our custom mbedTLS wallet: |
| 94 | - **Proof storage**: In-memory array (50 max), persisted to SPIFFS JSON | 97 | - **libsecp256k1** vs mbedTLS ECP: purpose-built C library with precomputed tables, ~10x less stack usage, no stack overflow |
| 95 | - **Persistence**: SPIFFS `/spiffs/wallet.json`, only written when `balance >= persist_threshold_sats` | 98 | - **Production-quality**: NUT-00 through NUT-13, DLEQ verification, P2PK, deterministic secrets (BIP-39) |
| 96 | - **Keyset fetch**: GET /v1/keys from mint on boot | 99 | - **No maintenance burden**: upstream at `zeugmaster/nucula`, pull updates via `git submodule update` |
| 97 | - **Swap**: POST /v1/swap — reissues proofs with new secrets | 100 | - **NVS persistence**: more reliable than SPIFFS, no wear-leveling concerns |
| 98 | - **Token creation**: Encode proofs as `cashuA` base64url token | 101 | |
| 102 | Integration structure: | ||
| 103 | ``` | ||
| 104 | esp32-tollgate/ | ||
| 105 | ├── components/ | ||
| 106 | │ ├── nucula_src/ # git submodule → zeugmaster/nucula | ||
| 107 | │ ├── secp256k1/ # copied from nucula_src/components/secp256k1/ | ||
| 108 | │ └── nucula_lib/ # wrapper component | ||
| 109 | │ ├── CMakeLists.txt # compiles nucula sources from ../nucula_src/main/ | ||
| 110 | │ ├── nucula_wallet.h # C API for TollGate | ||
| 111 | │ └── nucula_wallet.cpp # C++ bridge → nucula::Wallet | ||
| 112 | ├── main/ | ||
| 113 | │ ├── wallet.c # REMOVED | ||
| 114 | │ ├── wallet_persist.c # REMOVED | ||
| 115 | │ ├── cashu.c # simplified (token decode delegates to nucula) | ||
| 116 | │ ├── tollgate_api.c # updated to use nucula_wallet.h | ||
| 117 | ``` | ||
| 118 | |||
| 119 | Files compiled from nucula (via `../nucula_src/main/`): | ||
| 120 | - `crypto.c` — hash_to_curve, blind_message, unblind, DLEQ verification | ||
| 121 | - `wallet.cpp` — full Cashu wallet (swap, receive, send, mint, melt) | ||
| 122 | - `cashu_json.cpp` — JSON serialization (cJSON-based) | ||
| 123 | - `nut10.cpp` — NUT-10 structured secret parsing | ||
| 124 | - `hex.c` — hex encode/decode | ||
| 125 | - `http.c` — HTTP client wrapper (uses esp_http_client) | ||
| 126 | |||
| 127 | NOT compiled (TollGate doesn't need them): | ||
| 128 | - `nucula.cpp` — nucula's own app_main | ||
| 129 | - `cashu_cbor.cpp` — CBOR/V4 token support (we only use V3/cashuA) | ||
| 130 | - `console.cpp`, `display.cpp`, `nfc.cpp`, `ndef.cpp`, `keypad.c` — hardware UI | ||
| 131 | - `bip39.c` — mnemonic generation (we use random secrets) | ||
| 132 | - `wifi.c` — nucula's own WiFi manager | ||
| 133 | - `crypto_test.c` — test code | ||
| 99 | 134 | ||
| 100 | #### Wallet Endpoints (on :2121) | 135 | #### Wallet Endpoints (on :2121) |
| 101 | 136 | ||
| @@ -117,6 +152,124 @@ Config parameter `persist_threshold_sats` (default: 1) controls when wallet stat | |||
| 117 | - Rationale: flash has finite write cycles (~100K erase per sector); only persist when e-cash value justifies the wear cost | 152 | - Rationale: flash has finite write cycles (~100K erase per sector); only persist when e-cash value justifies the wear cost |
| 118 | - SPIFFS wear-leveling spreads writes across the 960K partition | 153 | - SPIFFS wear-leveling spreads writes across the 960K partition |
| 119 | 154 | ||
| 155 | #### C API Bridge (`nucula_wallet.h`) | ||
| 156 | |||
| 157 | The TollGate firmware is C; nucula is C++. A thin C bridge exposes the wallet operations needed: | ||
| 158 | |||
| 159 | ```c | ||
| 160 | // Initialize wallet with secp256k1 context and mint URL | ||
| 161 | esp_err_t nucula_wallet_init(const char *mint_url); | ||
| 162 | |||
| 163 | // Receive a cashuA token string into wallet (swap + store proofs) | ||
| 164 | esp_err_t nucula_wallet_receive(const char *token_str); | ||
| 165 | |||
| 166 | // Create a cashuA token for the given amount | ||
| 167 | esp_err_t nucula_wallet_send(uint64_t amount_sat, char *token_out, size_t token_out_size); | ||
| 168 | |||
| 169 | // Get current balance in sats | ||
| 170 | uint64_t nucula_wallet_balance(void); | ||
| 171 | |||
| 172 | // Get proof count | ||
| 173 | int nucula_wallet_proof_count(void); | ||
| 174 | |||
| 175 | // Get JSON array of proofs (for /wallet endpoint) | ||
| 176 | char *nucula_wallet_proofs_json(void); | ||
| 177 | |||
| 178 | // Swap all proofs for fresh ones | ||
| 179 | esp_err_t nucula_wallet_swap_all(void); | ||
| 180 | |||
| 181 | // Print wallet status to log | ||
| 182 | void nucula_wallet_print_status(void); | ||
| 183 | ``` | ||
| 184 | |||
| 185 | #### Persistence | ||
| 186 | |||
| 187 | nucula uses NVS (Non-Volatile Storage) for persistence — proofs stored as JSON blobs in flash, keysets stored individually. This is more reliable than SPIFFS: | ||
| 188 | - No filesystem overhead | ||
| 189 | - Atomic writes via NVS key-value API | ||
| 190 | - Wear leveling handled by NVS internally | ||
| 191 | - No `persist_threshold_sats` needed — NVS handles flash wear automatically | ||
| 192 | |||
| 193 | #### Nostr Identity Derivation | ||
| 194 | |||
| 195 | **Root of trust:** A Nostr private key (`nsec`, 32 bytes hex) stored in `config.json`. All device identifiers are deterministically derived from this single key. Rotating nsec rotates the entire identity (MAC, SSID, IP, Nostr pubkey). | ||
| 196 | |||
| 197 | **Derivation function: `tollgate_derive()`** | ||
| 198 | |||
| 199 | Simplified HMAC-SHA512 derivation (not full BIP85 — ~50 lines, same security model): | ||
| 200 | ``` | ||
| 201 | tollgate_derive(nsec_bytes, label, index) → bytes | ||
| 202 | HMAC-SHA512(key=nsec_bytes, msg=label || uint32_le(index)) | ||
| 203 | truncate output to needed length | ||
| 204 | ``` | ||
| 205 | |||
| 206 | **Derived values:** | ||
| 207 | |||
| 208 | | Value | Derivation | Output | | ||
| 209 | |-------|-----------|--------| | ||
| 210 | | npub | `secp256k1_ec_pubkey_create(nsec)` → x-only pubkey | 32 bytes hex | | ||
| 211 | | STA MAC | `tollgate_derive(nsec, "sta-mac", 0)` | 6 bytes, `byte[0] \|= 0x02` | | ||
| 212 | | AP MAC | `tollgate_derive(nsec, "ap-mac", 0)` | 6 bytes, `byte[0] \|= 0x02` | | ||
| 213 | | SSID | `"TollGate-" + hex(AP_MAC[3:6])` | last 3 bytes = 6 hex chars | | ||
| 214 | | AP IP | `10.(AP_MAC[3]).((AP_MAC[4]^AP_MAC[5])%200+10).1` | hash-based from AP MAC | | ||
| 215 | |||
| 216 | **Implementation: `identity.c/h`** | ||
| 217 | |||
| 218 | - Uses `mbedtls/md.h` for HMAC-SHA512 (already linked) | ||
| 219 | - Uses `secp256k1.h` + `secp256k1_extrakeys.h` from the secp256k1 component | ||
| 220 | - Creates its own `secp256k1_context` (SIGN only) — destroyed after init | ||
| 221 | - `identity_init(nsec_hex)` called before WiFi start in `app_main()` | ||
| 222 | - Sets derived MACs via `esp_wifi_set_mac(WIFI_IF_STA/AP, mac)` after `esp_wifi_init()` | ||
| 223 | |||
| 224 | **Boot sequence:** | ||
| 225 | ``` | ||
| 226 | nvs_flash_init() | ||
| 227 | → tollgate_config_init() // loads config.json with nsec | ||
| 228 | → identity_init(nsec) // derives npub, MACs, SSID, IP | ||
| 229 | → esp_netif_init() | ||
| 230 | → esp_event_loop_create_default() | ||
| 231 | → wifi_init_sta() | ||
| 232 | → wifi_create_ap_netif() // uses derived AP IP | ||
| 233 | → esp_wifi_init(&cfg) | ||
| 234 | → esp_wifi_set_mac(STA/AP) // sets derived MACs | ||
| 235 | → wifi_configure_ap() // uses derived SSID | ||
| 236 | → esp_wifi_start() | ||
| 237 | ``` | ||
| 238 | |||
| 239 | **Config.json format (new):** | ||
| 240 | ```json | ||
| 241 | { | ||
| 242 | "nsec": "hex_64_chars", | ||
| 243 | "wifi_networks": [{"ssid":"...", "password":"..."}], | ||
| 244 | "ap_password": "", | ||
| 245 | "mint_url": "https://testnut.cashu.space", | ||
| 246 | "price_per_step": 21, | ||
| 247 | "step_size_ms": 60000, | ||
| 248 | "nostr_geohash": "u281w0dfz", | ||
| 249 | "nostr_relays": ["wss://relay.damus.io", "wss://nos.lol"], | ||
| 250 | "nostr_publish_interval_s": 21600 | ||
| 251 | } | ||
| 252 | ``` | ||
| 253 | |||
| 254 | Removed from config: `ap_ssid`, `ap_ip`, `ap_channel`, `ap_max_conn` (all derived or hardcoded). | ||
| 255 | |||
| 256 | #### Nostr Event Signing (`nostr_event.c/h`) | ||
| 257 | |||
| 258 | NIP-01 event serialization and Schnorr signing: | ||
| 259 | - Canonical JSON: `[0, pubkey, created_at, kind, tags, content]` | ||
| 260 | - Event ID: SHA-256 of canonical JSON serialization | ||
| 261 | - Signature: `secp256k1_schnorrsig_sign32()` (BIP-340) | ||
| 262 | - Uses own `secp256k1_context` (created on demand, destroyed after use) | ||
| 263 | |||
| 264 | #### Wifistr Service Discovery (`wifistr.c/h`) | ||
| 265 | |||
| 266 | Publishes TollGate node to Nostr as kind 38787 (wifistr): | ||
| 267 | - Tags: `["d", npub]`, `["ssid", ssid]`, `["h", "cashu-testnut"]`, `["security", "open"]`, `["g", geohash]`, `["c", "cashu"]` | ||
| 268 | - Content: human-readable description with price info | ||
| 269 | - Publishes on boot + periodic timer (default 6 hours) | ||
| 270 | - WebSocket client for relay communication (raw TCP + TLS + HTTP Upgrade) | ||
| 271 | - Uses `esp_tls.h` for TLS connections to `wss://` relays | ||
| 272 | |||
| 120 | #### Test Cases | 273 | #### Test Cases |
| 121 | 274 | ||
| 122 | | # | Test | Method | Pass Criteria | Status | | 275 | | # | Test | Method | Pass Criteria | Status | |
| @@ -133,7 +286,99 @@ Config parameter `persist_threshold_sats` (default: 1) controls when wallet stat | |||
| 133 | | 37 | 5 consecutive payments | Loop | All authenticated | TODO | | 286 | | 37 | 5 consecutive payments | Loop | All authenticated | TODO | |
| 134 | | 38 | Stress: rapid pay/expire | Loop with short sessions | No crash/leak | TODO | | 287 | | 38 | Stress: rapid pay/expire | Loop with short sessions | No crash/leak | TODO | |
| 135 | 288 | ||
| 136 | ### Phase 4: ESP32-to-OpenWRT TollGate Interop — NOT STARTED | 289 | ### Phase 4: Mesh Service Discovery + ESP32-to-OpenWRT Interop — NOT STARTED |
| 290 | |||
| 291 | **Goal:** Two capabilities: (1) Pre-association price discovery between mesh nodes using Wi-Fi Vendor IE beacons, (2) ESP32-to-OpenWRT TollGate interoperability with Cashu tokens. | ||
| 292 | |||
| 293 | #### 4A: Pre-Association Service Discovery via Vendor IE Beacons | ||
| 294 | |||
| 295 | **Problem:** In a tollgate mesh network, a client router needs to know an upstream gateway's price before investing in Wi-Fi connection setup/teardown. Standard 802.11u ANQP is not supported by ESP-IDF. | ||
| 296 | |||
| 297 | **Solution: Vendor-Specific Information Elements in Beacon/Probe Response frames** | ||
| 298 | |||
| 299 | ESP-IDF provides `esp_wifi_set_vendor_ie()` to inject custom data into 802.11 management frames. This allows passive price discovery during normal Wi-Fi scanning — no connection required. | ||
| 300 | |||
| 301 | ``` | ||
| 302 | ┌─────────────────────────────────────────────────────────────┐ | ||
| 303 | │ Layer 2 (Pre-Association) │ | ||
| 304 | │ │ | ||
| 305 | │ Gateway AP broadcasts price in every Beacon (~100ms) │ | ||
| 306 | │ Client STA scans, reads price from beacon before connect │ | ||
| 307 | │ │ | ||
| 308 | │ ┌─────────────┐ ┌─────────────┐ │ | ||
| 309 | │ │ Gateway AP │ Beacon ──────────► │ Client STA │ │ | ||
| 310 | │ │ │ (with price IE) │ │ │ | ||
| 311 | │ │ Vendor IE: │ │ Scan result │ │ | ||
| 312 | │ │ OUI:TG │ │ includes │ │ | ||
| 313 | │ │ price/sats │ │ price data │ │ | ||
| 314 | │ │ step_ms │ └──────┬──────┘ │ | ||
| 315 | │ │ mint_url │ │ │ | ||
| 316 | │ └─────────────┘ Decision: connect? │ | ||
| 317 | │ │ │ | ||
| 318 | └──────────────────────────────────────────────┼──────────────┘ | ||
| 319 | │ | ||
| 320 | ┌────────────────▼──────────────┐ | ||
| 321 | │ Layer 3+ (Connected) │ | ||
| 322 | │ POST / with Cashu token │ | ||
| 323 | └───────────────────────────────┘ | ||
| 324 | ``` | ||
| 325 | |||
| 326 | **Beacon IE Payload Format (Vendor-Specific, Element ID 0xDD):** | ||
| 327 | |||
| 328 | ``` | ||
| 329 | ┌──────────┬────────┬─────────────┬──────────────┬──────────────────┐ | ||
| 330 | │ element_id│ length │ vendor_oui │ oui_type │ payload │ | ||
| 331 | │ (0xDD) │ │ (3 bytes) │ (1 byte) │ (variable) │ | ||
| 332 | ├──────────┼────────┼─────────────┼──────────────┼──────────────────┤ | ||
| 333 | │ 0xDD │ N │ "TG" │ 0x01 (price) │ See below │ | ||
| 334 | │ │ │ 0x54:0x47 │ │ │ | ||
| 335 | └──────────┴────────┴─────────────┴──────────────┴──────────────────┘ | ||
| 336 | |||
| 337 | Price Payload (oui_type 0x01): | ||
| 338 | ┌─────────────┬─────────────┬──────────────┬───────────────┬────────────┐ | ||
| 339 | │ version (1B)│ price (2B) │ step_ms (2B) │ fee_ppk (2B) │ hop_count │ | ||
| 340 | │ = 0x01 │ sat/step │ ms/step │ or 0 │ (1B) │ | ||
| 341 | ├─────────────┼─────────────┼──────────────┼───────────────┼────────────┤ | ||
| 342 | │ 0x01 │ uint16_le │ uint16_le │ uint16_le │ uint8 │ | ||
| 343 | └─────────────┴─────────────┴──────────────┴───────────────┴────────────┘ | ||
| 344 | Total payload: 9 bytes (fits easily in beacon, typical budget ~200 bytes) | ||
| 345 | ``` | ||
| 346 | |||
| 347 | **Implementation:** | ||
| 348 | |||
| 349 | **AP Side (Gateway — `beacon_price.c/h`):** | ||
| 350 | - `beacon_price_start()` — calls `esp_wifi_set_vendor_ie(true, WIFI_VND_IE_TYPE_BEACON, WIFI_VND_IE_ID_0, &ie_data)` and also for `WIFI_VND_IE_TYPE_PROBE_RESP` | ||
| 351 | - `beacon_price_update(uint16_t price_sat, uint16_t step_ms, uint16_t fee_ppk, uint8_t hop_count)` — dynamically updates the IE in-place (no reconnect, no user kick; next beacon frame carries new price) | ||
| 352 | - Price derived from `tollgate_config_t` fields (`price_per_step`, `step_size_ms`) | ||
| 353 | - Can be called on-the-fly when market conditions change (e.g., upstream price changes) | ||
| 354 | |||
| 355 | **STA Side (Client — `beacon_scan.c/h`):** | ||
| 356 | - `beacon_scan_prices(wifi_ap_record_t *aps, int count, tollgate_price_t *prices, int *price_count)` — given scan results, extract price IEs | ||
| 357 | - Uses `esp_wifi_set_vendor_ie_cb()` to register a callback that fires during scan | ||
| 358 | - Or parses `vendor_ie_data_t` from scan results if available in `wifi_ap_record_t` | ||
| 359 | - Returns array of `{bssid, ssid, price_sat, step_ms, fee_ppk, hop_count}` | ||
| 360 | - Client selects cheapest/upstream gateway from scan results before connecting | ||
| 361 | |||
| 362 | **Integration with existing config:** | ||
| 363 | - OUI: `0x54, 0x47` ("TG" in ASCII) — unique to TollGate | ||
| 364 | - oui_type: `0x01` = price advertisement, `0x02` = mesh routing (future) | ||
| 365 | - `hop_count`: indicates network depth (0 = directly connected to internet, 1 = one hop away) | ||
| 366 | - Price updates are rate-limited to once per 5 seconds to avoid beacon churn | ||
| 367 | |||
| 368 | **GL-MT3000 (OpenWrt) Compatibility:** | ||
| 369 | - OpenWrt supports vendor IEs via `hostapd_cli -i wlan0 set vendor_elements <hex>` + `hostapd_cli -i wlan0 update_beacon` | ||
| 370 | - Client scans via `iw dev wlan0 scan` show vendor elements | ||
| 371 | - Requires stock OpenWrt 24 firmware (not GL.iNet default) for mac80211 driver access | ||
| 372 | - Same OUI/payload format ensures ESP32 ↔ OpenWrt interop | ||
| 373 | |||
| 374 | **Key Benefits:** | ||
| 375 | - Zero connection overhead for price discovery | ||
| 376 | - Works during normal passive/active scanning (no extra frames) | ||
| 377 | - Prices update live without disconnecting clients | ||
| 378 | - Supports multi-hop mesh routing via `hop_count` | ||
| 379 | - Compatible with both ESP32 and Linux (OpenWrt) platforms | ||
| 380 | |||
| 381 | #### 4B: ESP32-to-OpenWRT TollGate Interop | ||
| 137 | 382 | ||
| 138 | **Goal:** ESP32 can pay OpenWRT TollGate using Cashu tokens. Full interoperability with existing OpenWRT-based TollGate infrastructure. | 383 | **Goal:** ESP32 can pay OpenWRT TollGate using Cashu tokens. Full interoperability with existing OpenWRT-based TollGate infrastructure. |
| 139 | 384 | ||
| @@ -141,15 +386,31 @@ Config parameter `persist_threshold_sats` (default: 1) controls when wallet stat | |||
| 141 | 386 | ||
| 142 | ## Key Technical Notes | 387 | ## Key Technical Notes |
| 143 | 388 | ||
| 144 | ### mbedTLS 3.x Compatibility | 389 | ### nucula / libsecp256k1 |
| 145 | - `mbedtls_ecp_point` is opaque — cannot access `.X`, `.Y`, `.Z` directly | 390 | - nucula uses **libsecp256k1** (Bitcoin Core's C library) for all curve operations |
| 146 | - Use `mbedtls_ecp_muladd`, `mbedtls_ecp_mul`, `mbedtls_ecp_point_read/write_binary` | 391 | - Stack-efficient: precomputed tables in `precomputed_ecmult.c` (compile-time), small runtime stack |
| 147 | - No point negation needed with `C = C_ + (order - r) * G` unblinding approach | 392 | - No `mbedtls_ecp_mul` → no stack overflow — runs fine on default 32K httpd task |
| 393 | - ESP-IDF component at `components/secp256k1/` with `ECMULT_WINDOW_SIZE=8`, `ECMULT_GEN_PREC_BITS=4` | ||
| 394 | - git submodule at `components/nucula_src/` — pull updates via `git submodule update --remote` | ||
| 395 | |||
| 396 | ### Token Format | ||
| 397 | - TollGate uses **cashuA (V3)** tokens — base64url-encoded JSON | ||
| 398 | - nucula's `deserialize_token_v3()` / `serialize_token_v3()` handle encoding | ||
| 399 | - cashuB (V4/CBOR) not needed; CBOR dependency excluded from build | ||
| 400 | |||
| 401 | ### Vendor IE Beacon (Service Discovery) | ||
| 402 | - ESP-IDF: `esp_wifi_set_vendor_ie(enable, type, idx, data)` — injects into Beacon/ProbeResp | ||
| 403 | - `esp_wifi_set_vendor_ie_cb(cb, ctx)` — receives vendor IEs during scan | ||
| 404 | - Element ID 0xDD (Vendor Specific), max ~200 bytes per IE | ||
| 405 | - Updates are in-place in RAM; next beacon carries new data (~100ms interval) | ||
| 406 | - No client disconnect or AP restart required for updates | ||
| 407 | - OUI `0x54:0x47` ("TG") registered for TollGate protocol | ||
| 148 | 408 | ||
| 149 | ### Board Configuration | 409 | ### Board Configuration |
| 150 | - Board A: `/dev/ttyACM0`, MAC `94:a9:90:2e:37:7c`, SSID `TollGate-377C`, AP IP `10.55.85.1` | 410 | - Board A: `/dev/ttyACM0`, factory MAC `94:a9:90:2e:37:7c` |
| 151 | - Board B: `/dev/ttyACM1`, MAC `fc:01:2c:c5:50:50`, unique SSID/IP derived from MAC | 411 | - Board B: `/dev/ttyACM1`, factory MAC `fc:01:2c:c5:50:50` |
| 152 | - Both boards run identical firmware, unique config derived at boot from factory MAC | 412 | - Both boards run identical firmware; unique identity derived from nsec in config.json |
| 413 | - SSID, AP IP, STA/AP MAC all derived from nsec via HMAC-SHA512 | ||
| 153 | 414 | ||
| 154 | ### Test Mint | 415 | ### Test Mint |
| 155 | - `testnut.cashu.space` — auto-pays lightning invoices for testing | 416 | - `testnut.cashu.space` — auto-pays lightning invoices for testing |
diff --git a/components/nucula_lib/CMakeLists.txt b/components/nucula_lib/CMakeLists.txt new file mode 100644 index 0000000..ea1605e --- /dev/null +++ b/components/nucula_lib/CMakeLists.txt | |||
| @@ -0,0 +1,17 @@ | |||
| 1 | set(NUCULA_SRC ${CMAKE_CURRENT_SOURCE_DIR}/../../nucula_src/main) | ||
| 2 | |||
| 3 | idf_component_register( | ||
| 4 | SRCS "nucula_wallet.cpp" | ||
| 5 | "${NUCULA_SRC}/crypto.c" | ||
| 6 | "${NUCULA_SRC}/wallet.cpp" | ||
| 7 | "${NUCULA_SRC}/cashu_json.cpp" | ||
| 8 | "${NUCULA_SRC}/nut10.cpp" | ||
| 9 | "${NUCULA_SRC}/hex.c" | ||
| 10 | "${NUCULA_SRC}/http.c" | ||
| 11 | INCLUDE_DIRS "." | ||
| 12 | "${NUCULA_SRC}" | ||
| 13 | REQUIRES secp256k1 | ||
| 14 | PRIV_REQUIRES log mbedtls nvs_flash esp_http_client json | ||
| 15 | ) | ||
| 16 | |||
| 17 | target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-unused-function) | ||
diff --git a/components/nucula_lib/nucula_wallet.cpp b/components/nucula_lib/nucula_wallet.cpp new file mode 100644 index 0000000..50583f9 --- /dev/null +++ b/components/nucula_lib/nucula_wallet.cpp | |||
| @@ -0,0 +1,199 @@ | |||
| 1 | #include "nucula_wallet.h" | ||
| 2 | #include "wallet.hpp" | ||
| 3 | #include "cashu_json.hpp" | ||
| 4 | #include "crypto.h" | ||
| 5 | #include "hex.h" | ||
| 6 | #include "esp_log.h" | ||
| 7 | #include "secp256k1.h" | ||
| 8 | #include "cJSON.h" | ||
| 9 | #include <cstring> | ||
| 10 | #include <string> | ||
| 11 | #include <vector> | ||
| 12 | |||
| 13 | static const char *TAG = "nucula_wallet"; | ||
| 14 | |||
| 15 | static secp256k1_context *s_ctx = nullptr; | ||
| 16 | static cashu::Wallet *s_wallet = nullptr; | ||
| 17 | |||
| 18 | static std::vector<cashu::Proof> &mutable_proofs() | ||
| 19 | { | ||
| 20 | return const_cast<std::vector<cashu::Proof> &>(s_wallet->proofs()); | ||
| 21 | } | ||
| 22 | |||
| 23 | esp_err_t nucula_wallet_init(const char *mint_url) | ||
| 24 | { | ||
| 25 | if (s_wallet) return ESP_OK; | ||
| 26 | |||
| 27 | s_ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY); | ||
| 28 | if (!s_ctx) { | ||
| 29 | ESP_LOGE(TAG, "Failed to create secp256k1 context"); | ||
| 30 | return ESP_FAIL; | ||
| 31 | } | ||
| 32 | |||
| 33 | s_wallet = new cashu::Wallet(std::string(mint_url), s_ctx, 0); | ||
| 34 | if (!s_wallet) { | ||
| 35 | ESP_LOGE(TAG, "Failed to create wallet"); | ||
| 36 | secp256k1_context_destroy(s_ctx); | ||
| 37 | s_ctx = nullptr; | ||
| 38 | return ESP_FAIL; | ||
| 39 | } | ||
| 40 | |||
| 41 | s_wallet->load_from_nvs(); | ||
| 42 | |||
| 43 | if (!s_wallet->load_keysets()) { | ||
| 44 | ESP_LOGW(TAG, "Keyset load failed (may be offline)"); | ||
| 45 | } | ||
| 46 | |||
| 47 | ESP_LOGI(TAG, "Wallet initialized: balance=%d proofs=%d keysets=%d", | ||
| 48 | s_wallet->balance(), (int)s_wallet->proofs().size(), | ||
| 49 | (int)s_wallet->keysets().size()); | ||
| 50 | return ESP_OK; | ||
| 51 | } | ||
| 52 | |||
| 53 | esp_err_t nucula_wallet_receive(const char *token_str) | ||
| 54 | { | ||
| 55 | if (!s_wallet || !token_str) return ESP_FAIL; | ||
| 56 | |||
| 57 | cashu::Token tok; | ||
| 58 | bool decoded = false; | ||
| 59 | |||
| 60 | if (strncmp(token_str, "cashuA", 6) == 0) { | ||
| 61 | decoded = cashu::deserialize_token_v3(token_str, tok); | ||
| 62 | } | ||
| 63 | |||
| 64 | if (!decoded) { | ||
| 65 | ESP_LOGE(TAG, "Failed to decode token"); | ||
| 66 | return ESP_FAIL; | ||
| 67 | } | ||
| 68 | |||
| 69 | std::vector<cashu::Proof> proofs_out; | ||
| 70 | if (!s_wallet->receive(tok, proofs_out)) { | ||
| 71 | ESP_LOGE(TAG, "Receive failed"); | ||
| 72 | return ESP_FAIL; | ||
| 73 | } | ||
| 74 | |||
| 75 | int total = 0; | ||
| 76 | for (const auto &p : proofs_out) total += p.amount; | ||
| 77 | ESP_LOGI(TAG, "Received %d sat (%d proofs), new balance=%d", | ||
| 78 | total, (int)proofs_out.size(), s_wallet->balance()); | ||
| 79 | return ESP_OK; | ||
| 80 | } | ||
| 81 | |||
| 82 | esp_err_t nucula_wallet_send(uint64_t amount_sat, char *token_out, size_t token_out_size) | ||
| 83 | { | ||
| 84 | if (!s_wallet) return ESP_FAIL; | ||
| 85 | |||
| 86 | int amount = (int)amount_sat; | ||
| 87 | std::vector<cashu::Proof> selected, remaining; | ||
| 88 | if (!s_wallet->select_proofs(amount, selected, remaining)) { | ||
| 89 | ESP_LOGE(TAG, "Insufficient balance for %d sat", amount); | ||
| 90 | return ESP_FAIL; | ||
| 91 | } | ||
| 92 | |||
| 93 | std::vector<cashu::Proof> new_proofs, change; | ||
| 94 | if (!s_wallet->swap(selected, (int)amount_sat, new_proofs, change)) { | ||
| 95 | ESP_LOGE(TAG, "Swap for send failed"); | ||
| 96 | return ESP_FAIL; | ||
| 97 | } | ||
| 98 | |||
| 99 | cashu::Token token; | ||
| 100 | token.mint = s_wallet->mint_url(); | ||
| 101 | token.unit = "sat"; | ||
| 102 | for (auto &p : new_proofs) token.proofs.push_back(p); | ||
| 103 | |||
| 104 | std::string encoded = cashu::serialize_token_v3(token); | ||
| 105 | if (encoded.empty()) { | ||
| 106 | ESP_LOGE(TAG, "Token serialization failed"); | ||
| 107 | return ESP_FAIL; | ||
| 108 | } | ||
| 109 | |||
| 110 | if (encoded.size() >= token_out_size) { | ||
| 111 | ESP_LOGE(TAG, "Token too large: %zu >= %zu", encoded.size(), token_out_size); | ||
| 112 | return ESP_FAIL; | ||
| 113 | } | ||
| 114 | |||
| 115 | memcpy(token_out, encoded.c_str(), encoded.size() + 1); | ||
| 116 | |||
| 117 | auto &proofs = mutable_proofs(); | ||
| 118 | proofs = remaining; | ||
| 119 | for (auto &p : change) proofs.push_back(p); | ||
| 120 | s_wallet->save_proofs(); | ||
| 121 | |||
| 122 | ESP_LOGI(TAG, "Sent %llu sat, token=%zu bytes, remaining balance=%d", | ||
| 123 | (unsigned long long)amount_sat, encoded.size(), s_wallet->balance()); | ||
| 124 | return ESP_OK; | ||
| 125 | } | ||
| 126 | |||
| 127 | uint64_t nucula_wallet_balance(void) | ||
| 128 | { | ||
| 129 | if (!s_wallet) return 0; | ||
| 130 | return (uint64_t)s_wallet->balance(); | ||
| 131 | } | ||
| 132 | |||
| 133 | int nucula_wallet_proof_count(void) | ||
| 134 | { | ||
| 135 | if (!s_wallet) return 0; | ||
| 136 | return (int)s_wallet->proofs().size(); | ||
| 137 | } | ||
| 138 | |||
| 139 | char *nucula_wallet_proofs_json(void) | ||
| 140 | { | ||
| 141 | if (!s_wallet) return nullptr; | ||
| 142 | |||
| 143 | const auto &proofs = s_wallet->proofs(); | ||
| 144 | cJSON *arr = cJSON_CreateArray(); | ||
| 145 | for (const auto &p : proofs) { | ||
| 146 | cJSON *obj = cJSON_CreateObject(); | ||
| 147 | cJSON_AddNumberToObject(obj, "amount", p.amount); | ||
| 148 | cJSON_AddStringToObject(obj, "id", p.id.c_str()); | ||
| 149 | cJSON_AddItemToArray(arr, obj); | ||
| 150 | } | ||
| 151 | char *json = cJSON_PrintUnformatted(arr); | ||
| 152 | cJSON_Delete(arr); | ||
| 153 | return json; | ||
| 154 | } | ||
| 155 | |||
| 156 | esp_err_t nucula_wallet_swap_all(void) | ||
| 157 | { | ||
| 158 | if (!s_wallet) return ESP_FAIL; | ||
| 159 | |||
| 160 | auto &proofs = mutable_proofs(); | ||
| 161 | if (proofs.empty()) { | ||
| 162 | ESP_LOGW(TAG, "No proofs to swap"); | ||
| 163 | return ESP_FAIL; | ||
| 164 | } | ||
| 165 | |||
| 166 | int old_balance = s_wallet->balance(); | ||
| 167 | |||
| 168 | std::vector<cashu::Proof> inputs = proofs; | ||
| 169 | std::vector<cashu::Proof> new_proofs, change; | ||
| 170 | if (!s_wallet->swap(inputs, -1, new_proofs, change)) { | ||
| 171 | ESP_LOGE(TAG, "Swap failed"); | ||
| 172 | return ESP_FAIL; | ||
| 173 | } | ||
| 174 | |||
| 175 | proofs.clear(); | ||
| 176 | for (auto &p : new_proofs) proofs.push_back(p); | ||
| 177 | for (auto &p : change) proofs.push_back(p); | ||
| 178 | s_wallet->save_proofs(); | ||
| 179 | |||
| 180 | ESP_LOGI(TAG, "Swap complete: %d -> %d sat (%d proofs)", | ||
| 181 | old_balance, s_wallet->balance(), (int)proofs.size()); | ||
| 182 | return ESP_OK; | ||
| 183 | } | ||
| 184 | |||
| 185 | void nucula_wallet_print_status(void) | ||
| 186 | { | ||
| 187 | if (!s_wallet) { | ||
| 188 | ESP_LOGI(TAG, "Wallet not initialized"); | ||
| 189 | return; | ||
| 190 | } | ||
| 191 | ESP_LOGI(TAG, "Wallet: balance=%d proofs=%d keysets=%d", | ||
| 192 | s_wallet->balance(), (int)s_wallet->proofs().size(), | ||
| 193 | (int)s_wallet->keysets().size()); | ||
| 194 | const auto &proofs = s_wallet->proofs(); | ||
| 195 | for (size_t i = 0; i < proofs.size(); i++) { | ||
| 196 | ESP_LOGI(TAG, " [%d] amount=%d id=%s", (int)i, | ||
| 197 | proofs[i].amount, proofs[i].id.c_str()); | ||
| 198 | } | ||
| 199 | } | ||
diff --git a/components/nucula_lib/nucula_wallet.h b/components/nucula_lib/nucula_wallet.h new file mode 100644 index 0000000..64b7c24 --- /dev/null +++ b/components/nucula_lib/nucula_wallet.h | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | #ifndef NUCULA_WALLET_H | ||
| 2 | #define NUCULA_WALLET_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | |||
| 7 | #ifdef __cplusplus | ||
| 8 | extern "C" { | ||
| 9 | #endif | ||
| 10 | |||
| 11 | esp_err_t nucula_wallet_init(const char *mint_url); | ||
| 12 | |||
| 13 | esp_err_t nucula_wallet_receive(const char *token_str); | ||
| 14 | |||
| 15 | esp_err_t nucula_wallet_send(uint64_t amount_sat, char *token_out, size_t token_out_size); | ||
| 16 | |||
| 17 | uint64_t nucula_wallet_balance(void); | ||
| 18 | |||
| 19 | int nucula_wallet_proof_count(void); | ||
| 20 | |||
| 21 | char *nucula_wallet_proofs_json(void); | ||
| 22 | |||
| 23 | esp_err_t nucula_wallet_swap_all(void); | ||
| 24 | |||
| 25 | void nucula_wallet_print_status(void); | ||
| 26 | |||
| 27 | #ifdef __cplusplus | ||
| 28 | } | ||
| 29 | #endif | ||
| 30 | |||
| 31 | #endif | ||
diff --git a/components/secp256k1 b/components/secp256k1 new file mode 120000 index 0000000..187b270 --- /dev/null +++ b/components/secp256k1 | |||
| @@ -0,0 +1 @@ | |||
| /home/c03rad0r/esp32-tollgate/nucula_src/components/secp256k1 \ No newline at end of file | |||
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 2eef030..df69283 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt | |||
| @@ -6,9 +6,12 @@ idf_component_register(SRCS "tollgate_main.c" | |||
| 6 | "cashu.c" | 6 | "cashu.c" |
| 7 | "session.c" | 7 | "session.c" |
| 8 | "tollgate_api.c" | 8 | "tollgate_api.c" |
| 9 | "wallet.c" | 9 | "identity.c" |
| 10 | "wallet_persist.c" | 10 | "nostr_event.c" |
| 11 | INCLUDE_DIRS "." "${IDF_PATH}/components/lwip/include/apps" | 11 | "geohash.c" |
| 12 | "wifistr.c" | ||
| 13 | INCLUDE_DIRS "." | ||
| 12 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server | 14 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server |
| 13 | lwip json esp_http_client mbedtls esp-tls log spiffs | 15 | lwip json esp_http_client mbedtls esp-tls log spiffs |
| 16 | nucula_lib secp256k1 | ||
| 14 | PRIV_REQUIRES esp-tls) | 17 | PRIV_REQUIRES esp-tls) |
diff --git a/main/config.c b/main/config.c index 7e8a14c..47d631f 100644 --- a/main/config.c +++ b/main/config.c | |||
| @@ -1,4 +1,5 @@ | |||
| 1 | #include "config.h" | 1 | #include "config.h" |
| 2 | #include "identity.h" | ||
| 2 | #include "esp_log.h" | 3 | #include "esp_log.h" |
| 3 | #include "esp_spiffs.h" | 4 | #include "esp_spiffs.h" |
| 4 | #include "esp_system.h" | 5 | #include "esp_system.h" |
| @@ -20,6 +21,7 @@ esp_err_t tollgate_config_init(void) | |||
| 20 | g_config.price_per_step = 21; | 21 | g_config.price_per_step = 21; |
| 21 | g_config.step_size_ms = 60000; | 22 | g_config.step_size_ms = 60000; |
| 22 | g_config.persist_threshold_sats = 1; | 23 | g_config.persist_threshold_sats = 1; |
| 24 | g_config.nostr_publish_interval_s = 21600; | ||
| 23 | 25 | ||
| 24 | esp_vfs_spiffs_conf_t conf = { | 26 | esp_vfs_spiffs_conf_t conf = { |
| 25 | .base_path = "/spiffs", | 27 | .base_path = "/spiffs", |
| @@ -37,16 +39,17 @@ esp_err_t tollgate_config_init(void) | |||
| 37 | if (!f) { | 39 | if (!f) { |
| 38 | ESP_LOGW(TAG, "No config.json found, generating default"); | 40 | ESP_LOGW(TAG, "No config.json found, generating default"); |
| 39 | const char *default_json = "{" | 41 | const char *default_json = "{" |
| 42 | "\"nsec\":\"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\"," | ||
| 40 | "\"wifi_networks\":[" | 43 | "\"wifi_networks\":[" |
| 41 | "{\"ssid\":\"EnterSSID-2.4GHz\",\"password\":\"c03rad0r123!\"}" | 44 | "{\"ssid\":\"EnterSSID-2.4GHz\",\"password\":\"c03rad0r123!\"}" |
| 42 | "]," | 45 | "]," |
| 43 | "\"ap_ssid\":\"TollGate\"," | ||
| 44 | "\"ap_password\":\"\"," | 46 | "\"ap_password\":\"\"," |
| 45 | "\"ap_channel\":1," | ||
| 46 | "\"mint_url\":\"https://testnut.cashu.space\"," | 47 | "\"mint_url\":\"https://testnut.cashu.space\"," |
| 47 | "\"lnurl_url\":\"https://redeem.cashu.me/.well-known/lnurlp/tollgate\"," | ||
| 48 | "\"price_per_step\":21," | 48 | "\"price_per_step\":21," |
| 49 | "\"step_size_ms\":60000" | 49 | "\"step_size_ms\":60000," |
| 50 | "\"nostr_geohash\":\"u281w0dfz\"," | ||
| 51 | "\"nostr_relays\":[\"wss://relay.damus.io\",\"wss://nos.lol\"]," | ||
| 52 | "\"nostr_publish_interval_s\":21600" | ||
| 50 | "}"; | 53 | "}"; |
| 51 | f = fopen("/spiffs/config.json", "w"); | 54 | f = fopen("/spiffs/config.json", "w"); |
| 52 | if (f) { | 55 | if (f) { |
| @@ -80,6 +83,15 @@ esp_err_t tollgate_config_init(void) | |||
| 80 | return ESP_FAIL; | 83 | return ESP_FAIL; |
| 81 | } | 84 | } |
| 82 | 85 | ||
| 86 | cJSON *nsec = cJSON_GetObjectItem(root, "nsec"); | ||
| 87 | if (nsec && cJSON_IsString(nsec)) { | ||
| 88 | strncpy(g_config.nsec, nsec->valuestring, sizeof(g_config.nsec) - 1); | ||
| 89 | } else { | ||
| 90 | ESP_LOGE(TAG, "Missing 'nsec' in config.json"); | ||
| 91 | cJSON_Delete(root); | ||
| 92 | return ESP_FAIL; | ||
| 93 | } | ||
| 94 | |||
| 83 | cJSON *networks = cJSON_GetObjectItem(root, "wifi_networks"); | 95 | cJSON *networks = cJSON_GetObjectItem(root, "wifi_networks"); |
| 84 | if (networks && cJSON_IsArray(networks)) { | 96 | if (networks && cJSON_IsArray(networks)) { |
| 85 | int count = cJSON_GetArraySize(networks); | 97 | int count = cJSON_GetArraySize(networks); |
| @@ -96,16 +108,9 @@ esp_err_t tollgate_config_init(void) | |||
| 96 | } | 108 | } |
| 97 | } | 109 | } |
| 98 | 110 | ||
| 99 | cJSON *ap_ssid = cJSON_GetObjectItem(root, "ap_ssid"); | ||
| 100 | if (ap_ssid) strncpy(g_config.ap_ssid, ap_ssid->valuestring, sizeof(g_config.ap_ssid) - 1); | ||
| 101 | else strncpy(g_config.ap_ssid, "TollGate", sizeof(g_config.ap_ssid) - 1); | ||
| 102 | |||
| 103 | cJSON *ap_pass = cJSON_GetObjectItem(root, "ap_password"); | 111 | cJSON *ap_pass = cJSON_GetObjectItem(root, "ap_password"); |
| 104 | if (ap_pass) strncpy(g_config.ap_password, ap_pass->valuestring, sizeof(g_config.ap_password) - 1); | 112 | if (ap_pass) strncpy(g_config.ap_password, ap_pass->valuestring, sizeof(g_config.ap_password) - 1); |
| 105 | 113 | ||
| 106 | cJSON *ap_ch = cJSON_GetObjectItem(root, "ap_channel"); | ||
| 107 | if (ap_ch) g_config.ap_channel = ap_ch->valueint; | ||
| 108 | |||
| 109 | cJSON *mint = cJSON_GetObjectItem(root, "mint_url"); | 114 | cJSON *mint = cJSON_GetObjectItem(root, "mint_url"); |
| 110 | if (mint) strncpy(g_config.mint_url, mint->valuestring, sizeof(g_config.mint_url) - 1); | 115 | if (mint) strncpy(g_config.mint_url, mint->valuestring, sizeof(g_config.mint_url) - 1); |
| 111 | 116 | ||
| @@ -121,9 +126,37 @@ esp_err_t tollgate_config_init(void) | |||
| 121 | cJSON *persist = cJSON_GetObjectItem(root, "persist_threshold_sats"); | 126 | cJSON *persist = cJSON_GetObjectItem(root, "persist_threshold_sats"); |
| 122 | if (persist) g_config.persist_threshold_sats = (uint64_t)persist->valuedouble; | 127 | if (persist) g_config.persist_threshold_sats = (uint64_t)persist->valuedouble; |
| 123 | 128 | ||
| 129 | cJSON *geohash = cJSON_GetObjectItem(root, "nostr_geohash"); | ||
| 130 | if (geohash) strncpy(g_config.nostr_geohash, geohash->valuestring, sizeof(g_config.nostr_geohash) - 1); | ||
| 131 | else strncpy(g_config.nostr_geohash, "u281w0dfz", sizeof(g_config.nostr_geohash) - 1); | ||
| 132 | |||
| 133 | cJSON *relays = cJSON_GetObjectItem(root, "nostr_relays"); | ||
| 134 | if (relays && cJSON_IsArray(relays)) { | ||
| 135 | int rcount = cJSON_GetArraySize(relays); | ||
| 136 | if (rcount > TOLLGATE_MAX_RELAYS) rcount = TOLLGATE_MAX_RELAYS; | ||
| 137 | for (int i = 0; i < rcount; i++) { | ||
| 138 | cJSON *r = cJSON_GetArrayItem(relays, i); | ||
| 139 | if (r && cJSON_IsString(r)) { | ||
| 140 | strncpy(g_config.nostr_relays[i], r->valuestring, sizeof(g_config.nostr_relays[i]) - 1); | ||
| 141 | g_config.nostr_relay_count++; | ||
| 142 | } | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | cJSON *pub_interval = cJSON_GetObjectItem(root, "nostr_publish_interval_s"); | ||
| 147 | if (pub_interval) g_config.nostr_publish_interval_s = pub_interval->valueint; | ||
| 148 | |||
| 124 | cJSON_Delete(root); | 149 | cJSON_Delete(root); |
| 125 | ESP_LOGI(TAG, "Config loaded: AP='%s', %d WiFi networks, price=%d sats/%dms", | 150 | |
| 126 | g_config.ap_ssid, g_config.network_count, g_config.price_per_step, g_config.step_size_ms); | 151 | if (g_config.nostr_relay_count == 0) { |
| 152 | strncpy(g_config.nostr_relays[0], "wss://relay.damus.io", sizeof(g_config.nostr_relays[0]) - 1); | ||
| 153 | strncpy(g_config.nostr_relays[1], "wss://nos.lol", sizeof(g_config.nostr_relays[1]) - 1); | ||
| 154 | g_config.nostr_relay_count = 2; | ||
| 155 | } | ||
| 156 | |||
| 157 | ESP_LOGI(TAG, "Config loaded: nsec=%s...%s, %d WiFi networks, price=%d sats/%dms", | ||
| 158 | g_config.nsec, g_config.nsec + 60, g_config.network_count, | ||
| 159 | g_config.price_per_step, g_config.step_size_ms); | ||
| 127 | return ESP_OK; | 160 | return ESP_OK; |
| 128 | } | 161 | } |
| 129 | 162 | ||
| @@ -151,22 +184,23 @@ esp_err_t tollgate_config_get_next_wifi(wifi_config_t *wifi_config) | |||
| 151 | 184 | ||
| 152 | void tollgate_config_derive_unique(tollgate_config_t *cfg) | 185 | void tollgate_config_derive_unique(tollgate_config_t *cfg) |
| 153 | { | 186 | { |
| 154 | if (cfg->unique_derived) return; | 187 | if (cfg->identity_initialized) return; |
| 155 | |||
| 156 | uint8_t mac[6]; | ||
| 157 | esp_read_mac(mac, ESP_MAC_WIFI_STA); | ||
| 158 | 188 | ||
| 159 | snprintf(cfg->ap_ssid + strlen(cfg->ap_ssid), | 189 | const tollgate_identity_t *id = identity_get(); |
| 160 | TOLLGATE_MAX_AP_SSID_LEN - strlen(cfg->ap_ssid), | 190 | if (!id || !id->initialized) { |
| 161 | "-%02X%02X", mac[4], mac[5]); | 191 | ESP_LOGE(TAG, "Cannot derive unique config: identity not initialized"); |
| 192 | return; | ||
| 193 | } | ||
| 162 | 194 | ||
| 163 | uint8_t b5 = mac[4]; | 195 | strncpy(cfg->ap_ssid, id->ap_ssid, sizeof(cfg->ap_ssid) - 1); |
| 164 | uint8_t b6 = mac[5]; | 196 | memcpy(cfg->sta_mac, id->sta_mac, 6); |
| 165 | uint8_t subnet = (b5 ^ b6) % 200 + 10; | 197 | memcpy(cfg->ap_mac, id->ap_mac, 6); |
| 166 | IP4_ADDR(&cfg->ap_ip, 10, b5, subnet, 1); | 198 | cfg->ap_ip = id->ap_ip; |
| 167 | snprintf(cfg->ap_ip_str, sizeof(cfg->ap_ip_str), IPSTR, IP2STR(&cfg->ap_ip)); | 199 | strncpy(cfg->ap_ip_str, id->ap_ip_str, sizeof(cfg->ap_ip_str) - 1); |
| 200 | strncpy(cfg->npub, id->npub_hex, sizeof(cfg->npub) - 1); | ||
| 168 | 201 | ||
| 169 | cfg->unique_derived = true; | 202 | cfg->identity_initialized = true; |
| 170 | 203 | ||
| 171 | ESP_LOGI(TAG, "Unique config: SSID='%s', AP_IP=%s", cfg->ap_ssid, cfg->ap_ip_str); | 204 | ESP_LOGI(TAG, "Unique config derived from nsec: SSID='%s', AP_IP=%s", |
| 205 | cfg->ap_ssid, cfg->ap_ip_str); | ||
| 172 | } | 206 | } |
diff --git a/main/config.h b/main/config.h index 2bcd400..8254a62 100644 --- a/main/config.h +++ b/main/config.h | |||
| @@ -10,6 +10,7 @@ | |||
| 10 | #define TOLLGATE_MAX_MINT_URLS 3 | 10 | #define TOLLGATE_MAX_MINT_URLS 3 |
| 11 | #define TOLLGATE_MAX_AP_SSID_LEN 32 | 11 | #define TOLLGATE_MAX_AP_SSID_LEN 32 |
| 12 | #define TOLLGATE_MAX_AP_PASS_LEN 64 | 12 | #define TOLLGATE_MAX_AP_PASS_LEN 64 |
| 13 | #define TOLLGATE_MAX_RELAYS 4 | ||
| 13 | 14 | ||
| 14 | typedef struct { | 15 | typedef struct { |
| 15 | char ssid[32]; | 16 | char ssid[32]; |
| @@ -22,11 +23,17 @@ typedef struct { | |||
| 22 | int current_network; | 23 | int current_network; |
| 23 | int max_retry; | 24 | int max_retry; |
| 24 | 25 | ||
| 26 | char nsec[65]; | ||
| 27 | char npub[65]; | ||
| 28 | |||
| 25 | char ap_ssid[TOLLGATE_MAX_AP_SSID_LEN]; | 29 | char ap_ssid[TOLLGATE_MAX_AP_SSID_LEN]; |
| 26 | char ap_password[TOLLGATE_MAX_AP_PASS_LEN]; | 30 | char ap_password[TOLLGATE_MAX_AP_PASS_LEN]; |
| 27 | uint8_t ap_channel; | 31 | uint8_t ap_channel; |
| 28 | uint8_t ap_max_conn; | 32 | uint8_t ap_max_conn; |
| 29 | 33 | ||
| 34 | uint8_t sta_mac[6]; | ||
| 35 | uint8_t ap_mac[6]; | ||
| 36 | |||
| 30 | esp_ip4_addr_t ap_ip; | 37 | esp_ip4_addr_t ap_ip; |
| 31 | char ap_ip_str[16]; | 38 | char ap_ip_str[16]; |
| 32 | 39 | ||
| @@ -36,7 +43,12 @@ typedef struct { | |||
| 36 | int step_size_ms; | 43 | int step_size_ms; |
| 37 | uint64_t persist_threshold_sats; | 44 | uint64_t persist_threshold_sats; |
| 38 | 45 | ||
| 39 | bool unique_derived; | 46 | char nostr_geohash[16]; |
| 47 | char nostr_relays[TOLLGATE_MAX_RELAYS][128]; | ||
| 48 | int nostr_relay_count; | ||
| 49 | int nostr_publish_interval_s; | ||
| 50 | |||
| 51 | bool identity_initialized; | ||
| 40 | } tollgate_config_t; | 52 | } tollgate_config_t; |
| 41 | 53 | ||
| 42 | void tollgate_config_derive_unique(tollgate_config_t *cfg); | 54 | void tollgate_config_derive_unique(tollgate_config_t *cfg); |
diff --git a/main/geohash.c b/main/geohash.c new file mode 100644 index 0000000..f649824 --- /dev/null +++ b/main/geohash.c | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | #include "geohash.h" | ||
| 2 | #include <string.h> | ||
| 3 | |||
| 4 | static const char BASE32[] = "0123456789bcdefghjkmnpqrstuvwxyz"; | ||
| 5 | |||
| 6 | void geohash_encode(double lat, double lon, int precision, char *out) | ||
| 7 | { | ||
| 8 | double lat_range[2] = { -90.0, 90.0 }; | ||
| 9 | double lon_range[2] = { -180.0, 180.0 }; | ||
| 10 | uint8_t hash_bytes[16]; | ||
| 11 | int bit_count = precision * 5; | ||
| 12 | int byte_count = (bit_count + 7) / 8; | ||
| 13 | memset(hash_bytes, 0, sizeof(hash_bytes)); | ||
| 14 | |||
| 15 | for (int i = 0; i < bit_count; i++) { | ||
| 16 | int byte_idx = i / 8; | ||
| 17 | int bit_idx = 7 - (i % 8); | ||
| 18 | |||
| 19 | if (i % 2 == 0) { | ||
| 20 | double mid = (lon_range[0] + lon_range[1]) / 2.0; | ||
| 21 | if (lon >= mid) { | ||
| 22 | hash_bytes[byte_idx] |= (1 << bit_idx); | ||
| 23 | lon_range[0] = mid; | ||
| 24 | } else { | ||
| 25 | lon_range[1] = mid; | ||
| 26 | } | ||
| 27 | } else { | ||
| 28 | double mid = (lat_range[0] + lat_range[1]) / 2.0; | ||
| 29 | if (lat >= mid) { | ||
| 30 | hash_bytes[byte_idx] |= (1 << bit_idx); | ||
| 31 | lat_range[0] = mid; | ||
| 32 | } else { | ||
| 33 | lat_range[1] = mid; | ||
| 34 | } | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 38 | for (int i = 0; i < precision; i++) { | ||
| 39 | int byte_idx = (i * 5) / 8; | ||
| 40 | int bit_offset = (i * 5) % 8; | ||
| 41 | uint16_t val = (hash_bytes[byte_idx] << 8); | ||
| 42 | if (byte_idx + 1 < (int)sizeof(hash_bytes)) | ||
| 43 | val |= hash_bytes[byte_idx + 1]; | ||
| 44 | val = (val >> (16 - 5 - bit_offset)) & 0x1F; | ||
| 45 | out[i] = BASE32[val]; | ||
| 46 | } | ||
| 47 | out[precision] = '\0'; | ||
| 48 | } | ||
diff --git a/main/geohash.h b/main/geohash.h new file mode 100644 index 0000000..f8eb69d --- /dev/null +++ b/main/geohash.h | |||
| @@ -0,0 +1,8 @@ | |||
| 1 | #ifndef GEOHASH_H | ||
| 2 | #define GEOHASH_H | ||
| 3 | |||
| 4 | #include <stddef.h> | ||
| 5 | |||
| 6 | void geohash_encode(double lat, double lon, int precision, char *out); | ||
| 7 | |||
| 8 | #endif | ||
diff --git a/main/identity.c b/main/identity.c new file mode 100644 index 0000000..1dab415 --- /dev/null +++ b/main/identity.c | |||
| @@ -0,0 +1,124 @@ | |||
| 1 | #include "identity.h" | ||
| 2 | #include "config.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include "lwip/ip4_addr.h" | ||
| 5 | #include "mbedtls/md.h" | ||
| 6 | #include "secp256k1.h" | ||
| 7 | #include "secp256k1_extrakeys.h" | ||
| 8 | #include <string.h> | ||
| 9 | #include <stdio.h> | ||
| 10 | #include <stdlib.h> | ||
| 11 | |||
| 12 | static const char *TAG = "identity"; | ||
| 13 | static tollgate_identity_t s_identity; | ||
| 14 | |||
| 15 | static int hex_to_bytes(const char *hex, uint8_t *out, size_t out_len) | ||
| 16 | { | ||
| 17 | if (strlen(hex) != out_len * 2) return 0; | ||
| 18 | for (size_t i = 0; i < out_len; i++) { | ||
| 19 | unsigned int byte; | ||
| 20 | if (sscanf(hex + i * 2, "%02x", &byte) != 1) return 0; | ||
| 21 | out[i] = (uint8_t)byte; | ||
| 22 | } | ||
| 23 | return 1; | ||
| 24 | } | ||
| 25 | |||
| 26 | static void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex) | ||
| 27 | { | ||
| 28 | for (size_t i = 0; i < len; i++) | ||
| 29 | sprintf(hex + i * 2, "%02x", bytes[i]); | ||
| 30 | hex[len * 2] = '\0'; | ||
| 31 | } | ||
| 32 | |||
| 33 | static void tollgate_derive(const uint8_t nsec[32], const char *label, | ||
| 34 | uint32_t index, uint8_t *out, size_t out_len) | ||
| 35 | { | ||
| 36 | size_t label_len = strlen(label); | ||
| 37 | size_t msg_len = label_len + 4; | ||
| 38 | uint8_t *msg = (uint8_t *)malloc(msg_len); | ||
| 39 | memcpy(msg, label, label_len); | ||
| 40 | msg[label_len] = (uint8_t)(index & 0xff); | ||
| 41 | msg[label_len + 1] = (uint8_t)((index >> 8) & 0xff); | ||
| 42 | msg[label_len + 2] = (uint8_t)((index >> 16) & 0xff); | ||
| 43 | msg[label_len + 3] = (uint8_t)((index >> 24) & 0xff); | ||
| 44 | |||
| 45 | uint8_t hmac[64]; | ||
| 46 | mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA512), | ||
| 47 | nsec, 32, msg, msg_len, hmac); | ||
| 48 | free(msg); | ||
| 49 | |||
| 50 | memcpy(out, hmac, out_len); | ||
| 51 | } | ||
| 52 | |||
| 53 | esp_err_t identity_init(const char *nsec_hex) | ||
| 54 | { | ||
| 55 | memset(&s_identity, 0, sizeof(s_identity)); | ||
| 56 | |||
| 57 | if (!nsec_hex || strlen(nsec_hex) != 64) { | ||
| 58 | ESP_LOGE(TAG, "Invalid nsec: must be 64 hex chars"); | ||
| 59 | return ESP_ERR_INVALID_ARG; | ||
| 60 | } | ||
| 61 | |||
| 62 | strncpy(s_identity.nsec_hex, nsec_hex, sizeof(s_identity.nsec_hex) - 1); | ||
| 63 | |||
| 64 | if (!hex_to_bytes(nsec_hex, s_identity.nsec, 32)) { | ||
| 65 | ESP_LOGE(TAG, "Failed to parse nsec hex"); | ||
| 66 | return ESP_ERR_INVALID_ARG; | ||
| 67 | } | ||
| 68 | |||
| 69 | secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); | ||
| 70 | if (!ctx) { | ||
| 71 | ESP_LOGE(TAG, "Failed to create secp256k1 context"); | ||
| 72 | return ESP_ERR_NO_MEM; | ||
| 73 | } | ||
| 74 | |||
| 75 | secp256k1_pubkey pubkey; | ||
| 76 | if (!secp256k1_ec_pubkey_create(ctx, &pubkey, s_identity.nsec)) { | ||
| 77 | ESP_LOGE(TAG, "Invalid nsec: secp256k1 key creation failed"); | ||
| 78 | secp256k1_context_destroy(ctx); | ||
| 79 | return ESP_ERR_INVALID_ARG; | ||
| 80 | } | ||
| 81 | |||
| 82 | secp256k1_xonly_pubkey xonly; | ||
| 83 | secp256k1_xonly_pubkey_from_pubkey(ctx, &xonly, NULL, &pubkey); | ||
| 84 | uint8_t npub_bytes[32]; | ||
| 85 | secp256k1_xonly_pubkey_serialize(ctx, npub_bytes, &xonly); | ||
| 86 | bytes_to_hex(npub_bytes, 32, s_identity.npub_hex); | ||
| 87 | |||
| 88 | tollgate_derive(s_identity.nsec, "sta-mac", 0, s_identity.sta_mac, 6); | ||
| 89 | s_identity.sta_mac[0] = (s_identity.sta_mac[0] | 0x02) & 0xFE; | ||
| 90 | |||
| 91 | tollgate_derive(s_identity.nsec, "ap-mac", 0, s_identity.ap_mac, 6); | ||
| 92 | s_identity.ap_mac[0] = (s_identity.ap_mac[0] | 0x02) & 0xFE; | ||
| 93 | |||
| 94 | snprintf(s_identity.ap_ssid, sizeof(s_identity.ap_ssid), | ||
| 95 | "TollGate-%02X%02X%02X", | ||
| 96 | s_identity.ap_mac[3], s_identity.ap_mac[4], s_identity.ap_mac[5]); | ||
| 97 | |||
| 98 | uint8_t b3 = s_identity.ap_mac[3]; | ||
| 99 | uint8_t b4 = s_identity.ap_mac[4]; | ||
| 100 | uint8_t b5 = s_identity.ap_mac[5]; | ||
| 101 | uint8_t subnet = (b4 ^ b5) % 200 + 10; | ||
| 102 | IP4_ADDR(&s_identity.ap_ip, 10, b3, subnet, 1); | ||
| 103 | snprintf(s_identity.ap_ip_str, sizeof(s_identity.ap_ip_str), | ||
| 104 | IPSTR, IP2STR(&s_identity.ap_ip)); | ||
| 105 | |||
| 106 | secp256k1_context_destroy(ctx); | ||
| 107 | s_identity.initialized = true; | ||
| 108 | |||
| 109 | ESP_LOGI(TAG, "Identity: npub=%s", s_identity.npub_hex); | ||
| 110 | ESP_LOGI(TAG, " STA MAC: %02X:%02X:%02X:%02X:%02X:%02X", | ||
| 111 | s_identity.sta_mac[0], s_identity.sta_mac[1], s_identity.sta_mac[2], | ||
| 112 | s_identity.sta_mac[3], s_identity.sta_mac[4], s_identity.sta_mac[5]); | ||
| 113 | ESP_LOGI(TAG, " AP MAC: %02X:%02X:%02X:%02X:%02X:%02X", | ||
| 114 | s_identity.ap_mac[0], s_identity.ap_mac[1], s_identity.ap_mac[2], | ||
| 115 | s_identity.ap_mac[3], s_identity.ap_mac[4], s_identity.ap_mac[5]); | ||
| 116 | ESP_LOGI(TAG, " SSID: %s, AP IP: %s", s_identity.ap_ssid, s_identity.ap_ip_str); | ||
| 117 | |||
| 118 | return ESP_OK; | ||
| 119 | } | ||
| 120 | |||
| 121 | const tollgate_identity_t *identity_get(void) | ||
| 122 | { | ||
| 123 | return &s_identity; | ||
| 124 | } | ||
diff --git a/main/identity.h b/main/identity.h new file mode 100644 index 0000000..2990455 --- /dev/null +++ b/main/identity.h | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | #ifndef IDENTITY_H | ||
| 2 | #define IDENTITY_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include "esp_wifi.h" | ||
| 6 | #include "esp_netif.h" | ||
| 7 | #include <stdint.h> | ||
| 8 | #include <stdbool.h> | ||
| 9 | |||
| 10 | typedef struct { | ||
| 11 | uint8_t nsec[32]; | ||
| 12 | char nsec_hex[65]; | ||
| 13 | char npub_hex[65]; | ||
| 14 | |||
| 15 | uint8_t sta_mac[6]; | ||
| 16 | uint8_t ap_mac[6]; | ||
| 17 | |||
| 18 | char ap_ssid[32]; | ||
| 19 | esp_ip4_addr_t ap_ip; | ||
| 20 | char ap_ip_str[16]; | ||
| 21 | |||
| 22 | bool initialized; | ||
| 23 | } tollgate_identity_t; | ||
| 24 | |||
| 25 | esp_err_t identity_init(const char *nsec_hex); | ||
| 26 | |||
| 27 | const tollgate_identity_t *identity_get(void); | ||
| 28 | |||
| 29 | #endif | ||
diff --git a/main/nostr_event.c b/main/nostr_event.c new file mode 100644 index 0000000..b55c47d --- /dev/null +++ b/main/nostr_event.c | |||
| @@ -0,0 +1,112 @@ | |||
| 1 | #include "nostr_event.h" | ||
| 2 | #include "esp_log.h" | ||
| 3 | #include "esp_err.h" | ||
| 4 | #include "mbedtls/sha256.h" | ||
| 5 | #include "secp256k1.h" | ||
| 6 | #include "secp256k1_extrakeys.h" | ||
| 7 | #include "secp256k1_schnorrsig.h" | ||
| 8 | #include "cJSON.h" | ||
| 9 | #include <string.h> | ||
| 10 | #include <stdio.h> | ||
| 11 | #include <sys/time.h> | ||
| 12 | |||
| 13 | static const char *TAG = "nostr_event"; | ||
| 14 | |||
| 15 | static void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex) | ||
| 16 | { | ||
| 17 | for (size_t i = 0; i < len; i++) | ||
| 18 | sprintf(hex + i * 2, "%02x", bytes[i]); | ||
| 19 | hex[len * 2] = '\0'; | ||
| 20 | } | ||
| 21 | |||
| 22 | esp_err_t nostr_event_init(nostr_event_t *event, const char *npub_hex, | ||
| 23 | int kind, const char *tags_json, const char *content) | ||
| 24 | { | ||
| 25 | memset(event, 0, sizeof(*event)); | ||
| 26 | strncpy(event->pubkey, npub_hex, sizeof(event->pubkey) - 1); | ||
| 27 | event->kind = kind; | ||
| 28 | event->tags_json = tags_json ? tags_json : "[]"; | ||
| 29 | event->content = content ? content : ""; | ||
| 30 | |||
| 31 | struct timeval tv; | ||
| 32 | gettimeofday(&tv, NULL); | ||
| 33 | event->created_at = (uint64_t)tv.tv_sec; | ||
| 34 | |||
| 35 | cJSON *serial = cJSON_CreateArray(); | ||
| 36 | cJSON_AddItemToArray(serial, cJSON_CreateNumber(0)); | ||
| 37 | cJSON_AddItemToArray(serial, cJSON_CreateString(event->pubkey)); | ||
| 38 | cJSON_AddItemToArray(serial, cJSON_CreateNumber((double)event->created_at)); | ||
| 39 | cJSON_AddItemToArray(serial, cJSON_CreateNumber(event->kind)); | ||
| 40 | cJSON_AddItemToArray(serial, cJSON_Parse(event->tags_json)); | ||
| 41 | cJSON_AddItemToArray(serial, cJSON_CreateString(event->content)); | ||
| 42 | |||
| 43 | char *serialized = cJSON_PrintUnformatted(serial); | ||
| 44 | cJSON_Delete(serial); | ||
| 45 | |||
| 46 | uint8_t hash[32]; | ||
| 47 | mbedtls_sha256((const unsigned char *)serialized, strlen(serialized), | ||
| 48 | hash, 0); | ||
| 49 | free(serialized); | ||
| 50 | |||
| 51 | bytes_to_hex(hash, 32, event->id); | ||
| 52 | return ESP_OK; | ||
| 53 | } | ||
| 54 | |||
| 55 | esp_err_t nostr_event_sign(nostr_event_t *event, const uint8_t nsec[32]) | ||
| 56 | { | ||
| 57 | uint8_t msg[32]; | ||
| 58 | for (size_t i = 0; i < 32; i++) { | ||
| 59 | unsigned int byte; | ||
| 60 | sscanf(event->id + i * 2, "%02x", &byte); | ||
| 61 | msg[i] = (uint8_t)byte; | ||
| 62 | } | ||
| 63 | |||
| 64 | secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); | ||
| 65 | if (!ctx) { | ||
| 66 | ESP_LOGE(TAG, "Failed to create secp256k1 context"); | ||
| 67 | return ESP_ERR_NO_MEM; | ||
| 68 | } | ||
| 69 | |||
| 70 | secp256k1_keypair keypair; | ||
| 71 | if (!secp256k1_keypair_create(ctx, &keypair, nsec)) { | ||
| 72 | ESP_LOGE(TAG, "Invalid nsec for signing"); | ||
| 73 | secp256k1_context_destroy(ctx); | ||
| 74 | return ESP_ERR_INVALID_ARG; | ||
| 75 | } | ||
| 76 | |||
| 77 | uint8_t sig[64]; | ||
| 78 | if (!secp256k1_schnorrsig_sign32(ctx, sig, msg, &keypair, NULL)) { | ||
| 79 | ESP_LOGE(TAG, "Schnorr signing failed"); | ||
| 80 | secp256k1_context_destroy(ctx); | ||
| 81 | return ESP_FAIL; | ||
| 82 | } | ||
| 83 | |||
| 84 | bytes_to_hex(sig, 64, event->sig); | ||
| 85 | secp256k1_context_destroy(ctx); | ||
| 86 | return ESP_OK; | ||
| 87 | } | ||
| 88 | |||
| 89 | esp_err_t nostr_event_to_json(const nostr_event_t *event, char *buf, size_t buf_len) | ||
| 90 | { | ||
| 91 | cJSON *root = cJSON_CreateObject(); | ||
| 92 | cJSON_AddStringToObject(root, "id", event->id); | ||
| 93 | cJSON_AddStringToObject(root, "pubkey", event->pubkey); | ||
| 94 | cJSON_AddNumberToObject(root, "created_at", (double)event->created_at); | ||
| 95 | cJSON_AddNumberToObject(root, "kind", event->kind); | ||
| 96 | cJSON_AddItemToObject(root, "tags", cJSON_Parse(event->tags_json)); | ||
| 97 | cJSON_AddStringToObject(root, "content", event->content); | ||
| 98 | cJSON_AddStringToObject(root, "sig", event->sig); | ||
| 99 | |||
| 100 | char *json = cJSON_PrintUnformatted(root); | ||
| 101 | cJSON_Delete(root); | ||
| 102 | |||
| 103 | if (!json) return ESP_FAIL; | ||
| 104 | size_t len = strlen(json); | ||
| 105 | if (len >= buf_len) { | ||
| 106 | free(json); | ||
| 107 | return ESP_ERR_NO_MEM; | ||
| 108 | } | ||
| 109 | memcpy(buf, json, len + 1); | ||
| 110 | free(json); | ||
| 111 | return ESP_OK; | ||
| 112 | } | ||
diff --git a/main/nostr_event.h b/main/nostr_event.h new file mode 100644 index 0000000..ce15900 --- /dev/null +++ b/main/nostr_event.h | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | #ifndef NOSTR_EVENT_H | ||
| 2 | #define NOSTR_EVENT_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stddef.h> | ||
| 7 | |||
| 8 | typedef struct { | ||
| 9 | char pubkey[65]; | ||
| 10 | uint64_t created_at; | ||
| 11 | int kind; | ||
| 12 | const char *tags_json; | ||
| 13 | const char *content; | ||
| 14 | char id[65]; | ||
| 15 | char sig[129]; | ||
| 16 | } nostr_event_t; | ||
| 17 | |||
| 18 | esp_err_t nostr_event_init(nostr_event_t *event, const char *npub_hex, | ||
| 19 | int kind, const char *tags_json, const char *content); | ||
| 20 | |||
| 21 | esp_err_t nostr_event_sign(nostr_event_t *event, const uint8_t nsec[32]); | ||
| 22 | |||
| 23 | esp_err_t nostr_event_to_json(const nostr_event_t *event, char *buf, size_t buf_len); | ||
| 24 | |||
| 25 | #endif | ||
diff --git a/main/tollgate_api.c b/main/tollgate_api.c index e6880e0..72ed726 100644 --- a/main/tollgate_api.c +++ b/main/tollgate_api.c | |||
| @@ -3,7 +3,7 @@ | |||
| 3 | #include "config.h" | 3 | #include "config.h" |
| 4 | #include "session.h" | 4 | #include "session.h" |
| 5 | #include "firewall.h" | 5 | #include "firewall.h" |
| 6 | #include "wallet.h" | 6 | #include "nucula_wallet.h" |
| 7 | #include "esp_log.h" | 7 | #include "esp_log.h" |
| 8 | #include "cJSON.h" | 8 | #include "cJSON.h" |
| 9 | #include "lwip/sockets.h" | 9 | #include "lwip/sockets.h" |
| @@ -194,6 +194,7 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 194 | return ESP_OK; | 194 | return ESP_OK; |
| 195 | } | 195 | } |
| 196 | esp_err_t err = cashu_decode_token(body, token); | 196 | esp_err_t err = cashu_decode_token(body, token); |
| 197 | char *body_copy = strdup(body); | ||
| 197 | free(body); | 198 | free(body); |
| 198 | 199 | ||
| 199 | if (err != ESP_OK) { | 200 | if (err != ESP_OK) { |
| @@ -319,17 +320,7 @@ static esp_err_t api_post_payment(httpd_req_t *req) | |||
| 319 | cJSON_free(json); | 320 | cJSON_free(json); |
| 320 | cJSON_Delete(session_event); | 321 | cJSON_Delete(session_event); |
| 321 | 322 | ||
| 322 | { | 323 | nucula_wallet_receive(body_copy); |
| 323 | wallet_proof_t wproofs[CASHU_MAX_PROOFS]; | ||
| 324 | int wcount = token->proof_count > CASHU_MAX_PROOFS ? CASHU_MAX_PROOFS : token->proof_count; | ||
| 325 | for (int i = 0; i < wcount; i++) { | ||
| 326 | wproofs[i].amount = token->proofs[i].amount; | ||
| 327 | strncpy(wproofs[i].id, token->proofs[i].id, WALLET_KEYSET_ID_LEN - 1); | ||
| 328 | strncpy(wproofs[i].secret, token->proofs[i].secret, WALLET_SECRET_LEN - 1); | ||
| 329 | strncpy(wproofs[i].c, token->proofs[i].c, WALLET_SIG_LEN - 1); | ||
| 330 | } | ||
| 331 | wallet_add_proofs(wproofs, wcount); | ||
| 332 | } | ||
| 333 | 324 | ||
| 334 | free(states); | 325 | free(states); |
| 335 | free(token); | 326 | free(token); |
| @@ -381,20 +372,18 @@ static esp_err_t api_get_whoami(httpd_req_t *req) | |||
| 381 | 372 | ||
| 382 | static esp_err_t api_get_wallet(httpd_req_t *req) | 373 | static esp_err_t api_get_wallet(httpd_req_t *req) |
| 383 | { | 374 | { |
| 384 | wallet_t *w = wallet_get(); | ||
| 385 | cJSON *root = cJSON_CreateObject(); | 375 | cJSON *root = cJSON_CreateObject(); |
| 386 | cJSON_AddNumberToObject(root, "balance", (double)w->balance); | 376 | cJSON_AddNumberToObject(root, "balance", (double)nucula_wallet_balance()); |
| 387 | cJSON_AddNumberToObject(root, "proof_count", w->proof_count); | 377 | cJSON_AddNumberToObject(root, "proof_count", nucula_wallet_proof_count()); |
| 388 | cJSON_AddNumberToObject(root, "keyset_count", w->keyset_count); | 378 | |
| 389 | 379 | char *proofs_json = nucula_wallet_proofs_json(); | |
| 390 | cJSON *proofs = cJSON_CreateArray(); | 380 | if (proofs_json) { |
| 391 | for (int i = 0; i < w->proof_count; i++) { | 381 | cJSON *proofs = cJSON_Parse(proofs_json); |
| 392 | cJSON *p = cJSON_CreateObject(); | 382 | free(proofs_json); |
| 393 | cJSON_AddNumberToObject(p, "amount", (double)w->proofs[i].amount); | 383 | cJSON_AddItemToObject(root, "proofs", proofs); |
| 394 | cJSON_AddStringToObject(p, "id", w->proofs[i].id); | 384 | } else { |
| 395 | cJSON_AddItemToArray(proofs, p); | 385 | cJSON_AddItemToObject(root, "proofs", cJSON_CreateArray()); |
| 396 | } | 386 | } |
| 397 | cJSON_AddItemToObject(root, "proofs", proofs); | ||
| 398 | 387 | ||
| 399 | char *json = cJSON_PrintUnformatted(root); | 388 | char *json = cJSON_PrintUnformatted(root); |
| 400 | httpd_resp_set_type(req, "application/json"); | 389 | httpd_resp_set_type(req, "application/json"); |
| @@ -406,27 +395,16 @@ static esp_err_t api_get_wallet(httpd_req_t *req) | |||
| 406 | 395 | ||
| 407 | static esp_err_t api_post_wallet_swap(httpd_req_t *req) | 396 | static esp_err_t api_post_wallet_swap(httpd_req_t *req) |
| 408 | { | 397 | { |
| 409 | const tollgate_config_t *cfg = tollgate_config_get(); | 398 | if (nucula_wallet_balance() == 0) { |
| 410 | |||
| 411 | if (wallet_balance() == 0) { | ||
| 412 | httpd_resp_set_status(req, "400 Bad Request"); | 399 | httpd_resp_set_status(req, "400 Bad Request"); |
| 413 | httpd_resp_set_type(req, "application/json"); | 400 | httpd_resp_set_type(req, "application/json"); |
| 414 | httpd_resp_send(req, "{\"error\":\"no proofs to swap\"}", 27); | 401 | httpd_resp_send(req, "{\"error\":\"no proofs to swap\"}", 27); |
| 415 | return ESP_OK; | 402 | return ESP_OK; |
| 416 | } | 403 | } |
| 417 | 404 | ||
| 418 | wallet_print_status(); | 405 | nucula_wallet_print_status(); |
| 419 | |||
| 420 | esp_err_t err = wallet_fetch_keysets(cfg->mint_url); | ||
| 421 | if (err != ESP_OK) { | ||
| 422 | httpd_resp_set_status(req, "502 Bad Gateway"); | ||
| 423 | httpd_resp_set_type(req, "application/json"); | ||
| 424 | httpd_resp_send(req, "{\"error\":\"keyset fetch failed\"}", 29); | ||
| 425 | return ESP_OK; | ||
| 426 | } | ||
| 427 | 406 | ||
| 428 | wallet_t *w = wallet_get(); | 407 | esp_err_t err = nucula_wallet_swap_all(); |
| 429 | err = wallet_swap_proofs(cfg->mint_url, 0, w->proof_count); | ||
| 430 | if (err != ESP_OK) { | 408 | if (err != ESP_OK) { |
| 431 | httpd_resp_set_status(req, "502 Bad Gateway"); | 409 | httpd_resp_set_status(req, "502 Bad Gateway"); |
| 432 | httpd_resp_set_type(req, "application/json"); | 410 | httpd_resp_set_type(req, "application/json"); |
| @@ -434,11 +412,11 @@ static esp_err_t api_post_wallet_swap(httpd_req_t *req) | |||
| 434 | return ESP_OK; | 412 | return ESP_OK; |
| 435 | } | 413 | } |
| 436 | 414 | ||
| 437 | wallet_print_status(); | 415 | nucula_wallet_print_status(); |
| 438 | 416 | ||
| 439 | cJSON *root = cJSON_CreateObject(); | 417 | cJSON *root = cJSON_CreateObject(); |
| 440 | cJSON_AddNumberToObject(root, "balance", (double)wallet_balance()); | 418 | cJSON_AddNumberToObject(root, "balance", (double)nucula_wallet_balance()); |
| 441 | cJSON_AddNumberToObject(root, "proof_count", wallet_get()->proof_count); | 419 | cJSON_AddNumberToObject(root, "proof_count", nucula_wallet_proof_count()); |
| 442 | char *json = cJSON_PrintUnformatted(root); | 420 | char *json = cJSON_PrintUnformatted(root); |
| 443 | httpd_resp_set_type(req, "application/json"); | 421 | httpd_resp_set_type(req, "application/json"); |
| 444 | httpd_resp_send(req, json, strlen(json)); | 422 | httpd_resp_send(req, json, strlen(json)); |
| @@ -472,9 +450,8 @@ static esp_err_t api_post_wallet_send(httpd_req_t *req) | |||
| 472 | return ESP_OK; | 450 | return ESP_OK; |
| 473 | } | 451 | } |
| 474 | 452 | ||
| 475 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 476 | char token[4096]; | 453 | char token[4096]; |
| 477 | esp_err_t err = wallet_send(cfg->mint_url, amount, token, sizeof(token)); | 454 | esp_err_t err = nucula_wallet_send(amount, token, sizeof(token)); |
| 478 | if (err != ESP_OK) { | 455 | if (err != ESP_OK) { |
| 479 | httpd_resp_set_status(req, "402 Payment Required"); | 456 | httpd_resp_set_status(req, "402 Payment Required"); |
| 480 | httpd_resp_set_type(req, "text/plain"); | 457 | httpd_resp_set_type(req, "text/plain"); |
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index d4b29bc..7fa1be1 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c | |||
| @@ -11,12 +11,14 @@ | |||
| 11 | #include "lwip/dns.h" | 11 | #include "lwip/dns.h" |
| 12 | #include "dhcpserver/dhcpserver.h" | 12 | #include "dhcpserver/dhcpserver.h" |
| 13 | #include "config.h" | 13 | #include "config.h" |
| 14 | #include "identity.h" | ||
| 14 | #include "dns_server.h" | 15 | #include "dns_server.h" |
| 15 | #include "captive_portal.h" | 16 | #include "captive_portal.h" |
| 16 | #include "firewall.h" | 17 | #include "firewall.h" |
| 17 | #include "session.h" | 18 | #include "session.h" |
| 18 | #include "tollgate_api.h" | 19 | #include "tollgate_api.h" |
| 19 | #include "wallet.h" | 20 | #include "nucula_wallet.h" |
| 21 | #include "wifistr.h" | ||
| 20 | 22 | ||
| 21 | #define MAX_STA_RETRY 5 | 23 | #define MAX_STA_RETRY 5 |
| 22 | static const char *TAG = "tollgate_main"; | 24 | static const char *TAG = "tollgate_main"; |
| @@ -92,8 +94,16 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base, | |||
| 92 | static void wallet_init_task(void *pvParameters) | 94 | static void wallet_init_task(void *pvParameters) |
| 93 | { | 95 | { |
| 94 | const tollgate_config_t *cfg = tollgate_config_get(); | 96 | const tollgate_config_t *cfg = tollgate_config_get(); |
| 95 | wallet_init(); | 97 | nucula_wallet_init(cfg->mint_url); |
| 96 | wallet_fetch_keysets(cfg->mint_url); | 98 | vTaskDelete(NULL); |
| 99 | } | ||
| 100 | |||
| 101 | static void publish_wifistr_task(void *pvParameters) | ||
| 102 | { | ||
| 103 | vTaskDelay(pdMS_TO_TICKS(5000)); | ||
| 104 | wifistr_publish(); | ||
| 105 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 106 | wifistr_start_periodic(cfg->nostr_publish_interval_s); | ||
| 97 | vTaskDelete(NULL); | 107 | vTaskDelete(NULL); |
| 98 | } | 108 | } |
| 99 | 109 | ||
| @@ -123,6 +133,8 @@ static void start_services(void) | |||
| 123 | captive_portal_start(cfg->ap_ip_str); | 133 | captive_portal_start(cfg->ap_ip_str); |
| 124 | tollgate_api_start(); | 134 | tollgate_api_start(); |
| 125 | 135 | ||
| 136 | xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); | ||
| 137 | |||
| 126 | s_services_running = true; | 138 | s_services_running = true; |
| 127 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); | 139 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); |
| 128 | ESP_LOGI(TAG, "=== TollGate services started ==="); | 140 | ESP_LOGI(TAG, "=== TollGate services started ==="); |
| @@ -214,7 +226,11 @@ void app_main(void) | |||
| 214 | ESP_ERROR_CHECK(ret); | 226 | ESP_ERROR_CHECK(ret); |
| 215 | 227 | ||
| 216 | ESP_ERROR_CHECK(tollgate_config_init()); | 228 | ESP_ERROR_CHECK(tollgate_config_init()); |
| 229 | |||
| 230 | ESP_ERROR_CHECK(identity_init(tollgate_config_get()->nsec)); | ||
| 231 | |||
| 217 | tollgate_config_derive_unique((tollgate_config_t *)tollgate_config_get()); | 232 | tollgate_config_derive_unique((tollgate_config_t *)tollgate_config_get()); |
| 233 | |||
| 218 | ESP_ERROR_CHECK(esp_netif_init()); | 234 | ESP_ERROR_CHECK(esp_netif_init()); |
| 219 | ESP_ERROR_CHECK(esp_event_loop_create_default()); | 235 | ESP_ERROR_CHECK(esp_event_loop_create_default()); |
| 220 | 236 | ||
| @@ -227,6 +243,11 @@ void app_main(void) | |||
| 227 | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); | 243 | wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); |
| 228 | ESP_ERROR_CHECK(esp_wifi_init(&cfg)); | 244 | ESP_ERROR_CHECK(esp_wifi_init(&cfg)); |
| 229 | 245 | ||
| 246 | const tollgate_config_t *tcfg = tollgate_config_get(); | ||
| 247 | ESP_ERROR_CHECK(esp_wifi_set_mac(WIFI_IF_STA, tcfg->sta_mac)); | ||
| 248 | ESP_ERROR_CHECK(esp_wifi_set_mac(WIFI_IF_AP, tcfg->ap_mac)); | ||
| 249 | ESP_LOGI(TAG, "MACs set from identity"); | ||
| 250 | |||
| 230 | ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, | 251 | ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, |
| 231 | &wifi_event_handler, NULL, NULL)); | 252 | &wifi_event_handler, NULL, NULL)); |
| 232 | ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, | 253 | ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, |
| @@ -241,8 +262,8 @@ void app_main(void) | |||
| 241 | wifi_config_t sta_config; | 262 | wifi_config_t sta_config; |
| 242 | if (tollgate_config_get_wifi(&sta_config) == ESP_OK) { | 263 | if (tollgate_config_get_wifi(&sta_config) == ESP_OK) { |
| 243 | ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); | 264 | ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); |
| 244 | const tollgate_config_t *tcfg = tollgate_config_get(); | 265 | const tollgate_config_t *tcfg2 = tollgate_config_get(); |
| 245 | ESP_LOGI(TAG, "STA configured for SSID: %s", tcfg->networks[tcfg->current_network].ssid); | 266 | ESP_LOGI(TAG, "STA configured for SSID: %s", tcfg2->networks[tcfg2->current_network].ssid); |
| 246 | } | 267 | } |
| 247 | 268 | ||
| 248 | ESP_ERROR_CHECK(esp_wifi_start()); | 269 | ESP_ERROR_CHECK(esp_wifi_start()); |
diff --git a/main/wallet.c b/main/wallet.c deleted file mode 100644 index 3f65220..0000000 --- a/main/wallet.c +++ /dev/null | |||
| @@ -1,639 +0,0 @@ | |||
| 1 | #include "wallet.h" | ||
| 2 | #include "wallet_persist.h" | ||
| 3 | #include "config.h" | ||
| 4 | #include "esp_log.h" | ||
| 5 | #include "esp_random.h" | ||
| 6 | #include "esp_http_client.h" | ||
| 7 | #include "esp_crt_bundle.h" | ||
| 8 | #include "cJSON.h" | ||
| 9 | #include "mbedtls/ecp.h" | ||
| 10 | #include "mbedtls/bignum.h" | ||
| 11 | #include "mbedtls/sha256.h" | ||
| 12 | #include "mbedtls/base64.h" | ||
| 13 | #include "freertos/FreeRTOS.h" | ||
| 14 | #include "freertos/task.h" | ||
| 15 | #include "freertos/semphr.h" | ||
| 16 | #include "esp_heap_caps.h" | ||
| 17 | #include <string.h> | ||
| 18 | #include <stdio.h> | ||
| 19 | |||
| 20 | static const char *TAG = "wallet"; | ||
| 21 | static wallet_t s_wallet; | ||
| 22 | |||
| 23 | static const char DOMAIN_SEPARATOR[] = "Secp256k1_HashToCurve_Cashu_"; | ||
| 24 | |||
| 25 | static mbedtls_ecp_group s_grp; | ||
| 26 | static mbedtls_mpi s_order; | ||
| 27 | static bool s_grp_loaded = false; | ||
| 28 | |||
| 29 | static esp_err_t init_ecp_group(void) | ||
| 30 | { | ||
| 31 | if (s_grp_loaded) return ESP_OK; | ||
| 32 | mbedtls_ecp_group_init(&s_grp); | ||
| 33 | mbedtls_mpi_init(&s_order); | ||
| 34 | int ret = mbedtls_ecp_group_load(&s_grp, MBEDTLS_ECP_DP_SECP256K1); | ||
| 35 | if (ret != 0) { | ||
| 36 | ESP_LOGE(TAG, "Failed to load secp256k1 group: -0x%x", -ret); | ||
| 37 | return ESP_FAIL; | ||
| 38 | } | ||
| 39 | mbedtls_mpi_copy(&s_order, &s_grp.N); | ||
| 40 | s_grp_loaded = true; | ||
| 41 | return ESP_OK; | ||
| 42 | } | ||
| 43 | |||
| 44 | static void random_bytes(uint8_t *buf, size_t len) | ||
| 45 | { | ||
| 46 | esp_fill_random(buf, len); | ||
| 47 | } | ||
| 48 | |||
| 49 | static esp_err_t random_scalar(mbedtls_mpi *r) | ||
| 50 | { | ||
| 51 | uint8_t buf[32]; | ||
| 52 | random_bytes(buf, 32); | ||
| 53 | mbedtls_mpi_init(r); | ||
| 54 | int ret = mbedtls_mpi_read_binary(r, buf, 32); | ||
| 55 | if (ret != 0) return ESP_FAIL; | ||
| 56 | ret = mbedtls_mpi_mod_mpi(r, r, &s_order); | ||
| 57 | if (ret != 0) return ESP_FAIL; | ||
| 58 | if (mbedtls_mpi_cmp_int(r, 1) < 0) { | ||
| 59 | mbedtls_mpi_add_int(r, r, 1); | ||
| 60 | } | ||
| 61 | return ESP_OK; | ||
| 62 | } | ||
| 63 | |||
| 64 | static esp_err_t hash_to_curve(const uint8_t *msg, size_t msg_len, mbedtls_ecp_point *Y) | ||
| 65 | { | ||
| 66 | uint8_t msg_hash[32]; | ||
| 67 | size_t ds_len = strlen(DOMAIN_SEPARATOR); | ||
| 68 | uint8_t *hash_input = malloc(ds_len + msg_len); | ||
| 69 | if (!hash_input) return ESP_FAIL; | ||
| 70 | memcpy(hash_input, DOMAIN_SEPARATOR, ds_len); | ||
| 71 | memcpy(hash_input + ds_len, msg, msg_len); | ||
| 72 | mbedtls_sha256(hash_input, ds_len + msg_len, msg_hash, 0); | ||
| 73 | free(hash_input); | ||
| 74 | |||
| 75 | mbedtls_ecp_point_init(Y); | ||
| 76 | for (uint32_t counter = 0; counter < 256; counter++) { | ||
| 77 | uint8_t counter_bytes[4]; | ||
| 78 | counter_bytes[0] = counter & 0xFF; | ||
| 79 | counter_bytes[1] = (counter >> 8) & 0xFF; | ||
| 80 | counter_bytes[2] = (counter >> 16) & 0xFF; | ||
| 81 | counter_bytes[3] = (counter >> 24) & 0xFF; | ||
| 82 | |||
| 83 | uint8_t to_hash[32 + 4 + 1]; | ||
| 84 | memcpy(to_hash, msg_hash, 32); | ||
| 85 | memcpy(to_hash + 32, counter_bytes, 4); | ||
| 86 | |||
| 87 | uint8_t point_hash[32]; | ||
| 88 | mbedtls_sha256(to_hash, 36, point_hash, 0); | ||
| 89 | |||
| 90 | uint8_t compressed[33]; | ||
| 91 | compressed[0] = 0x02; | ||
| 92 | memcpy(compressed + 1, point_hash, 32); | ||
| 93 | |||
| 94 | int ret = mbedtls_ecp_point_read_binary(&s_grp, Y, compressed, 33); | ||
| 95 | if (ret == 0) { | ||
| 96 | ret = mbedtls_ecp_check_pubkey(&s_grp, Y); | ||
| 97 | if (ret == 0) return ESP_OK; | ||
| 98 | } | ||
| 99 | |||
| 100 | compressed[0] = 0x03; | ||
| 101 | ret = mbedtls_ecp_point_read_binary(&s_grp, Y, compressed, 33); | ||
| 102 | if (ret == 0) { | ||
| 103 | ret = mbedtls_ecp_check_pubkey(&s_grp, Y); | ||
| 104 | if (ret == 0) return ESP_OK; | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | ESP_LOGE(TAG, "hash_to_curve failed after 256 attempts"); | ||
| 109 | return ESP_FAIL; | ||
| 110 | } | ||
| 111 | |||
| 112 | static esp_err_t point_add(const mbedtls_ecp_point *A, const mbedtls_ecp_point *B, | ||
| 113 | mbedtls_ecp_point *R) | ||
| 114 | { | ||
| 115 | mbedtls_mpi one; | ||
| 116 | mbedtls_mpi_init(&one); | ||
| 117 | mbedtls_mpi_lset(&one, 1); | ||
| 118 | int ret = mbedtls_ecp_muladd(&s_grp, R, &one, A, &one, B); | ||
| 119 | if (ret != 0) { | ||
| 120 | ESP_LOGE(TAG, "point_add failed: -0x%x", -ret); | ||
| 121 | } | ||
| 122 | mbedtls_mpi_free(&one); | ||
| 123 | return (ret == 0) ? ESP_OK : ESP_FAIL; | ||
| 124 | } | ||
| 125 | |||
| 126 | static esp_err_t scalar_mul(const mbedtls_mpi *m, const mbedtls_ecp_point *P, | ||
| 127 | mbedtls_ecp_point *R) | ||
| 128 | { | ||
| 129 | int ret = mbedtls_ecp_mul(&s_grp, R, m, P, NULL, NULL); | ||
| 130 | if (ret != 0) { | ||
| 131 | ESP_LOGE(TAG, "scalar_mul failed: -0x%x", -ret); | ||
| 132 | } | ||
| 133 | return (ret == 0) ? ESP_OK : ESP_FAIL; | ||
| 134 | } | ||
| 135 | |||
| 136 | static int hex_to_bytes(const char *hex, uint8_t *bytes, size_t bytes_len) | ||
| 137 | { | ||
| 138 | size_t hex_len = strlen(hex); | ||
| 139 | if (hex_len / 2 > bytes_len) return -1; | ||
| 140 | for (size_t i = 0; i < hex_len / 2; i++) { | ||
| 141 | unsigned int b; | ||
| 142 | sscanf(hex + i * 2, "%02x", &b); | ||
| 143 | bytes[i] = (uint8_t)b; | ||
| 144 | } | ||
| 145 | return hex_len / 2; | ||
| 146 | } | ||
| 147 | |||
| 148 | static void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex) | ||
| 149 | { | ||
| 150 | for (size_t i = 0; i < len; i++) { | ||
| 151 | sprintf(hex + i * 2, "%02x", bytes[i]); | ||
| 152 | } | ||
| 153 | hex[len * 2] = '\0'; | ||
| 154 | } | ||
| 155 | |||
| 156 | esp_err_t wallet_init(void) | ||
| 157 | { | ||
| 158 | memset(&s_wallet, 0, sizeof(s_wallet)); | ||
| 159 | esp_err_t err = init_ecp_group(); | ||
| 160 | if (err != ESP_OK) return err; | ||
| 161 | wallet_persist_load(); | ||
| 162 | ESP_LOGI(TAG, "Wallet initialized (secp256k1 loaded)"); | ||
| 163 | return ESP_OK; | ||
| 164 | } | ||
| 165 | |||
| 166 | wallet_t *wallet_get(void) | ||
| 167 | { | ||
| 168 | return &s_wallet; | ||
| 169 | } | ||
| 170 | |||
| 171 | uint64_t wallet_balance(void) | ||
| 172 | { | ||
| 173 | return s_wallet.balance; | ||
| 174 | } | ||
| 175 | |||
| 176 | esp_err_t wallet_add_proofs(const wallet_proof_t *proofs, int count) | ||
| 177 | { | ||
| 178 | for (int i = 0; i < count; i++) { | ||
| 179 | if (s_wallet.proof_count >= WALLET_MAX_PROOFS) { | ||
| 180 | ESP_LOGW(TAG, "Wallet full, cannot add more proofs"); | ||
| 181 | return ESP_ERR_NO_MEM; | ||
| 182 | } | ||
| 183 | memcpy(&s_wallet.proofs[s_wallet.proof_count], &proofs[i], sizeof(wallet_proof_t)); | ||
| 184 | s_wallet.balance += proofs[i].amount; | ||
| 185 | s_wallet.proof_count++; | ||
| 186 | ESP_LOGI(TAG, "Added proof: amount=%llu, total_balance=%llu", | ||
| 187 | (unsigned long long)proofs[i].amount, | ||
| 188 | (unsigned long long)s_wallet.balance); | ||
| 189 | } | ||
| 190 | wallet_persist_save(); | ||
| 191 | return ESP_OK; | ||
| 192 | } | ||
| 193 | |||
| 194 | esp_err_t wallet_remove_proof(int index) | ||
| 195 | { | ||
| 196 | if (index < 0 || index >= s_wallet.proof_count) return ESP_ERR_INVALID_ARG; | ||
| 197 | s_wallet.balance -= s_wallet.proofs[index].amount; | ||
| 198 | for (int i = index; i < s_wallet.proof_count - 1; i++) { | ||
| 199 | memcpy(&s_wallet.proofs[i], &s_wallet.proofs[i + 1], sizeof(wallet_proof_t)); | ||
| 200 | } | ||
| 201 | memset(&s_wallet.proofs[s_wallet.proof_count - 1], 0, sizeof(wallet_proof_t)); | ||
| 202 | s_wallet.proof_count--; | ||
| 203 | wallet_persist_save(); | ||
| 204 | return ESP_OK; | ||
| 205 | } | ||
| 206 | |||
| 207 | void wallet_clear(void) | ||
| 208 | { | ||
| 209 | s_wallet.balance = 0; | ||
| 210 | s_wallet.proof_count = 0; | ||
| 211 | wallet_persist_save(); | ||
| 212 | } | ||
| 213 | |||
| 214 | esp_err_t wallet_fetch_keysets(const char *mint_url) | ||
| 215 | { | ||
| 216 | char url[512]; | ||
| 217 | snprintf(url, sizeof(url), "%s/v1/keysets", mint_url); | ||
| 218 | |||
| 219 | char *resp_buf = malloc(8192); | ||
| 220 | if (!resp_buf) return ESP_ERR_NO_MEM; | ||
| 221 | |||
| 222 | esp_http_client_config_t config = { | ||
| 223 | .url = url, | ||
| 224 | .method = HTTP_METHOD_GET, | ||
| 225 | .timeout_ms = 10000, | ||
| 226 | .crt_bundle_attach = esp_crt_bundle_attach, | ||
| 227 | }; | ||
| 228 | esp_http_client_handle_t client = esp_http_client_init(&config); | ||
| 229 | if (!client) { free(resp_buf); return ESP_FAIL; } | ||
| 230 | |||
| 231 | esp_err_t err = esp_http_client_open(client, 0); | ||
| 232 | if (err != ESP_OK) { | ||
| 233 | ESP_LOGE(TAG, "Keyset fetch open failed: %s", esp_err_to_name(err)); | ||
| 234 | esp_http_client_cleanup(client); | ||
| 235 | free(resp_buf); | ||
| 236 | return err; | ||
| 237 | } | ||
| 238 | |||
| 239 | int content_length = esp_http_client_fetch_headers(client); | ||
| 240 | int status = esp_http_client_get_status_code(client); | ||
| 241 | ESP_LOGI(TAG, "Keyset fetch: status=%d content_length=%d", status, content_length); | ||
| 242 | |||
| 243 | int resp_len = esp_http_client_read(client, resp_buf, 8191); | ||
| 244 | ESP_LOGI(TAG, "Keyset fetch: read %d bytes", resp_len); | ||
| 245 | esp_http_client_cleanup(client); | ||
| 246 | |||
| 247 | if (status != 200 || resp_len <= 0) { | ||
| 248 | ESP_LOGE(TAG, "Keyset fetch failed: status=%d len=%d", status, resp_len); | ||
| 249 | free(resp_buf); | ||
| 250 | return ESP_FAIL; | ||
| 251 | } | ||
| 252 | resp_buf[resp_len] = '\0'; | ||
| 253 | |||
| 254 | cJSON *root = cJSON_Parse(resp_buf); | ||
| 255 | free(resp_buf); | ||
| 256 | if (!root) return ESP_FAIL; | ||
| 257 | |||
| 258 | cJSON *keysets = cJSON_GetObjectItemCaseSensitive(root, "keysets"); | ||
| 259 | if (!keysets || !cJSON_IsArray(keysets)) { | ||
| 260 | cJSON_Delete(root); | ||
| 261 | return ESP_FAIL; | ||
| 262 | } | ||
| 263 | |||
| 264 | s_wallet.keyset_count = 0; | ||
| 265 | int n = cJSON_GetArraySize(keysets); | ||
| 266 | for (int i = 0; i < n && i < WALLET_MAX_KEYSETS; i++) { | ||
| 267 | cJSON *ks = cJSON_GetArrayItem(keysets, i); | ||
| 268 | cJSON *id = cJSON_GetObjectItemCaseSensitive(ks, "id"); | ||
| 269 | if (id && cJSON_IsString(id)) { | ||
| 270 | strncpy(s_wallet.keysets[s_wallet.keyset_count].id, id->valuestring, | ||
| 271 | WALLET_KEYSET_ID_LEN - 1); | ||
| 272 | cJSON *fee = cJSON_GetObjectItemCaseSensitive(ks, "input_fee_ppk"); | ||
| 273 | s_wallet.keysets[s_wallet.keyset_count].input_fee_ppk = fee ? fee->valueint : 0; | ||
| 274 | s_wallet.keyset_count++; | ||
| 275 | } | ||
| 276 | } | ||
| 277 | |||
| 278 | cJSON_Delete(root); | ||
| 279 | ESP_LOGI(TAG, "Fetched %d keysets from %s", s_wallet.keyset_count, mint_url); | ||
| 280 | return ESP_OK; | ||
| 281 | } | ||
| 282 | |||
| 283 | esp_err_t wallet_swap_proofs(const char *mint_url, int start_index, int count) | ||
| 284 | { | ||
| 285 | ESP_LOGI(TAG, "wallet_swap_proofs called: start=%d count=%d keysets=%d proofs=%d", | ||
| 286 | start_index, count, s_wallet.keyset_count, s_wallet.proof_count); | ||
| 287 | |||
| 288 | if (s_wallet.keyset_count == 0) { | ||
| 289 | ESP_LOGE(TAG, "No keysets loaded, fetch first"); | ||
| 290 | return ESP_FAIL; | ||
| 291 | } | ||
| 292 | if (start_index < 0 || start_index + count > s_wallet.proof_count) { | ||
| 293 | return ESP_ERR_INVALID_ARG; | ||
| 294 | } | ||
| 295 | |||
| 296 | wallet_proof_t *old_proofs = &s_wallet.proofs[start_index]; | ||
| 297 | int n = count; | ||
| 298 | |||
| 299 | uint64_t total_input = 0; | ||
| 300 | for (int i = 0; i < n; i++) total_input += old_proofs[i].amount; | ||
| 301 | |||
| 302 | int fee_ppk = s_wallet.keysets[0].input_fee_ppk; | ||
| 303 | uint64_t fee_sats = (total_input * fee_ppk + 999) / 1000; | ||
| 304 | uint64_t total_output = total_input - fee_sats; | ||
| 305 | ESP_LOGI(TAG, "Swap: total_input=%llu fee_ppk=%d fee=%llu total_output=%llu", | ||
| 306 | (unsigned long long)total_input, fee_ppk, | ||
| 307 | (unsigned long long)fee_sats, (unsigned long long)total_output); | ||
| 308 | |||
| 309 | cJSON *inputs = cJSON_CreateArray(); | ||
| 310 | for (int i = 0; i < n; i++) { | ||
| 311 | cJSON *p = cJSON_CreateObject(); | ||
| 312 | cJSON_AddNumberToObject(p, "amount", (double)old_proofs[i].amount); | ||
| 313 | cJSON_AddStringToObject(p, "id", old_proofs[i].id); | ||
| 314 | cJSON_AddStringToObject(p, "secret", old_proofs[i].secret); | ||
| 315 | cJSON_AddStringToObject(p, "C", old_proofs[i].c); | ||
| 316 | cJSON_AddItemToArray(inputs, p); | ||
| 317 | } | ||
| 318 | |||
| 319 | typedef struct { | ||
| 320 | uint8_t secret[32]; | ||
| 321 | mbedtls_mpi r; | ||
| 322 | mbedtls_ecp_point Y; | ||
| 323 | } swap_output_t; | ||
| 324 | |||
| 325 | swap_output_t *outputs = heap_caps_malloc(n * sizeof(swap_output_t), MALLOC_CAP_SPIRAM); | ||
| 326 | if (!outputs) { cJSON_Delete(inputs); return ESP_ERR_NO_MEM; } | ||
| 327 | |||
| 328 | cJSON *blinded_msgs = cJSON_CreateArray(); | ||
| 329 | for (int i = 0; i < n; i++) { | ||
| 330 | random_bytes(outputs[i].secret, 32); | ||
| 331 | mbedtls_ecp_point_init(&outputs[i].Y); | ||
| 332 | esp_err_t htc_ret = hash_to_curve(outputs[i].secret, 32, &outputs[i].Y); | ||
| 333 | if (htc_ret != ESP_OK) { | ||
| 334 | ESP_LOGE(TAG, "hash_to_curve failed for output %d", i); | ||
| 335 | } | ||
| 336 | mbedtls_mpi_init(&outputs[i].r); | ||
| 337 | random_scalar(&outputs[i].r); | ||
| 338 | |||
| 339 | mbedtls_ecp_point rG, B_; | ||
| 340 | mbedtls_ecp_point_init(&rG); | ||
| 341 | mbedtls_ecp_point_init(&B_); | ||
| 342 | |||
| 343 | esp_err_t sm_ret = scalar_mul(&outputs[i].r, &s_grp.G, &rG); | ||
| 344 | if (sm_ret != ESP_OK) { | ||
| 345 | ESP_LOGE(TAG, "scalar_mul failed for output %d", i); | ||
| 346 | } | ||
| 347 | esp_err_t pa_ret = point_add(&outputs[i].Y, &rG, &B_); | ||
| 348 | if (pa_ret != ESP_OK) { | ||
| 349 | ESP_LOGE(TAG, "point_add failed for output %d", i); | ||
| 350 | } | ||
| 351 | |||
| 352 | uint8_t b_bytes[33]; | ||
| 353 | size_t olen = 0; | ||
| 354 | int wret = mbedtls_ecp_point_write_binary(&s_grp, &B_, MBEDTLS_ECP_PF_COMPRESSED, &olen, b_bytes, 33); | ||
| 355 | if (wret != 0 || olen == 0) { | ||
| 356 | ESP_LOGE(TAG, "Blinded point write failed: ret=-0x%x olen=%zu", -wret, olen); | ||
| 357 | olen = 1; | ||
| 358 | b_bytes[0] = 0x00; | ||
| 359 | } | ||
| 360 | char b_hex[67]; | ||
| 361 | bytes_to_hex(b_bytes, olen, b_hex); | ||
| 362 | |||
| 363 | uint64_t out_amount = old_proofs[i].amount; | ||
| 364 | if (i == n - 1) { | ||
| 365 | uint64_t running = 0; | ||
| 366 | for (int j = 0; j < n - 1; j++) running += old_proofs[j].amount; | ||
| 367 | out_amount = total_output - running; | ||
| 368 | } | ||
| 369 | |||
| 370 | cJSON *bm = cJSON_CreateObject(); | ||
| 371 | cJSON_AddNumberToObject(bm, "amount", (double)out_amount); | ||
| 372 | cJSON_AddStringToObject(bm, "id", s_wallet.keysets[0].id); | ||
| 373 | cJSON_AddStringToObject(bm, "B_", b_hex); | ||
| 374 | cJSON_AddItemToArray(blinded_msgs, bm); | ||
| 375 | |||
| 376 | mbedtls_ecp_point_free(&rG); | ||
| 377 | mbedtls_ecp_point_free(&B_); | ||
| 378 | } | ||
| 379 | |||
| 380 | cJSON *body = cJSON_CreateObject(); | ||
| 381 | cJSON_AddItemToObject(body, "inputs", inputs); | ||
| 382 | cJSON_AddItemToObject(body, "outputs", blinded_msgs); | ||
| 383 | char *body_str = cJSON_PrintUnformatted(body); | ||
| 384 | cJSON_Delete(body); | ||
| 385 | |||
| 386 | ESP_LOGI(TAG, "Swap request body (%zu bytes): %s", strlen(body_str), body_str); | ||
| 387 | |||
| 388 | char url[512]; | ||
| 389 | snprintf(url, sizeof(url), "%s/v1/swap", mint_url); | ||
| 390 | |||
| 391 | char *resp_buf = malloc(8192); | ||
| 392 | if (!resp_buf) { | ||
| 393 | free(body_str); | ||
| 394 | for (int i = 0; i < n; i++) { | ||
| 395 | mbedtls_mpi_free(&outputs[i].r); | ||
| 396 | mbedtls_ecp_point_free(&outputs[i].Y); | ||
| 397 | } | ||
| 398 | free(outputs); | ||
| 399 | return ESP_ERR_NO_MEM; | ||
| 400 | } | ||
| 401 | |||
| 402 | esp_http_client_config_t config = { | ||
| 403 | .url = url, | ||
| 404 | .method = HTTP_METHOD_POST, | ||
| 405 | .timeout_ms = 15000, | ||
| 406 | .crt_bundle_attach = esp_crt_bundle_attach, | ||
| 407 | }; | ||
| 408 | esp_http_client_handle_t client = esp_http_client_init(&config); | ||
| 409 | if (!client) { | ||
| 410 | free(body_str); | ||
| 411 | free(resp_buf); | ||
| 412 | for (int i = 0; i < n; i++) { | ||
| 413 | mbedtls_mpi_free(&outputs[i].r); | ||
| 414 | mbedtls_ecp_point_free(&outputs[i].Y); | ||
| 415 | } | ||
| 416 | free(outputs); | ||
| 417 | return ESP_FAIL; | ||
| 418 | } | ||
| 419 | |||
| 420 | esp_http_client_set_header(client, "Content-Type", "application/json"); | ||
| 421 | esp_http_client_open(client, strlen(body_str)); | ||
| 422 | esp_http_client_write(client, body_str, strlen(body_str)); | ||
| 423 | free(body_str); | ||
| 424 | |||
| 425 | esp_http_client_fetch_headers(client); | ||
| 426 | int resp_len = esp_http_client_read(client, resp_buf, 8191); | ||
| 427 | int status = esp_http_client_get_status_code(client); | ||
| 428 | esp_http_client_cleanup(client); | ||
| 429 | |||
| 430 | if (status != 200 || resp_len <= 0) { | ||
| 431 | if (resp_len > 0) { | ||
| 432 | resp_buf[resp_len] = '\0'; | ||
| 433 | ESP_LOGE(TAG, "Swap failed: status=%d body=%s", status, resp_buf); | ||
| 434 | } else { | ||
| 435 | ESP_LOGE(TAG, "Swap failed: status=%d len=%d", status, resp_len); | ||
| 436 | } | ||
| 437 | free(resp_buf); | ||
| 438 | for (int i = 0; i < n; i++) { | ||
| 439 | mbedtls_mpi_free(&outputs[i].r); | ||
| 440 | mbedtls_ecp_point_free(&outputs[i].Y); | ||
| 441 | } | ||
| 442 | free(outputs); | ||
| 443 | return ESP_FAIL; | ||
| 444 | } | ||
| 445 | resp_buf[resp_len] = '\0'; | ||
| 446 | |||
| 447 | cJSON *root = cJSON_Parse(resp_buf); | ||
| 448 | free(resp_buf); | ||
| 449 | if (!root) { | ||
| 450 | for (int i = 0; i < n; i++) { | ||
| 451 | mbedtls_mpi_free(&outputs[i].r); | ||
| 452 | mbedtls_ecp_point_free(&outputs[i].Y); | ||
| 453 | } | ||
| 454 | free(outputs); | ||
| 455 | return ESP_FAIL; | ||
| 456 | } | ||
| 457 | |||
| 458 | cJSON *signatures = cJSON_GetObjectItemCaseSensitive(root, "signatures"); | ||
| 459 | if (!signatures || !cJSON_IsArray(signatures)) { | ||
| 460 | ESP_LOGE(TAG, "No signatures in swap response"); | ||
| 461 | cJSON_Delete(root); | ||
| 462 | for (int i = 0; i < n; i++) { | ||
| 463 | mbedtls_mpi_free(&outputs[i].r); | ||
| 464 | mbedtls_ecp_point_free(&outputs[i].Y); | ||
| 465 | } | ||
| 466 | free(outputs); | ||
| 467 | return ESP_FAIL; | ||
| 468 | } | ||
| 469 | |||
| 470 | for (int i = start_index; i < start_index + n; i++) { | ||
| 471 | s_wallet.balance -= s_wallet.proofs[i].amount; | ||
| 472 | } | ||
| 473 | |||
| 474 | int sig_count = cJSON_GetArraySize(signatures); | ||
| 475 | for (int i = 0; i < sig_count && i < n; i++) { | ||
| 476 | cJSON *sig = cJSON_GetArrayItem(signatures, i); | ||
| 477 | cJSON *c_ = cJSON_GetObjectItemCaseSensitive(sig, "C_"); | ||
| 478 | cJSON *amt = cJSON_GetObjectItemCaseSensitive(sig, "amount"); | ||
| 479 | cJSON *id = cJSON_GetObjectItemCaseSensitive(sig, "id"); | ||
| 480 | |||
| 481 | if (!c_ || !cJSON_IsString(c_)) continue; | ||
| 482 | |||
| 483 | uint8_t c_bytes[33]; | ||
| 484 | int c_len = hex_to_bytes(c_->valuestring, c_bytes, 33); | ||
| 485 | |||
| 486 | mbedtls_ecp_point C_; | ||
| 487 | mbedtls_ecp_point_init(&C_); | ||
| 488 | mbedtls_ecp_point_read_binary(&s_grp, &C_, c_bytes, c_len); | ||
| 489 | |||
| 490 | char ks_id[WALLET_KEYSET_ID_LEN] = {0}; | ||
| 491 | if (id && cJSON_IsString(id)) { | ||
| 492 | strncpy(ks_id, id->valuestring, WALLET_KEYSET_ID_LEN - 1); | ||
| 493 | } | ||
| 494 | |||
| 495 | mbedtls_mpi neg_r; | ||
| 496 | mbedtls_mpi_init(&neg_r); | ||
| 497 | mbedtls_mpi_sub_mpi(&neg_r, &s_order, &outputs[i].r); | ||
| 498 | |||
| 499 | mbedtls_ecp_point neg_rG; | ||
| 500 | mbedtls_ecp_point_init(&neg_rG); | ||
| 501 | scalar_mul(&neg_r, &s_grp.G, &neg_rG); | ||
| 502 | |||
| 503 | mbedtls_ecp_point C; | ||
| 504 | mbedtls_ecp_point_init(&C); | ||
| 505 | point_add(&C_, &neg_rG, &C); | ||
| 506 | |||
| 507 | uint8_t c_final[33]; | ||
| 508 | size_t c_final_len; | ||
| 509 | mbedtls_ecp_point_write_binary(&s_grp, &C, MBEDTLS_ECP_PF_COMPRESSED, | ||
| 510 | &c_final_len, c_final, 33); | ||
| 511 | |||
| 512 | if (s_wallet.proof_count < WALLET_MAX_PROOFS) { | ||
| 513 | wallet_proof_t *wp = &s_wallet.proofs[s_wallet.proof_count]; | ||
| 514 | if (amt && cJSON_IsNumber(amt)) { | ||
| 515 | wp->amount = (uint64_t)amt->valuedouble; | ||
| 516 | } | ||
| 517 | strncpy(wp->id, ks_id, WALLET_KEYSET_ID_LEN - 1); | ||
| 518 | bytes_to_hex(outputs[i].secret, 32, wp->secret); | ||
| 519 | bytes_to_hex(c_final, c_final_len, wp->c); | ||
| 520 | s_wallet.balance += wp->amount; | ||
| 521 | s_wallet.proof_count++; | ||
| 522 | } | ||
| 523 | |||
| 524 | mbedtls_mpi_free(&neg_r); | ||
| 525 | mbedtls_ecp_point_free(&C_); | ||
| 526 | mbedtls_ecp_point_free(&neg_rG); | ||
| 527 | mbedtls_ecp_point_free(&C); | ||
| 528 | } | ||
| 529 | |||
| 530 | for (int i = 0; i < n; i++) { | ||
| 531 | int idx = start_index; | ||
| 532 | for (int j = idx; j < s_wallet.proof_count - 1; j++) { | ||
| 533 | memcpy(&s_wallet.proofs[j], &s_wallet.proofs[j + 1], sizeof(wallet_proof_t)); | ||
| 534 | } | ||
| 535 | s_wallet.proof_count--; | ||
| 536 | } | ||
| 537 | |||
| 538 | for (int i = 0; i < n; i++) { | ||
| 539 | mbedtls_mpi_free(&outputs[i].r); | ||
| 540 | mbedtls_ecp_point_free(&outputs[i].Y); | ||
| 541 | } | ||
| 542 | free(outputs); | ||
| 543 | cJSON_Delete(root); | ||
| 544 | |||
| 545 | ESP_LOGI(TAG, "Swap complete: %d proofs swapped, balance=%llu", | ||
| 546 | n, (unsigned long long)s_wallet.balance); | ||
| 547 | wallet_persist_save(); | ||
| 548 | return ESP_OK; | ||
| 549 | } | ||
| 550 | |||
| 551 | esp_err_t wallet_create_token(char *out, size_t out_size, uint64_t amount, | ||
| 552 | const char *mint_url) | ||
| 553 | { | ||
| 554 | if (s_wallet.proof_count == 0 || s_wallet.balance < amount) { | ||
| 555 | ESP_LOGE(TAG, "Insufficient balance: have=%llu need=%llu", | ||
| 556 | (unsigned long long)s_wallet.balance, (unsigned long long)amount); | ||
| 557 | return ESP_FAIL; | ||
| 558 | } | ||
| 559 | |||
| 560 | cJSON *proofs_arr = cJSON_CreateArray(); | ||
| 561 | uint64_t remaining = amount; | ||
| 562 | int indices_to_remove[10]; | ||
| 563 | int remove_count = 0; | ||
| 564 | |||
| 565 | for (int i = 0; i < s_wallet.proof_count && remaining > 0 && remove_count < 10; i++) { | ||
| 566 | if (s_wallet.proofs[i].amount <= remaining) { | ||
| 567 | cJSON *p = cJSON_CreateObject(); | ||
| 568 | cJSON_AddNumberToObject(p, "amount", (double)s_wallet.proofs[i].amount); | ||
| 569 | cJSON_AddStringToObject(p, "id", s_wallet.proofs[i].id); | ||
| 570 | cJSON_AddStringToObject(p, "secret", s_wallet.proofs[i].secret); | ||
| 571 | cJSON_AddStringToObject(p, "C", s_wallet.proofs[i].c); | ||
| 572 | cJSON_AddItemToArray(proofs_arr, p); | ||
| 573 | remaining -= s_wallet.proofs[i].amount; | ||
| 574 | indices_to_remove[remove_count++] = i; | ||
| 575 | } | ||
| 576 | } | ||
| 577 | |||
| 578 | if (remaining > 0) { | ||
| 579 | cJSON_Delete(proofs_arr); | ||
| 580 | ESP_LOGE(TAG, "Cannot make exact amount: %llu remaining", (unsigned long long)remaining); | ||
| 581 | return ESP_FAIL; | ||
| 582 | } | ||
| 583 | |||
| 584 | cJSON *token_obj = cJSON_CreateObject(); | ||
| 585 | cJSON *token_arr = cJSON_CreateArray(); | ||
| 586 | cJSON *mint_proofs = cJSON_CreateObject(); | ||
| 587 | cJSON_AddStringToObject(mint_proofs, "mint", mint_url); | ||
| 588 | cJSON_AddItemToObject(mint_proofs, "proofs", proofs_arr); | ||
| 589 | cJSON_AddItemToArray(token_arr, mint_proofs); | ||
| 590 | cJSON_AddItemToObject(token_obj, "token", token_arr); | ||
| 591 | |||
| 592 | char *json_str = cJSON_PrintUnformatted(token_obj); | ||
| 593 | cJSON_Delete(token_obj); | ||
| 594 | |||
| 595 | size_t b64_len; | ||
| 596 | mbedtls_base64_encode((unsigned char *)out + 6, out_size - 6, &b64_len, | ||
| 597 | (const unsigned char *)json_str, strlen(json_str)); | ||
| 598 | free(json_str); | ||
| 599 | |||
| 600 | memcpy(out, "cashuA", 6); | ||
| 601 | for (size_t i = 0; i < b64_len; i++) { | ||
| 602 | if (out[6 + i] == '+') out[6 + i] = '-'; | ||
| 603 | else if (out[6 + i] == '/') out[6 + i] = '_'; | ||
| 604 | else if (out[6 + i] == '=') { out[6 + i] = '\0'; break; } | ||
| 605 | } | ||
| 606 | out[6 + b64_len] = '\0'; | ||
| 607 | |||
| 608 | for (int i = remove_count - 1; i >= 0; i--) { | ||
| 609 | s_wallet.balance -= s_wallet.proofs[indices_to_remove[i]].amount; | ||
| 610 | for (int j = indices_to_remove[i]; j < s_wallet.proof_count - 1; j++) { | ||
| 611 | memcpy(&s_wallet.proofs[j], &s_wallet.proofs[j + 1], sizeof(wallet_proof_t)); | ||
| 612 | } | ||
| 613 | s_wallet.proof_count--; | ||
| 614 | } | ||
| 615 | |||
| 616 | ESP_LOGI(TAG, "Created token for %llu sats, remaining balance=%llu", | ||
| 617 | (unsigned long long)amount, (unsigned long long)s_wallet.balance); | ||
| 618 | wallet_persist_save(); | ||
| 619 | return ESP_OK; | ||
| 620 | } | ||
| 621 | |||
| 622 | esp_err_t wallet_send(const char *mint_url, uint64_t amount, | ||
| 623 | char *token_out, size_t token_out_size) | ||
| 624 | { | ||
| 625 | return wallet_create_token(token_out, token_out_size, amount, mint_url); | ||
| 626 | } | ||
| 627 | |||
| 628 | void wallet_print_status(void) | ||
| 629 | { | ||
| 630 | ESP_LOGI(TAG, "Wallet: %d proofs, balance=%llu sats, %d keysets", | ||
| 631 | s_wallet.proof_count, | ||
| 632 | (unsigned long long)s_wallet.balance, | ||
| 633 | s_wallet.keyset_count); | ||
| 634 | for (int i = 0; i < s_wallet.proof_count; i++) { | ||
| 635 | ESP_LOGI(TAG, " [%d] amount=%llu id=%s", i, | ||
| 636 | (unsigned long long)s_wallet.proofs[i].amount, | ||
| 637 | s_wallet.proofs[i].id); | ||
| 638 | } | ||
| 639 | } | ||
diff --git a/main/wallet.h b/main/wallet.h deleted file mode 100644 index 5089f93..0000000 --- a/main/wallet.h +++ /dev/null | |||
| @@ -1,53 +0,0 @@ | |||
| 1 | #ifndef WALLET_H | ||
| 2 | #define WALLET_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdbool.h> | ||
| 7 | |||
| 8 | #define WALLET_MAX_PROOFS 50 | ||
| 9 | #define WALLET_MAX_KEYSETS 5 | ||
| 10 | #define WALLET_KEYSET_ID_LEN 68 | ||
| 11 | #define WALLET_SECRET_LEN 65 | ||
| 12 | #define WALLET_SIG_LEN 67 | ||
| 13 | |||
| 14 | typedef struct { | ||
| 15 | uint64_t amount; | ||
| 16 | char id[WALLET_KEYSET_ID_LEN]; | ||
| 17 | char secret[WALLET_SECRET_LEN]; | ||
| 18 | char c[WALLET_SIG_LEN]; | ||
| 19 | } wallet_proof_t; | ||
| 20 | |||
| 21 | typedef struct { | ||
| 22 | char id[WALLET_KEYSET_ID_LEN]; | ||
| 23 | char public_key_33[67]; | ||
| 24 | uint64_t amount; | ||
| 25 | int input_fee_ppk; | ||
| 26 | } wallet_keyset_t; | ||
| 27 | |||
| 28 | typedef struct { | ||
| 29 | wallet_proof_t proofs[WALLET_MAX_PROOFS]; | ||
| 30 | int proof_count; | ||
| 31 | wallet_keyset_t keysets[WALLET_MAX_KEYSETS]; | ||
| 32 | int keyset_count; | ||
| 33 | uint64_t balance; | ||
| 34 | } wallet_t; | ||
| 35 | |||
| 36 | esp_err_t wallet_init(void); | ||
| 37 | wallet_t *wallet_get(void); | ||
| 38 | uint64_t wallet_balance(void); | ||
| 39 | |||
| 40 | esp_err_t wallet_add_proofs(const wallet_proof_t *proofs, int count); | ||
| 41 | esp_err_t wallet_remove_proof(int index); | ||
| 42 | void wallet_clear(void); | ||
| 43 | |||
| 44 | esp_err_t wallet_fetch_keysets(const char *mint_url); | ||
| 45 | esp_err_t wallet_swap_proofs(const char *mint_url, int start_index, int count); | ||
| 46 | |||
| 47 | esp_err_t wallet_create_token(char *out, size_t out_size, uint64_t amount, | ||
| 48 | const char *mint_url); | ||
| 49 | esp_err_t wallet_send(const char *mint_url, uint64_t amount, | ||
| 50 | char *token_out, size_t token_out_size); | ||
| 51 | |||
| 52 | void wallet_print_status(void); | ||
| 53 | #endif | ||
diff --git a/main/wallet_persist.c b/main/wallet_persist.c deleted file mode 100644 index 45c932f..0000000 --- a/main/wallet_persist.c +++ /dev/null | |||
| @@ -1,147 +0,0 @@ | |||
| 1 | #include "wallet_persist.h" | ||
| 2 | #include "wallet.h" | ||
| 3 | #include "config.h" | ||
| 4 | #include "esp_log.h" | ||
| 5 | #include "cJSON.h" | ||
| 6 | #include <string.h> | ||
| 7 | #include <stdio.h> | ||
| 8 | #include <unistd.h> | ||
| 9 | |||
| 10 | static const char *TAG = "wallet_persist"; | ||
| 11 | static const char *WALLET_FILE = "/spiffs/wallet.json"; | ||
| 12 | |||
| 13 | esp_err_t wallet_persist_save(void) | ||
| 14 | { | ||
| 15 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 16 | wallet_t *w = wallet_get(); | ||
| 17 | |||
| 18 | if (w->balance < cfg->persist_threshold_sats) { | ||
| 19 | if (w->proof_count == 0) { | ||
| 20 | unlink(WALLET_FILE); | ||
| 21 | ESP_LOGI(TAG, "Wallet empty, removed persist file"); | ||
| 22 | } | ||
| 23 | return ESP_OK; | ||
| 24 | } | ||
| 25 | |||
| 26 | cJSON *root = cJSON_CreateObject(); | ||
| 27 | cJSON_AddNumberToObject(root, "balance", (double)w->balance); | ||
| 28 | |||
| 29 | cJSON *proofs = cJSON_CreateArray(); | ||
| 30 | for (int i = 0; i < w->proof_count; i++) { | ||
| 31 | cJSON *p = cJSON_CreateObject(); | ||
| 32 | cJSON_AddNumberToObject(p, "amount", (double)w->proofs[i].amount); | ||
| 33 | cJSON_AddStringToObject(p, "id", w->proofs[i].id); | ||
| 34 | cJSON_AddStringToObject(p, "secret", w->proofs[i].secret); | ||
| 35 | cJSON_AddStringToObject(p, "C", w->proofs[i].c); | ||
| 36 | cJSON_AddItemToArray(proofs, p); | ||
| 37 | } | ||
| 38 | cJSON_AddItemToObject(root, "proofs", proofs); | ||
| 39 | |||
| 40 | cJSON *keysets = cJSON_CreateArray(); | ||
| 41 | for (int i = 0; i < w->keyset_count; i++) { | ||
| 42 | cJSON *ks = cJSON_CreateObject(); | ||
| 43 | cJSON_AddStringToObject(ks, "id", w->keysets[i].id); | ||
| 44 | cJSON_AddItemToArray(keysets, ks); | ||
| 45 | } | ||
| 46 | cJSON_AddItemToObject(root, "keysets", keysets); | ||
| 47 | |||
| 48 | char *json_str = cJSON_PrintUnformatted(root); | ||
| 49 | cJSON_Delete(root); | ||
| 50 | |||
| 51 | FILE *f = fopen(WALLET_FILE, "w"); | ||
| 52 | if (!f) { | ||
| 53 | ESP_LOGE(TAG, "Failed to open %s for writing", WALLET_FILE); | ||
| 54 | cJSON_free(json_str); | ||
| 55 | return ESP_FAIL; | ||
| 56 | } | ||
| 57 | |||
| 58 | size_t written = fwrite(json_str, 1, strlen(json_str), f); | ||
| 59 | fclose(f); | ||
| 60 | cJSON_free(json_str); | ||
| 61 | |||
| 62 | ESP_LOGI(TAG, "Wallet persisted: %d proofs, balance=%llu (%zu bytes)", | ||
| 63 | w->proof_count, (unsigned long long)w->balance, written); | ||
| 64 | return ESP_OK; | ||
| 65 | } | ||
| 66 | |||
| 67 | esp_err_t wallet_persist_load(void) | ||
| 68 | { | ||
| 69 | wallet_t *w = wallet_get(); | ||
| 70 | |||
| 71 | FILE *f = fopen(WALLET_FILE, "r"); | ||
| 72 | if (!f) { | ||
| 73 | ESP_LOGI(TAG, "No persisted wallet found, starting fresh"); | ||
| 74 | return ESP_OK; | ||
| 75 | } | ||
| 76 | |||
| 77 | fseek(f, 0, SEEK_END); | ||
| 78 | long fsize = ftell(f); | ||
| 79 | fseek(f, 0, SEEK_SET); | ||
| 80 | |||
| 81 | if (fsize <= 0 || fsize > 65536) { | ||
| 82 | fclose(f); | ||
| 83 | ESP_LOGW(TAG, "Wallet file size invalid: %ld", fsize); | ||
| 84 | return ESP_FAIL; | ||
| 85 | } | ||
| 86 | |||
| 87 | char *buf = malloc(fsize + 1); | ||
| 88 | if (!buf) { | ||
| 89 | fclose(f); | ||
| 90 | return ESP_ERR_NO_MEM; | ||
| 91 | } | ||
| 92 | |||
| 93 | fread(buf, 1, fsize, f); | ||
| 94 | buf[fsize] = '\0'; | ||
| 95 | fclose(f); | ||
| 96 | |||
| 97 | cJSON *root = cJSON_Parse(buf); | ||
| 98 | free(buf); | ||
| 99 | if (!root) { | ||
| 100 | ESP_LOGE(TAG, "Failed to parse wallet.json"); | ||
| 101 | return ESP_FAIL; | ||
| 102 | } | ||
| 103 | |||
| 104 | cJSON *balance_j = cJSON_GetObjectItemCaseSensitive(root, "balance"); | ||
| 105 | if (balance_j && cJSON_IsNumber(balance_j)) { | ||
| 106 | w->balance = (uint64_t)balance_j->valuedouble; | ||
| 107 | } | ||
| 108 | |||
| 109 | cJSON *proofs = cJSON_GetObjectItemCaseSensitive(root, "proofs"); | ||
| 110 | if (proofs && cJSON_IsArray(proofs)) { | ||
| 111 | int count = cJSON_GetArraySize(proofs); | ||
| 112 | if (count > WALLET_MAX_PROOFS) count = WALLET_MAX_PROOFS; | ||
| 113 | for (int i = 0; i < count; i++) { | ||
| 114 | cJSON *p = cJSON_GetArrayItem(proofs, i); | ||
| 115 | cJSON *amt = cJSON_GetObjectItemCaseSensitive(p, "amount"); | ||
| 116 | cJSON *id = cJSON_GetObjectItemCaseSensitive(p, "id"); | ||
| 117 | cJSON *secret = cJSON_GetObjectItemCaseSensitive(p, "secret"); | ||
| 118 | cJSON *c = cJSON_GetObjectItemCaseSensitive(p, "C"); | ||
| 119 | if (amt) w->proofs[i].amount = (uint64_t)amt->valuedouble; | ||
| 120 | if (id && cJSON_IsString(id)) | ||
| 121 | strncpy(w->proofs[i].id, id->valuestring, WALLET_KEYSET_ID_LEN - 1); | ||
| 122 | if (secret && cJSON_IsString(secret)) | ||
| 123 | strncpy(w->proofs[i].secret, secret->valuestring, WALLET_SECRET_LEN - 1); | ||
| 124 | if (c && cJSON_IsString(c)) | ||
| 125 | strncpy(w->proofs[i].c, c->valuestring, WALLET_SIG_LEN - 1); | ||
| 126 | w->proof_count++; | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | cJSON *keysets = cJSON_GetObjectItemCaseSensitive(root, "keysets"); | ||
| 131 | if (keysets && cJSON_IsArray(keysets)) { | ||
| 132 | int count = cJSON_GetArraySize(keysets); | ||
| 133 | if (count > WALLET_MAX_KEYSETS) count = WALLET_MAX_KEYSETS; | ||
| 134 | for (int i = 0; i < count; i++) { | ||
| 135 | cJSON *ks = cJSON_GetArrayItem(keysets, i); | ||
| 136 | cJSON *id = cJSON_GetObjectItemCaseSensitive(ks, "id"); | ||
| 137 | if (id && cJSON_IsString(id)) | ||
| 138 | strncpy(w->keysets[i].id, id->valuestring, WALLET_KEYSET_ID_LEN - 1); | ||
| 139 | w->keyset_count++; | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | cJSON_Delete(root); | ||
| 144 | ESP_LOGI(TAG, "Wallet loaded: %d proofs, %d keysets, balance=%llu", | ||
| 145 | w->proof_count, w->keyset_count, (unsigned long long)w->balance); | ||
| 146 | return ESP_OK; | ||
| 147 | } | ||
diff --git a/main/wallet_persist.h b/main/wallet_persist.h deleted file mode 100644 index 4dfcbfc..0000000 --- a/main/wallet_persist.h +++ /dev/null | |||
| @@ -1,9 +0,0 @@ | |||
| 1 | #ifndef WALLET_PERSIST_H | ||
| 2 | #define WALLET_PERSIST_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | |||
| 6 | esp_err_t wallet_persist_save(void); | ||
| 7 | esp_err_t wallet_persist_load(void); | ||
| 8 | |||
| 9 | #endif | ||
diff --git a/main/wifistr.c b/main/wifistr.c new file mode 100644 index 0000000..bf03b4d --- /dev/null +++ b/main/wifistr.c | |||
| @@ -0,0 +1,252 @@ | |||
| 1 | #include "wifistr.h" | ||
| 2 | #include "identity.h" | ||
| 3 | #include "nostr_event.h" | ||
| 4 | #include "config.h" | ||
| 5 | #include "esp_log.h" | ||
| 6 | #include "esp_tls.h" | ||
| 7 | #include "esp_crt_bundle.h" | ||
| 8 | #include "cJSON.h" | ||
| 9 | #include "freertos/task.h" | ||
| 10 | #include "freertos/timers.h" | ||
| 11 | #include <string.h> | ||
| 12 | #include <stdio.h> | ||
| 13 | #include <stdlib.h> | ||
| 14 | |||
| 15 | static const char *TAG = "wifistr"; | ||
| 16 | static TimerHandle_t s_publish_timer = NULL; | ||
| 17 | |||
| 18 | static esp_err_t ws_send_to_relay(const char *relay_url, const char *event_json) | ||
| 19 | { | ||
| 20 | char host[128] = {0}; | ||
| 21 | int port = 443; | ||
| 22 | char path[128] = "/"; | ||
| 23 | |||
| 24 | if (strncmp(relay_url, "wss://", 6) != 0) { | ||
| 25 | ESP_LOGW(TAG, "Unsupported relay URL: %s", relay_url); | ||
| 26 | return ESP_ERR_INVALID_ARG; | ||
| 27 | } | ||
| 28 | |||
| 29 | const char *url_start = relay_url + 6; | ||
| 30 | const char *path_ptr = strchr(url_start, '/'); | ||
| 31 | if (path_ptr) { | ||
| 32 | size_t host_len = path_ptr - url_start; | ||
| 33 | if (host_len >= sizeof(host)) host_len = sizeof(host) - 1; | ||
| 34 | memcpy(host, url_start, host_len); | ||
| 35 | host[host_len] = '\0'; | ||
| 36 | strncpy(path, path_ptr, sizeof(path) - 1); | ||
| 37 | } else { | ||
| 38 | strncpy(host, url_start, sizeof(host) - 1); | ||
| 39 | } | ||
| 40 | |||
| 41 | char *colon = strchr(host, ':'); | ||
| 42 | if (colon) { | ||
| 43 | *colon = '\0'; | ||
| 44 | port = atoi(colon + 1); | ||
| 45 | } | ||
| 46 | |||
| 47 | ESP_LOGI(TAG, "Connecting to %s:%d%s", host, port, path); | ||
| 48 | |||
| 49 | esp_tls_cfg_t tls_cfg = { | ||
| 50 | .crt_bundle_attach = esp_crt_bundle_attach, | ||
| 51 | }; | ||
| 52 | |||
| 53 | esp_tls_t *tls = esp_tls_init(); | ||
| 54 | if (!tls) { | ||
| 55 | ESP_LOGE(TAG, "Failed to allocate TLS handle"); | ||
| 56 | return ESP_ERR_NO_MEM; | ||
| 57 | } | ||
| 58 | |||
| 59 | int ret = esp_tls_conn_new_sync(host, strlen(host), port, &tls_cfg, tls); | ||
| 60 | if (ret < 0) { | ||
| 61 | ESP_LOGE(TAG, "TLS connect failed to %s", host); | ||
| 62 | esp_tls_conn_destroy(tls); | ||
| 63 | return ESP_FAIL; | ||
| 64 | } | ||
| 65 | |||
| 66 | char upgrade[512]; | ||
| 67 | snprintf(upgrade, sizeof(upgrade), | ||
| 68 | "GET %s HTTP/1.1\r\n" | ||
| 69 | "Host: %s\r\n" | ||
| 70 | "Upgrade: websocket\r\n" | ||
| 71 | "Connection: Upgrade\r\n" | ||
| 72 | "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" | ||
| 73 | "Sec-WebSocket-Version: 13\r\n" | ||
| 74 | "\r\n", | ||
| 75 | path, host); | ||
| 76 | |||
| 77 | int written = esp_tls_conn_write(tls, (const unsigned char *)upgrade, strlen(upgrade)); | ||
| 78 | if (written < 0) { | ||
| 79 | ESP_LOGE(TAG, "Failed to send upgrade request"); | ||
| 80 | esp_tls_conn_destroy(tls); | ||
| 81 | return ESP_FAIL; | ||
| 82 | } | ||
| 83 | |||
| 84 | char resp[1024]; | ||
| 85 | int rlen = esp_tls_conn_read(tls, (unsigned char *)resp, sizeof(resp) - 1); | ||
| 86 | if (rlen <= 0 || !strstr(resp, "101")) { | ||
| 87 | ESP_LOGE(TAG, "WebSocket upgrade failed (read %d bytes)", rlen); | ||
| 88 | esp_tls_conn_destroy(tls); | ||
| 89 | return ESP_FAIL; | ||
| 90 | } | ||
| 91 | |||
| 92 | cJSON *arr = cJSON_CreateArray(); | ||
| 93 | cJSON_AddItemToArray(arr, cJSON_CreateString("EVENT")); | ||
| 94 | cJSON_AddItemToArray(arr, cJSON_Parse(event_json)); | ||
| 95 | char *msg = cJSON_PrintUnformatted(arr); | ||
| 96 | cJSON_Delete(arr); | ||
| 97 | |||
| 98 | size_t msg_len = strlen(msg); | ||
| 99 | uint8_t ws_header[10]; | ||
| 100 | int header_len = 0; | ||
| 101 | ws_header[0] = 0x81; | ||
| 102 | if (msg_len <= 125) { | ||
| 103 | ws_header[1] = (uint8_t)msg_len; | ||
| 104 | header_len = 2; | ||
| 105 | } else if (msg_len <= 65535) { | ||
| 106 | ws_header[1] = 126; | ||
| 107 | ws_header[2] = (uint8_t)((msg_len >> 8) & 0xff); | ||
| 108 | ws_header[3] = (uint8_t)(msg_len & 0xff); | ||
| 109 | header_len = 4; | ||
| 110 | } else { | ||
| 111 | ws_header[1] = 127; | ||
| 112 | for (int i = 0; i < 8; i++) | ||
| 113 | ws_header[2 + i] = (uint8_t)((msg_len >> (56 - i * 8)) & 0xff); | ||
| 114 | header_len = 10; | ||
| 115 | } | ||
| 116 | |||
| 117 | esp_tls_conn_write(tls, ws_header, header_len); | ||
| 118 | esp_tls_conn_write(tls, (const unsigned char *)msg, msg_len); | ||
| 119 | |||
| 120 | free(msg); | ||
| 121 | |||
| 122 | uint8_t resp_buf[256]; | ||
| 123 | int resp_len = esp_tls_conn_read(tls, resp_buf, sizeof(resp_buf) - 1); | ||
| 124 | if (resp_len > 0) { | ||
| 125 | resp_buf[resp_len] = '\0'; | ||
| 126 | int mask_len = (resp_buf[1] & 0x80) ? 4 : 0; | ||
| 127 | int payload_offset = 2 + mask_len; | ||
| 128 | if (resp_len > payload_offset) { | ||
| 129 | ESP_LOGI(TAG, "Relay response: %.*s", resp_len - payload_offset, | ||
| 130 | (char *)resp_buf + payload_offset); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | uint8_t close_frame[2] = {0x88, 0x00}; | ||
| 135 | esp_tls_conn_write(tls, close_frame, 2); | ||
| 136 | esp_tls_conn_destroy(tls); | ||
| 137 | |||
| 138 | ESP_LOGI(TAG, "Published to %s", host); | ||
| 139 | return ESP_OK; | ||
| 140 | } | ||
| 141 | |||
| 142 | static char *build_wifistr_event(void) | ||
| 143 | { | ||
| 144 | const tollgate_identity_t *id = identity_get(); | ||
| 145 | if (!id || !id->initialized) { | ||
| 146 | ESP_LOGE(TAG, "Identity not initialized"); | ||
| 147 | return NULL; | ||
| 148 | } | ||
| 149 | |||
| 150 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 151 | |||
| 152 | cJSON *tags = cJSON_CreateArray(); | ||
| 153 | |||
| 154 | cJSON *d_tag = cJSON_CreateArray(); | ||
| 155 | cJSON_AddItemToArray(d_tag, cJSON_CreateString("d")); | ||
| 156 | cJSON_AddItemToArray(d_tag, cJSON_CreateString(id->npub_hex)); | ||
| 157 | cJSON_AddItemToArray(tags, d_tag); | ||
| 158 | |||
| 159 | cJSON *ssid_tag = cJSON_CreateArray(); | ||
| 160 | cJSON_AddItemToArray(ssid_tag, cJSON_CreateString("ssid")); | ||
| 161 | cJSON_AddItemToArray(ssid_tag, cJSON_CreateString(id->ap_ssid)); | ||
| 162 | cJSON_AddItemToArray(tags, ssid_tag); | ||
| 163 | |||
| 164 | cJSON *h_tag = cJSON_CreateArray(); | ||
| 165 | cJSON_AddItemToArray(h_tag, cJSON_CreateString("h")); | ||
| 166 | cJSON_AddItemToArray(h_tag, cJSON_CreateString("cashu-testnut")); | ||
| 167 | cJSON_AddItemToArray(tags, h_tag); | ||
| 168 | |||
| 169 | cJSON *sec_tag = cJSON_CreateArray(); | ||
| 170 | cJSON_AddItemToArray(sec_tag, cJSON_CreateString("security")); | ||
| 171 | cJSON_AddItemToArray(sec_tag, cJSON_CreateString("open")); | ||
| 172 | cJSON_AddItemToArray(tags, sec_tag); | ||
| 173 | |||
| 174 | cJSON *g_tag = cJSON_CreateArray(); | ||
| 175 | cJSON_AddItemToArray(g_tag, cJSON_CreateString("g")); | ||
| 176 | cJSON_AddItemToArray(g_tag, cJSON_CreateString(cfg->nostr_geohash)); | ||
| 177 | cJSON_AddItemToArray(tags, g_tag); | ||
| 178 | |||
| 179 | cJSON *c_tag = cJSON_CreateArray(); | ||
| 180 | cJSON_AddItemToArray(c_tag, cJSON_CreateString("c")); | ||
| 181 | cJSON_AddItemToArray(c_tag, cJSON_CreateString("cashu")); | ||
| 182 | cJSON_AddItemToArray(tags, c_tag); | ||
| 183 | |||
| 184 | char content[512]; | ||
| 185 | snprintf(content, sizeof(content), | ||
| 186 | "TollGate WiFi hotspot: %s | Price: %d sats/%dms | Mint: %s", | ||
| 187 | id->ap_ssid, cfg->price_per_step, cfg->step_size_ms, cfg->mint_url); | ||
| 188 | |||
| 189 | char *tags_str = cJSON_PrintUnformatted(tags); | ||
| 190 | cJSON_Delete(tags); | ||
| 191 | |||
| 192 | nostr_event_t event; | ||
| 193 | nostr_event_init(&event, id->npub_hex, 38787, tags_str, content); | ||
| 194 | nostr_event_sign(&event, id->nsec); | ||
| 195 | free(tags_str); | ||
| 196 | |||
| 197 | char *event_json = malloc(2048); | ||
| 198 | if (!event_json) return NULL; | ||
| 199 | |||
| 200 | esp_err_t ret = nostr_event_to_json(&event, event_json, 2048); | ||
| 201 | if (ret != ESP_OK) { | ||
| 202 | free(event_json); | ||
| 203 | return NULL; | ||
| 204 | } | ||
| 205 | |||
| 206 | return event_json; | ||
| 207 | } | ||
| 208 | |||
| 209 | esp_err_t wifistr_publish(void) | ||
| 210 | { | ||
| 211 | char *event_json = build_wifistr_event(); | ||
| 212 | if (!event_json) { | ||
| 213 | ESP_LOGE(TAG, "Failed to build wifistr event"); | ||
| 214 | return ESP_FAIL; | ||
| 215 | } | ||
| 216 | |||
| 217 | ESP_LOGI(TAG, "Wifistr event: %s", event_json); | ||
| 218 | |||
| 219 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 220 | esp_err_t last_err = ESP_FAIL; | ||
| 221 | |||
| 222 | for (int i = 0; i < cfg->nostr_relay_count; i++) { | ||
| 223 | esp_err_t err = ws_send_to_relay(cfg->nostr_relays[i], event_json); | ||
| 224 | if (err == ESP_OK) last_err = ESP_OK; | ||
| 225 | vTaskDelay(pdMS_TO_TICKS(500)); | ||
| 226 | } | ||
| 227 | |||
| 228 | free(event_json); | ||
| 229 | return last_err; | ||
| 230 | } | ||
| 231 | |||
| 232 | static void publish_task(void *pvParameters) | ||
| 233 | { | ||
| 234 | wifistr_publish(); | ||
| 235 | vTaskDelete(NULL); | ||
| 236 | } | ||
| 237 | |||
| 238 | static void timer_callback(TimerHandle_t timer) | ||
| 239 | { | ||
| 240 | xTaskCreate(publish_task, "wifistr_pub", 16384, NULL, 3, NULL); | ||
| 241 | } | ||
| 242 | |||
| 243 | void wifistr_start_periodic(int interval_s) | ||
| 244 | { | ||
| 245 | if (s_publish_timer) return; | ||
| 246 | s_publish_timer = xTimerCreate("wifistr", pdMS_TO_TICKS(interval_s * 1000), | ||
| 247 | pdTRUE, NULL, timer_callback); | ||
| 248 | if (s_publish_timer) { | ||
| 249 | xTimerStart(s_publish_timer, 0); | ||
| 250 | ESP_LOGI(TAG, "Periodic publish every %ds", interval_s); | ||
| 251 | } | ||
| 252 | } | ||
diff --git a/main/wifistr.h b/main/wifistr.h new file mode 100644 index 0000000..843b6be --- /dev/null +++ b/main/wifistr.h | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | #ifndef WIFISTR_H | ||
| 2 | #define WIFISTR_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | |||
| 6 | esp_err_t wifistr_publish(void); | ||
| 7 | |||
| 8 | void wifistr_start_periodic(int interval_s); | ||
| 9 | |||
| 10 | #endif | ||
diff --git a/nucula_src b/nucula_src new file mode 160000 | |||
| Subproject 0ecd83c404455b0885b35e15eb1d7f5447bc489 | |||
| @@ -961,8 +961,8 @@ CONFIG_ESP32S3_UNIVERSAL_MAC_ADDRESSES=4 | |||
| 961 | # | 961 | # |
| 962 | # Sleep Config | 962 | # Sleep Config |
| 963 | # | 963 | # |
| 964 | # CONFIG_ESP_SLEEP_POWER_DOWN_FLASH is not set | ||
| 965 | CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND=y | 964 | CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND=y |
| 965 | CONFIG_ESP_SLEEP_PSRAM_LEAKAGE_WORKAROUND=y | ||
| 966 | CONFIG_ESP_SLEEP_MSPI_NEED_ALL_IO_PU=y | 966 | CONFIG_ESP_SLEEP_MSPI_NEED_ALL_IO_PU=y |
| 967 | CONFIG_ESP_SLEEP_RTC_BUS_ISO_WORKAROUND=y | 967 | CONFIG_ESP_SLEEP_RTC_BUS_ISO_WORKAROUND=y |
| 968 | CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND=y | 968 | CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND=y |
| @@ -1071,7 +1071,36 @@ CONFIG_PM_RESTORE_CACHE_TAGMEM_AFTER_LIGHT_SLEEP=y | |||
| 1071 | # | 1071 | # |
| 1072 | # ESP PSRAM | 1072 | # ESP PSRAM |
| 1073 | # | 1073 | # |
| 1074 | # CONFIG_SPIRAM is not set | 1074 | CONFIG_SPIRAM=y |
| 1075 | |||
| 1076 | # | ||
| 1077 | # SPI RAM config | ||
| 1078 | # | ||
| 1079 | # CONFIG_SPIRAM_MODE_QUAD is not set | ||
| 1080 | CONFIG_SPIRAM_MODE_OCT=y | ||
| 1081 | CONFIG_SPIRAM_TYPE_AUTO=y | ||
| 1082 | # CONFIG_SPIRAM_TYPE_ESPPSRAM64 is not set | ||
| 1083 | CONFIG_SPIRAM_CLK_IO=30 | ||
| 1084 | CONFIG_SPIRAM_CS_IO=26 | ||
| 1085 | # CONFIG_SPIRAM_XIP_FROM_PSRAM is not set | ||
| 1086 | # CONFIG_SPIRAM_FETCH_INSTRUCTIONS is not set | ||
| 1087 | # CONFIG_SPIRAM_RODATA is not set | ||
| 1088 | CONFIG_SPIRAM_SPEED_80M=y | ||
| 1089 | # CONFIG_SPIRAM_SPEED_40M is not set | ||
| 1090 | CONFIG_SPIRAM_SPEED=80 | ||
| 1091 | # CONFIG_SPIRAM_ECC_ENABLE is not set | ||
| 1092 | CONFIG_SPIRAM_BOOT_INIT=y | ||
| 1093 | # CONFIG_SPIRAM_IGNORE_NOTFOUND is not set | ||
| 1094 | # CONFIG_SPIRAM_USE_MEMMAP is not set | ||
| 1095 | # CONFIG_SPIRAM_USE_CAPS_ALLOC is not set | ||
| 1096 | CONFIG_SPIRAM_USE_MALLOC=y | ||
| 1097 | CONFIG_SPIRAM_MEMTEST=y | ||
| 1098 | CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384 | ||
| 1099 | # CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP is not set | ||
| 1100 | CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 | ||
| 1101 | # CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY is not set | ||
| 1102 | # CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY is not set | ||
| 1103 | # end of SPI RAM config | ||
| 1075 | # end of ESP PSRAM | 1104 | # end of ESP PSRAM |
| 1076 | 1105 | ||
| 1077 | # | 1106 | # |
| @@ -1331,6 +1360,7 @@ CONFIG_FATFS_CODEPAGE=437 | |||
| 1331 | CONFIG_FATFS_FS_LOCK=0 | 1360 | CONFIG_FATFS_FS_LOCK=0 |
| 1332 | CONFIG_FATFS_TIMEOUT_MS=10000 | 1361 | CONFIG_FATFS_TIMEOUT_MS=10000 |
| 1333 | CONFIG_FATFS_PER_FILE_CACHE=y | 1362 | CONFIG_FATFS_PER_FILE_CACHE=y |
| 1363 | CONFIG_FATFS_ALLOC_PREFER_EXTRAM=y | ||
| 1334 | # CONFIG_FATFS_USE_FASTSEEK is not set | 1364 | # CONFIG_FATFS_USE_FASTSEEK is not set |
| 1335 | CONFIG_FATFS_USE_STRFUNC_NONE=y | 1365 | CONFIG_FATFS_USE_STRFUNC_NONE=y |
| 1336 | # CONFIG_FATFS_USE_STRFUNC_WITHOUT_CRLF_CONV is not set | 1366 | # CONFIG_FATFS_USE_STRFUNC_WITHOUT_CRLF_CONV is not set |
| @@ -1400,6 +1430,7 @@ CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER=y | |||
| 1400 | # | 1430 | # |
| 1401 | # Extra | 1431 | # Extra |
| 1402 | # | 1432 | # |
| 1433 | CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=y | ||
| 1403 | # end of Extra | 1434 | # end of Extra |
| 1404 | 1435 | ||
| 1405 | CONFIG_FREERTOS_PORT=y | 1436 | CONFIG_FREERTOS_PORT=y |
| @@ -1645,6 +1676,7 @@ CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_NONE=y | |||
| 1645 | # mbedTLS | 1676 | # mbedTLS |
| 1646 | # | 1677 | # |
| 1647 | CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y | 1678 | CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y |
| 1679 | # CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC is not set | ||
| 1648 | # CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set | 1680 | # CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set |
| 1649 | # CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set | 1681 | # CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set |
| 1650 | CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y | 1682 | CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y |
| @@ -1809,12 +1841,15 @@ CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y | |||
| 1809 | # CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set | 1841 | # CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set |
| 1810 | # end of Newlib | 1842 | # end of Newlib |
| 1811 | 1843 | ||
| 1844 | CONFIG_STDATOMIC_S32C1I_SPIRAM_WORKAROUND=y | ||
| 1845 | |||
| 1812 | # | 1846 | # |
| 1813 | # NVS | 1847 | # NVS |
| 1814 | # | 1848 | # |
| 1815 | # CONFIG_NVS_ENCRYPTION is not set | 1849 | # CONFIG_NVS_ENCRYPTION is not set |
| 1816 | # CONFIG_NVS_ASSERT_ERROR_CHECK is not set | 1850 | # CONFIG_NVS_ASSERT_ERROR_CHECK is not set |
| 1817 | # CONFIG_NVS_LEGACY_DUP_KEYS_COMPATIBILITY is not set | 1851 | # CONFIG_NVS_LEGACY_DUP_KEYS_COMPATIBILITY is not set |
| 1852 | # CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM is not set | ||
| 1818 | # end of NVS | 1853 | # end of NVS |
| 1819 | 1854 | ||
| 1820 | # | 1855 | # |
| @@ -2114,7 +2149,6 @@ CONFIG_POST_EVENTS_FROM_IRAM_ISR=y | |||
| 2114 | CONFIG_GDBSTUB_SUPPORT_TASKS=y | 2149 | CONFIG_GDBSTUB_SUPPORT_TASKS=y |
| 2115 | CONFIG_GDBSTUB_MAX_TASKS=32 | 2150 | CONFIG_GDBSTUB_MAX_TASKS=32 |
| 2116 | # CONFIG_OTA_ALLOW_HTTP is not set | 2151 | # CONFIG_OTA_ALLOW_HTTP is not set |
| 2117 | # CONFIG_ESP_SYSTEM_PD_FLASH is not set | ||
| 2118 | CONFIG_ESP32S3_DEEP_SLEEP_WAKEUP_DELAY=2000 | 2152 | CONFIG_ESP32S3_DEEP_SLEEP_WAKEUP_DELAY=2000 |
| 2119 | CONFIG_ESP_SLEEP_DEEP_SLEEP_WAKEUP_DELAY=2000 | 2153 | CONFIG_ESP_SLEEP_DEEP_SLEEP_WAKEUP_DELAY=2000 |
| 2120 | CONFIG_ESP32S3_RTC_CLK_SRC_INT_RC=y | 2154 | CONFIG_ESP32S3_RTC_CLK_SRC_INT_RC=y |
| @@ -2130,7 +2164,9 @@ CONFIG_ESP32_PHY_MAX_TX_POWER=20 | |||
| 2130 | # CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set | 2164 | # CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set |
| 2131 | CONFIG_ESP_SYSTEM_PM_POWER_DOWN_CPU=y | 2165 | CONFIG_ESP_SYSTEM_PM_POWER_DOWN_CPU=y |
| 2132 | CONFIG_PM_POWER_DOWN_TAGMEM_IN_LIGHT_SLEEP=y | 2166 | CONFIG_PM_POWER_DOWN_TAGMEM_IN_LIGHT_SLEEP=y |
| 2133 | # CONFIG_ESP32S3_SPIRAM_SUPPORT is not set | 2167 | CONFIG_ESP32S3_SPIRAM_SUPPORT=y |
| 2168 | CONFIG_DEFAULT_PSRAM_CLK_IO=30 | ||
| 2169 | CONFIG_DEFAULT_PSRAM_CS_IO=26 | ||
| 2134 | # CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set | 2170 | # CONFIG_ESP32S3_DEFAULT_CPU_FREQ_80 is not set |
| 2135 | CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160=y | 2171 | CONFIG_ESP32S3_DEFAULT_CPU_FREQ_160=y |
| 2136 | # CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set | 2172 | # CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240 is not set |
| @@ -2216,6 +2252,7 @@ CONFIG_TIMER_TASK_PRIORITY=1 | |||
| 2216 | CONFIG_TIMER_TASK_STACK_DEPTH=2048 | 2252 | CONFIG_TIMER_TASK_STACK_DEPTH=2048 |
| 2217 | CONFIG_TIMER_QUEUE_LENGTH=10 | 2253 | CONFIG_TIMER_QUEUE_LENGTH=10 |
| 2218 | # CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set | 2254 | # CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set |
| 2255 | CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY=y | ||
| 2219 | # CONFIG_HAL_ASSERTION_SILIENT is not set | 2256 | # CONFIG_HAL_ASSERTION_SILIENT is not set |
| 2220 | # CONFIG_L2_TO_L3_COPY is not set | 2257 | # CONFIG_L2_TO_L3_COPY is not set |
| 2221 | CONFIG_ESP_GRATUITOUS_ARP=y | 2258 | CONFIG_ESP_GRATUITOUS_ARP=y |