upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-19 13:21:25 +0530
committerYour Name <you@example.com>2026-05-19 13:31:08 +0530
commiteeba74a4a1c011e85e33dea4252b381e35a64ea4 (patch)
tree14862e7d300511e28e214c743fd2f699bc54c5b8 /main
parentb0d9d494f00ee77f9efc22d1ef2ea3c94b23ddbd (diff)
feat: multi-mint wallet with health tracking, WPA auto-detect, display gating
Squash merge of feature/multi-mint-support (21 commits): Multi-mint wallet: - Accept payments from 4 mints: minibits, coinos, 21mint, lnvoltz - Periodic health probing (300s interval, 3 recovery threshold) - Multi-wallet init with nucula_wallet_init_multi() - /mints and /wallet API endpoints WPA auto-detect: - wifi_auth_mode config field (default WPA2, supports WPA3) - Runtime mapping to wifi_auth_mode_t in STA config Display gating: - display_enabled config field (default true) - Guards display_init/display_update per-board Bug fixes: - 3s delay before service start prevents lwip mem_free assertion - Real npub in discovery (identity_get()->npub_hex) - Health probe interval 300s (production value) - Duplicate services_start_task call removed - UTF-8 arrow replaced with ASCII in log message Tests: 61+14 unit tests passing, firmware builds clean
Diffstat (limited to 'main')
-rw-r--r--main/CMakeLists.txt1
-rw-r--r--main/captive_portal.c68
-rw-r--r--main/cashu.c10
-rw-r--r--main/config.c111
-rw-r--r--main/config.h11
-rw-r--r--main/cvm_server.c23
-rw-r--r--main/display.c2
-rw-r--r--main/mint_health.c235
-rw-r--r--main/mint_health.h31
-rw-r--r--main/tollgate_api.c71
-rw-r--r--main/tollgate_main.c29
-rw-r--r--main/wifistr.c8
12 files changed, 502 insertions, 98 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index abbe53b..f21b4e0 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -16,6 +16,7 @@ idf_component_register(SRCS "tollgate_main.c"
16 "nip04.c" 16 "nip04.c"
17 "mcp_handler.c" 17 "mcp_handler.c"
18 "cvm_server.c" 18 "cvm_server.c"
19 "mint_health.c"
19 "display.c" 20 "display.c"
20 "font.c" 21 "font.c"
21 "local_relay.c" 22 "local_relay.c"
diff --git a/main/captive_portal.c b/main/captive_portal.c
index 1a3d5ce..c9bcf19 100644
--- a/main/captive_portal.c
+++ b/main/captive_portal.c
@@ -2,6 +2,7 @@
2#include "firewall.h" 2#include "firewall.h"
3#include "session.h" 3#include "session.h"
4#include "config.h" 4#include "config.h"
5#include "mint_health.h"
5#include "esp_log.h" 6#include "esp_log.h"
6#include "esp_wifi.h" 7#include "esp_wifi.h"
7#include "cJSON.h" 8#include "cJSON.h"
@@ -42,9 +43,14 @@ static const char PORTAL_HTML_TEMPLATE[] = \
42".btn:disabled{background:#333;color:#666;cursor:not-allowed}" 43".btn:disabled{background:#333;color:#666;cursor:not-allowed}"
43".mints{background:#252525;border-radius:12px;padding:12px;margin-top:16px;text-align:left}" 44".mints{background:#252525;border-radius:12px;padding:12px;margin-top:16px;text-align:left}"
44".mints-title{color:#888;font-size:12px;margin-bottom:8px}" 45".mints-title{color:#888;font-size:12px;margin-bottom:8px}"
45".mint-url{font-family:monospace;font-size:11px;color:#f7931a;word-break:break-all;" 46".mint-item{display:flex;align-items:center;padding:6px 8px;margin-bottom:4px;"
46"background:#1a1a1a;padding:8px;border-radius:6px;cursor:pointer}" 47"background:#1a1a1a;border-radius:6px;cursor:pointer}"
47".mint-url:active{opacity:0.7}" 48".mint-item:active{opacity:0.7}"
49".mint-dot{width:8px;height:8px;border-radius:50%;margin-right:8px;flex-shrink:0}"
50".mint-dot.green{background:#4caf50}"
51".mint-dot.grey{background:#666}"
52".mint-url{font-family:monospace;font-size:11px;color:#f7931a;word-break:break-all}"
53".mint-url.dim{color:#666}"
48".mint-hint{color:#666;font-size:10px;margin-top:4px}" 54".mint-hint{color:#666;font-size:10px;margin-top:4px}"
49"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}" 55"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}"
50"#status.success{display:block;background:#1a472a;color:#4caf50}" 56"#status.success{display:block;background:#1a472a;color:#4caf50}"
@@ -63,20 +69,21 @@ static const char PORTAL_HTML_TEMPLATE[] = \
63"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>" 69"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>"
64"<div class='mints'>" 70"<div class='mints'>"
65"<div class='mints-title'>SUPPORTED MINTS</div>" 71"<div class='mints-title'>SUPPORTED MINTS</div>"
66"<div class='mint-url' id='mintUrl' onclick='copyMint()'>__MINT_URL__</div>" 72"<div id='mintList'>__MINT_LIST__</div>"
67"<div class='mint-hint'>Tap to copy &bull; Mint tokens at this URL before paying</div>" 73"<div class='mint-hint'>Tap to copy &bull; Green = reachable</div>"
68"</div>" 74"</div>"
69"<div id='status'></div>" 75"<div id='status'></div>"
70"</div>" 76"</div>"
71"<script>" 77"<script>"
72"const mintUrlEl=document.getElementById('mintUrl');" 78"const mintListEl=document.getElementById('mintList');"
73"const mintUrl=mintUrlEl.textContent;"
74"const statusEl=document.getElementById('status');" 79"const statusEl=document.getElementById('status');"
75"const payBtn=document.getElementById('payBtn');" 80"const payBtn=document.getElementById('payBtn');"
76"const tokenInput=document.getElementById('tokenInput');" 81"const tokenInput=document.getElementById('tokenInput');"
77"function copyMint(){" 82"function copyMint(url){"
78"if(navigator.clipboard){navigator.clipboard.writeText(mintUrl);" 83"if(navigator.clipboard){navigator.clipboard.writeText(url);"
79"mintUrlEl.textContent='Copied!';setTimeout(()=>{mintUrlEl.textContent=mintUrl;},1000);}" 84"const el=event.currentTarget;const u=el.querySelector('.mint-url');"
85"const orig=u.textContent;u.textContent='Copied!';"
86"setTimeout(()=>{u.textContent=orig;},1000);}"
80"}" 87"}"
81"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}" 88"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}"
82"function payToken(){" 89"function payToken(){"
@@ -93,6 +100,20 @@ static const char PORTAL_HTML_TEMPLATE[] = \
93"else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}" 100"else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}"
94"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});" 101"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});"
95"}" 102"}"
103"function refreshMints(){"
104"fetch('http://__AP_IP__:2121/mints').then(r=>r.json()).then(data=>{"
105"let html='';"
106"for(const m of data){"
107"const cls=m.reachable?'green':'grey';"
108"const urlCls=m.reachable?'mint-url':'mint-url dim';"
109"html+='<div class=\"mint-item\" onclick=\"copyMint(\\''+m.url+'\\')\">';"
110"html+='<span class=\"mint-dot '+cls+'\"></span>';"
111"html+='<span class=\"'+urlCls+'\">'+m.url+'</span></div>';"
112"}"
113"if(html)mintListEl.innerHTML=html;"
114"}).catch(()=>{});"
115"}"
116"setInterval(refreshMints,30000);"
96"</script>" 117"</script>"
97"</body></html>"; 118"</body></html>";
98 119
@@ -122,10 +143,35 @@ static esp_err_t portal_handler(httpd_req_t *req)
122 const char *tpl = PORTAL_HTML_TEMPLATE; 143 const char *tpl = PORTAL_HTML_TEMPLATE;
123 size_t tpl_len = strlen(tpl); 144 size_t tpl_len = strlen(tpl);
124 145
146 char mint_list_html[4096];
147 size_t mint_list_cap = sizeof(mint_list_html);
148 size_t mint_list_len = 0;
149 mint_list_html[0] = '\0';
150 int mint_count = 0;
151 const mint_status_t *mints = mint_health_get_all(&mint_count);
152 for (int i = 0; i < mint_count; i++) {
153 const char *cls = mints[i].reachable ? "green" : "grey";
154 const char *url_cls = mints[i].reachable ? "mint-url" : "mint-url dim";
155 int written = snprintf(mint_list_html + mint_list_len, mint_list_cap - mint_list_len,
156 "<div class='mint-item' onclick='copyMint(\"%s\")'>"
157 "<span class='mint-dot %s'></span>"
158 "<span class='%s'>%s</span></div>",
159 mints[i].url, cls, url_cls, mints[i].url);
160 if (written > 0 && (size_t)written < mint_list_cap - mint_list_len) {
161 mint_list_len += (size_t)written;
162 }
163 }
164 if (mint_count == 0) {
165 const tollgate_config_t *cfg = tollgate_config_get();
166 snprintf(mint_list_html, sizeof(mint_list_html),
167 "<div class='mint-item'><span class='mint-dot grey'></span>"
168 "<span class='mint-url dim'>%s</span></div>", cfg->mint_url);
169 }
170
125 struct { const char *key; const char *val; } subs[] = { 171 struct { const char *key; const char *val; } subs[] = {
126 { "__AP_IP__", s_ap_ip_str }, 172 { "__AP_IP__", s_ap_ip_str },
127 { "__PRICE__", price_str }, 173 { "__PRICE__", price_str },
128 { "__MINT_URL__", cfg->mint_url }, 174 { "__MINT_LIST__", mint_list_html },
129 }; 175 };
130 int nsubs = sizeof(subs) / sizeof(subs[0]); 176 int nsubs = sizeof(subs) / sizeof(subs[0]);
131 177
diff --git a/main/cashu.c b/main/cashu.c
index ec0566c..da12ff9 100644
--- a/main/cashu.c
+++ b/main/cashu.c
@@ -1,5 +1,6 @@
1#include "cashu.h" 1#include "cashu.h"
2#include "config.h" 2#include "config.h"
3#include "mint_health.h"
3#include "esp_log.h" 4#include "esp_log.h"
4#include "esp_http_client.h" 5#include "esp_http_client.h"
5#include "cJSON.h" 6#include "cJSON.h"
@@ -267,6 +268,11 @@ bool cashu_is_mint_accepted(const char *mint_url)
267{ 268{
268 if (!mint_url || mint_url[0] == '\0') return false; 269 if (!mint_url || mint_url[0] == '\0') return false;
269 const tollgate_config_t *cfg = tollgate_config_get(); 270 const tollgate_config_t *cfg = tollgate_config_get();
270 if (strstr(mint_url, cfg->mint_url) != NULL) return true; 271 for (int i = 0; i < cfg->accepted_mint_count; i++) {
271 return (strcmp(mint_url, cfg->mint_url) == 0); 272 if (strstr(mint_url, cfg->accepted_mints[i]) != NULL ||
273 strcmp(mint_url, cfg->accepted_mints[i]) == 0) {
274 return mint_health_is_reachable(mint_url);
275 }
276 }
277 return false;
272} 278}
diff --git a/main/config.c b/main/config.c
index b991991..5e3b247 100644
--- a/main/config.c
+++ b/main/config.c
@@ -16,7 +16,7 @@ esp_err_t tollgate_config_init(void)
16{ 16{
17 memset(&g_config, 0, sizeof(g_config)); 17 memset(&g_config, 0, sizeof(g_config));
18 g_config.max_retry = 5; 18 g_config.max_retry = 5;
19 g_config.ap_channel = 6; 19 g_config.ap_channel = 1;
20 g_config.ap_max_conn = 4; 20 g_config.ap_max_conn = 4;
21 g_config.price_per_step = 21; 21 g_config.price_per_step = 21;
22 g_config.step_size_ms = 60000; 22 g_config.step_size_ms = 60000;
@@ -24,6 +24,8 @@ esp_err_t tollgate_config_init(void)
24 strncpy(g_config.metric, "milliseconds", sizeof(g_config.metric) - 1); 24 strncpy(g_config.metric, "milliseconds", sizeof(g_config.metric) - 1);
25 g_config.persist_threshold_sats = 1; 25 g_config.persist_threshold_sats = 1;
26 g_config.nostr_publish_interval_s = 21600; 26 g_config.nostr_publish_interval_s = 21600;
27 g_config.nostr_sync_interval_s = 1800;
28 g_config.nostr_fallback_sync_interval_s = 21600;
27 g_config.client_enabled = false; 29 g_config.client_enabled = false;
28 g_config.client_steps_to_buy = 1; 30 g_config.client_steps_to_buy = 1;
29 g_config.client_renewal_threshold_pct = 20; 31 g_config.client_renewal_threshold_pct = 20;
@@ -35,8 +37,8 @@ esp_err_t tollgate_config_init(void)
35 g_config.payout.mint_count = 0; 37 g_config.payout.mint_count = 0;
36 g_config.cvm_enabled = true; 38 g_config.cvm_enabled = true;
37 strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1); 39 strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1);
38 g_config.nostr_sync_interval_s = 1800; 40 strncpy(g_config.wifi_auth_mode, "WPA2", sizeof(g_config.wifi_auth_mode) - 1);
39 g_config.nostr_fallback_sync_interval_s = 21600; 41 g_config.display_enabled = true;
40 42
41 esp_vfs_spiffs_conf_t conf = { 43 esp_vfs_spiffs_conf_t conf = {
42 .base_path = "/spiffs", 44 .base_path = "/spiffs",
@@ -56,17 +58,18 @@ esp_err_t tollgate_config_init(void)
56 const char *default_json = "{" 58 const char *default_json = "{"
57 "\"nsec\":\"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\"," 59 "\"nsec\":\"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\","
58 "\"wifi_networks\":[" 60 "\"wifi_networks\":["
59 "{\"ssid\":\"EnterSSID-2.4GHz\",\"password\":\"c03rad0r123!\"}," 61 "{\"ssid\":\"c03rad0r\",\"password\":\"c03rad0r123\"}"
60 "{\"ssid\":\"c03rad0r\",\"password\":\"c03rad0r123\"},"
61 "{\"ssid\":\"TK-GAESTE\",\"password\":\"\"}"
62 "]," 62 "],"
63 "\"ap_password\":\"\"," 63 "\"ap_password\":\"\","
64 "\"mint_url\":\"https://testnut.cashu.space\"," 64 "\"mint_url\":\"https://testnut.cashu.space\","
65 "\"accepted_mints\":[\"https://testnut.cashu.space\"],"
65 "\"price_per_step\":21," 66 "\"price_per_step\":21,"
66 "\"step_size_ms\":60000," 67 "\"step_size_ms\":60000,"
67 "\"nostr_geohash\":\"u281w0dfz\"," 68 "\"nostr_geohash\":\"u281w0dfz\","
68 "\"nostr_relays\":[\"wss://relay.damus.io\",\"wss://nos.lol\"]," 69 "\"nostr_relays\":[\"wss://relay.damus.io\",\"wss://nos.lol\"],"
69 "\"nostr_publish_interval_s\":21600," 70 "\"nostr_publish_interval_s\":21600,"
71 "\"nostr_sync_interval_s\":1800,"
72 "\"nostr_fallback_sync_interval_s\":21600,"
70 "\"client_enabled\":false," 73 "\"client_enabled\":false,"
71 "\"client_steps_to_buy\":1," 74 "\"client_steps_to_buy\":1,"
72 "\"client_renewal_threshold_pct\":20," 75 "\"client_renewal_threshold_pct\":20,"
@@ -129,12 +132,36 @@ esp_err_t tollgate_config_init(void)
129 } 132 }
130 } 133 }
131 134
135 if (g_config.network_count == 0) {
136 cJSON *ssid = cJSON_GetObjectItem(root, "wifi_ssid");
137 cJSON *pass = cJSON_GetObjectItem(root, "wifi_password");
138 if (ssid && cJSON_IsString(ssid) && pass && cJSON_IsString(pass)) {
139 strncpy(g_config.networks[0].ssid, ssid->valuestring, sizeof(g_config.networks[0].ssid) - 1);
140 strncpy(g_config.networks[0].password, pass->valuestring, sizeof(g_config.networks[0].password) - 1);
141 g_config.network_count = 1;
142 }
143 }
144
132 cJSON *ap_pass = cJSON_GetObjectItem(root, "ap_password"); 145 cJSON *ap_pass = cJSON_GetObjectItem(root, "ap_password");
133 if (ap_pass) strncpy(g_config.ap_password, ap_pass->valuestring, sizeof(g_config.ap_password) - 1); 146 if (ap_pass) strncpy(g_config.ap_password, ap_pass->valuestring, sizeof(g_config.ap_password) - 1);
134 147
135 cJSON *mint = cJSON_GetObjectItem(root, "mint_url"); 148 cJSON *mint = cJSON_GetObjectItem(root, "mint_url");
136 if (mint) strncpy(g_config.mint_url, mint->valuestring, sizeof(g_config.mint_url) - 1); 149 if (mint) strncpy(g_config.mint_url, mint->valuestring, sizeof(g_config.mint_url) - 1);
137 150
151 cJSON *acc_mints = cJSON_GetObjectItem(root, "accepted_mints");
152 if (acc_mints && cJSON_IsArray(acc_mints)) {
153 int mcount = cJSON_GetArraySize(acc_mints);
154 if (mcount > TOLLGATE_MAX_MINT_URLS) mcount = TOLLGATE_MAX_MINT_URLS;
155 for (int i = 0; i < mcount; i++) {
156 cJSON *m = cJSON_GetArrayItem(acc_mints, i);
157 if (m && cJSON_IsString(m)) {
158 strncpy(g_config.accepted_mints[i], m->valuestring,
159 sizeof(g_config.accepted_mints[i]) - 1);
160 g_config.accepted_mint_count++;
161 }
162 }
163 }
164
138 cJSON *lnurl = cJSON_GetObjectItem(root, "lnurl_url"); 165 cJSON *lnurl = cJSON_GetObjectItem(root, "lnurl_url");
139 if (lnurl) strncpy(g_config.lnurl_url, lnurl->valuestring, sizeof(g_config.lnurl_url) - 1); 166 if (lnurl) strncpy(g_config.lnurl_url, lnurl->valuestring, sizeof(g_config.lnurl_url) - 1);
140 167
@@ -175,6 +202,26 @@ esp_err_t tollgate_config_init(void)
175 cJSON *pub_interval = cJSON_GetObjectItem(root, "nostr_publish_interval_s"); 202 cJSON *pub_interval = cJSON_GetObjectItem(root, "nostr_publish_interval_s");
176 if (pub_interval) g_config.nostr_publish_interval_s = pub_interval->valueint; 203 if (pub_interval) g_config.nostr_publish_interval_s = pub_interval->valueint;
177 204
205 cJSON *sync_interval = cJSON_GetObjectItem(root, "nostr_sync_interval_s");
206 if (sync_interval) g_config.nostr_sync_interval_s = sync_interval->valueint;
207
208 cJSON *fallback_interval = cJSON_GetObjectItem(root, "nostr_fallback_sync_interval_s");
209 if (fallback_interval) g_config.nostr_fallback_sync_interval_s = fallback_interval->valueint;
210
211 cJSON *seed_relays = cJSON_GetObjectItem(root, "nostr_seed_relays");
212 if (seed_relays && cJSON_IsArray(seed_relays)) {
213 int srcount = cJSON_GetArraySize(seed_relays);
214 if (srcount > TOLLGATE_MAX_SEED_RELAYS) srcount = TOLLGATE_MAX_SEED_RELAYS;
215 for (int i = 0; i < srcount; i++) {
216 cJSON *r = cJSON_GetArrayItem(seed_relays, i);
217 if (r && cJSON_IsString(r)) {
218 strncpy(g_config.nostr_seed_relays[i], r->valuestring,
219 sizeof(g_config.nostr_seed_relays[i]) - 1);
220 g_config.nostr_seed_relay_count++;
221 }
222 }
223 }
224
178 cJSON *client_enabled = cJSON_GetObjectItem(root, "client_enabled"); 225 cJSON *client_enabled = cJSON_GetObjectItem(root, "client_enabled");
179 if (client_enabled && cJSON_IsBool(client_enabled)) g_config.client_enabled = cJSON_IsTrue(client_enabled); 226 if (client_enabled && cJSON_IsBool(client_enabled)) g_config.client_enabled = cJSON_IsTrue(client_enabled);
180 227
@@ -251,6 +298,14 @@ esp_err_t tollgate_config_init(void)
251 } 298 }
252 } 299 }
253 300
301 cJSON *auth_mode = cJSON_GetObjectItem(root, "wifi_auth_mode");
302 if (auth_mode && cJSON_IsString(auth_mode)) {
303 strncpy(g_config.wifi_auth_mode, auth_mode->valuestring, sizeof(g_config.wifi_auth_mode) - 1);
304 }
305
306 cJSON *disp_en = cJSON_GetObjectItem(root, "display_enabled");
307 if (disp_en && cJSON_IsBool(disp_en)) g_config.display_enabled = cJSON_IsTrue(disp_en);
308
254 if (g_config.payout.mint_count == 0 && g_config.mint_url[0] != '\0') { 309 if (g_config.payout.mint_count == 0 && g_config.mint_url[0] != '\0') {
255 strncpy(g_config.payout.mints[0].url, g_config.mint_url, 310 strncpy(g_config.payout.mints[0].url, g_config.mint_url,
256 sizeof(g_config.payout.mints[0].url) - 1); 311 sizeof(g_config.payout.mints[0].url) - 1);
@@ -259,28 +314,6 @@ esp_err_t tollgate_config_init(void)
259 g_config.payout.mint_count = 1; 314 g_config.payout.mint_count = 1;
260 } 315 }
261 316
262 cJSON *seed_relays = cJSON_GetObjectItem(root, "nostr_seed_relays");
263 if (seed_relays && cJSON_IsArray(seed_relays)) {
264 int srcount = cJSON_GetArraySize(seed_relays);
265 if (srcount > TOLLGATE_MAX_SEED_RELAYS) srcount = TOLLGATE_MAX_SEED_RELAYS;
266 for (int i = 0; i < srcount; i++) {
267 cJSON *r = cJSON_GetArrayItem(seed_relays, i);
268 if (r && cJSON_IsString(r)) {
269 strncpy(g_config.nostr_seed_relays[i], r->valuestring,
270 sizeof(g_config.nostr_seed_relays[i]) - 1);
271 g_config.nostr_seed_relay_count++;
272 }
273 }
274 }
275
276 cJSON *sync_interval = cJSON_GetObjectItem(root, "nostr_sync_interval_s");
277 if (sync_interval) g_config.nostr_sync_interval_s = sync_interval->valueint;
278
279 cJSON *fallback_interval = cJSON_GetObjectItem(root, "nostr_fallback_sync_interval_s");
280 if (fallback_interval) g_config.nostr_fallback_sync_interval_s = fallback_interval->valueint;
281
282 cJSON_Delete(root);
283
284 if (g_config.payout.recipient_count == 0) { 317 if (g_config.payout.recipient_count == 0) {
285 strncpy(g_config.payout.recipients[0].lightning_address, "TollGate@coinos.io", 318 strncpy(g_config.payout.recipients[0].lightning_address, "TollGate@coinos.io",
286 sizeof(g_config.payout.recipients[0].lightning_address) - 1); 319 sizeof(g_config.payout.recipients[0].lightning_address) - 1);
@@ -288,6 +321,14 @@ esp_err_t tollgate_config_init(void)
288 g_config.payout.recipient_count = 1; 321 g_config.payout.recipient_count = 1;
289 } 322 }
290 323
324 cJSON_Delete(root);
325
326 if (g_config.accepted_mint_count == 0 && g_config.mint_url[0] != '\0') {
327 strncpy(g_config.accepted_mints[0], g_config.mint_url,
328 sizeof(g_config.accepted_mints[0]) - 1);
329 g_config.accepted_mint_count = 1;
330 }
331
291 if (g_config.nostr_relay_count == 0) { 332 if (g_config.nostr_relay_count == 0) {
292 strncpy(g_config.nostr_relays[0], "wss://relay.damus.io", sizeof(g_config.nostr_relays[0]) - 1); 333 strncpy(g_config.nostr_relays[0], "wss://relay.damus.io", sizeof(g_config.nostr_relays[0]) - 1);
293 strncpy(g_config.nostr_relays[1], "wss://nos.lol", sizeof(g_config.nostr_relays[1]) - 1); 334 strncpy(g_config.nostr_relays[1], "wss://nos.lol", sizeof(g_config.nostr_relays[1]) - 1);
@@ -306,9 +347,9 @@ esp_err_t tollgate_config_init(void)
306 g_config.nostr_seed_relay_count = 4; 347 g_config.nostr_seed_relay_count = 4;
307 } 348 }
308 349
309 ESP_LOGI(TAG, "Config loaded: nsec=%s...%s, %d WiFi networks, price=%d sats/%dms", 350 ESP_LOGI(TAG, "Config loaded: nsec=%s...%s, %d WiFi networks, %d accepted mints, price=%d sats/%dms",
310 g_config.nsec, g_config.nsec + 60, g_config.network_count, 351 g_config.nsec, g_config.nsec + 60, g_config.network_count,
311 g_config.price_per_step, g_config.step_size_ms); 352 g_config.accepted_mint_count, g_config.price_per_step, g_config.step_size_ms);
312 return ESP_OK; 353 return ESP_OK;
313} 354}
314 355
@@ -325,14 +366,18 @@ esp_err_t tollgate_config_get_wifi(wifi_config_t *wifi_config)
325 strncpy((char *)wifi_config->sta.ssid, g_config.networks[idx].ssid, sizeof(wifi_config->sta.ssid) - 1); 366 strncpy((char *)wifi_config->sta.ssid, g_config.networks[idx].ssid, sizeof(wifi_config->sta.ssid) - 1);
326 strncpy((char *)wifi_config->sta.password, g_config.networks[idx].password, sizeof(wifi_config->sta.password) - 1); 367 strncpy((char *)wifi_config->sta.password, g_config.networks[idx].password, sizeof(wifi_config->sta.password) - 1);
327 wifi_config->sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; 368 wifi_config->sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
328 wifi_config->sta.pmf_cfg.capable = true; 369 if (strstr(g_config.wifi_auth_mode, "WPA3")) {
329 wifi_config->sta.pmf_cfg.required = false; 370 wifi_config->sta.threshold.authmode = WIFI_AUTH_WPA3_PSK;
330 wifi_config->sta.scan_method = WIFI_ALL_CHANNEL_SCAN; 371 } else if (strstr(g_config.wifi_auth_mode, "WPA2")) {
372 wifi_config->sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
373 }
374 ESP_LOGI(TAG, "STA auth threshold: %s -> %d", g_config.wifi_auth_mode, wifi_config->sta.threshold.authmode);
331 return ESP_OK; 375 return ESP_OK;
332} 376}
333 377
334esp_err_t tollgate_config_get_next_wifi(wifi_config_t *wifi_config) 378esp_err_t tollgate_config_get_next_wifi(wifi_config_t *wifi_config)
335{ 379{
380 if (g_config.network_count == 0) return ESP_ERR_NOT_FOUND;
336 g_config.current_network = (g_config.current_network + 1) % g_config.network_count; 381 g_config.current_network = (g_config.current_network + 1) % g_config.network_count;
337 return tollgate_config_get_wifi(wifi_config); 382 return tollgate_config_get_wifi(wifi_config);
338} 383}
diff --git a/main/config.h b/main/config.h
index af372af..370e6cc 100644
--- a/main/config.h
+++ b/main/config.h
@@ -9,7 +9,7 @@
9#include "lightning_payout.h" 9#include "lightning_payout.h"
10 10
11#define TOLLGATE_MAX_WIFI_NETWORKS 5 11#define TOLLGATE_MAX_WIFI_NETWORKS 5
12#define TOLLGATE_MAX_MINT_URLS 3 12#define TOLLGATE_MAX_MINT_URLS 8
13#define TOLLGATE_MAX_AP_SSID_LEN 32 13#define TOLLGATE_MAX_AP_SSID_LEN 32
14#define TOLLGATE_MAX_AP_PASS_LEN 64 14#define TOLLGATE_MAX_AP_PASS_LEN 64
15#define TOLLGATE_MAX_RELAYS 4 15#define TOLLGATE_MAX_RELAYS 4
@@ -41,6 +41,8 @@ typedef struct {
41 char ap_ip_str[16]; 41 char ap_ip_str[16];
42 42
43 char mint_url[256]; 43 char mint_url[256];
44 char accepted_mints[TOLLGATE_MAX_MINT_URLS][256];
45 int accepted_mint_count;
44 char lnurl_url[256]; 46 char lnurl_url[256];
45 int price_per_step; 47 int price_per_step;
46 int step_size_ms; 48 int step_size_ms;
@@ -52,6 +54,8 @@ typedef struct {
52 char nostr_relays[TOLLGATE_MAX_RELAYS][128]; 54 char nostr_relays[TOLLGATE_MAX_RELAYS][128];
53 int nostr_relay_count; 55 int nostr_relay_count;
54 int nostr_publish_interval_s; 56 int nostr_publish_interval_s;
57 int nostr_sync_interval_s;
58 int nostr_fallback_sync_interval_s;
55 59
56 bool identity_initialized; 60 bool identity_initialized;
57 61
@@ -65,10 +69,11 @@ typedef struct {
65 bool cvm_enabled; 69 bool cvm_enabled;
66 char cvm_relays[256]; 70 char cvm_relays[256];
67 71
72 char wifi_auth_mode[16];
73 bool display_enabled;
74
68 char nostr_seed_relays[TOLLGATE_MAX_SEED_RELAYS][128]; 75 char nostr_seed_relays[TOLLGATE_MAX_SEED_RELAYS][128];
69 int nostr_seed_relay_count; 76 int nostr_seed_relay_count;
70 int nostr_sync_interval_s;
71 int nostr_fallback_sync_interval_s;
72 77
73 bool market_enabled; 78 bool market_enabled;
74 int market_scan_interval_s; 79 int market_scan_interval_s;
diff --git a/main/cvm_server.c b/main/cvm_server.c
index a4804d2..10af956 100644
--- a/main/cvm_server.c
+++ b/main/cvm_server.c
@@ -31,9 +31,6 @@ static void publish_announcements_via_ws(esp_tls_t *tls);
31#define CVM_WS_BUF_SIZE 8192 31#define CVM_WS_BUF_SIZE 8192
32#define CVM_MAX_RESPONSE_SIZE 4096 32#define CVM_MAX_RESPONSE_SIZE 4096
33#define CVM_RECONNECT_DELAY_MS 5000 33#define CVM_RECONNECT_DELAY_MS 5000
34#define CVM_WS_READ_TIMEOUT_MS 1000
35#define CVM_WS_PING_INTERVAL_S 30
36#define CVM_WS_MAX_CONSECUTIVE_TIMEOUTS 65
37 34
38static char *parse_ws_text_frame(const uint8_t *buf, int len) 35static char *parse_ws_text_frame(const uint8_t *buf, int len)
39{ 36{
@@ -557,19 +554,14 @@ static void cvm_relay_task(void *arg)
557 return; 554 return;
558 } 555 }
559 556
560 int64_t last_ping_time = (int64_t)esp_timer_get_time() / 1000000;
561 int consecutive_timeouts = 0; 557 int consecutive_timeouts = 0;
562
563 while (g_running) { 558 while (g_running) {
564 int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); 559 int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1);
565 if (rlen < 0) { 560 if (rlen < 0) {
566 consecutive_timeouts++; 561 ESP_LOGW(TAG, "Read error on %s (rlen=%d)", relay_url, rlen);
567 if (consecutive_timeouts >= CVM_WS_MAX_CONSECUTIVE_TIMEOUTS) { 562 break;
568 ESP_LOGW(TAG, "Read timeout on %s (%d consecutive)", relay_url, consecutive_timeouts); 563 }
569 break; 564 if (rlen == 0) {
570 }
571 } else if (rlen == 0) {
572 ESP_LOGW(TAG, "Connection closed by %s", relay_url);
573 break; 565 break;
574 } else { 566 } else {
575 consecutive_timeouts = 0; 567 consecutive_timeouts = 0;
@@ -591,13 +583,6 @@ static void cvm_relay_task(void *arg)
591 } 583 }
592 } 584 }
593 585
594 int64_t now = (int64_t)esp_timer_get_time() / 1000000;
595 if (now - last_ping_time >= CVM_WS_PING_INTERVAL_S) {
596 uint8_t ping[2] = {0x89, 0x00};
597 esp_tls_conn_write(tls, ping, 2);
598 last_ping_time = now;
599 ESP_LOGD(TAG, "Sent WS keepalive ping");
600 }
601 } 586 }
602 587
603 free(buf); 588 free(buf);
diff --git a/main/display.c b/main/display.c
index 72b7686..2b6cc88 100644
--- a/main/display.c
+++ b/main/display.c
@@ -42,7 +42,7 @@ static int qr_pixel_size(int len) {
42 return 2; 42 return 2;
43} 43}
44 44
45int escape_wifi_field(const char *src, char *dst, int dst_size) { 45static int escape_wifi_field(const char *src, char *dst, int dst_size) {
46 int si = 0, di = 0; 46 int si = 0, di = 0;
47 while (src[si] && di < dst_size - 2) { 47 while (src[si] && di < dst_size - 2) {
48 char c = src[si]; 48 char c = src[si];
diff --git a/main/mint_health.c b/main/mint_health.c
new file mode 100644
index 0000000..5853a39
--- /dev/null
+++ b/main/mint_health.c
@@ -0,0 +1,235 @@
1#include "mint_health.h"
2#include "esp_log.h"
3#include "esp_http_client.h"
4#include "esp_crt_bundle.h"
5#include "freertos/FreeRTOS.h"
6#include "freertos/task.h"
7#include "freertos/semphr.h"
8#include <string.h>
9#include <stdlib.h>
10
11static const char *TAG = "mint_health";
12
13static mint_status_t s_mints[MINT_HEALTH_MAX];
14static int s_mint_count = 0;
15static bool s_running = false;
16static TaskHandle_t s_task_handle = NULL;
17static SemaphoreHandle_t s_mutex = NULL;
18
19#define MAX_CALLBACKS 4
20static mint_health_changed_cb s_callbacks[MAX_CALLBACKS];
21static int s_callback_count = 0;
22
23static void fire_callbacks(void)
24{
25 for (int i = 0; i < s_callback_count; i++) {
26 if (s_callbacks[i]) s_callbacks[i]();
27 }
28}
29
30esp_err_t mint_health_init(const char urls[][256], int count)
31{
32 if (count > MINT_HEALTH_MAX) count = MINT_HEALTH_MAX;
33 s_mint_count = count;
34 s_callback_count = 0;
35
36 if (!s_mutex) s_mutex = xSemaphoreCreateMutex();
37
38 memset(s_mints, 0, sizeof(s_mints));
39 for (int i = 0; i < count; i++) {
40 strncpy(s_mints[i].url, urls[i], sizeof(s_mints[i].url) - 1);
41 s_mints[i].reachable = false;
42 s_mints[i].consecutive_successes = 0;
43 s_mints[i].last_probe_ms = 0;
44 s_mints[i].last_http_status = 0;
45 }
46
47 ESP_LOGI(TAG, "Initialized with %d mints", count);
48 return ESP_OK;
49}
50
51static bool probe_mint(const char *url)
52{
53 char probe_url[512];
54 snprintf(probe_url, sizeof(probe_url), "%s/v1/info", url);
55
56 esp_http_client_config_t config = {
57 .url = probe_url,
58 .method = HTTP_METHOD_GET,
59 .timeout_ms = MINT_HEALTH_PROBE_TIMEOUT_MS,
60 .crt_bundle_attach = esp_crt_bundle_attach,
61 };
62 esp_http_client_handle_t client = esp_http_client_init(&config);
63 if (!client) return false;
64
65 esp_err_t err = esp_http_client_open(client, 0);
66 if (err != ESP_OK) {
67 esp_http_client_cleanup(client);
68 return false;
69 }
70
71 int content_length = esp_http_client_fetch_headers(client);
72 int status = esp_http_client_get_status_code(client);
73
74 char *resp = NULL;
75 if (content_length > 0 && content_length < 8192) {
76 resp = malloc(content_length + 1);
77 if (resp) {
78 int read = esp_http_client_read(client, resp, content_length);
79 if (read > 0) resp[read] = '\0';
80 }
81 }
82 if (resp) free(resp);
83
84 esp_http_client_cleanup(client);
85 return (status >= 200 && status < 300);
86}
87
88static void run_probes(void)
89{
90 int old_reachable = 0;
91 int new_reachable = 0;
92
93 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) return;
94
95 for (int i = 0; i < s_mint_count; i++) {
96 if (s_mints[i].reachable) old_reachable++;
97 }
98
99 for (int i = 0; i < s_mint_count; i++) {
100 bool ok = probe_mint(s_mints[i].url);
101 s_mints[i].last_probe_ms = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
102 s_mints[i].last_http_status = ok ? 200 : 0;
103
104 if (ok) {
105 s_mints[i].consecutive_successes++;
106 if (s_mints[i].consecutive_successes >= MINT_HEALTH_RECOVERY_THRESHOLD) {
107 if (!s_mints[i].reachable) {
108 ESP_LOGI(TAG, "Mint RECOVERED: %s", s_mints[i].url);
109 }
110 s_mints[i].reachable = true;
111 }
112 } else {
113 if (s_mints[i].reachable) {
114 ESP_LOGW(TAG, "Mint UNREACHABLE: %s", s_mints[i].url);
115 }
116 s_mints[i].reachable = false;
117 s_mints[i].consecutive_successes = 0;
118 }
119
120 if (s_mints[i].reachable) new_reachable++;
121 }
122
123 bool changed = (old_reachable != new_reachable);
124 xSemaphoreGive(s_mutex);
125
126 if (changed) {
127 ESP_LOGI(TAG, "Reachable set changed: %d -> %d", old_reachable, new_reachable);
128 fire_callbacks();
129 }
130}
131
132static void run_initial_probes(void)
133{
134 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) return;
135
136 for (int i = 0; i < s_mint_count; i++) {
137 bool ok = probe_mint(s_mints[i].url);
138 s_mints[i].last_probe_ms = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
139 s_mints[i].last_http_status = ok ? 200 : 0;
140
141 if (ok) {
142 s_mints[i].consecutive_successes = MINT_HEALTH_RECOVERY_THRESHOLD;
143 s_mints[i].reachable = true;
144 ESP_LOGI(TAG, "Initial probe OK: %s (reachable)", s_mints[i].url);
145 } else {
146 s_mints[i].consecutive_successes = 0;
147 s_mints[i].reachable = false;
148 ESP_LOGW(TAG, "Initial probe FAIL: %s (unreachable)", s_mints[i].url);
149 }
150 }
151
152 xSemaphoreGive(s_mutex);
153 fire_callbacks();
154}
155
156static void health_task(void *pvParameters)
157{
158 ESP_LOGI(TAG, "Health probe task started, waiting for DNS to stabilize...");
159 vTaskDelay(pdMS_TO_TICKS(5000));
160 run_initial_probes();
161
162 while (s_running) {
163 vTaskDelay(pdMS_TO_TICKS(MINT_HEALTH_PROBE_INTERVAL_S * 1000));
164 if (!s_running) break;
165 run_probes();
166 }
167
168 s_task_handle = NULL;
169 vTaskDelete(NULL);
170}
171
172void mint_health_start(void)
173{
174 if (s_running) return;
175 s_running = true;
176 xTaskCreate(health_task, "mint_health", 16384, NULL, 3, &s_task_handle);
177}
178
179void mint_health_stop(void)
180{
181 s_running = false;
182 if (s_task_handle) {
183 vTaskDelay(pdMS_TO_TICKS(100));
184 }
185}
186
187const mint_status_t *mint_health_get_all(int *out_count)
188{
189 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
190 *out_count = 0;
191 return s_mints;
192 }
193 *out_count = s_mint_count;
194 xSemaphoreGive(s_mutex);
195 return s_mints;
196}
197
198bool mint_health_is_reachable(const char *url)
199{
200 if (!url) return false;
201 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return false;
202 bool result = false;
203 for (int i = 0; i < s_mint_count; i++) {
204 if (strcmp(s_mints[i].url, url) == 0 || strstr(url, s_mints[i].url) != NULL) {
205 result = s_mints[i].reachable;
206 break;
207 }
208 }
209 xSemaphoreGive(s_mutex);
210 return result;
211}
212
213void mint_health_mark_unreachable(const char *url)
214{
215 if (!url) return;
216 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return;
217 for (int i = 0; i < s_mint_count; i++) {
218 if (strcmp(s_mints[i].url, url) == 0 || strstr(url, s_mints[i].url) != NULL) {
219 if (s_mints[i].reachable) {
220 s_mints[i].reachable = false;
221 s_mints[i].consecutive_successes = 0;
222 ESP_LOGW(TAG, "Reactively marked unreachable: %s", url);
223 }
224 break;
225 }
226 }
227 xSemaphoreGive(s_mutex);
228}
229
230void mint_health_register_callback(mint_health_changed_cb cb)
231{
232 if (s_callback_count < MAX_CALLBACKS && cb) {
233 s_callbacks[s_callback_count++] = cb;
234 }
235}
diff --git a/main/mint_health.h b/main/mint_health.h
new file mode 100644
index 0000000..f047d6a
--- /dev/null
+++ b/main/mint_health.h
@@ -0,0 +1,31 @@
1#ifndef MINT_HEALTH_H
2#define MINT_HEALTH_H
3
4#include "esp_err.h"
5#include <stdint.h>
6#include <stdbool.h>
7
8#define MINT_HEALTH_MAX 8
9#define MINT_HEALTH_PROBE_INTERVAL_S 300
10#define MINT_HEALTH_PROBE_TIMEOUT_MS 15000
11#define MINT_HEALTH_RECOVERY_THRESHOLD 3
12
13typedef struct {
14 char url[256];
15 bool reachable;
16 uint8_t consecutive_successes;
17 int64_t last_probe_ms;
18 int last_http_status;
19} mint_status_t;
20
21typedef void (*mint_health_changed_cb)(void);
22
23esp_err_t mint_health_init(const char urls[][256], int count);
24void mint_health_start(void);
25void mint_health_stop(void);
26const mint_status_t *mint_health_get_all(int *out_count);
27bool mint_health_is_reachable(const char *url);
28void mint_health_mark_unreachable(const char *url);
29void mint_health_register_callback(mint_health_changed_cb cb);
30
31#endif
diff --git a/main/tollgate_api.c b/main/tollgate_api.c
index 15640c7..21bf9ef 100644
--- a/main/tollgate_api.c
+++ b/main/tollgate_api.c
@@ -1,6 +1,7 @@
1#include "tollgate_api.h" 1#include "tollgate_api.h"
2#include "cashu.h" 2#include "cashu.h"
3#include "config.h" 3#include "config.h"
4#include "identity.h"
4#include "session.h" 5#include "session.h"
5#include "firewall.h" 6#include "firewall.h"
6#include "nucula_wallet.h" 7#include "nucula_wallet.h"
@@ -17,8 +18,6 @@
17static const char *TAG = "tollgate_api"; 18static const char *TAG = "tollgate_api";
18static httpd_handle_t s_api_server = NULL; 19static httpd_handle_t s_api_server = NULL;
19 20
20static const char *TOLLGATE_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000000";
21
22static esp_err_t get_client_ip(httpd_req_t *req, uint32_t *ip_out) 21static esp_err_t get_client_ip(httpd_req_t *req, uint32_t *ip_out)
23{ 22{
24 int sockfd = httpd_req_to_sockfd(req); 23 int sockfd = httpd_req_to_sockfd(req);
@@ -35,7 +34,7 @@ static cJSON *create_notice(const char *level, const char *code, const char *con
35{ 34{
36 cJSON *root = cJSON_CreateObject(); 35 cJSON *root = cJSON_CreateObject();
37 cJSON_AddNumberToObject(root, "kind", 21023); 36 cJSON_AddNumberToObject(root, "kind", 21023);
38 cJSON_AddStringToObject(root, "pubkey", TOLLGATE_PUBKEY); 37 cJSON_AddStringToObject(root, "pubkey", identity_get()->npub_hex);
39 cJSON *tags = cJSON_CreateArray(); 38 cJSON *tags = cJSON_CreateArray();
40 cJSON *level_tag = cJSON_CreateArray(); 39 cJSON *level_tag = cJSON_CreateArray();
41 cJSON_AddItemToArray(level_tag, cJSON_CreateString("level")); 40 cJSON_AddItemToArray(level_tag, cJSON_CreateString("level"));
@@ -54,7 +53,7 @@ static cJSON *create_session_event(uint32_t client_ip, uint64_t allotment_ms)
54{ 53{
55 cJSON *root = cJSON_CreateObject(); 54 cJSON *root = cJSON_CreateObject();
56 cJSON_AddNumberToObject(root, "kind", 1022); 55 cJSON_AddNumberToObject(root, "kind", 1022);
57 cJSON_AddStringToObject(root, "pubkey", TOLLGATE_PUBKEY); 56 cJSON_AddStringToObject(root, "pubkey", identity_get()->npub_hex);
58 57
59 cJSON *tags = cJSON_CreateArray(); 58 cJSON *tags = cJSON_CreateArray();
60 59
@@ -96,7 +95,7 @@ static esp_err_t api_get_discovery(httpd_req_t *req)
96 95
97 cJSON *root = cJSON_CreateObject(); 96 cJSON *root = cJSON_CreateObject();
98 cJSON_AddNumberToObject(root, "kind", 10021); 97 cJSON_AddNumberToObject(root, "kind", 10021);
99 cJSON_AddStringToObject(root, "pubkey", TOLLGATE_PUBKEY); 98 cJSON_AddStringToObject(root, "pubkey", identity_get()->npub_hex);
100 99
101 cJSON *tags = cJSON_CreateArray(); 100 cJSON *tags = cJSON_CreateArray();
102 101
@@ -113,16 +112,36 @@ static esp_err_t api_get_discovery(httpd_req_t *req)
113 cJSON_AddItemToArray(step_tag, cJSON_CreateString(step_str)); 112 cJSON_AddItemToArray(step_tag, cJSON_CreateString(step_str));
114 cJSON_AddItemToArray(tags, step_tag); 113 cJSON_AddItemToArray(tags, step_tag);
115 114
116 cJSON *price_tag = cJSON_CreateArray();
117 cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step"));
118 cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu"));
119 char price_str[32]; 115 char price_str[32];
120 snprintf(price_str, sizeof(price_str), "%d", cfg->price_per_step); 116 snprintf(price_str, sizeof(price_str), "%d", cfg->price_per_step);
121 cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str)); 117
122 cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat")); 118 int mint_count = 0;
123 cJSON_AddItemToArray(price_tag, cJSON_CreateString(cfg->mint_url)); 119 const mint_status_t *mints = mint_health_get_all(&mint_count);
124 cJSON_AddItemToArray(price_tag, cJSON_CreateString("1")); 120 bool any_reachable = false;
125 cJSON_AddItemToArray(tags, price_tag); 121
122 for (int i = 0; i < mint_count; i++) {
123 if (!mints[i].reachable) continue;
124 any_reachable = true;
125 cJSON *price_tag = cJSON_CreateArray();
126 cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step"));
127 cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu"));
128 cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str));
129 cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat"));
130 cJSON_AddItemToArray(price_tag, cJSON_CreateString(mints[i].url));
131 cJSON_AddItemToArray(price_tag, cJSON_CreateString("1"));
132 cJSON_AddItemToArray(tags, price_tag);
133 }
134
135 if (!any_reachable) {
136 cJSON *price_tag = cJSON_CreateArray();
137 cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step"));
138 cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu"));
139 cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str));
140 cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat"));
141 cJSON_AddItemToArray(price_tag, cJSON_CreateString(cfg->mint_url));
142 cJSON_AddItemToArray(price_tag, cJSON_CreateString("1"));
143 cJSON_AddItemToArray(tags, price_tag);
144 }
126 145
127 cJSON *tips_tag = cJSON_CreateArray(); 146 cJSON *tips_tag = cJSON_CreateArray();
128 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("tips")); 147 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("tips"));
@@ -466,8 +485,28 @@ static esp_err_t api_post_wallet_send(httpd_req_t *req)
466 return ESP_OK; 485 return ESP_OK;
467} 486}
468 487
488static esp_err_t api_get_mints(httpd_req_t *req)
489{
490 int mint_count = 0;
491 const mint_status_t *mints = mint_health_get_all(&mint_count);
492 cJSON *arr = cJSON_CreateArray();
493 for (int i = 0; i < mint_count; i++) {
494 cJSON *obj = cJSON_CreateObject();
495 cJSON_AddStringToObject(obj, "url", mints[i].url);
496 cJSON_AddBoolToObject(obj, "reachable", mints[i].reachable);
497 cJSON_AddItemToArray(arr, obj);
498 }
499 char *json = cJSON_PrintUnformatted(arr);
500 httpd_resp_set_type(req, "application/json");
501 httpd_resp_send(req, json, strlen(json));
502 cJSON_free(json);
503 cJSON_Delete(arr);
504 return ESP_OK;
505}
506
469static const httpd_uri_t uri_discovery = { .uri = "/", .method = HTTP_GET, .handler = api_get_discovery }; 507static const httpd_uri_t uri_discovery = { .uri = "/", .method = HTTP_GET, .handler = api_get_discovery };
470static const httpd_uri_t uri_payment = { .uri = "/", .method = HTTP_POST, .handler = api_post_payment }; 508static const httpd_uri_t uri_payment = { .uri = "/", .method = HTTP_POST, .handler = api_post_payment };
509static const httpd_uri_t uri_mints = { .uri = "/mints", .method = HTTP_GET, .handler = api_get_mints };
471static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = api_get_usage }; 510static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = api_get_usage };
472static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = api_get_whoami }; 511static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = api_get_whoami };
473static const httpd_uri_t uri_wallet = { .uri = "/wallet", .method = HTTP_GET, .handler = api_get_wallet }; 512static const httpd_uri_t uri_wallet = { .uri = "/wallet", .method = HTTP_GET, .handler = api_get_wallet };
@@ -520,17 +559,19 @@ esp_err_t tollgate_api_start(void)
520 httpd_config_t config = HTTPD_DEFAULT_CONFIG(); 559 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
521 config.server_port = 2121; 560 config.server_port = 2121;
522 config.ctrl_port = 32769; 561 config.ctrl_port = 32769;
523 config.max_uri_handlers = 10; 562 config.max_uri_handlers = 12;
524 config.stack_size = 16384; 563 config.stack_size = 16384;
525 564
526 esp_err_t ret = httpd_start(&s_api_server, &config); 565 esp_err_t ret = httpd_start(&s_api_server, &config);
527 if (ret != ESP_OK) { 566 if (ret != ESP_OK) {
528 ESP_LOGE(TAG, "Failed to start API server: %s", esp_err_to_name(ret)); 567 ESP_LOGE(TAG, "Failed to start API server: %s (heap: %lu)", esp_err_to_name(ret), (unsigned long)esp_get_free_heap_size());
568 s_api_server = NULL;
529 return ret; 569 return ret;
530 } 570 }
531 571
532 httpd_register_uri_handler(s_api_server, &uri_discovery); 572 httpd_register_uri_handler(s_api_server, &uri_discovery);
533 httpd_register_uri_handler(s_api_server, &uri_payment); 573 httpd_register_uri_handler(s_api_server, &uri_payment);
574 httpd_register_uri_handler(s_api_server, &uri_mints);
534 httpd_register_uri_handler(s_api_server, &uri_usage); 575 httpd_register_uri_handler(s_api_server, &uri_usage);
535 httpd_register_uri_handler(s_api_server, &uri_whoami); 576 httpd_register_uri_handler(s_api_server, &uri_whoami);
536 httpd_register_uri_handler(s_api_server, &uri_wallet); 577 httpd_register_uri_handler(s_api_server, &uri_wallet);
diff --git a/main/tollgate_main.c b/main/tollgate_main.c
index f062cb6..33e5b90 100644
--- a/main/tollgate_main.c
+++ b/main/tollgate_main.c
@@ -5,6 +5,7 @@
5#include "esp_wifi.h" 5#include "esp_wifi.h"
6#include "esp_event.h" 6#include "esp_event.h"
7#include "esp_log.h" 7#include "esp_log.h"
8#include "esp_system.h"
8#include "nvs_flash.h" 9#include "nvs_flash.h"
9#include "esp_netif.h" 10#include "esp_netif.h"
10#include "lwip/netif.h" 11#include "lwip/netif.h"
@@ -22,6 +23,7 @@
22#include "wifistr.h" 23#include "wifistr.h"
23#include "tollgate_client.h" 24#include "tollgate_client.h"
24#include "lightning_payout.h" 25#include "lightning_payout.h"
26#include "mint_health.h"
25#include "cvm_server.h" 27#include "cvm_server.h"
26#include "display.h" 28#include "display.h"
27#include "local_relay.h" 29#include "local_relay.h"
@@ -119,6 +121,7 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base,
119 121
120static void services_start_task(void *pvParameters) 122static void services_start_task(void *pvParameters)
121{ 123{
124 vTaskDelay(pdMS_TO_TICKS(3000));
122 start_services(); 125 start_services();
123 vTaskDelete(NULL); 126 vTaskDelete(NULL);
124} 127}
@@ -187,7 +190,15 @@ static void start_services(void)
187 session_manager_init(); 190 session_manager_init();
188 191
189 const tollgate_config_t *cfg = tollgate_config_get(); 192 const tollgate_config_t *cfg = tollgate_config_get();
190 nucula_wallet_init(cfg->mint_url); 193
194 mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count);
195 mint_health_start();
196
197 if (cfg->accepted_mint_count > 1) {
198 nucula_wallet_init_multi(cfg->accepted_mints, cfg->accepted_mint_count);
199 } else {
200 nucula_wallet_init(cfg->mint_url);
201 }
191 lightning_payout_init(&cfg->payout); 202 lightning_payout_init(&cfg->payout);
192 203
193 dns_server_start(ap_ip_info.ip, upstream_dns); 204 dns_server_start(ap_ip_info.ip, upstream_dns);
@@ -216,10 +227,12 @@ static void start_services(void)
216 if (s_services_mutex) xSemaphoreGive(s_services_mutex); 227 if (s_services_mutex) xSemaphoreGive(s_services_mutex);
217 ESP_LOGI(TAG, "=== TollGate services started ==="); 228 ESP_LOGI(TAG, "=== TollGate services started ===");
218 229
219 display_set_state(DISPLAY_READY); 230 if (tollgate_config_get()->display_enabled) {
220 char portal_url[128]; 231 display_set_state(DISPLAY_READY);
221 snprintf(portal_url, sizeof(portal_url), "http://%s/", cfg->ap_ip_str); 232 char portal_url[128];
222 display_update(cfg->ap_ssid, 0, 0, portal_url); 233 snprintf(portal_url, sizeof(portal_url), "http://%s/", cfg->ap_ip_str);
234 display_update(cfg->ap_ssid, 0, 0, portal_url);
235 }
223} 236}
224 237
225static void stop_services(void) 238static void stop_services(void)
@@ -306,8 +319,10 @@ void app_main(void)
306{ 319{
307 ESP_LOGI(TAG, "=== TollGate ESP32 Starting ==="); 320 ESP_LOGI(TAG, "=== TollGate ESP32 Starting ===");
308 321
309 display_init(); 322 if (tollgate_config_get()->display_enabled) {
310 display_set_state(DISPLAY_BOOT); 323 display_init();
324 display_set_state(DISPLAY_BOOT);
325 }
311 326
312 esp_err_t ret = nvs_flash_init(); 327 esp_err_t ret = nvs_flash_init();
313 if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { 328 if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
diff --git a/main/wifistr.c b/main/wifistr.c
index 543aaf6..bf03b4d 100644
--- a/main/wifistr.c
+++ b/main/wifistr.c
@@ -2,7 +2,6 @@
2#include "identity.h" 2#include "identity.h"
3#include "nostr_event.h" 3#include "nostr_event.h"
4#include "config.h" 4#include "config.h"
5#include "local_relay.h"
6#include "esp_log.h" 5#include "esp_log.h"
7#include "esp_tls.h" 6#include "esp_tls.h"
8#include "esp_crt_bundle.h" 7#include "esp_crt_bundle.h"
@@ -217,13 +216,8 @@ esp_err_t wifistr_publish(void)
217 216
218 ESP_LOGI(TAG, "Wifistr event: %s", event_json); 217 ESP_LOGI(TAG, "Wifistr event: %s", event_json);
219 218
220 esp_err_t local_ret = local_relay_publish(event_json, strlen(event_json));
221 if (local_ret == ESP_OK) {
222 ESP_LOGI(TAG, "Published to local relay");
223 }
224
225 const tollgate_config_t *cfg = tollgate_config_get(); 219 const tollgate_config_t *cfg = tollgate_config_get();
226 esp_err_t last_err = local_ret; 220 esp_err_t last_err = ESP_FAIL;
227 221
228 for (int i = 0; i < cfg->nostr_relay_count; i++) { 222 for (int i = 0; i < cfg->nostr_relay_count; i++) {
229 esp_err_t err = ws_send_to_relay(cfg->nostr_relays[i], event_json); 223 esp_err_t err = ws_send_to_relay(cfg->nostr_relays[i], event_json);