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-18 14:50:41 +0530
committerYour Name <you@example.com>2026-05-18 14:50:41 +0530
commit2a86bec93273e2f4ceeab60683058c65dbb1da3d (patch)
tree56e66acc4eaa0b5a947bb78d28e494a0adc857e2 /main
parent0c3f08ac7cf8e970369ec137153107ca8edc3326 (diff)
feat: multi-mint health tracker, discovery, portal, multi-wallet (Phase 3-8)
- mint_health.h/c: FreeRTOS probing task, GET /v1/info every 5min, recovery threshold 3, immediate failure, mutex-protected state - cashu.c: health-gated acceptance (config match AND reachable) - tollgate_api.c: one price_per_step tag per reachable mint in discovery - captive_portal.c: mint list with green/grey indicators, /mints API, auto-refresh every 30s via JS - nucula_wallet.h/cpp: multi-wallet (up to 4), route receive to correct wallet by mint URL, balance sums across all wallets - tollgate_main.c: init health tracker + multi-wallet on service start - CMakeLists.txt: add mint_health.c
Diffstat (limited to 'main')
-rw-r--r--main/CMakeLists.txt5
-rw-r--r--main/captive_portal.c86
-rw-r--r--main/cashu.c7
-rw-r--r--main/mint_health.c234
-rw-r--r--main/mint_health.h31
-rw-r--r--main/tollgate_api.c37
-rw-r--r--main/tollgate_main.c12
7 files changed, 388 insertions, 24 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 9b0fb1c..1416e07 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -15,8 +15,9 @@ idf_component_register(SRCS "tollgate_main.c"
15 "lightning_payout.c" 15 "lightning_payout.c"
16 "nip04.c" 16 "nip04.c"
17 "mcp_handler.c" 17 "mcp_handler.c"
18 "cvm_server.c" 18 "cvm_server.c"
19 "display.c" 19 "mint_health.c"
20 "display.c"
20 "font.c" 21 "font.c"
21 INCLUDE_DIRS "." 22 INCLUDE_DIRS "."
22 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server 23 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server
diff --git a/main/captive_portal.c b/main/captive_portal.c
index 1a3d5ce..57ef65a 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,32 @@ 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 mint_list_html[0] = '\0';
148 int mint_count = 0;
149 const mint_status_t *mints = mint_health_get_all(&mint_count);
150 for (int i = 0; i < mint_count; i++) {
151 const char *cls = mints[i].reachable ? "green" : "grey";
152 const char *url_cls = mints[i].reachable ? "mint-url" : "mint-url dim";
153 char item[512];
154 snprintf(item, sizeof(item),
155 "<div class='mint-item' onclick='copyMint(\"%s\")'>"
156 "<span class='mint-dot %s'></span>"
157 "<span class='%s'>%s</span></div>",
158 mints[i].url, cls, url_cls, mints[i].url);
159 strncat(mint_list_html, item, sizeof(mint_list_html) - strlen(mint_list_html) - 1);
160 }
161 if (mint_count == 0) {
162 const tollgate_config_t *cfg = tollgate_config_get();
163 snprintf(mint_list_html, sizeof(mint_list_html),
164 "<div class='mint-item'><span class='mint-dot grey'></span>"
165 "<span class='mint-url dim'>%s</span></div>", cfg->mint_url);
166 }
167
125 struct { const char *key; const char *val; } subs[] = { 168 struct { const char *key; const char *val; } subs[] = {
126 { "__AP_IP__", s_ap_ip_str }, 169 { "__AP_IP__", s_ap_ip_str },
127 { "__PRICE__", price_str }, 170 { "__PRICE__", price_str },
128 { "__MINT_URL__", cfg->mint_url }, 171 { "__MINT_LIST__", mint_list_html },
129 }; 172 };
130 int nsubs = sizeof(subs) / sizeof(subs[0]); 173 int nsubs = sizeof(subs) / sizeof(subs[0]);
131 174
@@ -190,6 +233,25 @@ static esp_err_t grant_access_handler(httpd_req_t *req)
190 return ESP_OK; 233 return ESP_OK;
191} 234}
192 235
236static esp_err_t mints_handler(httpd_req_t *req)
237{
238 int mint_count = 0;
239 const mint_status_t *mints = mint_health_get_all(&mint_count);
240 cJSON *arr = cJSON_CreateArray();
241 for (int i = 0; i < mint_count; i++) {
242 cJSON *obj = cJSON_CreateObject();
243 cJSON_AddStringToObject(obj, "url", mints[i].url);
244 cJSON_AddBoolToObject(obj, "reachable", mints[i].reachable);
245 cJSON_AddItemToArray(arr, obj);
246 }
247 char *json = cJSON_PrintUnformatted(arr);
248 httpd_resp_set_type(req, "application/json");
249 httpd_resp_send(req, json, strlen(json));
250 cJSON_free(json);
251 cJSON_Delete(arr);
252 return ESP_OK;
253}
254
193static esp_err_t status_handler(httpd_req_t *req) 255static esp_err_t status_handler(httpd_req_t *req)
194{ 256{
195 const tollgate_config_t *cfg = tollgate_config_get(); 257 const tollgate_config_t *cfg = tollgate_config_get();
@@ -290,6 +352,7 @@ static esp_err_t catchall_handler(httpd_req_t *req)
290 352
291static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler }; 353static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler };
292static const httpd_uri_t uri_grant = { .uri = "/grant_access", .method = HTTP_GET, .handler = grant_access_handler }; 354static const httpd_uri_t uri_grant = { .uri = "/grant_access", .method = HTTP_GET, .handler = grant_access_handler };
355static const httpd_uri_t uri_mints = { .uri = "/mints", .method = HTTP_GET, .handler = mints_handler };
293static const httpd_uri_t uri_status = { .uri = "/api/status", .method = HTTP_GET, .handler = status_handler }; 356static const httpd_uri_t uri_status = { .uri = "/api/status", .method = HTTP_GET, .handler = status_handler };
294static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = whoami_handler }; 357static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = whoami_handler };
295static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = usage_handler }; 358static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = usage_handler };
@@ -320,6 +383,7 @@ esp_err_t captive_portal_start(const char *ap_ip_str)
320 383
321 httpd_register_uri_handler(s_server, &uri_portal); 384 httpd_register_uri_handler(s_server, &uri_portal);
322 httpd_register_uri_handler(s_server, &uri_grant); 385 httpd_register_uri_handler(s_server, &uri_grant);
386 httpd_register_uri_handler(s_server, &uri_mints);
323 httpd_register_uri_handler(s_server, &uri_status); 387 httpd_register_uri_handler(s_server, &uri_status);
324 httpd_register_uri_handler(s_server, &uri_whoami); 388 httpd_register_uri_handler(s_server, &uri_whoami);
325 httpd_register_uri_handler(s_server, &uri_usage); 389 httpd_register_uri_handler(s_server, &uri_usage);
diff --git a/main/cashu.c b/main/cashu.c
index 2912d1d..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"
@@ -268,8 +269,10 @@ bool cashu_is_mint_accepted(const char *mint_url)
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 for (int i = 0; i < cfg->accepted_mint_count; i++) { 271 for (int i = 0; i < cfg->accepted_mint_count; i++) {
271 if (strstr(mint_url, cfg->accepted_mints[i]) != NULL) return true; 272 if (strstr(mint_url, cfg->accepted_mints[i]) != NULL ||
272 if (strcmp(mint_url, cfg->accepted_mints[i]) == 0) return true; 273 strcmp(mint_url, cfg->accepted_mints[i]) == 0) {
274 return mint_health_is_reachable(mint_url);
275 }
273 } 276 }
274 return false; 277 return false;
275} 278}
diff --git a/main/mint_health.c b/main/mint_health.c
new file mode 100644
index 0000000..39b0e8e
--- /dev/null
+++ b/main/mint_health.c
@@ -0,0 +1,234 @@
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");
159 run_initial_probes();
160
161 while (s_running) {
162 vTaskDelay(pdMS_TO_TICKS(MINT_HEALTH_PROBE_INTERVAL_S * 1000));
163 if (!s_running) break;
164 run_probes();
165 }
166
167 s_task_handle = NULL;
168 vTaskDelete(NULL);
169}
170
171void mint_health_start(void)
172{
173 if (s_running) return;
174 s_running = true;
175 xTaskCreate(health_task, "mint_health", 16384, NULL, 3, &s_task_handle);
176}
177
178void mint_health_stop(void)
179{
180 s_running = false;
181 if (s_task_handle) {
182 vTaskDelay(pdMS_TO_TICKS(100));
183 }
184}
185
186const mint_status_t *mint_health_get_all(int *out_count)
187{
188 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
189 *out_count = 0;
190 return s_mints;
191 }
192 *out_count = s_mint_count;
193 xSemaphoreGive(s_mutex);
194 return s_mints;
195}
196
197bool mint_health_is_reachable(const char *url)
198{
199 if (!url) return false;
200 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return false;
201 bool result = false;
202 for (int i = 0; i < s_mint_count; i++) {
203 if (strcmp(s_mints[i].url, url) == 0 || strstr(url, s_mints[i].url) != NULL) {
204 result = s_mints[i].reachable;
205 break;
206 }
207 }
208 xSemaphoreGive(s_mutex);
209 return result;
210}
211
212void mint_health_mark_unreachable(const char *url)
213{
214 if (!url) return;
215 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return;
216 for (int i = 0; i < s_mint_count; i++) {
217 if (strcmp(s_mints[i].url, url) == 0 || strstr(url, s_mints[i].url) != NULL) {
218 if (s_mints[i].reachable) {
219 s_mints[i].reachable = false;
220 s_mints[i].consecutive_successes = 0;
221 ESP_LOGW(TAG, "Reactively marked unreachable: %s", url);
222 }
223 break;
224 }
225 }
226 xSemaphoreGive(s_mutex);
227}
228
229void mint_health_register_callback(mint_health_changed_cb cb)
230{
231 if (s_callback_count < MAX_CALLBACKS && cb) {
232 s_callbacks[s_callback_count++] = cb;
233 }
234}
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 650b0f3..b694729 100644
--- a/main/tollgate_api.c
+++ b/main/tollgate_api.c
@@ -4,6 +4,7 @@
4#include "session.h" 4#include "session.h"
5#include "firewall.h" 5#include "firewall.h"
6#include "nucula_wallet.h" 6#include "nucula_wallet.h"
7#include "mint_health.h"
7#include "esp_log.h" 8#include "esp_log.h"
8#include "cJSON.h" 9#include "cJSON.h"
9#include "lwip/sockets.h" 10#include "lwip/sockets.h"
@@ -110,16 +111,36 @@ static esp_err_t api_get_discovery(httpd_req_t *req)
110 cJSON_AddItemToArray(step_tag, cJSON_CreateString(step_str)); 111 cJSON_AddItemToArray(step_tag, cJSON_CreateString(step_str));
111 cJSON_AddItemToArray(tags, step_tag); 112 cJSON_AddItemToArray(tags, step_tag);
112 113
113 cJSON *price_tag = cJSON_CreateArray();
114 cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step"));
115 cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu"));
116 char price_str[32]; 114 char price_str[32];
117 snprintf(price_str, sizeof(price_str), "%d", cfg->price_per_step); 115 snprintf(price_str, sizeof(price_str), "%d", cfg->price_per_step);
118 cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str)); 116
119 cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat")); 117 int mint_count = 0;
120 cJSON_AddItemToArray(price_tag, cJSON_CreateString(cfg->mint_url)); 118 const mint_status_t *mints = mint_health_get_all(&mint_count);
121 cJSON_AddItemToArray(price_tag, cJSON_CreateString("1")); 119 bool any_reachable = false;
122 cJSON_AddItemToArray(tags, price_tag); 120
121 for (int i = 0; i < mint_count; i++) {
122 if (!mints[i].reachable) continue;
123 any_reachable = true;
124 cJSON *price_tag = cJSON_CreateArray();
125 cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step"));
126 cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu"));
127 cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str));
128 cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat"));
129 cJSON_AddItemToArray(price_tag, cJSON_CreateString(mints[i].url));
130 cJSON_AddItemToArray(price_tag, cJSON_CreateString("1"));
131 cJSON_AddItemToArray(tags, price_tag);
132 }
133
134 if (!any_reachable) {
135 cJSON *price_tag = cJSON_CreateArray();
136 cJSON_AddItemToArray(price_tag, cJSON_CreateString("price_per_step"));
137 cJSON_AddItemToArray(price_tag, cJSON_CreateString("cashu"));
138 cJSON_AddItemToArray(price_tag, cJSON_CreateString(price_str));
139 cJSON_AddItemToArray(price_tag, cJSON_CreateString("sat"));
140 cJSON_AddItemToArray(price_tag, cJSON_CreateString(cfg->mint_url));
141 cJSON_AddItemToArray(price_tag, cJSON_CreateString("1"));
142 cJSON_AddItemToArray(tags, price_tag);
143 }
123 144
124 cJSON *tips_tag = cJSON_CreateArray(); 145 cJSON *tips_tag = cJSON_CreateArray();
125 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("tips")); 146 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("tips"));
diff --git a/main/tollgate_main.c b/main/tollgate_main.c
index c0ff65f..5f3e0e1 100644
--- a/main/tollgate_main.c
+++ b/main/tollgate_main.c
@@ -22,6 +22,7 @@
22#include "wifistr.h" 22#include "wifistr.h"
23#include "tollgate_client.h" 23#include "tollgate_client.h"
24#include "lightning_payout.h" 24#include "lightning_payout.h"
25#include "mint_health.h"
25#include "cvm_server.h" 26#include "cvm_server.h"
26#include "display.h" 27#include "display.h"
27 28
@@ -151,7 +152,15 @@ static void start_services(void)
151 session_manager_init(); 152 session_manager_init();
152 153
153 const tollgate_config_t *cfg = tollgate_config_get(); 154 const tollgate_config_t *cfg = tollgate_config_get();
154 nucula_wallet_init(cfg->mint_url); 155
156 mint_health_init(cfg->accepted_mints, cfg->accepted_mint_count);
157 mint_health_start();
158
159 if (cfg->accepted_mint_count > 1) {
160 nucula_wallet_init_multi(cfg->accepted_mints, cfg->accepted_mint_count);
161 } else {
162 nucula_wallet_init(cfg->mint_url);
163 }
155 lightning_payout_init(&cfg->payout); 164 lightning_payout_init(&cfg->payout);
156 165
157 dns_server_start(ap_ip_info.ip, upstream_dns); 166 dns_server_start(ap_ip_info.ip, upstream_dns);
@@ -187,6 +196,7 @@ static void stop_services(void)
187 captive_portal_stop(); 196 captive_portal_stop();
188 tollgate_api_stop(); 197 tollgate_api_stop();
189 dns_server_stop(); 198 dns_server_stop();
199 mint_health_stop();
190 cvm_server_stop(); 200 cvm_server_stop();
191 firewall_revoke_all(); 201 firewall_revoke_all();
192 s_services_running = false; 202 s_services_running = false;