diff options
Diffstat (limited to 'docs/MULTI_MINT_DESIGN.md')
| -rw-r--r-- | docs/MULTI_MINT_DESIGN.md | 511 |
1 files changed, 511 insertions, 0 deletions
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 @@ | |||
| 1 | # Multi-Mint Support — Design Document | ||
| 2 | |||
| 3 | **Branch**: `feature/multi-mint-support` | ||
| 4 | **Date**: 2026-05-18 | ||
| 5 | **Status**: Implementation Phase | ||
| 6 | |||
| 7 | --- | ||
| 8 | |||
| 9 | ## 1. Overview | ||
| 10 | |||
| 11 | Extend the ESP32 TollGate firmware to accept Cashu ecash payments from **multiple mints** instead of a single hardcoded mint URL. The system must: | ||
| 12 | |||
| 13 | - Accept tokens from any of 4 configured mints | ||
| 14 | - Track mint reachability via periodic health probes | ||
| 15 | - Only accept payments from mints that are currently reachable (successful swap) | ||
| 16 | - Expose all reachable mints in the discovery endpoint and captive portal | ||
| 17 | - Manage per-mint wallets with independent keysets and proof storage | ||
| 18 | |||
| 19 | ### Supported Mints | ||
| 20 | |||
| 21 | | Mint | URL | | ||
| 22 | |------|-----| | ||
| 23 | | Minibits | `https://mint.minibits.cash/Bitcoin` | | ||
| 24 | | CoinOS | `https://mint.coinos.io` | | ||
| 25 | | 21mint | `https://21mint.me` | | ||
| 26 | | LNVoltz | `https://mint.lnvoltz.com` | | ||
| 27 | |||
| 28 | All verified reachable via `GET /v1/info` (HTTP 200). | ||
| 29 | |||
| 30 | --- | ||
| 31 | |||
| 32 | ## 2. Architecture | ||
| 33 | |||
| 34 | ``` | ||
| 35 | ┌─────────────────────────────────────────────────────┐ | ||
| 36 | │ config.json │ | ||
| 37 | │ "accepted_mints": ["url1", "url2", "url3", "url4"] │ | ||
| 38 | └──────────────────────┬──────────────────────────────┘ | ||
| 39 | │ | ||
| 40 | ┌────────────┼────────────────┐ | ||
| 41 | ▼ ▼ ▼ | ||
| 42 | ┌──────────┐ ┌──────────┐ ┌───────────────┐ | ||
| 43 | │ Config │ │ Health │ │ Multi-Wallet │ | ||
| 44 | │ Layer │ │ Tracker │ │ (Nucula) │ | ||
| 45 | │ │ │ │ │ │ | ||
| 46 | │ accepted_ │ │ probe │ │ Wallet[0] → │ | ||
| 47 | │ mints[] │ │ every │ │ mint A │ | ||
| 48 | │ │ │ 5min │ │ Wallet[1] → │ | ||
| 49 | │ │ │ │ │ mint B │ | ||
| 50 | │ │ │ recovery │ │ ... │ | ||
| 51 | │ │ │ thresh=3 │ │ │ | ||
| 52 | └─────┬─────┘ └────┬─────┘ └───────┬───────┘ | ||
| 53 | │ │ │ | ||
| 54 | ▼ ▼ ▼ | ||
| 55 | ┌─────────────────────────────────────────────────┐ | ||
| 56 | │ cashu_is_mint_accepted() │ | ||
| 57 | │ in config AND reachable → accept │ | ||
| 58 | └────────────────────┬────────────────────────────┘ | ||
| 59 | │ | ||
| 60 | ┌─────────────┼──────────────┐ | ||
| 61 | ▼ ▼ ▼ | ||
| 62 | ┌──────────┐ ┌───────────┐ ┌───────────┐ | ||
| 63 | │Discovery │ │ Captive │ │ Payment │ | ||
| 64 | │ Endpoint │ │ Portal │ │ Handler │ | ||
| 65 | │ │ │ │ │ │ | ||
| 66 | │ 1 tag │ │ mint list │ │ find right│ | ||
| 67 | │ per │ │ with │ │ wallet, │ | ||
| 68 | │ reachable│ │ indicators│ │ receive() │ | ||
| 69 | │ mint │ │ │ │ │ | ||
| 70 | └──────────┘ └───────────┘ └───────────┘ | ||
| 71 | ``` | ||
| 72 | |||
| 73 | --- | ||
| 74 | |||
| 75 | ## 3. Phase Details | ||
| 76 | |||
| 77 | ### Phase 1: Config Layer — Multi-Mint Array | ||
| 78 | |||
| 79 | **Files**: `main/config.h`, `main/config.c` | ||
| 80 | |||
| 81 | **Changes**: | ||
| 82 | |||
| 83 | - Increase `TOLLGATE_MAX_MINT_URLS` from `3` to `8` | ||
| 84 | - Add to `tollgate_config_t`: | ||
| 85 | ```c | ||
| 86 | char accepted_mints[TOLLGATE_MAX_MINT_URLS][256]; | ||
| 87 | int accepted_mint_count; | ||
| 88 | ``` | ||
| 89 | - Keep existing `mint_url[256]` for backward compatibility | ||
| 90 | - Parse new `"accepted_mints"` JSON array from config.json | ||
| 91 | - If `"accepted_mints"` absent, populate from `"mint_url"` (backward compat) | ||
| 92 | - Update default config.json generation to include `"accepted_mints"` | ||
| 93 | |||
| 94 | **Config.json format** (new): | ||
| 95 | ```json | ||
| 96 | { | ||
| 97 | "nsec": "...", | ||
| 98 | "accepted_mints": [ | ||
| 99 | "https://mint.minibits.cash/Bitcoin", | ||
| 100 | "https://mint.coinos.io", | ||
| 101 | "https://21mint.me", | ||
| 102 | "https://mint.lnvoltz.com" | ||
| 103 | ], | ||
| 104 | "mint_url": "https://mint.minibits.cash/Bitcoin" | ||
| 105 | } | ||
| 106 | ``` | ||
| 107 | |||
| 108 | The `"mint_url"` field is kept as fallback / primary mint identifier. | ||
| 109 | |||
| 110 | --- | ||
| 111 | |||
| 112 | ### Phase 2: Mint Acceptance — Multi-Mint Check | ||
| 113 | |||
| 114 | **Files**: `main/cashu.c`, `main/cashu.h` | ||
| 115 | |||
| 116 | Replace single-mint check in `cashu_is_mint_accepted()`: | ||
| 117 | |||
| 118 | ```c | ||
| 119 | bool cashu_is_mint_accepted(const char *mint_url) { | ||
| 120 | if (!mint_url || mint_url[0] == '\0') return false; | ||
| 121 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 122 | for (int i = 0; i < cfg->accepted_mint_count; i++) { | ||
| 123 | if (strstr(mint_url, cfg->accepted_mints[i]) != NULL) | ||
| 124 | return true; | ||
| 125 | } | ||
| 126 | return false; | ||
| 127 | } | ||
| 128 | ``` | ||
| 129 | |||
| 130 | This is the config-only check. Phase 4 adds health gating. | ||
| 131 | |||
| 132 | --- | ||
| 133 | |||
| 134 | ### Phase 3: Mint Health Tracker | ||
| 135 | |||
| 136 | **New files**: `main/mint_health.h`, `main/mint_health.c` | ||
| 137 | |||
| 138 | **Data structures**: | ||
| 139 | |||
| 140 | ```c | ||
| 141 | #define MINT_HEALTH_MAX 8 | ||
| 142 | #define MINT_HEALTH_PROBE_INTERVAL_S 300 | ||
| 143 | #define MINT_HEALTH_PROBE_TIMEOUT_MS 15000 | ||
| 144 | #define MINT_HEALTH_RECOVERY_THRESHOLD 3 | ||
| 145 | |||
| 146 | typedef struct { | ||
| 147 | char url[256]; | ||
| 148 | bool reachable; | ||
| 149 | uint8_t consecutive_successes; | ||
| 150 | int64_t last_probe_ms; | ||
| 151 | int last_http_status; | ||
| 152 | } mint_status_t; | ||
| 153 | |||
| 154 | typedef void (*mint_health_changed_cb)(void); | ||
| 155 | ``` | ||
| 156 | |||
| 157 | **Public API**: | ||
| 158 | |||
| 159 | ```c | ||
| 160 | esp_err_t mint_health_init(const char urls[][256], int count); | ||
| 161 | void mint_health_start(void); | ||
| 162 | void mint_health_stop(void); | ||
| 163 | const mint_status_t *mint_health_get_all(int *out_count); | ||
| 164 | bool mint_health_is_reachable(const char *url); | ||
| 165 | void mint_health_mark_unreachable(const char *url); | ||
| 166 | void mint_health_register_callback(mint_health_changed_cb cb); | ||
| 167 | ``` | ||
| 168 | |||
| 169 | **Probing logic** (FreeRTOS task): | ||
| 170 | |||
| 171 | | Parameter | Value | Rationale | | ||
| 172 | |-----------|-------|-----------| | ||
| 173 | | Endpoint | `GET {url}/v1/info` | Lightweight, no auth required | | ||
| 174 | | Timeout | 15 seconds | ESP32 resource-constrained, 30s too long | | ||
| 175 | | Interval | 5 minutes (`vTaskDelay`) | Matches Go reference | | ||
| 176 | | Failure | Immediate | Single failed probe → unreachable | | ||
| 177 | | Recovery | 3 consecutive successes | 15 min sustained health (matches Go) | | ||
| 178 | | Initial | Success → reachable immediately | Set `consecutive_successes = threshold` | | ||
| 179 | |||
| 180 | **Thread safety**: Single FreeRTOS mutex protecting the status array. Callbacks dispatched after releasing the mutex. | ||
| 181 | |||
| 182 | **Reference**: Modeled after Go `MintHealthTracker` in `tollgate-module-basic-go/src/merchant/mint_health_tracker.go`. | ||
| 183 | |||
| 184 | --- | ||
| 185 | |||
| 186 | ### Phase 4: Health-Aware Acceptance | ||
| 187 | |||
| 188 | **Files**: `main/cashu.c` | ||
| 189 | |||
| 190 | Update `cashu_is_mint_accepted()` to gate on health: | ||
| 191 | |||
| 192 | ```c | ||
| 193 | bool cashu_is_mint_accepted(const char *mint_url) { | ||
| 194 | if (!mint_url || mint_url[0] == '\0') return false; | ||
| 195 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 196 | for (int i = 0; i < cfg->accepted_mint_count; i++) { | ||
| 197 | if (strstr(mint_url, cfg->accepted_mints[i]) != NULL) | ||
| 198 | return mint_health_is_reachable(mint_url); | ||
| 199 | } | ||
| 200 | return false; | ||
| 201 | } | ||
| 202 | ``` | ||
| 203 | |||
| 204 | 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. | ||
| 205 | |||
| 206 | --- | ||
| 207 | |||
| 208 | ### Phase 5: Multi-Mint Discovery Endpoint | ||
| 209 | |||
| 210 | **File**: `main/tollgate_api.c` | ||
| 211 | |||
| 212 | Replace single `price_per_step` tag in `api_get_discovery()` with one per reachable mint: | ||
| 213 | |||
| 214 | ```c | ||
| 215 | int count; | ||
| 216 | const mint_status_t *mints = mint_health_get_all(&count); | ||
| 217 | for (int i = 0; i < count; i++) { | ||
| 218 | if (!mints[i].reachable) continue; | ||
| 219 | cJSON *price_tag = cJSON_CreateArray(); | ||
| 220 | cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step")); | ||
| 221 | cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu")); | ||
| 222 | cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str)); | ||
| 223 | cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat")); | ||
| 224 | cJSON_AddItemToArray(price_tag, cJSON_CreateString(mints[i].url)); | ||
| 225 | cJSON_AddItemToArray(price_tag, cJSON_CreateString("1")); | ||
| 226 | cJSON_AddItemToArray(tags, price_tag); | ||
| 227 | } | ||
| 228 | ``` | ||
| 229 | |||
| 230 | If no mints are reachable, include a single tag with the primary `mint_url` as fallback (degraded mode signal). | ||
| 231 | |||
| 232 | --- | ||
| 233 | |||
| 234 | ### Phase 6: Multi-Mint Captive Portal UI | ||
| 235 | |||
| 236 | **File**: `main/captive_portal.c` | ||
| 237 | |||
| 238 | **Changes**: | ||
| 239 | |||
| 240 | 1. Replace `__MINT_URL__` template placeholder with `__MINT_LIST__` | ||
| 241 | 2. Generate HTML list of reachable mints with green dot indicators | ||
| 242 | 3. Unreachable mints shown greyed out (informative but not selectable) | ||
| 243 | 4. New API endpoint `GET /api/mints` → JSON array of mint status | ||
| 244 | |||
| 245 | **Portal mint list HTML**: | ||
| 246 | ```html | ||
| 247 | <div class="mints"> | ||
| 248 | <div class="mints-title">SUPPORTED MINTS</div> | ||
| 249 | <div class="mint-item reachable"> | ||
| 250 | <span class="mint-dot green"></span> | ||
| 251 | <span class="mint-url">mint.minibits.cash/Bitcoin</span> | ||
| 252 | </div> | ||
| 253 | <div class="mint-item unreachable"> | ||
| 254 | <span class="mint-dot grey"></span> | ||
| 255 | <span class="mint-url">mint.coinos.io</span> | ||
| 256 | </div> | ||
| 257 | </div> | ||
| 258 | ``` | ||
| 259 | |||
| 260 | **Auto-refresh**: JS polls `GET /api/mints` every 30s to update indicators. | ||
| 261 | |||
| 262 | --- | ||
| 263 | |||
| 264 | ### Phase 7: Multi-Mint Wallet (Nucula) | ||
| 265 | |||
| 266 | **Files**: `components/nucula_lib/nucula_wallet.h`, `components/nucula_lib/nucula_wallet.cpp` | ||
| 267 | |||
| 268 | **Approach**: Multi-wallet — one `cashu::Wallet` instance per mint. | ||
| 269 | |||
| 270 | **Why multi-wallet vs refactoring Wallet class**: | ||
| 271 | - Each mint has its own keysets, proofs, NVS slot — natural isolation | ||
| 272 | - No risk of cross-mint proof confusion | ||
| 273 | - `cashu::Wallet` class unchanged — zero regression risk | ||
| 274 | - NVS slot allocation already supported: `Wallet(url, ctx, nvs_slot)` | ||
| 275 | - `MAX_MINTS = 3` constant already defined in `wallet.hpp` | ||
| 276 | |||
| 277 | **Internal structure**: | ||
| 278 | ```cpp | ||
| 279 | static const int MAX_WALLETS = 4; | ||
| 280 | static cashu::Wallet *s_wallets[MAX_WALLETS]; | ||
| 281 | static int s_wallet_count = 0; | ||
| 282 | ``` | ||
| 283 | |||
| 284 | **API changes**: | ||
| 285 | |||
| 286 | | Old | New | Behavior | | ||
| 287 | |-----|-----|----------| | ||
| 288 | | `nucula_wallet_init(url)` | `nucula_wallet_init_multi(urls, count)` | Create wallet per mint | | ||
| 289 | | `nucula_wallet_init(url)` | Keep as compat wrapper | Creates single-wallet array | | ||
| 290 | | `nucula_wallet_receive(token)` | Same | Decode mint from token, route to correct wallet | | ||
| 291 | | `nucula_wallet_balance()` | Same | Sum across all wallets | | ||
| 292 | | `nucula_wallet_send(amount, ...)` | Same | Select wallet with sufficient balance | | ||
| 293 | | `nucula_wallet_swap_all()` | Same | Swap all wallets | | ||
| 294 | | `nucula_wallet_proof_count()` | Same | Sum across all wallets | | ||
| 295 | |||
| 296 | **Token routing in `receive()`**: | ||
| 297 | 1. Decode token to extract `mint_url` from the token JSON | ||
| 298 | 2. Find matching wallet by URL | ||
| 299 | 3. Call `wallet->receive(token, proofs_out)` on that wallet | ||
| 300 | 4. If no matching wallet found, try first wallet as fallback | ||
| 301 | |||
| 302 | **NVS slot mapping**: | ||
| 303 | |||
| 304 | | Mint index | NVS slot | NVS keys | | ||
| 305 | |-----------|----------|----------| | ||
| 306 | | 0 | 0 | `url_0`, `proofs_0`, `kn_0`, `k_0_0`..`k_0_9` | | ||
| 307 | | 1 | 1 | `url_1`, `proofs_1`, `kn_1`, `k_1_0`..`k_1_9` | | ||
| 308 | | 2 | 2 | `url_2`, `proofs_2`, `kn_2`, `k_2_0`..`k_2_9` | | ||
| 309 | | 3 | 3 | `url_3`, `proofs_3`, `kn_3`, `k_3_0`..`k_3_9` | | ||
| 310 | |||
| 311 | --- | ||
| 312 | |||
| 313 | ### Phase 8: Service Startup Integration | ||
| 314 | |||
| 315 | **File**: `main/tollgate_main.c` | ||
| 316 | |||
| 317 | **Changes to `start_services()`**: | ||
| 318 | |||
| 319 | ``` | ||
| 320 | 1. firewall_init() | ||
| 321 | 2. session_manager_init() | ||
| 322 | 3. mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count) | ||
| 323 | 4. mint_health_start() ← async probing begins | ||
| 324 | 5. nucula_wallet_init_multi(cfg->accepted_mints, cfg->accepted_mint_count) | ||
| 325 | 6. lightning_payout_init() | ||
| 326 | 7. dns_server_start() | ||
| 327 | 8. captive_portal_start() | ||
| 328 | 9. tollgate_api_start() | ||
| 329 | 10. wifistr_publish() | ||
| 330 | 11. cvm_server_start() | ||
| 331 | ``` | ||
| 332 | |||
| 333 | **Health callback**: When reachable set changes, trigger wifistr re-publish to update Nostr kind 38787 event with current mint list. | ||
| 334 | |||
| 335 | --- | ||
| 336 | |||
| 337 | ## 4. Data Flow | ||
| 338 | |||
| 339 | ### Payment Flow (Multi-Mint) | ||
| 340 | |||
| 341 | ``` | ||
| 342 | Client POST cashuA token | ||
| 343 | │ | ||
| 344 | ▼ | ||
| 345 | api_post_payment() | ||
| 346 | ├── cashu_decode_token() → extract mint_url from token | ||
| 347 | ├── cashu_is_mint_accepted(mint_url) | ||
| 348 | │ ├── Check in cfg->accepted_mints[] → config match | ||
| 349 | │ └── Check mint_health_is_reachable(mint_url) → health gate | ||
| 350 | ├── cashu_check_proof_states(mint_url, token) → POST {mint_url}/v1/checkstate | ||
| 351 | ├── session_create(client_ip, allotment) | ||
| 352 | └── nucula_wallet_receive(token_str) | ||
| 353 | ├── Decode token → extract mint_url | ||
| 354 | ├── Find wallet for that mint_url | ||
| 355 | └── wallet->receive(token, proofs_out) | ||
| 356 | ``` | ||
| 357 | |||
| 358 | ### Health Probe Flow | ||
| 359 | |||
| 360 | ``` | ||
| 361 | mint_health_task (FreeRTOS, 5min interval) | ||
| 362 | │ | ||
| 363 | for each mint in accepted_mints[]: | ||
| 364 | │ | ||
| 365 | ├── GET {url}/v1/info (15s timeout) | ||
| 366 | │ | ||
| 367 | ├── Success? | ||
| 368 | │ ├── YES → consecutive_successes++ | ||
| 369 | │ │ if >= RECOVERY_THRESHOLD → mark reachable | ||
| 370 | │ └── NO → mark unreachable, reset consecutive_successes = 0 | ||
| 371 | │ | ||
| 372 | └── Reachable set changed? → fire callback | ||
| 373 | ``` | ||
| 374 | |||
| 375 | --- | ||
| 376 | |||
| 377 | ## 5. Error Handling | ||
| 378 | |||
| 379 | | Scenario | Behavior | | ||
| 380 | |----------|----------| | ||
| 381 | | No internet at boot | No mints reachable, no tokens accepted until probe succeeds | | ||
| 382 | | All mints unreachable | Discovery shows primary mint (degraded), portal shows "Checking mints..." | | ||
| 383 | | Mint goes down mid-operation | `cashu_check_proof_states` fails → 502 Bad Gateway to client | | ||
| 384 | | Wallet init fails for one mint | Skip that mint, log error, continue with others | | ||
| 385 | | NVS full for multi-wallet | Fallback to single wallet, log warning | | ||
| 386 | | Probe timeout | Treat as unreachable (same as connection refused) | | ||
| 387 | |||
| 388 | --- | ||
| 389 | |||
| 390 | ## 6. Memory Budget | ||
| 391 | |||
| 392 | | Component | Estimated RAM | Notes | | ||
| 393 | |-----------|--------------|-------| | ||
| 394 | | `mint_status_t[8]` | ~2 KB | 256-byte URLs + metadata | | ||
| 395 | | Health probe task stack | 8 KB | HTTP client needs stack | | ||
| 396 | | `cashu::Wallet` per mint | ~4 KB each | Keysets + proofs in NVS, not RAM | | ||
| 397 | | 4 wallets total | ~16 KB | Within ESP32-S3 512KB SRAM budget | | ||
| 398 | | Health task TLS | ~40 KB | esp_http_client TLS buffer | | ||
| 399 | | **Total new overhead** | **~66 KB** | Acceptable with 512KB SRAM + 8MB PSRAM | | ||
| 400 | |||
| 401 | --- | ||
| 402 | |||
| 403 | ## 7. Testing Strategy | ||
| 404 | |||
| 405 | ### Unit Tests (host, `tests/unit/`) | ||
| 406 | |||
| 407 | | Test File | Covers | | ||
| 408 | |-----------|--------| | ||
| 409 | | `test_cashu.c` | Multi-mint acceptance (config-only) | | ||
| 410 | | `test_mint_health.c` | Health state machine, recovery, callbacks | | ||
| 411 | | `test_config.c` | Config parsing of `accepted_mints` array | | ||
| 412 | |||
| 413 | ### Integration Tests (device) | ||
| 414 | |||
| 415 | 1. Flash to Board A, verify discovery shows multiple mints | ||
| 416 | 2. Send token from each mint, verify accepted | ||
| 417 | 3. Block one mint at firewall level, verify becomes unreachable | ||
| 418 | 4. Verify recovery after unblocking | ||
| 419 | |||
| 420 | ### E2E Tests (Playwright) | ||
| 421 | |||
| 422 | 1. Captive portal shows mint list with indicators | ||
| 423 | 2. Pay with token from mint A → success | ||
| 424 | 3. Pay with token from unreachable mint → error shown in portal | ||
| 425 | |||
| 426 | --- | ||
| 427 | |||
| 428 | ## 8. Risks and Mitigations | ||
| 429 | |||
| 430 | | Risk | Likelihood | Impact | Mitigation | | ||
| 431 | |------|-----------|--------|------------| | ||
| 432 | | TLS memory pressure with 4 wallets | Medium | High | Each wallet shares single TLS context; only probe makes concurrent HTTP | | ||
| 433 | | NVS key namespace collision | Low | High | Use distinct `nvs_slot` per wallet (0-3) | | ||
| 434 | | Keyset loading OOM on multiple mints | Medium | Medium | Cap keysets per wallet at `MAX_KEYSETS=10` | | ||
| 435 | | Health probe blocks other tasks | Low | Medium | Dedicated FreeRTOS task, low priority | | ||
| 436 | | Backward compatibility break | Low | High | `mint_url` field still works as fallback | | ||
| 437 | |||
| 438 | --- | ||
| 439 | |||
| 440 | ## 9. Backward Compatibility | ||
| 441 | |||
| 442 | - Existing `config.json` with only `"mint_url"` → works (populates `accepted_mints[0]` from it) | ||
| 443 | - Existing SPIFFS images → no change needed | ||
| 444 | - NVS data → compatible (single wallet stays at slot 0) | ||
| 445 | - API endpoints → same paths, discovery just has more tags | ||
| 446 | - Captive portal → same UI flow, more mints shown | ||
| 447 | |||
| 448 | --- | ||
| 449 | |||
| 450 | ## 10. Git Worktree Strategy | ||
| 451 | |||
| 452 | Multiple LLM sessions work on this repo simultaneously. To avoid conflicts: | ||
| 453 | |||
| 454 | ### Setup | ||
| 455 | |||
| 456 | ``` | ||
| 457 | # Main worktree stays on master for other sessions | ||
| 458 | git -C /home/c03rad0r/esp32-tollgate checkout master | ||
| 459 | |||
| 460 | # Dedicated worktree for this feature | ||
| 461 | git -C /home/c03rad0r/esp32-tollgate worktree add /home/c03rad0r/esp32-tollgate-multi-mint feature/multi-mint-support | ||
| 462 | ``` | ||
| 463 | |||
| 464 | ### Worktree Locations | ||
| 465 | |||
| 466 | | Path | Branch | Purpose | | ||
| 467 | |------|--------|---------| | ||
| 468 | | `/home/c03rad0r/esp32-tollgate` | `master` | Main worktree, shared with other sessions | | ||
| 469 | | `/home/c03rad0r/esp32-tollgate-multi-mint` | `feature/multi-mint-support` | This feature's isolated workspace | | ||
| 470 | |||
| 471 | ### Conflict Avoidance Rules | ||
| 472 | |||
| 473 | | Rule | Why | | ||
| 474 | |------|-----| | ||
| 475 | | All edits happen in `/home/c03rad0r/esp32-tollgate-multi-mint` | Other sessions keep their own checkout untouched | | ||
| 476 | | Push after every green test | Other sessions can `git pull` to see progress | | ||
| 477 | | Never modify `master` directly | Merge only when feature is complete and tested | | ||
| 478 | | `git pull --rebase` before push | Avoid merge commits if others pushed to same branch | | ||
| 479 | |||
| 480 | ### Cleanup (after merge) | ||
| 481 | |||
| 482 | ``` | ||
| 483 | git -C /home/c03rad0r/esp32-tollgate worktree remove /home/c03rad0r/esp32-tollgate-multi-mint | ||
| 484 | ``` | ||
| 485 | |||
| 486 | --- | ||
| 487 | |||
| 488 | ## 11. Implementation Checklist | ||
| 489 | |||
| 490 | - [x] Create feature branch `feature/multi-mint-support` | ||
| 491 | - [x] Write design document `docs/MULTI_MINT_DESIGN.md` | ||
| 492 | - [x] Set up git worktree at `/home/c03rad0r/esp32-tollgate-multi-mint` | ||
| 493 | - [x] Phase 1: Config layer (`config.h`, `config.c`) — multi-mint array | ||
| 494 | - [x] Phase 2: Multi-mint acceptance (`cashu.c`) — iterate accepted_mints | ||
| 495 | - [x] Phase 3: Mint health tracker (`mint_health.h`, `mint_health.c`) — FreeRTOS probing task | ||
| 496 | - [x] Phase 4: Health-aware acceptance integration — gate on reachability | ||
| 497 | - [x] Phase 5: Multi-mint discovery endpoint (`tollgate_api.c`) — one tag per reachable mint | ||
| 498 | - [x] Phase 6: Multi-mint captive portal UI (`captive_portal.c`) — mint list with indicators | ||
| 499 | - [x] Phase 7: Multi-mint wallet (`nucula_wallet.h`, `nucula_wallet.cpp`) — multi-wallet approach | ||
| 500 | - [x] Phase 8: Service startup integration (`tollgate_main.c`) — init health + multi-wallet | ||
| 501 | - [x] Unit tests: update `test_cashu.c` for multi-mint acceptance (14/14 pass) | ||
| 502 | - [x] Unit tests: all 256 existing tests pass | ||
| 503 | - [x] Build verification (ESP-IDF compiles cleanly, no errors) | ||
| 504 | - [ ] Unit tests: `test_mint_health.c` — health state machine, recovery, callbacks | ||
| 505 | - [ ] Flash Board A and verify multi-mint discovery | ||
| 506 | - [ ] Flash Board B and verify multi-mint discovery | ||
| 507 | - [ ] Payment test with token from each supported mint | ||
| 508 | - [ ] Health probe test (verify reachable/unreachable transitions) | ||
| 509 | - [ ] Captive portal multi-mint display verification | ||
| 510 | - [ ] Push after every passing test (blocked: Nostr relay down) | ||
| 511 | - [ ] Merge to master | ||