diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | interop/AGENTS.md | 96 | ||||
| -rw-r--r-- | interop/INTEROP_PLAN.md | 149 | ||||
| -rw-r--r-- | interop/Makefile | 503 | ||||
| -rw-r--r-- | interop/PROGRESS.md | 70 | ||||
| -rw-r--r-- | interop/routers.env.example | 41 |
6 files changed, 860 insertions, 0 deletions
| @@ -19,3 +19,4 @@ tests/unit/test_identity | |||
| 19 | tests/unit/test_nostr_event | 19 | tests/unit/test_nostr_event |
| 20 | tests/unit/test_cashu | 20 | tests/unit/test_cashu |
| 21 | tests/unit/test_session | 21 | tests/unit/test_session |
| 22 | interop/routers.env | ||
diff --git a/interop/AGENTS.md b/interop/AGENTS.md new file mode 100644 index 0000000..33a2d6d --- /dev/null +++ b/interop/AGENTS.md | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | # AGENTS.md — Interop Test Standing Instructions | ||
| 2 | |||
| 3 | ## Overview | ||
| 4 | |||
| 5 | Cross-platform interoperability tests for ESP32 TollGate firmware vs OpenWRT TollGate (tollgate-module-basic-go). Makefile-driven tests that verify Cashu e-cash token compatibility and upstream payment flows between the two implementations. | ||
| 6 | |||
| 7 | ## Standing Instructions | ||
| 8 | |||
| 9 | 1. **Always maintain these files:** | ||
| 10 | - `INTEROP_PLAN.md` — up-to-date interop test plan | ||
| 11 | - `PROGRESS.md` — checklist of done/pending items | ||
| 12 | |||
| 13 | 2. **Testing requirements:** | ||
| 14 | - All interop targets must be idempotent — safe to re-run | ||
| 15 | - Cleanup targets must restore original state | ||
| 16 | - Never leave a device in a broken state | ||
| 17 | |||
| 18 | 3. **Commit discipline:** | ||
| 19 | - Commit every time a test scenario passes end-to-end | ||
| 20 | - Push after each commit | ||
| 21 | |||
| 22 | 4. **No comments in code** unless explicitly requested | ||
| 23 | |||
| 24 | 5. **No secrets in git** — all secrets in `routers.env` (gitignored) or `.env` | ||
| 25 | |||
| 26 | 6. **Device safety:** | ||
| 27 | - Always save upstream state before changing it | ||
| 28 | - Always restore upstream state after tests | ||
| 29 | - Use router mutex (if shared with other sessions) | ||
| 30 | |||
| 31 | ## Repository Context | ||
| 32 | |||
| 33 | This directory lives inside `esp32-tollgate/interop/`. The parent repo has its own `AGENTS.md` with firmware testing rules. This file covers interop testing only. | ||
| 34 | |||
| 35 | ## Device Access | ||
| 36 | |||
| 37 | | Device | Transport | Address | Notes | | ||
| 38 | |--------|-----------|---------|-------| | ||
| 39 | | OpenWRT (alpha) | SSH | `root@10.47.41.1` | Ethernet at `enx00e04c683d2d` | | ||
| 40 | | ESP32 Board A | WiFi API | `10.192.45.1` | WiFi at `wlp59s0`, SSID `TollGate-C0E9CA` | | ||
| 41 | | ESP32 Board A | Serial | `/dev/ttyACM0` | USB serial, 115200 baud | | ||
| 42 | | ESP32 Board B | Serial | `/dev/ttyACM1` | USB serial, 115200 baud | | ||
| 43 | |||
| 44 | ## Token Generation | ||
| 45 | |||
| 46 | | Target | Tool | Format | Command | | ||
| 47 | |--------|------|--------|---------| | ||
| 48 | | ESP32 | `cashu` CLI | V3 (cashuA) | `cashu --env-mint testnut.cashu.space send --legacy 21` | | ||
| 49 | | OpenWRT | `mint-token` | V4 | `/tmp/mint-token nofee.testnut.cashu.space 1` | | ||
| 50 | |||
| 51 | **ESP32 only accepts V3 tokens.** OpenWRT accepts both V3 and V4. | ||
| 52 | |||
| 53 | ## Mint URLs | ||
| 54 | |||
| 55 | | Mint | URL | Auto-pay | | ||
| 56 | |------|-----|----------| | ||
| 57 | | testnut | `https://testnut.cashu.space` | Yes | | ||
| 58 | | nofee-testnut | `https://nofee.testnut.cashu.space` | Yes | | ||
| 59 | |||
| 60 | Both must be in both devices' accepted_mints for cross-platform payment. | ||
| 61 | |||
| 62 | ## Key Commands | ||
| 63 | |||
| 64 | ```bash | ||
| 65 | # Show all device status | ||
| 66 | make interop-status | ||
| 67 | |||
| 68 | # Full setup (mints + wallets) | ||
| 69 | make interop-setup | ||
| 70 | |||
| 71 | # Run individual scenarios | ||
| 72 | make interop-laptop-esp32 | ||
| 73 | make interop-laptop-openwrt | ||
| 74 | make interop-openwrt-esp32 | ||
| 75 | make interop-esp32-esp32 | ||
| 76 | |||
| 77 | # Cleanup after tests | ||
| 78 | make interop-cleanup | ||
| 79 | ``` | ||
| 80 | |||
| 81 | ## Network Interfaces | ||
| 82 | |||
| 83 | | Interface | Device | Purpose | | ||
| 84 | |-----------|--------|---------| | ||
| 85 | | `enx00e04c683d2d` | Laptop | Ethernet to OpenWRT LAN (`10.47.41.x`) | | ||
| 86 | | `wlp59s0` | Laptop | WiFi to ESP32 AP (`10.192.45.x`) | | ||
| 87 | |||
| 88 | The laptop has simultaneous connectivity to both devices via different interfaces. | ||
| 89 | |||
| 90 | ## Troubleshooting | ||
| 91 | |||
| 92 | - **OpenWRT unreachable**: Check ethernet cable, `ip addr show enx00e04c683d2d` | ||
| 93 | - **ESP32 unreachable**: Check WiFi connection, `nmcli dev wifi connect TollGate-C0E9CA` | ||
| 94 | - **Token rejected**: Check mint URL matches accepted_mints on target device | ||
| 95 | - **OpenWRT won't auto-pay**: Check wallet balance > 0, check daemon logs `logread -e tollgate-wrt -f` | ||
| 96 | - **ESP32 serial**: `python3 -m serial.tools.miniterm /dev/ttyACM0 115200` | ||
diff --git a/interop/INTEROP_PLAN.md b/interop/INTEROP_PLAN.md new file mode 100644 index 0000000..c754e48 --- /dev/null +++ b/interop/INTEROP_PLAN.md | |||
| @@ -0,0 +1,149 @@ | |||
| 1 | # TollGate Interop Test Plan — ESP32 ↔ OpenWRT | ||
| 2 | |||
| 3 | ## Overview | ||
| 4 | |||
| 5 | Cross-platform interoperability tests between ESP32-based TollGate firmware and OpenWRT-based TollGate (tollgate-module-basic-go). Tests verify that Cashu e-cash tokens work across both implementations, and that the OpenWRT Go daemon can auto-pay the ESP32 for upstream internet access. | ||
| 6 | |||
| 7 | ## Device Inventory | ||
| 8 | |||
| 9 | | Device | Access | AP SSID | API | Mint | Metric | Price | | ||
| 10 | |--------|--------|---------|-----|------|--------|-------| | ||
| 11 | | OpenWRT (alpha) | SSH `root@10.47.41.1` | `TollGate-EVXZ-2.4GHz` / `TollGate-EVXZ-5GHz` | `http://10.47.41.1:2121/` | `nofee.testnut.cashu.space` | bytes (21MB/step) | 1 sat/step | | ||
| 12 | | ESP32 Board A | WiFi `10.192.45.1`, Serial `/dev/ttyACM0` | `TollGate-C0E9CA` | `http://10.192.45.1:2121/` | `testnut.cashu.space` | time (21 sats/60s) | 21 sats/step | | ||
| 13 | | ESP32 Board B | Serial `/dev/ttyACM1` | TBD | TBD | TBD | TBD | TBD | | ||
| 14 | |||
| 15 | ## Network Topology | ||
| 16 | |||
| 17 | ``` | ||
| 18 | ┌──────────────────────────────────────────────────┐ | ||
| 19 | │ Internet │ | ||
| 20 | └───────┬──────────────────────┬───────────────────┘ | ||
| 21 | │ │ | ||
| 22 | EnterSSID-5GHz (upstream) EnterSSID-2.4GHz (upstream) | ||
| 23 | │ │ | ||
| 24 | ┌────────┴────────┐ ┌────────┴────────┐ | ||
| 25 | │ OpenWRT Router │ │ ESP32 Board A │ | ||
| 26 | │ (alpha) │ │ (TollGate-C0E9CA) | ||
| 27 | │ 10.47.41.1 │ │ 10.192.45.1 │ | ||
| 28 | └────────┬────────┘ └────────┬────────┘ | ||
| 29 | │ │ | ||
| 30 | TollGate-EVXZ-2.4GHz TollGate-C0E9CA | ||
| 31 | TollGate-EVXZ-5GHz (open AP) | ||
| 32 | │ │ | ||
| 33 | ┌────────┴────────┐ ┌────────┴────────┐ | ||
| 34 | │ Laptop (eth0) │ │ Laptop (wlan0) │ | ||
| 35 | │ 10.47.41.106 │ │ 10.192.45.2 │ | ||
| 36 | └─────────────────┘ └─────────────────┘ | ||
| 37 | ``` | ||
| 38 | |||
| 39 | ## Mint Alignment Strategy | ||
| 40 | |||
| 41 | Both mints are test mints that auto-pay lightning invoices. For cross-platform interop, both devices accept tokens from either mint. | ||
| 42 | |||
| 43 | | Mint | Auto-pay | Used by | | ||
| 44 | |------|----------|---------| | ||
| 45 | | `testnut.cashu.space` | Yes | ESP32 (native), added to OpenWRT | | ||
| 46 | | `nofee.testnut.cashu.space` | Yes | OpenWRT (native), added to ESP32 | | ||
| 47 | |||
| 48 | ### Configuration Changes | ||
| 49 | |||
| 50 | **OpenWRT** — add `testnut.cashu.space` to `accepted_mints` in `/etc/tollgate/config.json` via SSH + jq. | ||
| 51 | |||
| 52 | **ESP32** — add `nofee.testnut.cashu.space` to `mint_url` in `config.json` on SPIFFS. Requires rebuild + reflash. | ||
| 53 | |||
| 54 | ## Token Format Compatibility | ||
| 55 | |||
| 56 | | Platform | V3 (cashuA) | V4 (cashuB/CBOR) | | ||
| 57 | |----------|-------------|-------------------| | ||
| 58 | | ESP32 | **Accepted** (only format supported) | Not supported | | ||
| 59 | | OpenWRT | Accepted | Accepted | | ||
| 60 | |||
| 61 | Token generation: | ||
| 62 | - **For ESP32**: `cashu --env-mint testnut.cashu.space send --legacy 21` → V3 | ||
| 63 | - **For OpenWRT**: `mint-token` Go binary → V4 (preferred), or `cashu --legacy` → V3 | ||
| 64 | |||
| 65 | ## Test Scenarios | ||
| 66 | |||
| 67 | ### Scenario 1: Laptop → ESP32 (Already Works) | ||
| 68 | |||
| 69 | Laptop connects to ESP32 AP, mints V3 token, pays ESP32 TollGate API, verifies internet. | ||
| 70 | |||
| 71 | This is the existing `make test-payment` flow, wrapped into the interop Makefile for consistency. | ||
| 72 | |||
| 73 | ### Scenario 2: Laptop → OpenWRT | ||
| 74 | |||
| 75 | Laptop connects to OpenWRT AP (or uses existing ethernet connection), mints V4 token, pays OpenWRT TollGate API, verifies internet. | ||
| 76 | |||
| 77 | **Steps:** | ||
| 78 | 1. Verify laptop can reach OpenWRT at `10.47.41.1` | ||
| 79 | 2. Check API advertisement at `http://10.47.41.1:2121/` (kind=10021) | ||
| 80 | 3. Mint V4 token via `mint-token` binary | ||
| 81 | 4. POST token to `http://10.47.41.1:2121/` | ||
| 82 | 5. Verify kind=1022 session response | ||
| 83 | 6. Verify internet via ping | ||
| 84 | |||
| 85 | ### Scenario 3: OpenWRT → ESP32 (Reseller) | ||
| 86 | |||
| 87 | OpenWRT connects its STA to ESP32's TollGate AP. OpenWRT's Go daemon auto-detects the TollGate upstream and pays with its wallet. ESP32 grants session. | ||
| 88 | |||
| 89 | **Steps:** | ||
| 90 | 1. Verify both devices accessible | ||
| 91 | 2. Fund ESP32 wallet (for receiving payment) | ||
| 92 | 3. Fund OpenWRT wallet (for paying upstream) | ||
| 93 | 4. Save OpenWRT's current upstream SSID | ||
| 94 | 5. Connect OpenWRT STA to `TollGate-C0E9CA` (ESP32's AP) | ||
| 95 | 6. Wait for DHCP + upstream detection | ||
| 96 | 7. Watch for auto-payment logs on OpenWRT | ||
| 97 | 8. Verify session on ESP32 (via serial or API) | ||
| 98 | 9. Restore OpenWRT upstream | ||
| 99 | 10. Restore production configs | ||
| 100 | |||
| 101 | ### Scenario 4: ESP32 → OpenWRT (Future) | ||
| 102 | |||
| 103 | ESP32 connects its STA to OpenWRT's TollGate AP. Requires ESP32 firmware to have TollGate client detection + auto-payment logic — **not yet implemented**. | ||
| 104 | |||
| 105 | ### Scenario 5: ESP32 ↔ ESP32 | ||
| 106 | |||
| 107 | Board A connects to Board B's AP (or vice versa), cross-payment test. Requires Board B to be flashed with unique nsec + funded wallet. | ||
| 108 | |||
| 109 | **Steps:** | ||
| 110 | 1. Flash Board B with different nsec | ||
| 111 | 2. Configure and fund both boards | ||
| 112 | 3. Board A connects STA to Board B's AP | ||
| 113 | 4. Manual curl payment test (POST token) | ||
| 114 | 5. Verify session + internet | ||
| 115 | |||
| 116 | ## Makefile Target Reference | ||
| 117 | |||
| 118 | | Target | Scenario | Description | | ||
| 119 | |--------|----------|-------------| | ||
| 120 | | `interop-status` | — | Show TollGate status for all devices | | ||
| 121 | | `interop-setup-mints` | — | Add both mints to both devices | | ||
| 122 | | `interop-fund-esp32` | — | Fund ESP32 wallet with test tokens | | ||
| 123 | | `interop-fund-openwrt` | — | Fund OpenWRT wallet with test tokens | | ||
| 124 | | `interop-setup` | — | Full setup: mints + fund both | | ||
| 125 | | `interop-laptop-esp32` | 1 | Laptop pays ESP32 | | ||
| 126 | | `interop-laptop-openwrt` | 2 | Laptop pays OpenWRT | | ||
| 127 | | `interop-openwrt-esp32` | 3 | OpenWRT auto-pays ESP32 for upstream | | ||
| 128 | | `interop-esp32-esp32` | 5 | Cross-board payment | | ||
| 129 | | `interop-cleanup` | — | Restore original configs on all devices | | ||
| 130 | |||
| 131 | ## Prerequisites | ||
| 132 | |||
| 133 | - Laptop connected to OpenWRT via ethernet (`enx00e04c683d2d`, `10.47.41.106`) | ||
| 134 | - Laptop connected to ESP32 via WiFi (`wlp59s0`, `10.192.45.2`) | ||
| 135 | - `mint-token` binary built: `cd physical-router-test-automation/scripts/mint-token && go build -o /tmp/mint-token .` | ||
| 136 | - `cashu` CLI installed: `pip install cashu` | ||
| 137 | - SSH key auth to OpenWRT: `ssh-copy-id root@10.47.41.1` | ||
| 138 | - ESP32 Board A flashed and running with funded wallet | ||
| 139 | |||
| 140 | ## Key Technical Notes | ||
| 141 | |||
| 142 | - OpenWRT uses `tollgate upstream connect <ssid>` CLI to switch upstream | ||
| 143 | - OpenWRT's daemon auto-detects TollGate upstream via HTTP GET to `:2121/` (kind=10021) | ||
| 144 | - ESP32 only accepts V3 tokens (`cashuA` prefix); OpenWRT accepts both V3 and V4 | ||
| 145 | - The `mint-token` binary mints from `nofee.testnut.cashu.space` and produces V4 tokens | ||
| 146 | - The `cashu` CLI with `--legacy` flag produces V3 tokens | ||
| 147 | - ESP32 has no TollGate client logic — cannot auto-pay upstream TollGates (future work) | ||
| 148 | - OpenWRT's `tollgate wallet fund` accepts piped V4 tokens | ||
| 149 | - ESP32's `POST /wallet/receive` accepts V3 tokens (via nucula) | ||
diff --git a/interop/Makefile b/interop/Makefile new file mode 100644 index 0000000..9b2a75b --- /dev/null +++ b/interop/Makefile | |||
| @@ -0,0 +1,503 @@ | |||
| 1 | # --------------------------------------------------------------------------- | ||
| 2 | # Makefile — ESP32 ↔ OpenWRT TollGate Interop Tests | ||
| 3 | # | ||
| 4 | # Tests cross-platform Cashu token compatibility between ESP32 firmware | ||
| 5 | # and OpenWRT tollgate-module-basic-go daemon. | ||
| 6 | # | ||
| 7 | # Setup: | ||
| 8 | # cp routers.env.example routers.env # then edit with real values | ||
| 9 | # make interop-setup # configure mints + fund wallets | ||
| 10 | # | ||
| 11 | # Quick reference: | ||
| 12 | # make interop-status # show all device status | ||
| 13 | # make interop-laptop-esp32 # laptop pays ESP32 | ||
| 14 | # make interop-laptop-openwrt # laptop pays OpenWRT | ||
| 15 | # make interop-openwrt-esp32 # OpenWRT auto-pays ESP32 upstream | ||
| 16 | # make interop-cleanup # restore original configs | ||
| 17 | # --------------------------------------------------------------------------- | ||
| 18 | |||
| 19 | .PHONY: help interop-status interop-setup interop-setup-mints interop-verify-mints \ | ||
| 20 | interop-fund-esp32 interop-fund-openwrt interop-setup \ | ||
| 21 | interop-laptop-esp32 interop-laptop-openwrt \ | ||
| 22 | interop-openwrt-esp32 interop-esp32-esp32 \ | ||
| 23 | interop-cleanup interop-save-state interop-restore-state | ||
| 24 | |||
| 25 | -include routers.env | ||
| 26 | |||
| 27 | BOLD := \033[1m | ||
| 28 | GREEN := \033[32m | ||
| 29 | RED := \033[31m | ||
| 30 | YELLOW := \033[33m | ||
| 31 | CYAN := \033[36m | ||
| 32 | RESET := \033[0m | ||
| 33 | |||
| 34 | define RESOLVE_ALPHA | ||
| 35 | alpha_host=$$(grep -E "^ROUTER_ALPHA_HOST=" routers.env | cut -d= -f2); \ | ||
| 36 | if [ -z "$$alpha_host" ]; then echo "$(RED)No ROUTER_ALPHA_HOST in routers.env$(RESET)"; exit 1; fi | ||
| 37 | endef | ||
| 38 | |||
| 39 | define RESOLVE_ESP32A | ||
| 40 | esp32_host=$$(grep -E "^ESP32_A_HOST=" routers.env | cut -d= -f2); \ | ||
| 41 | if [ -z "$$esp32_host" ]; then echo "$(RED)No ESP32_A_HOST in routers.env$(RESET)"; exit 1; fi | ||
| 42 | endef | ||
| 43 | |||
| 44 | MINT_TOKEN_BIN ?= /tmp/mint-token | ||
| 45 | |||
| 46 | help: ## Show this help | ||
| 47 | @echo "TollGate Interop Tests — ESP32 ↔ OpenWRT" | ||
| 48 | @echo "==========================================" | ||
| 49 | @echo "" | ||
| 50 | @echo "Setup:" | ||
| 51 | @echo " interop-setup Configure mints + fund wallets on both devices" | ||
| 52 | @echo " interop-setup-mints Add both mints to both devices" | ||
| 53 | @echo " interop-fund-esp32 Fund ESP32 wallet with V3 test tokens" | ||
| 54 | @echo " interop-fund-openwrt Fund OpenWRT wallet with V4 test tokens" | ||
| 55 | @echo "" | ||
| 56 | @echo "Test Scenarios:" | ||
| 57 | @echo " interop-status Show TollGate status for all devices" | ||
| 58 | @echo " interop-laptop-esp32 Scenario 1: Laptop pays ESP32 TollGate" | ||
| 59 | @echo " interop-laptop-openwrt Scenario 2: Laptop pays OpenWRT TollGate" | ||
| 60 | @echo " interop-openwrt-esp32 Scenario 3: OpenWRT auto-pays ESP32 for upstream" | ||
| 61 | @echo " interop-esp32-esp32 Scenario 5: ESP32 cross-board payment" | ||
| 62 | @echo "" | ||
| 63 | @echo "Cleanup:" | ||
| 64 | @echo " interop-cleanup Restore original configs on all devices" | ||
| 65 | @echo "" | ||
| 66 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ | ||
| 67 | awk 'BEGIN {FS = ":.*?## "}; {printf " $(CYAN)%-30s$(RESET) %s\n", $$1, $$2}' | ||
| 68 | |||
| 69 | # =========================================================================== | ||
| 70 | # Status | ||
| 71 | # =========================================================================== | ||
| 72 | |||
| 73 | interop-status: ## Show TollGate status for all devices | ||
| 74 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 75 | @echo "$(BOLD) TollGate Interop — Device Status$(RESET)" | ||
| 76 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 77 | @echo "" | ||
| 78 | @echo "$(CYAN)--- OpenWRT Router (alpha) ---$(RESET)" | ||
| 79 | @$(RESOLVE_ALPHA); \ | ||
| 80 | echo " Host: $$alpha_host"; \ | ||
| 81 | if ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "echo ok" >/dev/null 2>&1; then \ | ||
| 82 | echo " $(GREEN)SSH: reachable$(RESET)"; \ | ||
| 83 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate version 2>&1 | head -1"; \ | ||
| 84 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate status 2>&1 | grep -E 'running|wallet_ok|network_ok'"; \ | ||
| 85 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate wallet balance 2>&1"; \ | ||
| 86 | echo " Accepted mints:"; \ | ||
| 87 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "cat /etc/tollgate/config.json" 2>/dev/null | python3 -c "import sys,json; [print(' ' + m['url']) for m in json.load(sys.stdin).get('accepted_mints',[])]" 2>/dev/null || echo " (parse error)"; \ | ||
| 88 | else \ | ||
| 89 | echo " $(RED)SSH: unreachable$(RESET)"; \ | ||
| 90 | fi | ||
| 91 | @echo "" | ||
| 92 | @echo "$(CYAN)--- ESP32 Board A ---$(RESET)" | ||
| 93 | @$(RESOLVE_ESP32A); \ | ||
| 94 | echo " Host: $$esp32_host"; \ | ||
| 95 | if curl -s --connect-timeout 5 "http://$$esp32_host:2121/" >/dev/null 2>&1; then \ | ||
| 96 | echo " $(GREEN)API: reachable$(RESET)"; \ | ||
| 97 | curl -s --connect-timeout 5 "http://$$esp32_host:2121/" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' kind={d[\"kind\"]}, tags={len(d.get(\"tags\",[]))}')" 2>/dev/null || echo " (parse error)"; \ | ||
| 98 | curl -s --connect-timeout 5 "http://$$esp32_host:2121/wallet" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' Wallet: {d.get(\"balance\",\"?\")} sats, {d.get(\"proof_count\",\"?\")} proofs')" 2>/dev/null || echo " Wallet: (not available)"; \ | ||
| 99 | else \ | ||
| 100 | echo " $(RED)API: unreachable$(RESET)"; \ | ||
| 101 | fi | ||
| 102 | @echo "" | ||
| 103 | @echo "$(CYAN)--- Laptop Connectivity ---$(RESET)" | ||
| 104 | @wifi_if=$$(grep -E "^LAPTOP_WIFI=" routers.env | cut -d= -f2); \ | ||
| 105 | eth_if=$$(grep -E "^LAPTOP_ETH=" routers.env | cut -d= -f2); \ | ||
| 106 | echo " WiFi ($$wifi_if): $$(ip addr show $$wifi_if 2>/dev/null | grep 'inet ' | awk '{print $$2}' || echo 'no IP')"; \ | ||
| 107 | echo " Ethernet ($$eth_if): $$(ip addr show $$eth_if 2>/dev/null | grep 'inet ' | awk '{print $$2}' || echo 'no IP')" | ||
| 108 | |||
| 109 | # =========================================================================== | ||
| 110 | # Setup | ||
| 111 | # =========================================================================== | ||
| 112 | |||
| 113 | interop-setup-mints: ## Add both mints to both devices | ||
| 114 | @echo "$(BOLD)=== Adding both mints to both devices ===$(RESET)" | ||
| 115 | @echo "" | ||
| 116 | @echo "$(CYAN)Step 1: Add testnut.cashu.space to OpenWRT$(RESET)" | ||
| 117 | @$(RESOLVE_ALPHA); \ | ||
| 118 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "cat /etc/tollgate/config.json" 2>/dev/null | \ | ||
| 119 | python3 -c "import sys,json; d=json.load(sys.stdin); urls=[m['url'] for m in d.get('accepted_mints',[])]; print('testnut' if any('testnut.cashu.space' in u and 'nofee' not in u for u in urls) else 'missing')" 2>/dev/null | \ | ||
| 120 | grep -q testnut && echo " $(GREEN)Already present$(RESET)" || { \ | ||
| 121 | echo " Adding testnut.cashu.space..."; \ | ||
| 122 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "cat /etc/tollgate/config.json | python3 -c \"import sys,json; d=json.load(sys.stdin); d['accepted_mints'].append({'url':'https://testnut.cashu.space','price_per_step':21,'price_unit':'sats','min_balance':0}); json.dump(d,sys.stdout,indent=2)\" > /tmp/config-interop.json && mv /tmp/config-interop.json /etc/tollgate/config.json"; \ | ||
| 123 | echo " $(GREEN)Added$(RESET)"; \ | ||
| 124 | } | ||
| 125 | @echo "" | ||
| 126 | @echo "$(CYAN)Step 2: Restart OpenWRT service$(RESET)" | ||
| 127 | @$(RESOLVE_ALPHA); \ | ||
| 128 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "/etc/init.d/tollgate-wrt restart"; \ | ||
| 129 | echo " $(GREEN)Restarted$(RESET)" | ||
| 130 | @echo "" | ||
| 131 | @echo "$(YELLOW)Step 3: ESP32 mint config requires firmware rebuild$(RESET)" | ||
| 132 | @echo " ESP32 mint_url is in config.json on SPIFFS. To change it:" | ||
| 133 | @echo " 1. Edit main/config.json to add 'nofee.testnut.cashu.space' as secondary mint" | ||
| 134 | @echo " 2. make flash-a" | ||
| 135 | @echo " Skipping ESP32 mint change for now (both mints may not be needed for basic interop)." | ||
| 136 | |||
| 137 | interop-verify-mints: ## Verify both mints accepted on both sides | ||
| 138 | @echo "$(BOLD)=== Verifying Mint Configuration ===$(RESET)" | ||
| 139 | @$(RESOLVE_ALPHA); \ | ||
| 140 | echo ""; \ | ||
| 141 | echo "$(CYAN)OpenWRT accepted_mints:$(RESET)"; \ | ||
| 142 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "cat /etc/tollgate/config.json" 2>/dev/null | \ | ||
| 143 | python3 -c "import sys,json; [print(' ' + m['url']) for m in json.load(sys.stdin).get('accepted_mints',[])]" 2>/dev/null | ||
| 144 | @echo "" | ||
| 145 | @$(RESOLVE_ESP32A); \ | ||
| 146 | echo "$(CYAN)ESP32 API advertisement:$(RESET)"; \ | ||
| 147 | curl -s --connect-timeout 5 "http://$$esp32_host:2121/" | \ | ||
| 148 | python3 -c "import sys,json; d=json.load(sys.stdin); [print(' ' + t[3] + ' (price=' + t[2] + ' ' + t[1] + ')') for t in d.get('tags',[]) if t[0]=='price_per_step']" 2>/dev/null || echo " (parse error)" | ||
| 149 | |||
| 150 | interop-fund-esp32: ## Fund ESP32 wallet with V3 test tokens | ||
| 151 | @echo "$(BOLD)=== Funding ESP32 Wallet ===$(RESET)" | ||
| 152 | @$(RESOLVE_ESP32A); \ | ||
| 153 | echo "Minting 21 sats from testnut.cashu.space (V3 token)..."; \ | ||
| 154 | TOKEN=$$(cashu --env-mint testnut.cashu.space send --legacy 21 2>/dev/null | tail -1); \ | ||
| 155 | if [ -z "$$TOKEN" ]; then echo "$(RED)Failed to mint token$(RESET)"; exit 1; fi; \ | ||
| 156 | echo "Token minted (length $${#TOKEN}). Funding ESP32..."; \ | ||
| 157 | RESP=$$(curl -s --connect-timeout 5 -X POST -d "$$TOKEN" "http://$$esp32_host:2121/"); \ | ||
| 158 | echo "$$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' kind={d[\"kind\"]}'); [print(f' {t[0]}={t[1]}') for t in d.get('tags',[]) if t[0] in ('allotment','price_per_step')]" 2>/dev/null || echo " Response: $$RESP"; \ | ||
| 159 | echo ""; \ | ||
| 160 | echo "$(CYAN)ESP32 wallet status:$(RESET)"; \ | ||
| 161 | curl -s --connect-timeout 5 "http://$$esp32_host:2121/wallet" 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(f' Balance: {d.get(\"balance\",\"?\")} sats, {d.get(\"proof_count\",\"?\")} proofs')" 2>/dev/null || echo " (wallet endpoint not available)" | ||
| 162 | |||
| 163 | interop-fund-openwrt: ## Fund OpenWRT wallet with V4 test tokens | ||
| 164 | @echo "$(BOLD)=== Funding OpenWRT Wallet ===$(RESET)" | ||
| 165 | @if [ ! -x "$(MINT_TOKEN_BIN)" ]; then \ | ||
| 166 | echo "$(RED)mint-token not found at $(MINT_TOKEN_BIN)$(RESET)"; \ | ||
| 167 | echo "Build it: cd physical-router-test-automation/scripts/mint-token && go build -o /tmp/mint-token ."; \ | ||
| 168 | exit 1; \ | ||
| 169 | fi | ||
| 170 | @$(RESOLVE_ALPHA); \ | ||
| 171 | echo "Minting 1013 sats from nofee.testnut.cashu.space (V4 token)..."; \ | ||
| 172 | RAW=$$($(MINT_TOKEN_BIN) 2>/dev/null); \ | ||
| 173 | TOKEN=$$(echo "$$RAW" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])"); \ | ||
| 174 | if [ -z "$$TOKEN" ]; then echo "$(RED)Failed to mint token$(RESET)"; exit 1; fi; \ | ||
| 175 | echo "Token minted. Funding OpenWRT wallet..."; \ | ||
| 176 | echo "$$TOKEN" | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate wallet fund" 2>&1; \ | ||
| 177 | echo ""; \ | ||
| 178 | echo "$(CYAN)OpenWRT wallet status:$(RESET)"; \ | ||
| 179 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate wallet balance" | ||
| 180 | |||
| 181 | interop-setup: interop-setup-mints interop-verify-mints interop-fund-esp32 interop-fund-openwrt ## Full setup: mints + wallets | ||
| 182 | @echo "" | ||
| 183 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 184 | @echo "$(GREEN)$(BOLD) Interop setup complete$(RESET)" | ||
| 185 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 186 | |||
| 187 | # =========================================================================== | ||
| 188 | # Scenario 1: Laptop → ESP32 | ||
| 189 | # =========================================================================== | ||
| 190 | |||
| 191 | interop-laptop-esp32: ## Scenario 1: Laptop pays ESP32 TollGate with V3 token | ||
| 192 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 193 | @echo "$(BOLD) Scenario 1: Laptop → ESP32$(RESET)" | ||
| 194 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 195 | @$(RESOLVE_ESP32A); \ | ||
| 196 | echo ""; \ | ||
| 197 | echo "$(CYAN)1/6 — Verify ESP32 API reachable at $$esp32_host...$(RESET)"; \ | ||
| 198 | API=$$(curl -s --connect-timeout 5 "http://$$esp32_host:2121/"); \ | ||
| 199 | if [ -z "$$API" ]; then echo "$(RED)ESP32 API unreachable$(RESET)"; exit 1; fi; \ | ||
| 200 | KIND=$$(echo "$$API" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 201 | if [ "$$KIND" != "10021" ]; then echo "$(RED)Expected kind=10021, got $$KIND$(RESET)"; exit 1; fi; \ | ||
| 202 | echo " $(GREEN)kind=10021 advertisement received$(RESET)"; \ | ||
| 203 | echo ""; \ | ||
| 204 | echo "$(CYAN)2/6 — Minting V3 token (21 sats from testnut.cashu.space)...$(RESET)"; \ | ||
| 205 | TOKEN=$$(cashu --env-mint testnut.cashu.space send --legacy 21 2>/dev/null | tail -1); \ | ||
| 206 | if [ -z "$$TOKEN" ]; then echo "$(RED)Token minting failed$(RESET)"; exit 1; fi; \ | ||
| 207 | echo " $(GREEN)Token minted (length $${#TOKEN})$(RESET)"; \ | ||
| 208 | echo ""; \ | ||
| 209 | echo "$(CYAN)3/6 — POST token to ESP32 TollGate API...$(RESET)"; \ | ||
| 210 | RESP=$$(curl -s --connect-timeout 10 -X POST -d "$$TOKEN" "http://$$esp32_host:2121/"); \ | ||
| 211 | RKIND=$$(echo "$$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 212 | if [ "$$RKIND" != "1022" ]; then \ | ||
| 213 | echo "$(RED)Payment failed: kind=$$RKIND$(RESET)"; \ | ||
| 214 | echo "$$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); [print(f' {t}') for t in d.get('tags',[])]" 2>/dev/null; \ | ||
| 215 | exit 1; \ | ||
| 216 | fi; \ | ||
| 217 | echo " $(GREEN)kind=1022 session created$(RESET)"; \ | ||
| 218 | ALLOT=$$(echo "$$RESP" | python3 -c "import sys,json; [print(t[1]) for t in json.load(sys.stdin).get('tags',[]) if t[0]=='allotment']" 2>/dev/null); \ | ||
| 219 | echo " Allotment: $$ALLOT"; \ | ||
| 220 | echo ""; \ | ||
| 221 | echo "$(CYAN)4/6 — Verify internet through ESP32...$(RESET)"; \ | ||
| 222 | sleep 1; \ | ||
| 223 | PING_OK=0; \ | ||
| 224 | wifi_if=$$(grep -E "^LAPTOP_WIFI=" routers.env | cut -d= -f2); \ | ||
| 225 | for i in 1 2 3; do \ | ||
| 226 | if ping -c 2 -W 3 -I $$wifi_if 8.8.8.8 2>/dev/null | grep -q "0% packet loss"; then \ | ||
| 227 | PING_OK=1; break; \ | ||
| 228 | fi; \ | ||
| 229 | sleep 2; \ | ||
| 230 | done; \ | ||
| 231 | if [ "$$PING_OK" = "1" ]; then echo " $(GREEN)Internet works through ESP32$(RESET)"; \ | ||
| 232 | else echo " $(YELLOW)WARN: No internet (ESP32 may have no upstream)$(RESET)"; fi; \ | ||
| 233 | echo ""; \ | ||
| 234 | echo "$(CYAN)5/6 — Test spent token rejection...$(RESET)"; \ | ||
| 235 | RESP2=$$(curl -s --connect-timeout 5 -X POST -d "$$TOKEN" "http://$$esp32_host:2121/"); \ | ||
| 236 | RKIND2=$$(echo "$$RESP2" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 237 | if [ "$$RKIND2" = "21023" ]; then echo " $(GREEN)Spent token rejected (kind=21023)$(RESET)"; \ | ||
| 238 | else echo " $(YELLOW)WARN: Expected kind=21023 for spent token, got $$RKIND2$(RESET)"; fi; \ | ||
| 239 | echo ""; \ | ||
| 240 | echo "$(CYAN)6/6 — Test invalid token rejection...$(RESET)"; \ | ||
| 241 | RESP3=$$(curl -s --connect-timeout 5 -X POST -d "garbage_not_a_token" "http://$$esp32_host:2121/"); \ | ||
| 242 | RKIND3=$$(echo "$$RESP3" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 243 | if [ "$$RKIND3" = "21023" ]; then echo " $(GREEN)Invalid token rejected (kind=21023)$(RESET)"; \ | ||
| 244 | else echo " $(YELLOW)WARN: Expected kind=21023 for invalid token, got $$RKIND3$(RESET)"; fi; \ | ||
| 245 | echo ""; \ | ||
| 246 | echo "$(BOLD)=======================================$(RESET)"; \ | ||
| 247 | echo "$(GREEN)$(BOLD) Scenario 1 PASSED: Laptop → ESP32$(RESET)"; \ | ||
| 248 | echo "$(BOLD)=======================================$(RESET)" | ||
| 249 | |||
| 250 | # =========================================================================== | ||
| 251 | # Scenario 2: Laptop → OpenWRT | ||
| 252 | # =========================================================================== | ||
| 253 | |||
| 254 | interop-laptop-openwrt: ## Scenario 2: Laptop pays OpenWRT TollGate with V4 token | ||
| 255 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 256 | @echo "$(BOLD) Scenario 2: Laptop → OpenWRT$(RESET)" | ||
| 257 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 258 | @if [ ! -x "$(MINT_TOKEN_BIN)" ]; then \ | ||
| 259 | echo "$(RED)mint-token not found at $(MINT_TOKEN_BIN)$(RESET)"; \ | ||
| 260 | echo "Build it: cd physical-router-test-automation/scripts/mint-token && go build -o /tmp/mint-token ."; \ | ||
| 261 | exit 1; \ | ||
| 262 | fi | ||
| 263 | @$(RESOLVE_ALPHA); \ | ||
| 264 | echo ""; \ | ||
| 265 | echo "$(CYAN)1/6 — Verify OpenWRT API reachable at $$alpha_host...$(RESET)"; \ | ||
| 266 | API=$$(curl -s --connect-timeout 5 "http://$$alpha_host:2121/"); \ | ||
| 267 | if [ -z "$$API" ]; then echo "$(RED)OpenWRT API unreachable$(RESET)"; exit 1; fi; \ | ||
| 268 | KIND=$$(echo "$$API" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 269 | if [ "$$KIND" != "10021" ]; then echo "$(RED)Expected kind=10021, got $$KIND$(RESET)"; exit 1; fi; \ | ||
| 270 | echo " $(GREEN)kind=10021 advertisement received$(RESET)"; \ | ||
| 271 | PRICE=$$(echo "$$API" | python3 -c "import sys,json; [print(t[2] + ' ' + t[1]) for t in json.load(sys.stdin).get('tags',[]) if t[0]=='price_per_step']" 2>/dev/null | head -1); \ | ||
| 272 | echo " Price: $$PRICE"; \ | ||
| 273 | echo ""; \ | ||
| 274 | echo "$(CYAN)2/6 — Minting V4 token (1 sat from nofee.testnut.cashu.space)...$(RESET)"; \ | ||
| 275 | RAW=$$($(MINT_TOKEN_BIN) https://nofee.testnut.cashu.space 1 2>/dev/null); \ | ||
| 276 | TOKEN=$$(echo "$$RAW" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])"); \ | ||
| 277 | AMOUNT=$$(echo "$$RAW" | python3 -c "import sys,json; print(json.load(sys.stdin)['amount'])"); \ | ||
| 278 | if [ -z "$$TOKEN" ]; then echo "$(RED)Token minting failed$(RESET)"; exit 1; fi; \ | ||
| 279 | echo " $(GREEN)Token minted: $$AMOUNT sats (length $${#TOKEN})$(RESET)"; \ | ||
| 280 | echo ""; \ | ||
| 281 | echo "$(CYAN)3/6 — POST token to OpenWRT TollGate API...$(RESET)"; \ | ||
| 282 | RESP=$$(curl -s --connect-timeout 10 -X POST -d "$$TOKEN" "http://$$alpha_host:2121/"); \ | ||
| 283 | RKIND=$$(echo "$$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 284 | if [ "$$RKIND" != "1022" ]; then \ | ||
| 285 | echo "$(RED)Payment failed: kind=$$RKIND$(RESET)"; \ | ||
| 286 | echo "$$RESP" | python3 -m json.tool 2>/dev/null || echo "$$RESP"; \ | ||
| 287 | exit 1; \ | ||
| 288 | fi; \ | ||
| 289 | echo " $(GREEN)kind=1022 session created$(RESET)"; \ | ||
| 290 | echo ""; \ | ||
| 291 | echo "$(CYAN)4/6 — Verify internet through OpenWRT...$(RESET)"; \ | ||
| 292 | sleep 1; \ | ||
| 293 | PING_OK=0; \ | ||
| 294 | for i in 1 2 3; do \ | ||
| 295 | if ping -c 2 -W 3 8.8.8.8 2>/dev/null | grep -q "0% packet loss"; then \ | ||
| 296 | PING_OK=1; break; \ | ||
| 297 | fi; \ | ||
| 298 | sleep 2; \ | ||
| 299 | done; \ | ||
| 300 | if [ "$$PING_OK" = "1" ]; then echo " $(GREEN)Internet works$(RESET)"; \ | ||
| 301 | else echo " $(YELLOW)WARN: No internet (check routing)$(RESET)"; fi; \ | ||
| 302 | echo ""; \ | ||
| 303 | echo "$(CYAN)5/6 — Test spent token rejection...$(RESET)"; \ | ||
| 304 | RESP2=$$(curl -s --connect-timeout 5 -X POST -d "$$TOKEN" "http://$$alpha_host:2121/"); \ | ||
| 305 | RKIND2=$$(echo "$$RESP2" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 306 | if [ "$$RKIND2" = "21023" ]; then echo " $(GREEN)Spent token rejected (kind=21023)$(RESET)"; \ | ||
| 307 | else echo " $(YELLOW)WARN: Expected kind=21023 for spent token, got $$RKIND2$(RESET)"; fi; \ | ||
| 308 | echo ""; \ | ||
| 309 | echo "$(CYAN)6/6 — Test invalid token rejection...$(RESET)"; \ | ||
| 310 | RESP3=$$(curl -s --connect-timeout 5 -X POST -d "garbage_not_a_token" "http://$$alpha_host:2121/"); \ | ||
| 311 | RKIND3=$$(echo "$$RESP3" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 312 | if [ "$$RKIND3" = "21023" ]; then echo " $(GREEN)Invalid token rejected (kind=21023)$(RESET)"; \ | ||
| 313 | else echo " $(YELLOW)WARN: Expected kind=21023 for invalid, got $$RKIND3$(RESET)"; fi; \ | ||
| 314 | echo ""; \ | ||
| 315 | echo "$(BOLD)=======================================$(RESET)"; \ | ||
| 316 | echo "$(GREEN)$(BOLD) Scenario 2 PASSED: Laptop → OpenWRT$(RESET)"; \ | ||
| 317 | echo "$(BOLD)=======================================$(RESET)" | ||
| 318 | |||
| 319 | # =========================================================================== | ||
| 320 | # Scenario 3: OpenWRT → ESP32 (Reseller) | ||
| 321 | # =========================================================================== | ||
| 322 | |||
| 323 | interop-openwrt-esp32: ## Scenario 3: OpenWRT auto-pays ESP32 for upstream internet | ||
| 324 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 325 | @echo "$(BOLD) Scenario 3: OpenWRT → ESP32 (Reseller)$(RESET)" | ||
| 326 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 327 | @echo "" | ||
| 328 | @$(RESOLVE_ALPHA); \ | ||
| 329 | $(RESOLVE_ESP32A); \ | ||
| 330 | esp32_ssid=$$(grep -E "^ESP32_A_SSID=" routers.env | cut -d= -f2); \ | ||
| 331 | upstream_ssid=$$(grep -E "^UPSTREAM_SSID=" routers.env | cut -d= -f2); \ | ||
| 332 | upstream_pass=$$(grep -E "^UPSTREAM_PASS=" routers.env | cut -d= -f2); \ | ||
| 333 | \ | ||
| 334 | echo "$(CYAN)Step 0: Pre-flight$(RESET)"; \ | ||
| 335 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "echo alpha-ok" 2>/dev/null | grep -q alpha-ok || { echo "$(RED)OpenWRT unreachable$(RESET)"; exit 1; }; \ | ||
| 336 | curl -s --connect-timeout 5 "http://$$esp32_host:2121/" >/dev/null 2>&1 || { echo "$(RED)ESP32 API unreachable$(RESET)"; exit 1; }; \ | ||
| 337 | echo " $(GREEN)Both devices reachable$(RESET)"; \ | ||
| 338 | \ | ||
| 339 | echo ""; \ | ||
| 340 | echo "$(CYAN)Step 1: Save OpenWRT's current upstream$(RESET)"; \ | ||
| 341 | prev_ssid=$$(ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream list 2>/dev/null" | grep ACTIVE | awk '{print $$1}'); \ | ||
| 342 | echo " Active upstream: $$prev_ssid"; \ | ||
| 343 | echo "$$prev_ssid" > /tmp/interop-upstream-prev.txt; \ | ||
| 344 | \ | ||
| 345 | echo ""; \ | ||
| 346 | echo "$(CYAN)Step 2: Check ESP32 API advertisement$(RESET)"; \ | ||
| 347 | esp32_api=$$(curl -s --connect-timeout 5 "http://$$esp32_host:2121/"); \ | ||
| 348 | esp32_kind=$$(echo "$$esp32_api" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 349 | echo " ESP32 API kind=$$esp32_kind"; \ | ||
| 350 | if [ "$$esp32_kind" != "10021" ]; then echo "$(YELLOW)WARN: ESP32 not advertising TollGate service$(RESET)"; fi; \ | ||
| 351 | \ | ||
| 352 | echo ""; \ | ||
| 353 | echo "$(CYAN)Step 3: Connect OpenWRT to ESP32's AP ($$esp32_ssid)$(RESET)"; \ | ||
| 354 | echo "$(YELLOW)This will disrupt OpenWRT's current upstream connectivity.$(RESET)"; \ | ||
| 355 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream connect '$$esp32_ssid' 2>&1"; \ | ||
| 356 | echo " $(GREEN)Connect command sent$(RESET)"; \ | ||
| 357 | \ | ||
| 358 | echo ""; \ | ||
| 359 | echo "$(CYAN)Step 4: Wait for DHCP on wwan (up to 60s)$(RESET)"; \ | ||
| 360 | for i in 1 2 3 4 5 6 7 8 9 10 11 12; do \ | ||
| 361 | sleep 5; \ | ||
| 362 | if ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "ifstatus wwan 2>/dev/null | jsonfilter -e '@.up' 2>/dev/null | grep -q true" 2>/dev/null; then \ | ||
| 363 | echo "$(GREEN)Connected after $$((i*5))s$(RESET)"; \ | ||
| 364 | break; \ | ||
| 365 | fi; \ | ||
| 366 | if [ "$$i" = "12" ]; then \ | ||
| 367 | echo "$(RED)Failed to connect$(RESET)"; \ | ||
| 368 | echo "$(YELLOW)Restoring upstream...$(RESET)"; \ | ||
| 369 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream connect '$$prev_ssid' '$$upstream_pass'" 2>/dev/null || \ | ||
| 370 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream connect '$$prev_ssid'" 2>/dev/null; \ | ||
| 371 | exit 1; \ | ||
| 372 | fi; \ | ||
| 373 | echo " ... $$((i*5))s"; \ | ||
| 374 | done; \ | ||
| 375 | \ | ||
| 376 | echo ""; \ | ||
| 377 | echo "$(CYAN)Step 5: Watch for auto-payment (up to 30s)$(RESET)"; \ | ||
| 378 | timeout 30 ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "logread -e tollgate-wrt -f" 2>/dev/null | grep --line-buffered -i "payment\|session\|purchase\|allotment" | head -5; \ | ||
| 379 | \ | ||
| 380 | echo ""; \ | ||
| 381 | echo "$(CYAN)Step 6: Verify session on ESP32 (via serial log)$(RESET)"; \ | ||
| 382 | echo "$(YELLOW)Check ESP32 serial output for 'Session created' log.$(RESET)"; \ | ||
| 383 | echo "$(YELLOW)Or check: curl http://$$esp32_host:2121/wallet$(RESET)"; \ | ||
| 384 | \ | ||
| 385 | echo ""; \ | ||
| 386 | echo "$(CYAN)Step 7: Restore OpenWRT upstream to $$prev_ssid$(RESET)"; \ | ||
| 387 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream connect '$$prev_ssid' '$$upstream_pass'" 2>&1 || \ | ||
| 388 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream connect '$$prev_ssid'" 2>&1; \ | ||
| 389 | echo " $(GREEN)Upstream restored$(RESET)"; \ | ||
| 390 | \ | ||
| 391 | echo ""; \ | ||
| 392 | echo "$(CYAN)Step 8: Wait for OpenWRT recovery$(RESET)"; \ | ||
| 393 | for i in 1 2 3 4 5 6; do \ | ||
| 394 | sleep 10; \ | ||
| 395 | if ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "echo ok" 2>/dev/null | grep -q ok; then \ | ||
| 396 | echo "$(GREEN)OpenWRT recovered after $$((i*10))s$(RESET)"; \ | ||
| 397 | break; \ | ||
| 398 | fi; \ | ||
| 399 | if [ "$$i" = "6" ]; then echo "$(RED)OpenWRT not back after 60s$(RESET)"; exit 1; fi; \ | ||
| 400 | echo " ... $$((i*10))s"; \ | ||
| 401 | done; \ | ||
| 402 | \ | ||
| 403 | echo ""; \ | ||
| 404 | echo "$(BOLD)=======================================$(RESET)"; \ | ||
| 405 | echo "$(GREEN)$(BOLD) Scenario 3 complete: OpenWRT → ESP32$(RESET)"; \ | ||
| 406 | echo "$(BOLD)=======================================$(RESET)"; \ | ||
| 407 | rm -f /tmp/interop-upstream-prev.txt | ||
| 408 | |||
| 409 | # =========================================================================== | ||
| 410 | # Scenario 5: ESP32 ↔ ESP32 | ||
| 411 | # =========================================================================== | ||
| 412 | |||
| 413 | interop-esp32-esp32: ## Scenario 5: ESP32 cross-board payment (needs Board B flashed) | ||
| 414 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 415 | @echo "$(BOLD) Scenario 5: ESP32 ↔ ESP32$(RESET)" | ||
| 416 | @echo "$(BOLD)=======================================$(RESET)" | ||
| 417 | @echo "" | ||
| 418 | @echo "$(YELLOW)This scenario requires Board B to be flashed with unique nsec.$(RESET)" | ||
| 419 | @echo "$(YELLOW)Board B setup has not been automated yet.$(RESET)" | ||
| 420 | @echo "" | ||
| 421 | @$(RESOLVE_ESP32A); \ | ||
| 422 | esp32_b_ssid=$$(grep -E "^ESP32_B_SSID=" routers.env | cut -d= -f2); \ | ||
| 423 | esp32_b_host=$$(grep -E "^ESP32_B_HOST=" routers.env | cut -d= -f2); \ | ||
| 424 | if [ "$$esp32_b_ssid" = "TBD" ] || [ -z "$$esp32_b_host" ]; then \ | ||
| 425 | echo "$(RED)Board B not configured. Update routers.env with ESP32_B_SSID and ESP32_B_HOST.$(RESET)"; \ | ||
| 426 | echo "Steps to set up Board B:"; \ | ||
| 427 | echo " 1. Generate a new nsec: openssl rand -hex 32"; \ | ||
| 428 | echo " 2. Edit main/config.json with new nsec"; \ | ||
| 429 | echo " 3. make flash-b"; \ | ||
| 430 | echo " 4. Note the derived SSID and IP from serial output"; \ | ||
| 431 | echo " 5. Update routers.env"; \ | ||
| 432 | exit 1; \ | ||
| 433 | fi; \ | ||
| 434 | echo "Board B: SSID=$$esp32_b_ssid, Host=$$esp32_b_host"; \ | ||
| 435 | echo ""; \ | ||
| 436 | echo "$(CYAN)Step 1: Verify both boards reachable$(RESET)"; \ | ||
| 437 | curl -s --connect-timeout 5 "http://$$esp32_host:2121/" >/dev/null 2>&1 || { echo "$(RED)Board A unreachable$(RESET)"; exit 1; }; \ | ||
| 438 | curl -s --connect-timeout 5 "http://$$esp32_b_host:2121/" >/dev/null 2>&1 || { echo "$(RED)Board B unreachable (connect to its AP first)$(RESET)"; exit 1; }; \ | ||
| 439 | echo " $(GREEN)Both boards reachable$(RESET)"; \ | ||
| 440 | \ | ||
| 441 | echo ""; \ | ||
| 442 | echo "$(CYAN)Step 2: Mint V3 token and pay Board B$(RESET)"; \ | ||
| 443 | TOKEN=$$(cashu --env-mint testnut.cashu.space send --legacy 21 2>/dev/null | tail -1); \ | ||
| 444 | if [ -z "$$TOKEN" ]; then echo "$(RED)Token minting failed$(RESET)"; exit 1; fi; \ | ||
| 445 | echo " Token minted"; \ | ||
| 446 | RESP=$$(curl -s --connect-timeout 10 -X POST -d "$$TOKEN" "http://$$esp32_b_host:2121/"); \ | ||
| 447 | RKIND=$$(echo "$$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['kind'])" 2>/dev/null); \ | ||
| 448 | echo " Payment response: kind=$$RKIND"; \ | ||
| 449 | if [ "$$RKIND" = "1022" ]; then \ | ||
| 450 | echo " $(GREEN)Board B accepted payment$(RESET)"; \ | ||
| 451 | else \ | ||
| 452 | echo " $(YELLOW)Board B payment response: $$RESP$(RESET)"; \ | ||
| 453 | fi | ||
| 454 | |||
| 455 | # =========================================================================== | ||
| 456 | # Cleanup | ||
| 457 | # =========================================================================== | ||
| 458 | |||
| 459 | interop-cleanup: ## Restore original configs on all devices | ||
| 460 | @echo "$(BOLD)=== Interop Cleanup ===$(RESET)" | ||
| 461 | @$(RESOLVE_ALPHA); \ | ||
| 462 | echo "$(CYAN)Restoring OpenWRT production config...$(RESET)"; \ | ||
| 463 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "if [ -f /etc/tollgate/config.json.prod-backup ]; then mv /etc/tollgate/config.json.prod-backup /etc/tollgate/config.json && echo ' Config restored from backup'; else echo ' No backup found, keeping current config'; fi"; \ | ||
| 464 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "rm -f /etc/tollgate/config.json.bak /etc/tollgate/config.json.bak2 2>/dev/null"; \ | ||
| 465 | prev_ssid=$$(cat /tmp/interop-upstream-prev.txt 2>/dev/null); \ | ||
| 466 | if [ -n "$$prev_ssid" ]; then \ | ||
| 467 | upstream_pass=$$(grep -E "^UPSTREAM_PASS=" routers.env | cut -d= -f2); \ | ||
| 468 | echo " Restoring upstream to $$prev_ssid..."; \ | ||
| 469 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream connect '$$prev_ssid' '$$upstream_pass'" 2>/dev/null || \ | ||
| 470 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream connect '$$prev_ssid'" 2>/dev/null; \ | ||
| 471 | rm -f /tmp/interop-upstream-prev.txt; \ | ||
| 472 | fi; \ | ||
| 473 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "/etc/init.d/tollgate-wrt restart" 2>/dev/null; \ | ||
| 474 | echo " $(GREEN)OpenWRT cleanup done$(RESET)" | ||
| 475 | @echo "" | ||
| 476 | @echo "$(YELLOW)ESP32: No automated cleanup (firmware rebuild required for config changes).$(RESET)" | ||
| 477 | @echo "$(GREEN)Interop cleanup complete.$(RESET)" | ||
| 478 | |||
| 479 | interop-save-state: ## Save current device state before testing | ||
| 480 | @echo "$(BOLD)=== Saving Device State ===$(RESET)" | ||
| 481 | @$(RESOLVE_ALPHA); \ | ||
| 482 | echo "$(CYAN)Saving OpenWRT config...$(RESET)"; \ | ||
| 483 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "cp /etc/tollgate/config.json /etc/tollgate/config.json.prod-backup && echo 'Saved' || echo 'No config to save'"; \ | ||
| 484 | prev_ssid=$$(ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream list 2>/dev/null" | grep ACTIVE | awk '{print $$1}'); \ | ||
| 485 | echo " Current upstream: $$prev_ssid"; \ | ||
| 486 | echo "$$prev_ssid" > /tmp/interop-upstream-prev.txt; \ | ||
| 487 | echo "$(GREEN)State saved$(RESET)" | ||
| 488 | |||
| 489 | interop-restore-state: ## Restore saved device state | ||
| 490 | @echo "$(BOLD)=== Restoring Device State ===$(RESET)" | ||
| 491 | @$(RESOLVE_ALPHA); \ | ||
| 492 | echo "$(CYAN)Restoring OpenWRT config...$(RESET)"; \ | ||
| 493 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "if [ -f /etc/tollgate/config.json.prod-backup ]; then mv /etc/tollgate/config.json.prod-backup /etc/tollgate/config.json && echo 'Config restored'; else echo 'No backup found'; fi"; \ | ||
| 494 | prev_ssid=$$(cat /tmp/interop-upstream-prev.txt 2>/dev/null); \ | ||
| 495 | if [ -n "$$prev_ssid" ]; then \ | ||
| 496 | upstream_pass=$$(grep -E "^UPSTREAM_PASS=" routers.env | cut -d= -f2); \ | ||
| 497 | echo "Restoring upstream to $$prev_ssid..."; \ | ||
| 498 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream connect '$$prev_ssid' '$$upstream_pass'" 2>/dev/null || \ | ||
| 499 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "tollgate upstream connect '$$prev_ssid'" 2>/dev/null; \ | ||
| 500 | fi; \ | ||
| 501 | ssh $(SSH_OPTS) $(ROUTER_USER)@$$alpha_host "/etc/init.d/tollgate-wrt restart" 2>/dev/null; \ | ||
| 502 | rm -f /tmp/interop-upstream-prev.txt; \ | ||
| 503 | echo "$(GREEN)State restored$(RESET)" | ||
diff --git a/interop/PROGRESS.md b/interop/PROGRESS.md new file mode 100644 index 0000000..576eff2 --- /dev/null +++ b/interop/PROGRESS.md | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | # PROGRESS.md — Interop Test Checklist | ||
| 2 | |||
| 3 | ## Setup | ||
| 4 | |||
| 5 | - [ ] Create `interop/routers.env` from `routers.env.example` | ||
| 6 | - [ ] Verify SSH access to OpenWRT: `ssh root@10.47.41.1 echo ok` | ||
| 7 | - [ ] Verify WiFi connection to ESP32: `ping -c 2 10.192.45.1` | ||
| 8 | - [ ] Build `mint-token` binary: `cd physical-router-test-automation/scripts/mint-token && go build -o /tmp/mint-token .` | ||
| 9 | - [ ] Install `cashu` CLI: `pip install cashu` | ||
| 10 | |||
| 11 | ## Mint Alignment | ||
| 12 | |||
| 13 | - [ ] Add `testnut.cashu.space` to OpenWRT's `accepted_mints` | ||
| 14 | - [ ] Add `nofee.testnut.cashu.space` to ESP32's config | ||
| 15 | - [ ] Verify both mints accepted on OpenWRT | ||
| 16 | - [ ] Verify both mints accepted on ESP32 | ||
| 17 | |||
| 18 | ## Wallet Funding | ||
| 19 | |||
| 20 | - [ ] Fund ESP32 wallet via `cashu send --legacy` (V3 token) | ||
| 21 | - [ ] Fund OpenWRT wallet via `mint-token` (V4 token) | ||
| 22 | - [ ] Verify ESP32 balance > 0 | ||
| 23 | - [ ] Verify OpenWRT balance > 0 | ||
| 24 | |||
| 25 | ## Scenario 1: Laptop → ESP32 | ||
| 26 | |||
| 27 | - [ ] `make interop-laptop-esp32` — mint V3 token, POST to ESP32, verify internet | ||
| 28 | - [ ] Token accepted (kind=1022) | ||
| 29 | - [ ] Internet works after payment | ||
| 30 | - [ ] Spent token rejected (kind=21023) | ||
| 31 | |||
| 32 | ## Scenario 2: Laptop → OpenWRT | ||
| 33 | |||
| 34 | - [ ] `make interop-laptop-openwrt` — mint V4 token, POST to OpenWRT, verify internet | ||
| 35 | - [ ] Token accepted (kind=1022) | ||
| 36 | - [ ] Internet works after payment | ||
| 37 | - [ ] Spent token rejected (kind=21023) | ||
| 38 | |||
| 39 | ## Scenario 3: OpenWRT → ESP32 (Reseller) | ||
| 40 | |||
| 41 | - [ ] `make interop-openwrt-esp32` — OpenWRT connects to ESP32 AP, auto-pays | ||
| 42 | - [ ] OpenWRT STA connects to `TollGate-C0E9CA` | ||
| 43 | - [ ] OpenWRT daemon detects TollGate upstream | ||
| 44 | - [ ] Auto-payment succeeds (ESP32 session created) | ||
| 45 | - [ ] OpenWRT has internet through ESP32 | ||
| 46 | - [ ] ESP32 wallet balance increased | ||
| 47 | - [ ] Cleanup: restore OpenWRT upstream | ||
| 48 | |||
| 49 | ## Scenario 5: ESP32 ↔ ESP32 | ||
| 50 | |||
| 51 | - [ ] Flash Board B with different nsec | ||
| 52 | - [ ] Configure Board B's config.json | ||
| 53 | - [ ] Fund Board B wallet | ||
| 54 | - [ ] `make interop-esp32-esp32` — cross-board payment | ||
| 55 | - [ ] Cleanup: restore both boards | ||
| 56 | |||
| 57 | ## Cleanup | ||
| 58 | |||
| 59 | - [ ] Restore OpenWRT production config | ||
| 60 | - [ ] Restore ESP32 original config (if changed) | ||
| 61 | - [ ] Verify both devices back to normal operation | ||
| 62 | |||
| 63 | ## Infrastructure | ||
| 64 | |||
| 65 | - [x] `interop/INTEROP_PLAN.md` written | ||
| 66 | - [ ] `interop/AGENTS.md` written | ||
| 67 | - [ ] `interop/routers.env.example` written | ||
| 68 | - [ ] `interop/Makefile` written | ||
| 69 | - [ ] `interop-status` target tested against real hardware | ||
| 70 | - [ ] Committed and pushed | ||
diff --git a/interop/routers.env.example b/interop/routers.env.example new file mode 100644 index 0000000..4f07a36 --- /dev/null +++ b/interop/routers.env.example | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | # Router Access Configuration — Interop Tests | ||
| 2 | # Copy this file to routers.env and fill in your values. | ||
| 3 | # cp routers.env.example routers.env | ||
| 4 | # | ||
| 5 | # routers.env is gitignored — credentials never leave your machine. | ||
| 6 | |||
| 7 | ROUTER_USER ?= root | ||
| 8 | SSH_OPTS ?= -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new | ||
| 9 | |||
| 10 | # --- OpenWRT Router (alpha) --- | ||
| 11 | ROUTER_ALPHA_HOST=10.47.41.1 | ||
| 12 | ROUTER_ALPHA_LABEL=openwrt-alpha | ||
| 13 | ROUTER_ALPHA_SSID_24=TollGate-EVXZ-2.4GHz | ||
| 14 | ROUTER_ALPHA_SSID_5=TollGate-EVXZ-5GHz | ||
| 15 | ROUTER_ALPHA_PRIVATE_SSID=c03rad0r-EVXZ | ||
| 16 | ROUTER_ALPHA_PRIVATE_PASS=alpha-juliet-quebec-81 | ||
| 17 | |||
| 18 | # --- ESP32 Board A --- | ||
| 19 | ESP32_A_HOST=10.192.45.1 | ||
| 20 | ESP32_A_SSID=TollGate-C0E9CA | ||
| 21 | ESP32_A_SERIAL=/dev/ttyACM0 | ||
| 22 | |||
| 23 | # --- ESP32 Board B --- | ||
| 24 | ESP32_B_SERIAL=/dev/ttyACM1 | ||
| 25 | ESP32_B_SSID=TBD | ||
| 26 | ESP32_B_HOST=TBD | ||
| 27 | |||
| 28 | # --- Laptop interfaces --- | ||
| 29 | LAPTOP_ETH=enx00e04c683d2d | ||
| 30 | LAPTOP_WIFI=wlp59s0 | ||
| 31 | |||
| 32 | # --- Mints --- | ||
| 33 | MINT_TESTNUT=https://testnut.cashu.space | ||
| 34 | MINT_NOFEE=https://nofee.testnut.cashu.space | ||
| 35 | |||
| 36 | # --- Upstream WiFi (for restore-after-test) --- | ||
| 37 | UPSTREAM_SSID=EnterSSID-5GHz | ||
| 38 | UPSTREAM_PASS=c03rad0r123! | ||
| 39 | |||
| 40 | # --- Mint token tool --- | ||
| 41 | MINT_TOKEN_BIN=/tmp/mint-token | ||