From eeba74a4a1c011e85e33dea4252b381e35a64ea4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 13:21:25 +0530 Subject: feat: multi-mint wallet with health tracking, WPA auto-detect, display gating Squash merge of feature/multi-mint-support (21 commits): Multi-mint wallet: - Accept payments from 4 mints: minibits, coinos, 21mint, lnvoltz - Periodic health probing (300s interval, 3 recovery threshold) - Multi-wallet init with nucula_wallet_init_multi() - /mints and /wallet API endpoints WPA auto-detect: - wifi_auth_mode config field (default WPA2, supports WPA3) - Runtime mapping to wifi_auth_mode_t in STA config Display gating: - display_enabled config field (default true) - Guards display_init/display_update per-board Bug fixes: - 3s delay before service start prevents lwip mem_free assertion - Real npub in discovery (identity_get()->npub_hex) - Health probe interval 300s (production value) - Duplicate services_start_task call removed - UTF-8 arrow replaced with ASCII in log message Tests: 61+14 unit tests passing, firmware builds clean --- docs/MULTI_MINT_DESIGN.md | 511 +++++++++++++++++++++++++++++++++++++++ docs/REBASE-SQUASH-MERGE-PLAN.md | 92 +++++++ docs/WPA-AUTO-DETECT-PLAN.md | 121 +++++++++ 3 files changed, 724 insertions(+) create mode 100644 docs/MULTI_MINT_DESIGN.md create mode 100644 docs/REBASE-SQUASH-MERGE-PLAN.md create mode 100644 docs/WPA-AUTO-DETECT-PLAN.md (limited to 'docs') diff --git a/docs/MULTI_MINT_DESIGN.md b/docs/MULTI_MINT_DESIGN.md new file mode 100644 index 0000000..f4db06b --- /dev/null +++ b/docs/MULTI_MINT_DESIGN.md @@ -0,0 +1,511 @@ +# Multi-Mint Support — Design Document + +**Branch**: `feature/multi-mint-support` +**Date**: 2026-05-18 +**Status**: Implementation Phase + +--- + +## 1. Overview + +Extend the ESP32 TollGate firmware to accept Cashu ecash payments from **multiple mints** instead of a single hardcoded mint URL. The system must: + +- Accept tokens from any of 4 configured mints +- Track mint reachability via periodic health probes +- Only accept payments from mints that are currently reachable (successful swap) +- Expose all reachable mints in the discovery endpoint and captive portal +- Manage per-mint wallets with independent keysets and proof storage + +### Supported Mints + +| Mint | URL | +|------|-----| +| Minibits | `https://mint.minibits.cash/Bitcoin` | +| CoinOS | `https://mint.coinos.io` | +| 21mint | `https://21mint.me` | +| LNVoltz | `https://mint.lnvoltz.com` | + +All verified reachable via `GET /v1/info` (HTTP 200). + +--- + +## 2. Architecture + +``` +┌─────────────────────────────────────────────────────┐ +│ config.json │ +│ "accepted_mints": ["url1", "url2", "url3", "url4"] │ +└──────────────────────┬──────────────────────────────┘ + │ + ┌────────────┼────────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌───────────────┐ + │ Config │ │ Health │ │ Multi-Wallet │ + │ Layer │ │ Tracker │ │ (Nucula) │ + │ │ │ │ │ │ + │ accepted_ │ │ probe │ │ Wallet[0] → │ + │ mints[] │ │ every │ │ mint A │ + │ │ │ 5min │ │ Wallet[1] → │ + │ │ │ │ │ mint B │ + │ │ │ recovery │ │ ... │ + │ │ │ thresh=3 │ │ │ + └─────┬─────┘ └────┬─────┘ └───────┬───────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────────────────────────────────────┐ + │ cashu_is_mint_accepted() │ + │ in config AND reachable → accept │ + └────────────────────┬────────────────────────────┘ + │ + ┌─────────────┼──────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌───────────┐ ┌───────────┐ + │Discovery │ │ Captive │ │ Payment │ + │ Endpoint │ │ Portal │ │ Handler │ + │ │ │ │ │ │ + │ 1 tag │ │ mint list │ │ find right│ + │ per │ │ with │ │ wallet, │ + │ reachable│ │ indicators│ │ receive() │ + │ mint │ │ │ │ │ + └──────────┘ └───────────┘ └───────────┘ +``` + +--- + +## 3. Phase Details + +### Phase 1: Config Layer — Multi-Mint Array + +**Files**: `main/config.h`, `main/config.c` + +**Changes**: + +- Increase `TOLLGATE_MAX_MINT_URLS` from `3` to `8` +- Add to `tollgate_config_t`: + ```c + char accepted_mints[TOLLGATE_MAX_MINT_URLS][256]; + int accepted_mint_count; + ``` +- Keep existing `mint_url[256]` for backward compatibility +- Parse new `"accepted_mints"` JSON array from config.json +- If `"accepted_mints"` absent, populate from `"mint_url"` (backward compat) +- Update default config.json generation to include `"accepted_mints"` + +**Config.json format** (new): +```json +{ + "nsec": "...", + "accepted_mints": [ + "https://mint.minibits.cash/Bitcoin", + "https://mint.coinos.io", + "https://21mint.me", + "https://mint.lnvoltz.com" + ], + "mint_url": "https://mint.minibits.cash/Bitcoin" +} +``` + +The `"mint_url"` field is kept as fallback / primary mint identifier. + +--- + +### Phase 2: Mint Acceptance — Multi-Mint Check + +**Files**: `main/cashu.c`, `main/cashu.h` + +Replace single-mint check in `cashu_is_mint_accepted()`: + +```c +bool cashu_is_mint_accepted(const char *mint_url) { + if (!mint_url || mint_url[0] == '\0') return false; + const tollgate_config_t *cfg = tollgate_config_get(); + for (int i = 0; i < cfg->accepted_mint_count; i++) { + if (strstr(mint_url, cfg->accepted_mints[i]) != NULL) + return true; + } + return false; +} +``` + +This is the config-only check. Phase 4 adds health gating. + +--- + +### Phase 3: Mint Health Tracker + +**New files**: `main/mint_health.h`, `main/mint_health.c` + +**Data structures**: + +```c +#define MINT_HEALTH_MAX 8 +#define MINT_HEALTH_PROBE_INTERVAL_S 300 +#define MINT_HEALTH_PROBE_TIMEOUT_MS 15000 +#define MINT_HEALTH_RECOVERY_THRESHOLD 3 + +typedef struct { + char url[256]; + bool reachable; + uint8_t consecutive_successes; + int64_t last_probe_ms; + int last_http_status; +} mint_status_t; + +typedef void (*mint_health_changed_cb)(void); +``` + +**Public API**: + +```c +esp_err_t mint_health_init(const char urls[][256], int count); +void mint_health_start(void); +void mint_health_stop(void); +const mint_status_t *mint_health_get_all(int *out_count); +bool mint_health_is_reachable(const char *url); +void mint_health_mark_unreachable(const char *url); +void mint_health_register_callback(mint_health_changed_cb cb); +``` + +**Probing logic** (FreeRTOS task): + +| Parameter | Value | Rationale | +|-----------|-------|-----------| +| Endpoint | `GET {url}/v1/info` | Lightweight, no auth required | +| Timeout | 15 seconds | ESP32 resource-constrained, 30s too long | +| Interval | 5 minutes (`vTaskDelay`) | Matches Go reference | +| Failure | Immediate | Single failed probe → unreachable | +| Recovery | 3 consecutive successes | 15 min sustained health (matches Go) | +| Initial | Success → reachable immediately | Set `consecutive_successes = threshold` | + +**Thread safety**: Single FreeRTOS mutex protecting the status array. Callbacks dispatched after releasing the mutex. + +**Reference**: Modeled after Go `MintHealthTracker` in `tollgate-module-basic-go/src/merchant/mint_health_tracker.go`. + +--- + +### Phase 4: Health-Aware Acceptance + +**Files**: `main/cashu.c` + +Update `cashu_is_mint_accepted()` to gate on health: + +```c +bool cashu_is_mint_accepted(const char *mint_url) { + if (!mint_url || mint_url[0] == '\0') return false; + const tollgate_config_t *cfg = tollgate_config_get(); + for (int i = 0; i < cfg->accepted_mint_count; i++) { + if (strstr(mint_url, cfg->accepted_mints[i]) != NULL) + return mint_health_is_reachable(mint_url); + } + return false; +} +``` + +On cold start with no internet: no mints reachable → no tokens accepted (matches Go degraded behavior). Once first probe succeeds, that mint becomes reachable and tokens are accepted. + +--- + +### Phase 5: Multi-Mint Discovery Endpoint + +**File**: `main/tollgate_api.c` + +Replace single `price_per_step` tag in `api_get_discovery()` with one per reachable mint: + +```c +int count; +const mint_status_t *mints = mint_health_get_all(&count); +for (int i = 0; i < count; i++) { + if (!mints[i].reachable) continue; + cJSON *price_tag = cJSON_CreateArray(); + cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step")); + cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu")); + cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str)); + cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat")); + cJSON_AddItemToArray(price_tag, cJSON_CreateString(mints[i].url)); + cJSON_AddItemToArray(price_tag, cJSON_CreateString("1")); + cJSON_AddItemToArray(tags, price_tag); +} +``` + +If no mints are reachable, include a single tag with the primary `mint_url` as fallback (degraded mode signal). + +--- + +### Phase 6: Multi-Mint Captive Portal UI + +**File**: `main/captive_portal.c` + +**Changes**: + +1. Replace `__MINT_URL__` template placeholder with `__MINT_LIST__` +2. Generate HTML list of reachable mints with green dot indicators +3. Unreachable mints shown greyed out (informative but not selectable) +4. New API endpoint `GET /api/mints` → JSON array of mint status + +**Portal mint list HTML**: +```html +
+
SUPPORTED MINTS
+
+ + mint.minibits.cash/Bitcoin +
+
+ + mint.coinos.io +
+
+``` + +**Auto-refresh**: JS polls `GET /api/mints` every 30s to update indicators. + +--- + +### Phase 7: Multi-Mint Wallet (Nucula) + +**Files**: `components/nucula_lib/nucula_wallet.h`, `components/nucula_lib/nucula_wallet.cpp` + +**Approach**: Multi-wallet — one `cashu::Wallet` instance per mint. + +**Why multi-wallet vs refactoring Wallet class**: +- Each mint has its own keysets, proofs, NVS slot — natural isolation +- No risk of cross-mint proof confusion +- `cashu::Wallet` class unchanged — zero regression risk +- NVS slot allocation already supported: `Wallet(url, ctx, nvs_slot)` +- `MAX_MINTS = 3` constant already defined in `wallet.hpp` + +**Internal structure**: +```cpp +static const int MAX_WALLETS = 4; +static cashu::Wallet *s_wallets[MAX_WALLETS]; +static int s_wallet_count = 0; +``` + +**API changes**: + +| Old | New | Behavior | +|-----|-----|----------| +| `nucula_wallet_init(url)` | `nucula_wallet_init_multi(urls, count)` | Create wallet per mint | +| `nucula_wallet_init(url)` | Keep as compat wrapper | Creates single-wallet array | +| `nucula_wallet_receive(token)` | Same | Decode mint from token, route to correct wallet | +| `nucula_wallet_balance()` | Same | Sum across all wallets | +| `nucula_wallet_send(amount, ...)` | Same | Select wallet with sufficient balance | +| `nucula_wallet_swap_all()` | Same | Swap all wallets | +| `nucula_wallet_proof_count()` | Same | Sum across all wallets | + +**Token routing in `receive()`**: +1. Decode token to extract `mint_url` from the token JSON +2. Find matching wallet by URL +3. Call `wallet->receive(token, proofs_out)` on that wallet +4. If no matching wallet found, try first wallet as fallback + +**NVS slot mapping**: + +| Mint index | NVS slot | NVS keys | +|-----------|----------|----------| +| 0 | 0 | `url_0`, `proofs_0`, `kn_0`, `k_0_0`..`k_0_9` | +| 1 | 1 | `url_1`, `proofs_1`, `kn_1`, `k_1_0`..`k_1_9` | +| 2 | 2 | `url_2`, `proofs_2`, `kn_2`, `k_2_0`..`k_2_9` | +| 3 | 3 | `url_3`, `proofs_3`, `kn_3`, `k_3_0`..`k_3_9` | + +--- + +### Phase 8: Service Startup Integration + +**File**: `main/tollgate_main.c` + +**Changes to `start_services()`**: + +``` +1. firewall_init() +2. session_manager_init() +3. mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count) +4. mint_health_start() ← async probing begins +5. nucula_wallet_init_multi(cfg->accepted_mints, cfg->accepted_mint_count) +6. lightning_payout_init() +7. dns_server_start() +8. captive_portal_start() +9. tollgate_api_start() +10. wifistr_publish() +11. cvm_server_start() +``` + +**Health callback**: When reachable set changes, trigger wifistr re-publish to update Nostr kind 38787 event with current mint list. + +--- + +## 4. Data Flow + +### Payment Flow (Multi-Mint) + +``` +Client POST cashuA token + │ + ▼ +api_post_payment() + ├── cashu_decode_token() → extract mint_url from token + ├── cashu_is_mint_accepted(mint_url) + │ ├── Check in cfg->accepted_mints[] → config match + │ └── Check mint_health_is_reachable(mint_url) → health gate + ├── cashu_check_proof_states(mint_url, token) → POST {mint_url}/v1/checkstate + ├── session_create(client_ip, allotment) + └── nucula_wallet_receive(token_str) + ├── Decode token → extract mint_url + ├── Find wallet for that mint_url + └── wallet->receive(token, proofs_out) +``` + +### Health Probe Flow + +``` +mint_health_task (FreeRTOS, 5min interval) + │ + for each mint in accepted_mints[]: + │ + ├── GET {url}/v1/info (15s timeout) + │ + ├── Success? + │ ├── YES → consecutive_successes++ + │ │ if >= RECOVERY_THRESHOLD → mark reachable + │ └── NO → mark unreachable, reset consecutive_successes = 0 + │ + └── Reachable set changed? → fire callback +``` + +--- + +## 5. Error Handling + +| Scenario | Behavior | +|----------|----------| +| No internet at boot | No mints reachable, no tokens accepted until probe succeeds | +| All mints unreachable | Discovery shows primary mint (degraded), portal shows "Checking mints..." | +| Mint goes down mid-operation | `cashu_check_proof_states` fails → 502 Bad Gateway to client | +| Wallet init fails for one mint | Skip that mint, log error, continue with others | +| NVS full for multi-wallet | Fallback to single wallet, log warning | +| Probe timeout | Treat as unreachable (same as connection refused) | + +--- + +## 6. Memory Budget + +| Component | Estimated RAM | Notes | +|-----------|--------------|-------| +| `mint_status_t[8]` | ~2 KB | 256-byte URLs + metadata | +| Health probe task stack | 8 KB | HTTP client needs stack | +| `cashu::Wallet` per mint | ~4 KB each | Keysets + proofs in NVS, not RAM | +| 4 wallets total | ~16 KB | Within ESP32-S3 512KB SRAM budget | +| Health task TLS | ~40 KB | esp_http_client TLS buffer | +| **Total new overhead** | **~66 KB** | Acceptable with 512KB SRAM + 8MB PSRAM | + +--- + +## 7. Testing Strategy + +### Unit Tests (host, `tests/unit/`) + +| Test File | Covers | +|-----------|--------| +| `test_cashu.c` | Multi-mint acceptance (config-only) | +| `test_mint_health.c` | Health state machine, recovery, callbacks | +| `test_config.c` | Config parsing of `accepted_mints` array | + +### Integration Tests (device) + +1. Flash to Board A, verify discovery shows multiple mints +2. Send token from each mint, verify accepted +3. Block one mint at firewall level, verify becomes unreachable +4. Verify recovery after unblocking + +### E2E Tests (Playwright) + +1. Captive portal shows mint list with indicators +2. Pay with token from mint A → success +3. Pay with token from unreachable mint → error shown in portal + +--- + +## 8. Risks and Mitigations + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|------------| +| TLS memory pressure with 4 wallets | Medium | High | Each wallet shares single TLS context; only probe makes concurrent HTTP | +| NVS key namespace collision | Low | High | Use distinct `nvs_slot` per wallet (0-3) | +| Keyset loading OOM on multiple mints | Medium | Medium | Cap keysets per wallet at `MAX_KEYSETS=10` | +| Health probe blocks other tasks | Low | Medium | Dedicated FreeRTOS task, low priority | +| Backward compatibility break | Low | High | `mint_url` field still works as fallback | + +--- + +## 9. Backward Compatibility + +- Existing `config.json` with only `"mint_url"` → works (populates `accepted_mints[0]` from it) +- Existing SPIFFS images → no change needed +- NVS data → compatible (single wallet stays at slot 0) +- API endpoints → same paths, discovery just has more tags +- Captive portal → same UI flow, more mints shown + +--- + +## 10. Git Worktree Strategy + +Multiple LLM sessions work on this repo simultaneously. To avoid conflicts: + +### Setup + +``` +# Main worktree stays on master for other sessions +git -C /home/c03rad0r/esp32-tollgate checkout master + +# Dedicated worktree for this feature +git -C /home/c03rad0r/esp32-tollgate worktree add /home/c03rad0r/esp32-tollgate-multi-mint feature/multi-mint-support +``` + +### Worktree Locations + +| Path | Branch | Purpose | +|------|--------|---------| +| `/home/c03rad0r/esp32-tollgate` | `master` | Main worktree, shared with other sessions | +| `/home/c03rad0r/esp32-tollgate-multi-mint` | `feature/multi-mint-support` | This feature's isolated workspace | + +### Conflict Avoidance Rules + +| Rule | Why | +|------|-----| +| All edits happen in `/home/c03rad0r/esp32-tollgate-multi-mint` | Other sessions keep their own checkout untouched | +| Push after every green test | Other sessions can `git pull` to see progress | +| Never modify `master` directly | Merge only when feature is complete and tested | +| `git pull --rebase` before push | Avoid merge commits if others pushed to same branch | + +### Cleanup (after merge) + +``` +git -C /home/c03rad0r/esp32-tollgate worktree remove /home/c03rad0r/esp32-tollgate-multi-mint +``` + +--- + +## 11. Implementation Checklist + +- [x] Create feature branch `feature/multi-mint-support` +- [x] Write design document `docs/MULTI_MINT_DESIGN.md` +- [x] Set up git worktree at `/home/c03rad0r/esp32-tollgate-multi-mint` +- [x] Phase 1: Config layer (`config.h`, `config.c`) — multi-mint array +- [x] Phase 2: Multi-mint acceptance (`cashu.c`) — iterate accepted_mints +- [x] Phase 3: Mint health tracker (`mint_health.h`, `mint_health.c`) — FreeRTOS probing task +- [x] Phase 4: Health-aware acceptance integration — gate on reachability +- [x] Phase 5: Multi-mint discovery endpoint (`tollgate_api.c`) — one tag per reachable mint +- [x] Phase 6: Multi-mint captive portal UI (`captive_portal.c`) — mint list with indicators +- [x] Phase 7: Multi-mint wallet (`nucula_wallet.h`, `nucula_wallet.cpp`) — multi-wallet approach +- [x] Phase 8: Service startup integration (`tollgate_main.c`) — init health + multi-wallet +- [x] Unit tests: update `test_cashu.c` for multi-mint acceptance (14/14 pass) +- [x] Unit tests: all 256 existing tests pass +- [x] Build verification (ESP-IDF compiles cleanly, no errors) +- [ ] Unit tests: `test_mint_health.c` — health state machine, recovery, callbacks +- [ ] Flash Board A and verify multi-mint discovery +- [ ] Flash Board B and verify multi-mint discovery +- [ ] Payment test with token from each supported mint +- [ ] Health probe test (verify reachable/unreachable transitions) +- [ ] Captive portal multi-mint display verification +- [ ] Push after every passing test (blocked: Nostr relay down) +- [ ] Merge to master diff --git a/docs/REBASE-SQUASH-MERGE-PLAN.md b/docs/REBASE-SQUASH-MERGE-PLAN.md new file mode 100644 index 0000000..f4bd98f --- /dev/null +++ b/docs/REBASE-SQUASH-MERGE-PLAN.md @@ -0,0 +1,92 @@ +# Multi-Mint Support — Rebase, Backup, Squash & Merge Plan + +## Goal +Rebase `feature/multi-mint-support` onto `master`, create a backup branch, squash all 20 commits into one clean commit, then merge to master. + +## Current State +- **Branch**: `feature/multi-mint-support` in worktree `/home/c03rad0r/esp32-tollgate-multi-mint` +- **Commits on branch**: 20 (since `master` at `77031f0`) +- **Remote**: `origin` → Nostr relay `relay.ngit.dev` (currently down) +- **Worktree**: shared repo — other sessions use other worktrees on different branches + +## Procedure + +### Phase 1: Pre-flight +1. Verify working tree is clean (no uncommitted changes) +2. Verify build passes +3. Verify unit tests pass (75/75) + +### Phase 2: Backup +4. Create backup branch `backup/multi-mint-support-pre-rebase` at current HEAD +5. Create backup branch `backup/multi-mint-support-pre-squash` (same point, used after rebase) + +### Phase 3: Rebase +6. `git rebase master` — rebase all 20 commits onto master +7. Resolve any conflicts +8. Verify build + tests still pass after rebase + +### Phase 4: Post-rebase Backup +9. Create `backup/multi-mint-support-rebased` at the rebased HEAD +10. This preserves every individual commit even after squashing + +### Phase 5: Squash +11. `git reset --soft master` — soft reset to master, keeping all changes staged +12. `git commit -m "feat: multi-mint Cashu wallet with health tracking, WPA auto-detect, CVM"` — single clean commit +13. Verify build + tests pass after squash + +### Phase 6: Merge +14. Merge to master (fast-forward since squashed branch sits on top) +15. Verify master builds and tests pass + +## Checklist + +### Pre-flight +- [ ] Working tree clean +- [ ] Build passes (`idf.py build`) +- [ ] Unit tests pass (`make test-unit`) + +### Backup +- [ ] `backup/multi-mint-support-pre-rebase` created at current HEAD (`3aa372c`) + +### Rebase +- [ ] `git rebase master` completed +- [ ] Conflicts resolved (if any) +- [ ] Build passes after rebase +- [ ] Unit tests pass after rebase + +### Post-rebase Backup +- [ ] `backup/multi-mint-support-rebased` created at rebased HEAD + +### Squash +- [ ] `git reset --soft master` done +- [ ] Single commit created with clean message +- [ ] Build passes after squash +- [ ] Unit tests pass after squash + +### Merge +- [ ] Merged to master (fast-forward) +- [ ] Master builds and tests pass +- [ ] Worktree updated + +## Remaining Work After Merge +1. **Push to Nostr relay** — blocked until `relay.ngit.dev` recovers +2. **NVS keyset storage** — `ESP_ERR_NVS_NOT_ENOUGH_SPACE` errors; factory partition at `0x10000` limits NVS to 24KB. Options: + - Store keysets in SPIFFS instead of NVS + - Compress keyset data + - Only cache active keysets +3. **Board A crash** — hardware-specific (~50s uptime), not software. Possible causes: + - Bad power supply on QinHeng UART adapter (serial `5A84017819`) + - Failing flash chip on that ESP32-S3 board + - Swap physical boards between UART adapters to isolate +4. **Integration test WiFi stability** — `test-multi-mint-*` targets fail on early steps because WiFi disconnects during 30s probe wait. Fix: `_connect-b-if-needed` should run before each curl call +5. **Display AXS15231B `ESP_ERR_NO_MEM`** — SPI flush fails every ~1s (307KB PSRAM framebuffer). The `display_enabled` config field allows disabling, but proper fix needs: + - Reduce framebuffer (partial refresh instead of full-screen) + - Or use SPI DMA with larger chunk sizes +6. **Health probe recovery threshold** — 3 consecutive successes × 300s interval = 15min before a mint is marked reachable. Consider reducing `MINT_HEALTH_RECOVERY_THRESHOLD` to 1 for initial probes +7. **Makefile WPA auto-detect** — `detect-wpa-security` + `generate-spiffs` + `flash-spiffs-{a,b,c}` targets added to `physical-router-test-automation/esp32/Makefile`. Needs separate commit/merge there + +## Backup Branch Names +| Branch | Purpose | Created At | +|--------|---------|------------| +| `backup/multi-mint-support-pre-rebase` | Full history before rebase | Before `git rebase master` | +| `backup/multi-mint-support-rebased` | All 20 commits after rebase | After `git rebase master` | diff --git a/docs/WPA-AUTO-DETECT-PLAN.md b/docs/WPA-AUTO-DETECT-PLAN.md new file mode 100644 index 0000000..dbbc0c8 --- /dev/null +++ b/docs/WPA-AUTO-DETECT-PLAN.md @@ -0,0 +1,121 @@ +# WPA Auto-Detect + STA Connectivity Fix + +## Problem + +`config.c:322` hardcodes `WIFI_AUTH_WPA3_PSK` as the STA auth threshold. The home +router (`EnterSSID-2.4GHz`) uses **WPA2**, so the ESP32 silently refuses +association and never gets internet. This blocks health probes, real payments, +and all downstream testing. + +Additionally, concurrent HTTP client connections at boot (wallet init + health probes ++ CVM + wifistr) caused an lwip `mem_free` assertion crash. + +## Solution + +### 1. Runtime WPA Threshold (Firmware) + +Add `wifi_auth_mode` field to `tollgate_config_t`. Parse it from `config.json` +as a string (`"WPA2"`, `"WPA3"`, `"WPA2_WPA3"`). Map to ESP-IDF +`wifi_auth_mode_t` enum at runtime. Default to `WIFI_AUTH_WPA2_PSK` which +accepts both WPA2 and WPA3 networks. + +### 2. Makefile Auto-Detect (Build Time) + +Add Makefile targets that scan WiFi with `nmcli`, detect WPA2 vs WPA3, and +generate a SPIFFS image with the correct `wifi_auth_mode` baked into +`config.json`. + +### 3. Reduced Probe Interval (Testing) + +Temporarily reduce `MINT_HEALTH_PROBE_INTERVAL_S` from 300 to 30 so health +probes actually fire during short board uptime windows. + +### 4. Boot Sequence Stabilization + +- 3-second delay before starting services after IP obtained (DNS stabilization) +- 5-second delay before initial health probes (DNS resolution readiness) + +## Files Changed + +| File | Change | +|------|--------| +| `main/config.h` | Add `wifi_auth_mode` field to `tollgate_config_t` | +| `main/config.c` | Parse `wifi_auth_mode` from config.json; use it in `tollgate_config_get_wifi()` | +| `main/mint_health.h` | Reduce probe interval 300 → 30 | +| `main/mint_health.c` | Add 5s DNS stabilization delay before initial probes | +| `main/tollgate_main.c` | Add 3s delay in services_start_task before starting services | +| `physical-router-test-automation/esp32/Makefile` | Add `detect-wpa-security`, `generate-spiffs`, `flash-spiffs-{a,b,c}` targets | + +## Hardware Verification (Board A, 2026-05-19) + +### STA Connectivity +- `STA auth threshold: WPA2 → 3` confirmed in serial log +- `Got IP:192.168.2.16, GW:192.168.2.1` — connected to home router via WPA2 +- SNTP time sync started +- No lwip crashes + +### Health Probes +- `Initial probe OK: https://mint.minibits.cash/Bitcoin (reachable)` +- `Initial probe OK: https://mint.coinos.io (reachable)` +- `Initial probe OK: https://21mint.me (reachable)` +- `Initial probe OK: https://mint.lnvoltz.com (reachable)` +- All 4 accepted mints confirmed reachable via `GET /v1/info` + +### API Endpoints +- `GET /:2121` (discovery) — kind=10021, metric=milliseconds, only reachable mint in price_per_step tag +- `GET /mints` — 4 mints with boolean `reachable` field (3 false, 1 true initially) +- `GET /wallet` — balance=0, proof_count=0 +- `GET /usage` — returns data +- `GET /whoami` — ip + mac + +### Multi-Wallet +- 4/4 wallets initialized with real keysets from live mints +- Keyset load confirmed for minibits, coinos, 21mint, lnvoltz +- NVS save errors for some keysets (ESP_ERR_NVS_NOT_ENOUGH_SPACE) — non-critical + +## Checklist + +### Firmware Changes +- [x] Add `wifi_auth_mode` string field (16 bytes) to `tollgate_config_t` in `config.h` +- [x] Parse `wifi_auth_mode` from `config.json` in `config.c` with default `"WPA2"` +- [x] Map `wifi_auth_mode` string to `wifi_auth_mode_t` in `tollgate_config_get_wifi()` +- [x] Remove hardcoded `WIFI_AUTH_WPA3_PSK` at `config.c:322` +- [x] Reduce `MINT_HEALTH_PROBE_INTERVAL_S` from 300 to 30 in `mint_health.h` +- [x] Add boot sequence delays to prevent lwip crash + +### Makefile Auto-Detect +- [x] Add `detect-wpa-security` target (nmcli scan → extract WPA mode for SSID) +- [x] Add `generate-spiffs` target (create config.json → spiffsgen.py) +- [x] Add `flash-spiffs-a`, `flash-spiffs-b`, `flash-spiffs-c` targets +- [ ] Wire `flash-{a,b,c}` to auto-generate SPIFFS before flashing (optional) + +### Build & Test +- [x] Build firmware — `idf.py build` passes +- [x] Unit tests pass — 75/75 (61 + 14 mint_health) +- [x] Wait for board unlock (no force-unlock) — Board A was available +- [x] Lock board, flash firmware + SPIFFS +- [x] Verify STA connects via serial (`Got IP:192.168.2.16`) +- [x] Verify health probes fire and mints show `reachable: true` +- [x] Run API endpoint tests (discovery, mints, wallet, usage, whoami) +- [x] Run `make test-discovery-b`, `make test-mints-b`, `make test-multi-mint-b` — all pass +- [x] All 4 mints confirmed reachable via health probes on Board B +- [x] Discovery shows 4 `price_per_step` tags (one per reachable mint) +- [x] Wallet has 40 sats balance from previous payment (proofs stored in NVS) +- [ ] Test 6 previously-skipped scenarios (real payment, unreachable transition, etc.) + +### Commit +- [x] Commit all changes with descriptive message (`2ad2ed4`) +- [ ] Push when Nostr relay recovers (relay.ngit.dev still down) + +## Commits +- `b387982` wip: disable display for stability testing +- `d21fc93` docs: update WPA auto-detect plan with hardware verification results +- `2ad2ed4` feat: WPA auto-detect, STA connectivity fix, lwip crash fix (feature/multi-mint-support) +- `64e81b5` feat: WPA auto-detect SPIFFS generation + per-board flash targets (physical-router-test-automation) + +## Remaining Work +1. Push commits when Nostr relay recovers +2. Test 6 skipped scenarios with stable board (reachable↔unreachable transitions, real payment, etc.) +3. Revert `MINT_HEALTH_PROBE_INTERVAL_S` from 30 to 300 before production +4. Address NVS `ESP_ERR_NVS_NOT_ENOUGH_SPACE` errors for keyset storage +5. Investigate display `ESP_ERR_NO_MEM` errors (307KB PSRAM framebuffer) -- cgit v1.2.3