upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/interop
diff options
context:
space:
mode:
Diffstat (limited to 'interop')
-rw-r--r--interop/AGENTS.md96
-rw-r--r--interop/INTEROP_PLAN.md149
-rw-r--r--interop/Makefile503
-rw-r--r--interop/PROGRESS.md70
-rw-r--r--interop/routers.env.example41
5 files changed, 859 insertions, 0 deletions
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
5Cross-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
91. **Always maintain these files:**
10 - `INTEROP_PLAN.md` — up-to-date interop test plan
11 - `PROGRESS.md` — checklist of done/pending items
12
132. **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
183. **Commit discipline:**
19 - Commit every time a test scenario passes end-to-end
20 - Push after each commit
21
224. **No comments in code** unless explicitly requested
23
245. **No secrets in git** — all secrets in `routers.env` (gitignored) or `.env`
25
266. **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
33This 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
60Both must be in both devices' accepted_mints for cross-platform payment.
61
62## Key Commands
63
64```bash
65# Show all device status
66make interop-status
67
68# Full setup (mints + wallets)
69make interop-setup
70
71# Run individual scenarios
72make interop-laptop-esp32
73make interop-laptop-openwrt
74make interop-openwrt-esp32
75make interop-esp32-esp32
76
77# Cleanup after tests
78make 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
88The 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
5Cross-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
41Both 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
61Token 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
69Laptop connects to ESP32 AP, mints V3 token, pays ESP32 TollGate API, verifies internet.
70
71This is the existing `make test-payment` flow, wrapped into the interop Makefile for consistency.
72
73### Scenario 2: Laptop → OpenWRT
74
75Laptop connects to OpenWRT AP (or uses existing ethernet connection), mints V4 token, pays OpenWRT TollGate API, verifies internet.
76
77**Steps:**
781. Verify laptop can reach OpenWRT at `10.47.41.1`
792. Check API advertisement at `http://10.47.41.1:2121/` (kind=10021)
803. Mint V4 token via `mint-token` binary
814. POST token to `http://10.47.41.1:2121/`
825. Verify kind=1022 session response
836. Verify internet via ping
84
85### Scenario 3: OpenWRT → ESP32 (Reseller)
86
87OpenWRT 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:**
901. Verify both devices accessible
912. Fund ESP32 wallet (for receiving payment)
923. Fund OpenWRT wallet (for paying upstream)
934. Save OpenWRT's current upstream SSID
945. Connect OpenWRT STA to `TollGate-C0E9CA` (ESP32's AP)
956. Wait for DHCP + upstream detection
967. Watch for auto-payment logs on OpenWRT
978. Verify session on ESP32 (via serial or API)
989. Restore OpenWRT upstream
9910. Restore production configs
100
101### Scenario 4: ESP32 → OpenWRT (Future)
102
103ESP32 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
107Board 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:**
1101. Flash Board B with different nsec
1112. Configure and fund both boards
1123. Board A connects STA to Board B's AP
1134. Manual curl payment test (POST token)
1145. 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
27BOLD := \033[1m
28GREEN := \033[32m
29RED := \033[31m
30YELLOW := \033[33m
31CYAN := \033[36m
32RESET := \033[0m
33
34define RESOLVE_ALPHA
35alpha_host=$$(grep -E "^ROUTER_ALPHA_HOST=" routers.env | cut -d= -f2); \
36if [ -z "$$alpha_host" ]; then echo "$(RED)No ROUTER_ALPHA_HOST in routers.env$(RESET)"; exit 1; fi
37endef
38
39define RESOLVE_ESP32A
40esp32_host=$$(grep -E "^ESP32_A_HOST=" routers.env | cut -d= -f2); \
41if [ -z "$$esp32_host" ]; then echo "$(RED)No ESP32_A_HOST in routers.env$(RESET)"; exit 1; fi
42endef
43
44MINT_TOKEN_BIN ?= /tmp/mint-token
45
46help: ## 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
73interop-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
113interop-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
137interop-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
150interop-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
163interop-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
181interop-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
191interop-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
254interop-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
323interop-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
413interop-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
459interop-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
479interop-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
489interop-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
7ROUTER_USER ?= root
8SSH_OPTS ?= -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new
9
10# --- OpenWRT Router (alpha) ---
11ROUTER_ALPHA_HOST=10.47.41.1
12ROUTER_ALPHA_LABEL=openwrt-alpha
13ROUTER_ALPHA_SSID_24=TollGate-EVXZ-2.4GHz
14ROUTER_ALPHA_SSID_5=TollGate-EVXZ-5GHz
15ROUTER_ALPHA_PRIVATE_SSID=c03rad0r-EVXZ
16ROUTER_ALPHA_PRIVATE_PASS=alpha-juliet-quebec-81
17
18# --- ESP32 Board A ---
19ESP32_A_HOST=10.192.45.1
20ESP32_A_SSID=TollGate-C0E9CA
21ESP32_A_SERIAL=/dev/ttyACM0
22
23# --- ESP32 Board B ---
24ESP32_B_SERIAL=/dev/ttyACM1
25ESP32_B_SSID=TBD
26ESP32_B_HOST=TBD
27
28# --- Laptop interfaces ---
29LAPTOP_ETH=enx00e04c683d2d
30LAPTOP_WIFI=wlp59s0
31
32# --- Mints ---
33MINT_TESTNUT=https://testnut.cashu.space
34MINT_NOFEE=https://nofee.testnut.cashu.space
35
36# --- Upstream WiFi (for restore-after-test) ---
37UPSTREAM_SSID=EnterSSID-5GHz
38UPSTREAM_PASS=c03rad0r123!
39
40# --- Mint token tool ---
41MINT_TOKEN_BIN=/tmp/mint-token