diff options
Diffstat (limited to 'AGENTS.md')
| -rw-r--r-- | AGENTS.md | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f5d4f7e --- /dev/null +++ b/AGENTS.md | |||
| @@ -0,0 +1,195 @@ | |||
| 1 | # AGENTS.md — Instructions for AI Coding Agents | ||
| 2 | |||
| 3 | ## Project Overview | ||
| 4 | |||
| 5 | TollGate ESP32 firmware: captive portal WiFi hotspot with Cashu e-cash payments, on-device wallet, Nostr identity derivation, and wifistr service discovery. Runs on two ESP32-S3 boards. | ||
| 6 | |||
| 7 | ## Technology Stack | ||
| 8 | |||
| 9 | - **Framework:** ESP-IDF v5.4.1 (C/C++) | ||
| 10 | - **Target:** ESP32-S3, 16MB flash, 8MB PSRAM (OCT mode) | ||
| 11 | - **Wallet:** nucula library (libsecp256k1) via git submodule | ||
| 12 | - **Identity:** Nostr nsec → HMAC-SHA512 → deterministic MAC/SSID/IP | ||
| 13 | - **Service discovery:** wifistr (Nostr kind 38787) via WebSocket | ||
| 14 | - **Testing:** Host C unit tests (gcc), Node.js integration tests (live board), Playwright E2E | ||
| 15 | |||
| 16 | ## Board Configuration | ||
| 17 | |||
| 18 | | Board | Port | Factory MAC | Notes | | ||
| 19 | |-------|------|-------------|-------| | ||
| 20 | | A | `/dev/ttyACM0` | `94:a9:90:2e:37:7c` | Primary test target | | ||
| 21 | | B | `/dev/ttyACM1` | `fc:01:2c:c5:50:50` | Secondary | | ||
| 22 | |||
| 23 | Identity (SSID, IP, MAC) is derived from `nsec` in config.json. Each board gets a unique nsec. | ||
| 24 | |||
| 25 | ## Boot Sequence | ||
| 26 | |||
| 27 | ``` | ||
| 28 | nvs_flash_init() | ||
| 29 | → tollgate_config_init() // loads config.json with nsec from SPIFFS | ||
| 30 | → identity_init(nsec) // derives npub, STA/AP MAC, SSID, IP via HMAC-SHA512 | ||
| 31 | → tollgate_config_derive_unique() // copies derived values into config struct | ||
| 32 | → esp_netif_init() + esp_event_loop_create_default() | ||
| 33 | → wifi_init_sta() + wifi_create_ap_netif() // AP netif with derived IP | ||
| 34 | → esp_wifi_init() | ||
| 35 | → esp_wifi_set_mac(STA/AP) // sets derived MACs | ||
| 36 | → esp_wifi_set_mode(APSTA) | ||
| 37 | → wifi_configure_ap() // uses derived SSID | ||
| 38 | → esp_wifi_start() | ||
| 39 | → [on STA got IP] start_services(): | ||
| 40 | firewall_init, session_init, wallet_init, dns_server, captive_portal, api, wifistr_publish | ||
| 41 | ``` | ||
| 42 | |||
| 43 | ## Key Files | ||
| 44 | |||
| 45 | ### Source (main/) | ||
| 46 | - `tollgate_main.c` — entry point, WiFi AP+STA, event loop, service lifecycle | ||
| 47 | - `config.c/h` — SPIFFS config.json parsing, nsec/nostr/wifi/mint settings | ||
| 48 | - `identity.c/h` — HMAC-SHA512 derivation from nsec, npub/MAC/SSID/IP | ||
| 49 | - `nostr_event.c/h` — NIP-01 event serialization + BIP-340 Schnorr signing | ||
| 50 | - `geohash.c/h` — lat/lon to geohash encoding | ||
| 51 | - `wifistr.c/h` — kind 38787 event builder + WebSocket relay publish | ||
| 52 | - `captive_portal.c/h` — HTTP :80 portal, captive detection, grant/reset | ||
| 53 | - `dns_server.c/h` — DNS hijack/forward per-client, DoT reject | ||
| 54 | - `firewall.c/h` — NAPT on/off per-client, MAC resolution | ||
| 55 | - `session.c/h` — time-based sessions, spent-secret tracking | ||
| 56 | - `cashu.c/h` — Cashu token decode, checkstate, allotment calc | ||
| 57 | - `tollgate_api.c/h` — HTTP :2121, payment endpoints, wallet endpoints | ||
| 58 | |||
| 59 | ### Components | ||
| 60 | - `nucula_lib/` — C++ bridge to nucula::Wallet (C API in nucula_wallet.h) | ||
| 61 | - `secp256k1/` — symlink to nucula_src/components/secp256k1/ | ||
| 62 | |||
| 63 | ### Config Format (config.json on SPIFFS) | ||
| 64 | ```json | ||
| 65 | { | ||
| 66 | "nsec": "<64-char hex>", | ||
| 67 | "wifi_networks": [{"ssid":"...", "password":"..."}], | ||
| 68 | "ap_password": "", | ||
| 69 | "mint_url": "https://testnut.cashu.space", | ||
| 70 | "price_per_step": 21, | ||
| 71 | "step_size_ms": 60000, | ||
| 72 | "nostr_geohash": "u281w0dfz", | ||
| 73 | "nostr_relays": ["wss://relay.damus.io", "wss://nos.lol"], | ||
| 74 | "nostr_publish_interval_s": 21600 | ||
| 75 | } | ||
| 76 | ``` | ||
| 77 | |||
| 78 | ## Testing Rules — MANDATORY | ||
| 79 | |||
| 80 | ### Rule 1: Every new C source file MUST have unit tests | ||
| 81 | - Place test in `tests/unit/test_<module>.c` | ||
| 82 | - Test pure-logic functions with known input/output vectors | ||
| 83 | - Compile with host gcc via `make -C tests/unit` | ||
| 84 | - Source files remain untouched — stubs in `tests/unit/stubs/` provide ESP-IDF types | ||
| 85 | - **Run `make test-unit` after any code change. Must pass before commit.** | ||
| 86 | |||
| 87 | ### Rule 2: Every new HTTP endpoint MUST have integration tests | ||
| 88 | - Place in `tests/integration/phase<N>.mjs` | ||
| 89 | - Test against live board using curl + `TOLLGATE_IP` env var | ||
| 90 | - Never hardcode IP addresses — always use `process.env.TOLLGATE_IP` | ||
| 91 | |||
| 92 | ### Rule 3: Every new browser-visible feature MUST have Playwright E2E tests | ||
| 93 | - Place in `tests/e2e/<feature>.spec.mjs` | ||
| 94 | - Test the full user-visible flow in a browser | ||
| 95 | |||
| 96 | ### Rule 4: All tests must pass before commit | ||
| 97 | - `make test-unit` — host unit tests (no hardware needed) | ||
| 98 | - `make test-integration` — against live Board A (needs hardware) | ||
| 99 | - `make test-e2e` — Playwright browser tests (needs hardware) | ||
| 100 | |||
| 101 | ### Rule 5: Test naming conventions | ||
| 102 | | Test type | Location | Naming | Run command | | ||
| 103 | |-----------|----------|--------|-------------| | ||
| 104 | | Host unit | `tests/unit/` | `test_<module>.c` | `make test-unit` | | ||
| 105 | | Integration | `tests/integration/` | `phase<N>.mjs` or `<feature>.mjs` | `make test-integration` | | ||
| 106 | | E2E | `tests/e2e/` | `<feature>.spec.mjs` | `make test-e2e` | | ||
| 107 | |||
| 108 | ### Rule 6: Coverage requirements by code type | ||
| 109 | | Code type | Required test type | Examples | | ||
| 110 | |-----------|-------------------|----------| | ||
| 111 | | Pure math/logic | Unit test | geohash, allotment calc, derivation | | ||
| 112 | | Crypto operations | Unit test with known vectors | HMAC derivation, Schnorr signing, SHA-256 | | ||
| 113 | | Token parsing | Unit test with known tokens | Cashu token decode | | ||
| 114 | | State management | Unit test with mocks | Session lifecycle, firewall client list | | ||
| 115 | | HTTP endpoints | Integration test | GET /wallet, POST /, POST /wallet/send | | ||
| 116 | | HTML pages | Playwright E2E | Portal rendering, payment flow | | ||
| 117 | | Network behavior | Integration test | DNS hijack, NAT, connectivity | | ||
| 118 | |||
| 119 | ## How to Run Tests | ||
| 120 | |||
| 121 | ```bash | ||
| 122 | # Host unit tests (no hardware needed) | ||
| 123 | make test-unit | ||
| 124 | |||
| 125 | # Integration tests (needs Board A connected and flashed) | ||
| 126 | export TOLLGATE_IP=10.192.45.1 | ||
| 127 | export TOLLGATE_SSID=TollGate-C0E9CA | ||
| 128 | make test-integration | ||
| 129 | |||
| 130 | # E2E tests (needs Board A + browser) | ||
| 131 | make test-e2e | ||
| 132 | |||
| 133 | # All tests | ||
| 134 | make test-all | ||
| 135 | |||
| 136 | # Quick smoke (30s, needs hardware) | ||
| 137 | make smoke | ||
| 138 | ``` | ||
| 139 | |||
| 140 | ## Build & Flash | ||
| 141 | |||
| 142 | ```bash | ||
| 143 | source ~/esp/esp-idf/export.sh | ||
| 144 | make flash # build + flash to Board A | ||
| 145 | make flash-a # same | ||
| 146 | make flash-b # flash to Board B | ||
| 147 | ``` | ||
| 148 | |||
| 149 | ## Test Infrastructure | ||
| 150 | |||
| 151 | ### Host Unit Tests (`tests/unit/`) | ||
| 152 | - Compile with system gcc, link against `libmbedcrypto` + `libcjson` + secp256k1 | ||
| 153 | - ESP-IDF types provided by stubs in `tests/unit/stubs/` | ||
| 154 | - Each test file is a standalone binary that returns 0 on success, 1 on failure | ||
| 155 | - Uses a minimal assert macro: `ASSERT(cond, msg)` | ||
| 156 | - Golden test vectors: known nsec → expected npub/MAC/SSID/IP | ||
| 157 | |||
| 158 | ### Integration Tests (`tests/integration/`) | ||
| 159 | - Node.js scripts that run curl/ping/nmcli against a live ESP32 board | ||
| 160 | - Require `TOLLGATE_IP` env var (default: auto-detect or error) | ||
| 161 | - Token generation via nutshell CLI: `cashu -h https://testnut.cashu.space send --legacy 21` | ||
| 162 | |||
| 163 | ### E2E Tests (`tests/e2e/`) | ||
| 164 | - Playwright browser tests | ||
| 165 | - Config in `tests/e2e/playwright.config.mjs` | ||
| 166 | - Test the captive portal UI and payment flow | ||
| 167 | |||
| 168 | ## Environment Variables | ||
| 169 | |||
| 170 | | Variable | Default | Purpose | | ||
| 171 | |----------|---------|---------| | ||
| 172 | | `TOLLGATE_IP` | (none, must set) | Board A's AP IP (e.g., `10.192.45.1`) | | ||
| 173 | | `TOLLGATE_SSID` | `TollGate-C0E9CA` | Board A's AP SSID | | ||
| 174 | | `TEST_TOKEN` | (none) | Cashu token for payment tests | | ||
| 175 | | `SUDO_PW` | `c03rad0r123` | sudo password for route management | | ||
| 176 | |||
| 177 | ## External Dependencies | ||
| 178 | |||
| 179 | - **Test mint:** `testnut.cashu.space` — auto-pays lightning invoices | ||
| 180 | - **Nostr relays:** `relay.damus.io`, `nos.lol` — for wifistr events | ||
| 181 | - **Nutshell CLI:** `cashu` command for token generation | ||
| 182 | - **ESP-IDF:** `source ~/esp/esp-idf/export.sh` before `idf.py` commands | ||
| 183 | - **System libs for unit tests:** `libmbedtls-dev`, `libcjson-dev` | ||
| 184 | |||
| 185 | ## Reminders | ||
| 186 | |||
| 187 | - **Commit + push every time a test passes that previously didn't pass.** Green tests = checkpoint. Don't batch multiple test fixes into one commit. | ||
| 188 | - Commit + push after each working change | ||
| 189 | - Board A is at `/dev/ttyACM0`, Board B at `/dev/ttyACM1` | ||
| 190 | - `sudo` password: `c03rad0r123` | ||
| 191 | - SPIFFS is at offset `0x410000`, size `0xF0000` — erase with `esptool.py erase_region 0x410000 0xF0000` if config is stale | ||
| 192 | - NVS stores wallet proofs — erasing NVS clears wallet balance | ||
| 193 | - The `nostr_event.c` `created_at` field uses `gettimeofday()` — mock this in unit tests | ||
| 194 | - Wifistr event signing uses `secp256k1_schnorrsig_sign32()` — verify with `_verify()` in tests | ||
| 195 | - Portal HTML has server-side template substitution (`__AP_IP__`, `__PRICE__`, `__MINT_URL__`) — no JS fetch | ||