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:
Diffstat (limited to 'main')
-rw-r--r--main/CMakeLists.txt4
-rw-r--r--main/config.c13
-rw-r--r--main/cvm_server.c815
-rw-r--r--main/cvm_server.h4
-rw-r--r--main/mcp_handler.c236
-rw-r--r--main/mcp_handler.h12
-rw-r--r--main/session.c10
-rw-r--r--main/session.h3
-rw-r--r--main/tollgate_main.c21
9 files changed, 984 insertions, 134 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 91748f2..9b0fb1c 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -16,8 +16,10 @@ 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 "display.c"
20 "font.c"
19 INCLUDE_DIRS "." 21 INCLUDE_DIRS "."
20 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server 22 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server
21 lwip json esp_http_client mbedtls esp-tls log spiffs 23 lwip json esp_http_client mbedtls esp-tls log spiffs
22 nucula_lib secp256k1 24 nucula_lib secp256k1 axs15231b qrcode
23 PRIV_REQUIRES esp-tls) 25 PRIV_REQUIRES esp-tls)
diff --git a/main/config.c b/main/config.c
index e937fb3..9dd2a1d 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 = 1; 19 g_config.ap_channel = 6;
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;
@@ -33,8 +33,8 @@ esp_err_t tollgate_config_init(void)
33 g_config.payout.check_interval_s = 60; 33 g_config.payout.check_interval_s = 60;
34 g_config.payout.recipient_count = 0; 34 g_config.payout.recipient_count = 0;
35 g_config.payout.mint_count = 0; 35 g_config.payout.mint_count = 0;
36 g_config.cvm_enabled = false; 36 g_config.cvm_enabled = true;
37 strncpy(g_config.cvm_relays, "wss://relay.damus.io", sizeof(g_config.cvm_relays) - 1); 37 strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1);
38 38
39 esp_vfs_spiffs_conf_t conf = { 39 esp_vfs_spiffs_conf_t conf = {
40 .base_path = "/spiffs", 40 .base_path = "/spiffs",
@@ -54,7 +54,9 @@ esp_err_t tollgate_config_init(void)
54 const char *default_json = "{" 54 const char *default_json = "{"
55 "\"nsec\":\"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\"," 55 "\"nsec\":\"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\","
56 "\"wifi_networks\":[" 56 "\"wifi_networks\":["
57 "{\"ssid\":\"EnterSSID-2.4GHz\",\"password\":\"c03rad0r123!\"}" 57 "{\"ssid\":\"EnterSSID-2.4GHz\",\"password\":\"c03rad0r123!\"},"
58 "{\"ssid\":\"c03rad0r\",\"password\":\"c03rad0r123\"},"
59 "{\"ssid\":\"TK-GAESTE\",\"password\":\"\"}"
58 "]," 60 "],"
59 "\"ap_password\":\"\"," 61 "\"ap_password\":\"\","
60 "\"mint_url\":\"https://testnut.cashu.space\"," 62 "\"mint_url\":\"https://testnut.cashu.space\","
@@ -289,6 +291,9 @@ esp_err_t tollgate_config_get_wifi(wifi_config_t *wifi_config)
289 strncpy((char *)wifi_config->sta.ssid, g_config.networks[idx].ssid, sizeof(wifi_config->sta.ssid) - 1); 291 strncpy((char *)wifi_config->sta.ssid, g_config.networks[idx].ssid, sizeof(wifi_config->sta.ssid) - 1);
290 strncpy((char *)wifi_config->sta.password, g_config.networks[idx].password, sizeof(wifi_config->sta.password) - 1); 292 strncpy((char *)wifi_config->sta.password, g_config.networks[idx].password, sizeof(wifi_config->sta.password) - 1);
291 wifi_config->sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; 293 wifi_config->sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
294 wifi_config->sta.pmf_cfg.capable = true;
295 wifi_config->sta.pmf_cfg.required = false;
296 wifi_config->sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
292 return ESP_OK; 297 return ESP_OK;
293} 298}
294 299
diff --git a/main/cvm_server.c b/main/cvm_server.c
index 5addd88..b93e176 100644
--- a/main/cvm_server.c
+++ b/main/cvm_server.c
@@ -2,219 +2,771 @@
2#include "mcp_handler.h" 2#include "mcp_handler.h"
3#include "nip04.h" 3#include "nip04.h"
4#include "identity.h" 4#include "identity.h"
5#include "nostr_event.h"
5#include "config.h" 6#include "config.h"
7#include "session.h"
6#include "nucula_wallet.h" 8#include "nucula_wallet.h"
7#include "cJSON.h" 9#include "cJSON.h"
8#include "esp_log.h" 10#include "esp_log.h"
9#include "esp_http_client.h" 11#include "esp_tls.h"
12#include "esp_crt_bundle.h"
13#include "esp_random.h"
14#include "esp_timer.h"
10#include "freertos/FreeRTOS.h" 15#include "freertos/FreeRTOS.h"
11#include "freertos/task.h" 16#include "freertos/task.h"
12#include <string.h> 17#include <string.h>
13#include <stdio.h> 18#include <stdio.h>
19#include <stdlib.h>
14 20
15static const char *TAG = "cvm_server"; 21static const char *TAG = "cvm_server";
16 22
17static bool g_running = false; 23static bool g_running = false;
18static TaskHandle_t g_task = NULL; 24static TaskHandle_t g_task = NULL;
19 25
20static const char *DEFAULT_RELAY = "wss://relay.damus.io"; 26static void publish_announcements_via_ws(esp_tls_t *tls);
21 27
22static char *fetch_relays(void) 28#define CVM_VERSION "2025-07-02"
29#define CVM_SERVER_NAME "TollGate"
30#define CVM_SERVER_VERSION "1.0.0"
31#define CVM_WS_BUF_SIZE 8192
32#define CVM_MAX_RESPONSE_SIZE 4096
33#define CVM_RECONNECT_DELAY_MS 5000
34#define CVM_WS_READ_TIMEOUT_MS 60000
35#define CVM_WS_PING_INTERVAL_S 30
36
37static char *parse_ws_text_frame(const uint8_t *buf, int len)
23{ 38{
24 const tollgate_config_t *cfg = tollgate_config_get(); 39 if (len < 2) return NULL;
25 if (cfg && cfg->cvm_relays[0]) { 40 bool masked = (buf[1] & 0x80) != 0;
26 return cfg->cvm_relays; 41 uint64_t payload_len = buf[1] & 0x7F;
42 int offset = 2;
43
44 if (payload_len == 126) {
45 if (len < 4) return NULL;
46 payload_len = ((uint64_t)buf[2] << 8) | buf[3];
47 offset = 4;
48 } else if (payload_len == 127) {
49 if (len < 10) return NULL;
50 payload_len = 0;
51 for (int i = 0; i < 8; i++)
52 payload_len = (payload_len << 8) | buf[2 + i];
53 offset = 10;
54 }
55
56 if (masked) offset += 4;
57 if (offset + payload_len > (uint64_t)len) return NULL;
58
59 char *text = malloc((size_t)payload_len + 1);
60 if (!text) return NULL;
61
62 if (masked) {
63 uint8_t mask[4] = { buf[offset - 4], buf[offset - 3], buf[offset - 2], buf[offset - 1] };
64 for (uint64_t i = 0; i < payload_len; i++)
65 text[i] = buf[offset + i] ^ mask[i & 3];
66 } else {
67 memcpy(text, buf + offset, (size_t)payload_len);
68 }
69 text[payload_len] = '\0';
70 return text;
71}
72
73static int ws_send_text(esp_tls_t *tls, const char *msg)
74{
75 size_t len = strlen(msg);
76 uint8_t mask[4];
77 esp_fill_random(mask, 4);
78
79 size_t frame_len = 6 + len;
80 if (len > 125) frame_len += 2;
81 if (len > 65535) frame_len += 6;
82
83 uint8_t *frame = malloc(frame_len + len);
84 if (!frame) return -1;
85
86 int pos = 0;
87 frame[pos++] = 0x81;
88 if (len <= 125) {
89 frame[pos++] = (uint8_t)(0x80 | len);
90 } else if (len <= 65535) {
91 frame[pos++] = 0x80 | 126;
92 frame[pos++] = (uint8_t)((len >> 8) & 0xff);
93 frame[pos++] = (uint8_t)(len & 0xff);
94 } else {
95 frame[pos++] = 0x80 | 127;
96 for (int i = 0; i < 8; i++)
97 frame[pos++] = (uint8_t)((len >> (56 - i * 8)) & 0xff);
98 }
99 memcpy(frame + pos, mask, 4);
100 pos += 4;
101
102 for (size_t i = 0; i < len; i++)
103 frame[pos + i] = (uint8_t)msg[i] ^ mask[i & 3];
104 pos += len;
105
106 int total = pos;
107 int written = 0;
108 while (written < total) {
109 int w = esp_tls_conn_write(tls, frame + written, total - written);
110 if (w < 0) {
111 ESP_LOGE(TAG, "ws_send: write failed at %d/%d", written, total);
112 free(frame);
113 return -1;
114 }
115 if (w == 0) {
116 ESP_LOGW(TAG, "ws_send: write returned 0 at %d/%d", written, total);
117 vTaskDelay(pdMS_TO_TICKS(1));
118 }
119 written += w;
27 } 120 }
28 return (char *)DEFAULT_RELAY; 121 ESP_LOGD(TAG, "ws_send: sent %d bytes (payload %d)", total, (int)len);
122 free(frame);
123 return 0;
29} 124}
30 125
31static char *http_get(const char *url, int timeout_ms) 126static esp_err_t ws_connect(const char *relay_url, esp_tls_t **tls_out)
32{ 127{
33 char *buf = malloc(8192); 128 char host[128] = {0};
34 if (!buf) return NULL; 129 int port = 443;
35 int total = 0; 130 char path[128] = "/";
131
132 if (strncmp(relay_url, "wss://", 6) != 0) return ESP_ERR_INVALID_ARG;
133
134 const char *url_start = relay_url + 6;
135 const char *path_ptr = strchr(url_start, '/');
136 if (path_ptr) {
137 size_t host_len = path_ptr - url_start;
138 if (host_len >= sizeof(host)) host_len = sizeof(host) - 1;
139 memcpy(host, url_start, host_len);
140 host[host_len] = '\0';
141 strncpy(path, path_ptr, sizeof(path) - 1);
142 } else {
143 strncpy(host, url_start, sizeof(host) - 1);
144 }
36 145
37 esp_http_client_config_t config = { 146 char *colon = strchr(host, ':');
38 .url = url, 147 if (colon) {
39 .method = HTTP_METHOD_GET, 148 *colon = '\0';
40 .timeout_ms = timeout_ms, 149 port = atoi(colon + 1);
150 }
151
152 esp_tls_cfg_t tls_cfg = {
153 .crt_bundle_attach = esp_crt_bundle_attach,
154 .timeout_ms = CVM_WS_READ_TIMEOUT_MS,
41 }; 155 };
42 esp_http_client_handle_t client = esp_http_client_init(&config); 156 esp_tls_t *tls = esp_tls_init();
43 if (!client) { free(buf); return NULL; } 157 if (!tls) return ESP_ERR_NO_MEM;
44 158
45 esp_err_t err = esp_http_client_open(client, 0); 159 int ret = esp_tls_conn_new_sync(host, strlen(host), port, &tls_cfg, tls);
46 if (err != ESP_OK) { 160 if (ret < 0) {
47 esp_http_client_cleanup(client); 161 esp_tls_conn_destroy(tls);
48 free(buf); 162 return ESP_FAIL;
49 return NULL;
50 } 163 }
51 164
52 int content_length = esp_http_client_fetch_headers(client); 165 char upgrade[512];
53 int max_read = content_length > 0 ? content_length : 8191; 166 snprintf(upgrade, sizeof(upgrade),
167 "GET %s HTTP/1.1\r\n"
168 "Host: %s\r\n"
169 "Upgrade: websocket\r\n"
170 "Connection: Upgrade\r\n"
171 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
172 "Sec-WebSocket-Version: 13\r\n"
173 "\r\n",
174 path, host);
175
176 int written = esp_tls_conn_write(tls, (const unsigned char *)upgrade, strlen(upgrade));
177 if (written < 0) {
178 esp_tls_conn_destroy(tls);
179 return ESP_FAIL;
180 }
54 181
55 while (total < max_read) { 182 char resp[1024];
56 int n = esp_http_client_read(client, buf + total, max_read - total); 183 int rlen = esp_tls_conn_read(tls, (unsigned char *)resp, sizeof(resp) - 1);
57 if (n <= 0) break; 184 if (rlen <= 0 || !strstr(resp, "101")) {
58 total += n; 185 ESP_LOGE(TAG, "WS upgrade failed to %s (read %d)", host, rlen);
186 esp_tls_conn_destroy(tls);
187 return ESP_FAIL;
59 } 188 }
60 buf[total] = '\0'; 189
61 esp_http_client_cleanup(client); 190 *tls_out = tls;
62 return buf; 191 ESP_LOGI(TAG, "Connected to %s", host);
192 return ESP_OK;
63} 193}
64 194
65static cJSON *build_filter(const char *npub) 195static cJSON *build_tools_list(void)
66{ 196{
67 cJSON *filter = cJSON_CreateObject(); 197 cJSON *tools = cJSON_CreateArray();
68 cJSON *kinds = cJSON_CreateArray(); 198
69 cJSON_AddItemToArray(kinds, cJSON_CreateNumber(4)); 199 const char *tool_defs[][3] = {
70 cJSON_AddItemToObject(filter, "kinds", kinds); 200 {"get_config", "Get current device configuration", "{\"type\":\"object\",\"properties\":{},\"required\":[]}"},
71 cJSON_AddStringToObject(filter, "#p", npub); 201 {"set_config", "Update device configuration", "{\"type\":\"object\",\"properties\":{\"price_per_step\":{\"type\":\"integer\"},\"step_size_ms\":{\"type\":\"integer\"},\"step_size_bytes\":{\"type\":\"integer\"},\"metric\":{\"type\":\"string\"},\"client_enabled\":{\"type\":\"boolean\"},\"payout_enabled\":{\"type\":\"boolean\"}}}"},
72 cJSON_AddNumberToObject(filter, "limit", 10); 202 {"get_balance", "Get wallet balance and proof count", "{\"type\":\"object\",\"properties\":{},\"required\":[]}"},
73 return filter; 203 {"wallet_send", "Send e-cash tokens from wallet", "{\"type\":\"object\",\"properties\":{\"amount\":{\"type\":\"integer\",\"description\":\"Amount in sats\"}},\"required\":[\"amount\"]}"},
204 {"get_sessions","Get active client sessions", "{\"type\":\"object\",\"properties\":{},\"required\":[]}"},
205 {"get_usage", "Get current billing usage info", "{\"type\":\"object\",\"properties\":{},\"required\":[]}"},
206 {"set_payout", "Configure payout recipients", "{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"boolean\"},\"recipients\":{\"type\":\"array\"}}}"},
207 {"set_metric", "Set billing metric", "{\"type\":\"object\",\"properties\":{\"metric\":{\"type\":\"string\",\"enum\":[\"bytes\",\"milliseconds\"]}},\"required\":[\"metric\"]}"},
208 {"set_price", "Set price per step", "{\"type\":\"object\",\"properties\":{\"price_per_step\":{\"type\":\"integer\",\"minimum\":1}},\"required\":[\"price_per_step\"]}"},
209 {"wallet_melt", "Melt tokens for lightning payment", "{\"type\":\"object\",\"properties\":{\"bolt11\":{\"type\":\"string\"},\"max_fee_sats\":{\"type\":\"integer\"}},\"required\":[\"bolt11\"]}"},
210 };
211
212 for (int i = 0; i < 10; i++) {
213 cJSON *tool = cJSON_CreateObject();
214 cJSON_AddStringToObject(tool, "name", tool_defs[i][0]);
215 cJSON_AddStringToObject(tool, "description", tool_defs[i][1]);
216 cJSON *schema = cJSON_Parse(tool_defs[i][2]);
217 if (schema) cJSON_AddItemToObject(tool, "inputSchema", schema);
218 cJSON_AddItemToArray(tools, tool);
219 }
220
221 return tools;
74} 222}
75 223
76static cJSON *build_subscription(const char *npub) 224static char *build_initialize_response(const char *request_id_str, const char *client_pubkey)
77{ 225{
78 cJSON *sub = cJSON_CreateArray(); 226 cJSON *response = cJSON_CreateObject();
79 cJSON_AddItemToArray(sub, cJSON_CreateString("REQ")); 227 cJSON_AddStringToObject(response, "jsonrpc", "2.0");
80 cJSON_AddItemToArray(sub, cJSON_CreateString("cvm_sub_01")); 228 cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 0);
81 cJSON_AddItemToArray(sub, build_filter(npub)); 229
82 return sub; 230 cJSON *result = cJSON_CreateObject();
231 cJSON_AddStringToObject(result, "protocolVersion", CVM_VERSION);
232
233 cJSON *capabilities = cJSON_CreateObject();
234 cJSON_AddItemToObject(capabilities, "tools", cJSON_CreateObject());
235 cJSON_AddItemToObject(result, "capabilities", capabilities);
236
237 cJSON *serverInfo = cJSON_CreateObject();
238 cJSON_AddStringToObject(serverInfo, "name", CVM_SERVER_NAME);
239 cJSON_AddStringToObject(serverInfo, "version", CVM_SERVER_VERSION);
240 cJSON_AddItemToObject(result, "serverInfo", serverInfo);
241
242 cJSON_AddItemToObject(response, "result", result);
243
244 char *json = cJSON_PrintUnformatted(response);
245 cJSON_Delete(response);
246 return json;
247}
248
249static char *build_tools_list_response(const char *request_id_str)
250{
251 cJSON *response = cJSON_CreateObject();
252 cJSON_AddStringToObject(response, "jsonrpc", "2.0");
253 cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 1);
254
255 cJSON *result = cJSON_CreateObject();
256 cJSON *tools = build_tools_list();
257 cJSON_AddItemToObject(result, "tools", tools);
258 cJSON_AddItemToObject(response, "result", result);
259
260 char *json = cJSON_PrintUnformatted(response);
261 cJSON_Delete(response);
262 return json;
263}
264
265static char *build_tool_call_response(const char *request_id_str, const mcp_response_t *mcp_resp)
266{
267 cJSON *response = cJSON_CreateObject();
268 cJSON_AddStringToObject(response, "jsonrpc", "2.0");
269 cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 2);
270
271 if (mcp_resp->success) {
272 cJSON *result = cJSON_CreateObject();
273 cJSON_AddItemToObject(result, "content", cJSON_CreateArray());
274 cJSON *content_arr = cJSON_GetObjectItem(result, "content");
275 cJSON *text_item = cJSON_CreateObject();
276 cJSON_AddStringToObject(text_item, "type", "text");
277 cJSON_AddStringToObject(text_item, "text", mcp_resp->result_json);
278 cJSON_AddItemToArray(content_arr, text_item);
279 cJSON_AddBoolToObject(result, "isError", false);
280 cJSON_AddItemToObject(response, "result", result);
281 } else {
282 cJSON *error = cJSON_CreateObject();
283 cJSON_AddNumberToObject(error, "code", -32603);
284 cJSON_AddStringToObject(error, "message", mcp_resp->error);
285 cJSON_AddItemToObject(response, "error", error);
286 }
287
288 char *json = cJSON_PrintUnformatted(response);
289 cJSON_Delete(response);
290 return json;
291}
292
293static char *build_ping_response(const char *request_id_str)
294{
295 cJSON *response = cJSON_CreateObject();
296 cJSON_AddStringToObject(response, "jsonrpc", "2.0");
297 cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 0);
298 cJSON *result = cJSON_CreateObject();
299 cJSON_AddItemToObject(response, "result", result);
300 char *json = cJSON_PrintUnformatted(response);
301 cJSON_Delete(response);
302 return json;
83} 303}
84 304
85static void process_dm(const char *sender_pubkey, const char *encrypted_content) 305static esp_err_t publish_event_to_relay(const char *relay_url, const char *event_json)
306{
307 esp_tls_t *tls = NULL;
308 esp_err_t err = ws_connect(relay_url, &tls);
309 if (err != ESP_OK) return err;
310
311 char *msg;
312 size_t event_len2 = strlen(event_json);
313 size_t msg_len2 = 10 + event_len2 + 2;
314 msg = malloc(msg_len2);
315 snprintf(msg, msg_len2, "[\"EVENT\",%s]", event_json);
316
317 ws_send_text(tls, msg);
318 free(msg);
319
320 uint8_t resp_buf[256];
321 esp_tls_conn_read(tls, resp_buf, sizeof(resp_buf) - 1);
322
323 uint8_t close_frame[2] = {0x88, 0x00};
324 esp_tls_conn_write(tls, close_frame, 2);
325 esp_tls_conn_destroy(tls);
326 return ESP_OK;
327}
328
329static esp_err_t publish_kind_25910_response_ws(esp_tls_t *tls,
330 const char *content_json,
331 const char *request_event_id)
86{ 332{
87 const tollgate_identity_t *id = identity_get(); 333 const tollgate_identity_t *id = identity_get();
88 if (!id || !id->initialized) { 334 if (!id || !id->initialized) return ESP_FAIL;
89 ESP_LOGE(TAG, "Identity not initialized"); 335
90 return; 336 cJSON *tags = cJSON_CreateArray();
337 cJSON *e_tag = cJSON_CreateArray();
338 cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
339 cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_event_id));
340 cJSON_AddItemToArray(tags, e_tag);
341
342 char *tags_str = cJSON_PrintUnformatted(tags);
343 cJSON_Delete(tags);
344
345 nostr_event_t event;
346 nostr_event_init(&event, id->npub_hex, 25910, tags_str, content_json);
347 nostr_event_sign(&event, id->nsec);
348
349 char *event_json = malloc(8192);
350 if (!event_json) {
351 free(tags_str);
352 return ESP_ERR_NO_MEM;
91 } 353 }
92 354
93 uint8_t sender_pk[64]; 355 esp_err_t ret = nostr_event_to_json(&event, event_json, 8192);
94 for (int i = 0; i < 64; i++) { 356 free(tags_str);
95 char hex[3] = { sender_pubkey[i*2], sender_pubkey[i*2+1], 0 }; 357 if (ret != ESP_OK) {
96 sender_pk[i] = (uint8_t)strtol(hex, NULL, 16); 358 free(event_json);
359 return ret;
97 } 360 }
98 361
99 char plaintext[2048]; 362 size_t msg_len = 10 + strlen(event_json) + 2;
100 int pt_len = nip04_decrypt(id->nsec, sender_pk, encrypted_content, plaintext, sizeof(plaintext)); 363 char *msg = malloc(msg_len);
101 if (pt_len < 0) { 364 if (!msg) {
102 ESP_LOGE(TAG, "Failed to decrypt DM from %.8s", sender_pubkey); 365 free(event_json);
103 return; 366 return ESP_ERR_NO_MEM;
104 } 367 }
368 snprintf(msg, msg_len, "[\"EVENT\",%s]", event_json);
369 ESP_LOGD(TAG, "Sending WS response (%d bytes)", (int)strlen(msg));
370 int rc = ws_send_text(tls, msg);
371 ESP_LOGD(TAG, "WS send result: %d", rc);
372 free(msg);
373 free(event_json);
374 return ESP_OK;
375}
376
377static esp_err_t publish_kind_25910_response(const char *relay_url,
378 const char *content_json,
379 const char *request_event_id)
380{
381 const tollgate_identity_t *id = identity_get();
382 if (!id || !id->initialized) return ESP_FAIL;
383
384 cJSON *tags = cJSON_CreateArray();
385 cJSON *e_tag = cJSON_CreateArray();
386 cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
387 cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_event_id));
388 cJSON_AddItemToArray(tags, e_tag);
389
390 char *tags_str = cJSON_PrintUnformatted(tags);
391 cJSON_Delete(tags);
392
393 nostr_event_t event;
394 nostr_event_init(&event, id->npub_hex, 25910, tags_str, content_json);
395 nostr_event_sign(&event, id->nsec);
396 free(tags_str);
397
398 char *event_json = malloc(8192);
399 if (!event_json) return ESP_ERR_NO_MEM;
400
401 esp_err_t ret = nostr_event_to_json(&event, event_json, 8192);
402 if (ret != ESP_OK) {
403 free(event_json);
404 return ret;
405 }
406
407 ret = publish_event_to_relay(relay_url, event_json);
408 free(event_json);
409 return ret;
410}
105 411
106 ESP_LOGI(TAG, "Decrypted DM from %.8s: %s", sender_pubkey, plaintext); 412static bool is_owner_pubkey(const char *pubkey_hex)
413{
414 const tollgate_identity_t *id = identity_get();
415 if (!id || !id->initialized) return false;
416 if (!pubkey_hex) return false;
417 return strcmp(id->npub_hex, pubkey_hex) == 0;
418}
107 419
108 cJSON *msg = cJSON_Parse(plaintext); 420static void handle_mcp_message(esp_tls_t *tls, const char *sender_pubkey,
421 const char *event_id, const char *content)
422{
423 cJSON *msg = cJSON_Parse(content);
109 if (!msg) { 424 if (!msg) {
110 ESP_LOGE(TAG, "Invalid JSON in DM"); 425 ESP_LOGW(TAG, "Invalid JSON in kind 25910 content");
111 return; 426 return;
112 } 427 }
113 428
114 cJSON *method = cJSON_GetObjectItem(msg, "method"); 429 cJSON *method = cJSON_GetObjectItem(msg, "method");
115 cJSON *params = cJSON_GetObjectItem(msg, "params"); 430 cJSON *id_field = cJSON_GetObjectItem(msg, "id");
116 if (!method || !cJSON_IsString(method)) { 431 const char *id_str = (id_field && cJSON_IsNumber(id_field))
117 cJSON_Delete(msg); 432 ? cJSON_PrintUnformatted(id_field) : "0";
118 ESP_LOGE(TAG, "Missing 'method' in CVM request"); 433
119 return; 434 if (method && cJSON_IsString(method)) {
435 const char *m = method->valuestring;
436
437 if (strcmp(m, "initialize") == 0) {
438 ESP_LOGI(TAG, "MCP initialize from %s", sender_pubkey);
439 char *resp = build_initialize_response(id_str, sender_pubkey);
440 if (tls) {
441 publish_kind_25910_response_ws(tls, resp, event_id);
442 } else {
443 ESP_LOGW(TAG, "No TLS for response");
444 }
445 free(resp);
446 } else if (strcmp(m, "notifications/initialized") == 0) {
447 ESP_LOGI(TAG, "Client initialized: %s", sender_pubkey);
448 } else if (strcmp(m, "tools/list") == 0) {
449 ESP_LOGI(TAG, "tools/list from %s", sender_pubkey);
450 char *resp = build_tools_list_response(id_str);
451 if (tls) {
452 publish_kind_25910_response_ws(tls, resp, event_id);
453 }
454 free(resp);
455 } else if (strcmp(m, "tools/call") == 0) {
456 cJSON *params = cJSON_GetObjectItem(msg, "params");
457 cJSON *name = params ? cJSON_GetObjectItem(params, "name") : NULL;
458 cJSON *arguments = params ? cJSON_GetObjectItem(params, "arguments") : NULL;
459
460 if (name && cJSON_IsString(name)) {
461 ESP_LOGI(TAG, "tools/call %s from %s", name->valuestring, sender_pubkey);
462
463 mcp_request_t req = {0};
464 req.tool = mcp_parse_tool(name->valuestring);
465 strncpy(req.method, name->valuestring, sizeof(req.method) - 1);
466 if (arguments) {
467 char *ajson = cJSON_PrintUnformatted(arguments);
468 strncpy(req.params_json, ajson, sizeof(req.params_json) - 1);
469 cJSON_free(ajson);
470 }
471
472 mcp_response_t mcp_resp = mcp_dispatch(&req);
473 char *resp = build_tool_call_response(id_str, &mcp_resp);
474 if (tls) {
475 publish_kind_25910_response_ws(tls, resp, event_id);
476 }
477 free(resp);
478 }
479 } else if (strcmp(m, "ping") == 0) {
480 char *resp = build_ping_response(id_str);
481 if (tls) {
482 publish_kind_25910_response_ws(tls, resp, event_id);
483 }
484 free(resp);
485 } else {
486 ESP_LOGW(TAG, "Unknown MCP method: %s", m);
487 }
120 } 488 }
121 489
122 mcp_request_t req = {0}; 490 if (id_field && cJSON_IsNumber(id_field) && id_str[0] != '0') {
123 req.tool = mcp_parse_tool(method->valuestring); 491 free((void *)id_str);
124 strncpy(req.method, method->valuestring, sizeof(req.method) - 1); 492 } else if (id_str[0] != '0') {
125 if (params && cJSON_IsString(params)) {
126 strncpy(req.params_json, params->valuestring, sizeof(req.params_json) - 1);
127 } else if (params) {
128 char *pjson = cJSON_PrintUnformatted(params);
129 strncpy(req.params_json, pjson, sizeof(req.params_json) - 1);
130 cJSON_free(pjson);
131 } 493 }
132
133 mcp_response_t resp = mcp_dispatch(&req);
134 cJSON_Delete(msg); 494 cJSON_Delete(msg);
135
136 cJSON *response_msg = cJSON_CreateObject();
137 if (resp.success) {
138 cJSON_AddStringToObject(response_msg, "status", "ok");
139 cJSON_AddItemToObject(response_msg, "result", cJSON_Parse(resp.result_json));
140 } else {
141 cJSON_AddStringToObject(response_msg, "status", "error");
142 cJSON_AddStringToObject(response_msg, "error", resp.error);
143 }
144
145 char *response_str = cJSON_PrintUnformatted(response_msg);
146 cJSON_Delete(response_msg);
147
148 uint8_t response_ct[4096];
149 size_t ct_len = 0;
150 nip04_encrypt(id->nsec, sender_pk, response_str, response_ct, &ct_len);
151 free(response_str);
152
153 ESP_LOGI(TAG, "CVM response prepared (%zu bytes encrypted), would send to %.8s", ct_len, sender_pubkey);
154} 495}
155 496
156static void parse_nostr_events(const char *data) 497static void process_relay_message(esp_tls_t *tls, const char *relay_url, const char *msg_str)
157{ 498{
158 cJSON *arr = cJSON_Parse(data); 499 cJSON *arr = cJSON_Parse(msg_str);
159 if (!arr || !cJSON_IsArray(arr)) { 500 if (!arr || !cJSON_IsArray(arr)) {
160 if (arr) cJSON_Delete(arr); 501 if (arr) cJSON_Delete(arr);
161 return; 502 return;
162 } 503 }
163 504
164 cJSON *item = NULL; 505 cJSON *cmd = cJSON_GetArrayItem(arr, 0);
165 cJSON_ArrayForEach(item, arr) { 506 if (!cmd || !cJSON_IsString(cmd)) {
166 if (!cJSON_IsArray(item)) continue; 507 cJSON_Delete(arr);
167 int arr_size = cJSON_GetArraySize(item); 508 return;
168 if (arr_size < 3) continue; 509 }
169 510
170 cJSON *cmd = cJSON_GetArrayItem(item, 0); 511 if (strcmp(cmd->valuestring, "OK") == 0) {
171 if (!cmd || !cJSON_IsString(cmd) || strcmp(cmd->valuestring, "EVENT") != 0) continue; 512 cJSON *ev_id = cJSON_GetArrayItem(arr, 1);
513 cJSON *ok = cJSON_GetArrayItem(arr, 2);
514 cJSON *reason = cJSON_GetArrayItem(arr, 3);
515 ESP_LOGI(TAG, "Relay OK: id=%.16s success=%s reason=%s",
516 ev_id ? ev_id->valuestring : "?",
517 ok ? (cJSON_IsTrue(ok) ? "true" : "FALSE") : "?",
518 reason ? reason->valuestring : "");
519 cJSON_Delete(arr);
520 return;
521 }
172 522
173 cJSON *event = cJSON_GetArrayItem(item, 2); 523 if (strcmp(cmd->valuestring, "EVENT") != 0) {
174 if (!event) continue; 524 ESP_LOGI(TAG, "Relay msg: %.100s", msg_str);
525 cJSON_Delete(arr);
526 return;
527 }
175 528
176 cJSON *kind = cJSON_GetObjectItem(event, "kind"); 529 cJSON *event = cJSON_GetArrayItem(arr, 2);
177 if (!kind || kind->valueint != 4) continue; 530 if (!event) {
531 cJSON_Delete(arr);
532 return;
533 }
178 534
179 cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey"); 535 cJSON *kind = cJSON_GetObjectItem(event, "kind");
180 cJSON *content = cJSON_GetObjectItem(event, "content"); 536 if (!kind || kind->valueint != 25910) {
181 if (pubkey && content) { 537 cJSON_Delete(arr);
182 process_dm(pubkey->valuestring, content->valuestring); 538 return;
183 } 539 }
540
541 cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey");
542 cJSON *event_id = cJSON_GetObjectItem(event, "id");
543 cJSON *content = cJSON_GetObjectItem(event, "content");
544
545 if (!pubkey || !content || !event_id) {
546 cJSON_Delete(arr);
547 return;
184 } 548 }
549
550 if (!is_owner_pubkey(pubkey->valuestring)) {
551 ESP_LOGW(TAG, "Ignoring request from non-owner: %.16s...", pubkey->valuestring);
552 cJSON_Delete(arr);
553 return;
554 }
555
556 handle_mcp_message(tls, pubkey->valuestring, event_id->valuestring, content->valuestring);
185 cJSON_Delete(arr); 557 cJSON_Delete(arr);
186} 558}
187 559
188static void cvm_task(void *arg) 560static esp_err_t subscribe_to_relay(esp_tls_t *tls, const char *npub)
561{
562 cJSON *sub = cJSON_CreateArray();
563 cJSON_AddItemToArray(sub, cJSON_CreateString("REQ"));
564 cJSON_AddItemToArray(sub, cJSON_CreateString("cvm_sub"));
565 cJSON *filter = cJSON_CreateObject();
566 cJSON *kinds = cJSON_CreateArray();
567 cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910));
568 cJSON_AddItemToObject(filter, "kinds", kinds);
569 cJSON *p_tags = cJSON_CreateArray();
570 cJSON_AddItemToArray(p_tags, cJSON_CreateString(npub));
571 cJSON_AddItemToObject(filter, "#p", p_tags);
572 cJSON_AddNumberToObject(filter, "limit", 100);
573 cJSON_AddItemToArray(sub, filter);
574
575 char *msg = cJSON_PrintUnformatted(sub);
576 cJSON_Delete(sub);
577
578 int rc = ws_send_text(tls, msg);
579 free(msg);
580 return rc == 0 ? ESP_OK : ESP_FAIL;
581}
582
583static void cvm_relay_task(void *arg)
189{ 584{
585 const char *relay_url = (const char *)arg;
190 const tollgate_identity_t *id = identity_get(); 586 const tollgate_identity_t *id = identity_get();
191 if (!id || !id->initialized) { 587 if (!id || !id->initialized) {
192 ESP_LOGE(TAG, "Cannot start: identity not initialized"); 588 ESP_LOGE(TAG, "Identity not initialized");
193 vTaskDelete(NULL); 589 vTaskDelete(NULL);
194 return; 590 return;
195 } 591 }
196 592
197 char *relays = fetch_relays();
198 ESP_LOGI(TAG, "CVM server started, relays: %s", relays);
199
200 while (g_running) { 593 while (g_running) {
201 ESP_LOGI(TAG, "Polling for DMs..."); 594 esp_tls_t *tls = NULL;
595 esp_err_t err = ws_connect(relay_url, &tls);
596 if (err != ESP_OK) {
597 ESP_LOGW(TAG, "Connect failed to %s, retrying", relay_url);
598 vTaskDelay(pdMS_TO_TICKS(CVM_RECONNECT_DELAY_MS));
599 continue;
600 }
601
602 err = subscribe_to_relay(tls, id->npub_hex);
603 if (err != ESP_OK) {
604 esp_tls_conn_destroy(tls);
605 vTaskDelay(pdMS_TO_TICKS(CVM_RECONNECT_DELAY_MS));
606 continue;
607 }
202 608
203 cJSON *sub = build_subscription(id->npub_hex); 609 ESP_LOGI(TAG, "Listening on %s for kind 25910 events", relay_url);
204 char *sub_json = cJSON_PrintUnformatted(sub); 610 publish_announcements_via_ws(tls);
205 cJSON_Delete(sub);
206 611
207 char url[256]; 612 uint8_t *buf = malloc(CVM_WS_BUF_SIZE);
208 snprintf(url, sizeof(url), "%s/cvm_poll", relays); 613 if (!buf) {
209 free(sub_json); 614 esp_tls_conn_destroy(tls);
615 vTaskDelete(NULL);
616 return;
617 }
210 618
211 vTaskDelay(pdMS_TO_TICKS(30000)); 619 int64_t last_ping_time = 0;
620
621 while (g_running) {
622 int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1);
623 if (rlen < 0) {
624 ESP_LOGW(TAG, "Read error on %s (rlen=%d)", relay_url, rlen);
625 break;
626 }
627 if (rlen == 0) {
628 break;
629 }
630
631 if ((buf[0] & 0x0F) == 0x01) {
632 char *text = parse_ws_text_frame(buf, rlen);
633 if (text) {
634 if (strlen(text) > 0) {
635 process_relay_message(tls, relay_url, text);
636 }
637 free(text);
638 }
639 } else if ((buf[0] & 0x0F) == 0x09) {
640 uint8_t pong[2] = {0x8A, 0x00};
641 esp_tls_conn_write(tls, pong, 2);
642 }
643
644 int64_t now = (int64_t)esp_timer_get_time() / 1000000;
645 if (now - last_ping_time >= CVM_WS_PING_INTERVAL_S) {
646 uint8_t ping[2] = {0x89, 0x00};
647 esp_tls_conn_write(tls, ping, 2);
648 last_ping_time = now;
649 }
650 }
651
652 free(buf);
653 uint8_t close_frame[2] = {0x88, 0x00};
654 esp_tls_conn_write(tls, close_frame, 2);
655 esp_tls_conn_destroy(tls);
656 ESP_LOGW(TAG, "Disconnected from %s, reconnecting", relay_url);
657 vTaskDelay(pdMS_TO_TICKS(CVM_RECONNECT_DELAY_MS));
212 } 658 }
213 659
214 ESP_LOGI(TAG, "CVM server stopped");
215 vTaskDelete(NULL); 660 vTaskDelete(NULL);
216} 661}
217 662
663static esp_err_t publish_event_via_ws(esp_tls_t *tls, int kind,
664 const char *content, const char *tags_json)
665{
666 const tollgate_identity_t *id = identity_get();
667 if (!id || !id->initialized) return ESP_FAIL;
668
669 nostr_event_t event;
670 nostr_event_init(&event, id->npub_hex, kind, tags_json, content);
671 nostr_event_sign(&event, id->nsec);
672
673 char *event_json = malloc(4096);
674 if (!event_json) return ESP_ERR_NO_MEM;
675
676 esp_err_t ret = nostr_event_to_json(&event, event_json, 4096);
677 if (ret != ESP_OK) {
678 free(event_json);
679 return ret;
680 }
681
682 char *msg;
683 size_t event_len = strlen(event_json);
684 size_t msg_len = 10 + event_len + 2;
685 msg = malloc(msg_len);
686 snprintf(msg, msg_len, "[\"EVENT\",%s]", event_json);
687
688 ws_send_text(tls, msg);
689 ESP_LOGI(TAG, "Published kind %d event (%d bytes)", kind, (int)strlen(event_json));
690 free(msg);
691 free(event_json);
692 return ESP_OK;
693}
694
695static void publish_announcements_via_ws(esp_tls_t *tls)
696{
697 const tollgate_identity_t *id = identity_get();
698 if (!id || !id->initialized) return;
699
700 ESP_LOGI(TAG, "Publishing CEP-6 announcements via active WS");
701
702 cJSON *ann_content = cJSON_CreateObject();
703 cJSON_AddStringToObject(ann_content, "protocolVersion", CVM_VERSION);
704 cJSON *capabilities = cJSON_CreateObject();
705 cJSON *tools_cap = cJSON_CreateObject();
706 cJSON_AddBoolToObject(tools_cap, "listChanged", true);
707 cJSON_AddItemToObject(capabilities, "tools", tools_cap);
708 cJSON_AddItemToObject(ann_content, "capabilities", capabilities);
709 cJSON *serverInfo = cJSON_CreateObject();
710 cJSON_AddStringToObject(serverInfo, "name", CVM_SERVER_NAME);
711 cJSON_AddStringToObject(serverInfo, "version", CVM_SERVER_VERSION);
712 cJSON_AddItemToObject(ann_content, "serverInfo", serverInfo);
713 char *ann_str = cJSON_PrintUnformatted(ann_content);
714 cJSON_Delete(ann_content);
715
716 cJSON *ann_tags = cJSON_CreateArray();
717 cJSON *name_tag = cJSON_CreateArray();
718 cJSON_AddItemToArray(name_tag, cJSON_CreateString("name"));
719 cJSON_AddItemToArray(name_tag, cJSON_CreateString(CVM_SERVER_NAME));
720 cJSON_AddItemToArray(ann_tags, name_tag);
721 cJSON *about_tag = cJSON_CreateArray();
722 cJSON_AddItemToArray(about_tag, cJSON_CreateString("about"));
723 cJSON_AddItemToArray(about_tag, cJSON_CreateString("ESP32 TollGate WiFi hotspot with Cashu e-cash payments"));
724 cJSON_AddItemToArray(ann_tags, about_tag);
725 char *ann_tags_str = cJSON_PrintUnformatted(ann_tags);
726 cJSON_Delete(ann_tags);
727
728 publish_event_via_ws(tls, 11316, ann_str, ann_tags_str);
729 free(ann_str);
730 free(ann_tags_str);
731
732 cJSON *tools = build_tools_list();
733 cJSON *tools_content = cJSON_CreateObject();
734 cJSON_AddItemToObject(tools_content, "tools", tools);
735 char *tools_str = cJSON_PrintUnformatted(tools_content);
736 cJSON_Delete(tools_content);
737
738 publish_event_via_ws(tls, 11317, tools_str, "[]");
739 free(tools_str);
740
741 cJSON *relay_tags = cJSON_CreateArray();
742 const char *relays[] = {"wss://relay.primal.net", "wss://nostr-pub.wellorder.net", NULL};
743 for (int i = 0; relays[i]; i++) {
744 cJSON *r_tag = cJSON_CreateArray();
745 cJSON_AddItemToArray(r_tag, cJSON_CreateString("r"));
746 cJSON_AddItemToArray(r_tag, cJSON_CreateString(relays[i]));
747 cJSON_AddItemToArray(relay_tags, r_tag);
748 }
749 char *relay_tags_str = cJSON_PrintUnformatted(relay_tags);
750 cJSON_Delete(relay_tags);
751
752 publish_event_via_ws(tls, 10002, "", relay_tags_str);
753 free(relay_tags_str);
754
755 ESP_LOGI(TAG, "CEP-6 announcements published (kinds 11316, 11317, 10002)");
756}
757
758esp_err_t cvm_publish_announcements(void)
759{
760 return ESP_OK;
761}
762
763const char *cvm_get_pubkey_hex(void)
764{
765 const tollgate_identity_t *id = identity_get();
766 if (!id || !id->initialized) return NULL;
767 return id->npub_hex;
768}
769
218esp_err_t cvm_server_init(void) 770esp_err_t cvm_server_init(void)
219{ 771{
220 ESP_LOGI(TAG, "CVM server initialized"); 772 ESP_LOGI(TAG, "CVM server initialized");
@@ -225,7 +777,12 @@ void cvm_server_start(void)
225{ 777{
226 if (g_running) return; 778 if (g_running) return;
227 g_running = true; 779 g_running = true;
228 xTaskCreate(cvm_task, "cvm_server", 8192, NULL, 5, &g_task); 780
781 const tollgate_config_t *cfg = tollgate_config_get();
782 const char *relay = (cfg->cvm_relays[0]) ? cfg->cvm_relays : "wss://relay.primal.net";
783
784 char *relay_copy = strdup(relay);
785 xTaskCreate(cvm_relay_task, "cvm_relay", 16384, relay_copy, 5, &g_task);
229} 786}
230 787
231void cvm_server_stop(void) 788void cvm_server_stop(void)
diff --git a/main/cvm_server.h b/main/cvm_server.h
index d336514..864973b 100644
--- a/main/cvm_server.h
+++ b/main/cvm_server.h
@@ -7,4 +7,8 @@ esp_err_t cvm_server_init(void);
7void cvm_server_start(void); 7void cvm_server_start(void);
8void cvm_server_stop(void); 8void cvm_server_stop(void);
9 9
10esp_err_t cvm_publish_announcements(void);
11
12const char *cvm_get_pubkey_hex(void);
13
10#endif 14#endif
diff --git a/main/mcp_handler.c b/main/mcp_handler.c
index f40c1bd..93bfba9 100644
--- a/main/mcp_handler.c
+++ b/main/mcp_handler.c
@@ -1,7 +1,9 @@
1#include "mcp_handler.h" 1#include "mcp_handler.h"
2#include "config.h" 2#include "config.h"
3#include "nucula_wallet.h" 3#include "nucula_wallet.h"
4#include "session.h"
4#include "cJSON.h" 5#include "cJSON.h"
6#include "lwip/ip4_addr.h"
5#include <string.h> 7#include <string.h>
6#include <stdio.h> 8#include <stdio.h>
7 9
@@ -14,6 +16,12 @@ mcp_tool_t mcp_parse_tool(const char *method)
14 if (strcmp(method, "set_config") == 0) return MCP_TOOL_SET_CONFIG; 16 if (strcmp(method, "set_config") == 0) return MCP_TOOL_SET_CONFIG;
15 if (strcmp(method, "get_balance") == 0) return MCP_TOOL_GET_BALANCE; 17 if (strcmp(method, "get_balance") == 0) return MCP_TOOL_GET_BALANCE;
16 if (strcmp(method, "wallet_send") == 0) return MCP_TOOL_WALLET_SEND; 18 if (strcmp(method, "wallet_send") == 0) return MCP_TOOL_WALLET_SEND;
19 if (strcmp(method, "get_sessions") == 0) return MCP_TOOL_GET_SESSIONS;
20 if (strcmp(method, "get_usage") == 0) return MCP_TOOL_GET_USAGE;
21 if (strcmp(method, "set_payout") == 0) return MCP_TOOL_SET_PAYOUT;
22 if (strcmp(method, "set_metric") == 0) return MCP_TOOL_SET_METRIC;
23 if (strcmp(method, "set_price") == 0) return MCP_TOOL_SET_PRICE;
24 if (strcmp(method, "wallet_melt") == 0) return MCP_TOOL_WALLET_MELT;
17 return MCP_TOOL_UNKNOWN; 25 return MCP_TOOL_UNKNOWN;
18} 26}
19 27
@@ -146,6 +154,222 @@ mcp_response_t mcp_handle_wallet_send(const char *params_json)
146 return resp; 154 return resp;
147} 155}
148 156
157mcp_response_t mcp_handle_get_sessions(void)
158{
159 mcp_response_t resp = {0};
160 extern session_t *cvm_get_sessions_array(void);
161 extern int cvm_get_sessions_count(void);
162
163 cJSON *arr = cJSON_CreateArray();
164 int count = cvm_get_sessions_count();
165 session_t *sessions = cvm_get_sessions_array();
166
167 if (sessions && count > 0) {
168 for (int i = 0; i < count; i++) {
169 if (!sessions[i].active) continue;
170 cJSON *s = cJSON_CreateObject();
171 esp_ip4_addr_t ip = { .addr = sessions[i].client_ip };
172 char ip_str[16];
173 snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip));
174 cJSON_AddStringToObject(s, "client_ip", ip_str);
175 if (sessions[i].mac[0])
176 cJSON_AddStringToObject(s, "mac", sessions[i].mac);
177 cJSON_AddNumberToObject(s, "allotment_ms", (double)sessions[i].allotment_ms);
178 cJSON_AddNumberToObject(s, "allotment_bytes", (double)sessions[i].allotment_bytes);
179 cJSON_AddNumberToObject(s, "bytes_consumed", (double)sessions[i].bytes_consumed);
180 cJSON_AddBoolToObject(s, "active", sessions[i].active);
181 cJSON_AddItemToArray(arr, s);
182 }
183 }
184
185 char *json = cJSON_PrintUnformatted(arr);
186 snprintf(resp.result_json, sizeof(resp.result_json), "%s", json);
187 cJSON_free(json);
188 cJSON_Delete(arr);
189 resp.success = true;
190 return resp;
191}
192
193mcp_response_t mcp_handle_get_usage(void)
194{
195 mcp_response_t resp = {0};
196 const tollgate_config_t *cfg = tollgate_config_get();
197
198 cJSON *root = cJSON_CreateObject();
199 cJSON_AddStringToObject(root, "metric", cfg->metric);
200 cJSON_AddNumberToObject(root, "price_per_step", cfg->price_per_step);
201 cJSON_AddNumberToObject(root, "step_size_ms", cfg->step_size_ms);
202 cJSON_AddNumberToObject(root, "step_size_bytes", cfg->step_size_bytes);
203 cJSON_AddBoolToObject(root, "client_enabled", cfg->client_enabled);
204
205 char *json = cJSON_PrintUnformatted(root);
206 snprintf(resp.result_json, sizeof(resp.result_json), "%s", json);
207 cJSON_free(json);
208 cJSON_Delete(root);
209 resp.success = true;
210 return resp;
211}
212
213mcp_response_t mcp_handle_set_payout(const char *params_json)
214{
215 mcp_response_t resp = {0};
216 cJSON *root = cJSON_Parse(params_json);
217 if (!root) {
218 resp.success = false;
219 snprintf(resp.error, sizeof(resp.error), "Invalid JSON params");
220 return resp;
221 }
222
223 tollgate_config_t *cfg = (tollgate_config_t *)tollgate_config_get();
224 if (!cfg) {
225 cJSON_Delete(root);
226 resp.success = false;
227 snprintf(resp.error, sizeof(resp.error), "Config not loaded");
228 return resp;
229 }
230
231 cJSON *enabled = cJSON_GetObjectItem(root, "enabled");
232 if (enabled && cJSON_IsBool(enabled)) cfg->payout.enabled = cJSON_IsTrue(enabled);
233
234 cJSON *recipients = cJSON_GetObjectItem(root, "recipients");
235 if (recipients && cJSON_IsArray(recipients)) {
236 int rcount = cJSON_GetArraySize(recipients);
237 if (rcount > PAYOUT_MAX_RECIPIENTS) rcount = PAYOUT_MAX_RECIPIENTS;
238 for (int i = 0; i < rcount; i++) {
239 cJSON *r = cJSON_GetArrayItem(recipients, i);
240 cJSON *addr = cJSON_GetObjectItem(r, "lightning_address");
241 cJSON *factor = cJSON_GetObjectItem(r, "factor");
242 if (addr && cJSON_IsString(addr)) {
243 strncpy(cfg->payout.recipients[i].lightning_address, addr->valuestring,
244 sizeof(cfg->payout.recipients[i].lightning_address) - 1);
245 }
246 if (factor && cJSON_IsNumber(factor)) {
247 cfg->payout.recipients[i].factor = factor->valuedouble;
248 }
249 }
250 cfg->payout.recipient_count = rcount;
251 }
252
253 cJSON_Delete(root);
254 resp.success = true;
255 snprintf(resp.result_json, sizeof(resp.result_json), "{\"status\":\"ok\"}");
256 return resp;
257}
258
259mcp_response_t mcp_handle_set_metric(const char *params_json)
260{
261 mcp_response_t resp = {0};
262 cJSON *root = cJSON_Parse(params_json);
263 if (!root) {
264 resp.success = false;
265 snprintf(resp.error, sizeof(resp.error), "Invalid JSON params");
266 return resp;
267 }
268
269 tollgate_config_t *cfg = (tollgate_config_t *)tollgate_config_get();
270 if (!cfg) {
271 cJSON_Delete(root);
272 resp.success = false;
273 snprintf(resp.error, sizeof(resp.error), "Config not loaded");
274 return resp;
275 }
276
277 cJSON *metric = cJSON_GetObjectItem(root, "metric");
278 if (metric && cJSON_IsString(metric)) {
279 const char *m = metric->valuestring;
280 if (strcmp(m, "bytes") == 0 || strcmp(m, "milliseconds") == 0) {
281 strncpy(cfg->metric, m, sizeof(cfg->metric) - 1);
282 } else {
283 cJSON_Delete(root);
284 resp.success = false;
285 snprintf(resp.error, sizeof(resp.error), "Invalid metric: must be 'bytes' or 'milliseconds'");
286 return resp;
287 }
288 } else {
289 cJSON_Delete(root);
290 resp.success = false;
291 snprintf(resp.error, sizeof(resp.error), "Missing 'metric' field");
292 return resp;
293 }
294
295 cJSON_Delete(root);
296 resp.success = true;
297 snprintf(resp.result_json, sizeof(resp.result_json), "{\"status\":\"ok\",\"metric\":\"%s\"}", cfg->metric);
298 return resp;
299}
300
301mcp_response_t mcp_handle_set_price(const char *params_json)
302{
303 mcp_response_t resp = {0};
304 cJSON *root = cJSON_Parse(params_json);
305 if (!root) {
306 resp.success = false;
307 snprintf(resp.error, sizeof(resp.error), "Invalid JSON params");
308 return resp;
309 }
310
311 tollgate_config_t *cfg = (tollgate_config_t *)tollgate_config_get();
312 if (!cfg) {
313 cJSON_Delete(root);
314 resp.success = false;
315 snprintf(resp.error, sizeof(resp.error), "Config not loaded");
316 return resp;
317 }
318
319 cJSON *price = cJSON_GetObjectItem(root, "price_per_step");
320 if (price && cJSON_IsNumber(price) && price->valueint > 0) {
321 cfg->price_per_step = price->valueint;
322 } else {
323 cJSON_Delete(root);
324 resp.success = false;
325 snprintf(resp.error, sizeof(resp.error), "Missing or invalid 'price_per_step' field");
326 return resp;
327 }
328
329 cJSON_Delete(root);
330 resp.success = true;
331 snprintf(resp.result_json, sizeof(resp.result_json),
332 "{\"status\":\"ok\",\"price_per_step\":%d}", cfg->price_per_step);
333 return resp;
334}
335
336mcp_response_t mcp_handle_wallet_melt(const char *params_json)
337{
338 mcp_response_t resp = {0};
339 cJSON *root = cJSON_Parse(params_json);
340 if (!root) {
341 resp.success = false;
342 snprintf(resp.error, sizeof(resp.error), "Invalid JSON params");
343 return resp;
344 }
345
346 cJSON *bolt11 = cJSON_GetObjectItem(root, "bolt11");
347 if (!bolt11 || !cJSON_IsString(bolt11)) {
348 cJSON_Delete(root);
349 resp.success = false;
350 snprintf(resp.error, sizeof(resp.error), "Missing 'bolt11' field");
351 return resp;
352 }
353
354 cJSON *max_fee = cJSON_GetObjectItem(root, "max_fee_sats");
355 uint64_t fee = 10;
356 if (max_fee && cJSON_IsNumber(max_fee)) fee = (uint64_t)max_fee->valuedouble;
357
358 esp_err_t rc = nucula_wallet_melt(bolt11->valuestring, fee);
359
360 if (rc != ESP_OK) {
361 cJSON_Delete(root);
362 resp.success = false;
363 snprintf(resp.error, sizeof(resp.error), "Melt failed: %s", esp_err_to_name(rc));
364 return resp;
365 }
366
367 cJSON_Delete(root);
368 resp.success = true;
369 snprintf(resp.result_json, sizeof(resp.result_json), "{\"status\":\"ok\"}");
370 return resp;
371}
372
149mcp_response_t mcp_dispatch(const mcp_request_t *req) 373mcp_response_t mcp_dispatch(const mcp_request_t *req)
150{ 374{
151 if (!req) { 375 if (!req) {
@@ -164,6 +388,18 @@ mcp_response_t mcp_dispatch(const mcp_request_t *req)
164 return mcp_handle_get_balance(); 388 return mcp_handle_get_balance();
165 case MCP_TOOL_WALLET_SEND: 389 case MCP_TOOL_WALLET_SEND:
166 return mcp_handle_wallet_send(req->params_json); 390 return mcp_handle_wallet_send(req->params_json);
391 case MCP_TOOL_GET_SESSIONS:
392 return mcp_handle_get_sessions();
393 case MCP_TOOL_GET_USAGE:
394 return mcp_handle_get_usage();
395 case MCP_TOOL_SET_PAYOUT:
396 return mcp_handle_set_payout(req->params_json);
397 case MCP_TOOL_SET_METRIC:
398 return mcp_handle_set_metric(req->params_json);
399 case MCP_TOOL_SET_PRICE:
400 return mcp_handle_set_price(req->params_json);
401 case MCP_TOOL_WALLET_MELT:
402 return mcp_handle_wallet_melt(req->params_json);
167 default: 403 default:
168 break; 404 break;
169 } 405 }
diff --git a/main/mcp_handler.h b/main/mcp_handler.h
index e42b5ee..09aab9f 100644
--- a/main/mcp_handler.h
+++ b/main/mcp_handler.h
@@ -9,6 +9,12 @@ typedef enum {
9 MCP_TOOL_SET_CONFIG = 1, 9 MCP_TOOL_SET_CONFIG = 1,
10 MCP_TOOL_GET_BALANCE = 2, 10 MCP_TOOL_GET_BALANCE = 2,
11 MCP_TOOL_WALLET_SEND = 3, 11 MCP_TOOL_WALLET_SEND = 3,
12 MCP_TOOL_GET_SESSIONS = 4,
13 MCP_TOOL_GET_USAGE = 5,
14 MCP_TOOL_SET_PAYOUT = 6,
15 MCP_TOOL_SET_METRIC = 7,
16 MCP_TOOL_SET_PRICE = 8,
17 MCP_TOOL_WALLET_MELT = 9,
12 MCP_TOOL_UNKNOWN = 99 18 MCP_TOOL_UNKNOWN = 99
13} mcp_tool_t; 19} mcp_tool_t;
14 20
@@ -30,6 +36,12 @@ mcp_response_t mcp_handle_get_config(void);
30mcp_response_t mcp_handle_set_config(const char *params_json); 36mcp_response_t mcp_handle_set_config(const char *params_json);
31mcp_response_t mcp_handle_get_balance(void); 37mcp_response_t mcp_handle_get_balance(void);
32mcp_response_t mcp_handle_wallet_send(const char *params_json); 38mcp_response_t mcp_handle_wallet_send(const char *params_json);
39mcp_response_t mcp_handle_get_sessions(void);
40mcp_response_t mcp_handle_get_usage(void);
41mcp_response_t mcp_handle_set_payout(const char *params_json);
42mcp_response_t mcp_handle_set_metric(const char *params_json);
43mcp_response_t mcp_handle_set_price(const char *params_json);
44mcp_response_t mcp_handle_wallet_melt(const char *params_json);
33 45
34mcp_response_t mcp_dispatch(const mcp_request_t *req); 46mcp_response_t mcp_dispatch(const mcp_request_t *req);
35 47
diff --git a/main/session.c b/main/session.c
index 9b4380c..81e1f96 100644
--- a/main/session.c
+++ b/main/session.c
@@ -178,3 +178,13 @@ void session_tick(void)
178{ 178{
179 session_check_expiry(); 179 session_check_expiry();
180} 180}
181
182session_t *cvm_get_sessions_array(void)
183{
184 return s_sessions;
185}
186
187int cvm_get_sessions_count(void)
188{
189 return SESSION_MAX_CLIENTS;
190}
diff --git a/main/session.h b/main/session.h
index ea5b476..36fe722 100644
--- a/main/session.h
+++ b/main/session.h
@@ -43,4 +43,7 @@ int session_active_count(void);
43 43
44void session_tick(void); 44void session_tick(void);
45 45
46session_t *cvm_get_sessions_array(void);
47int cvm_get_sessions_count(void);
48
46#endif 49#endif
diff --git a/main/tollgate_main.c b/main/tollgate_main.c
index 1350d70..ad5211a 100644
--- a/main/tollgate_main.c
+++ b/main/tollgate_main.c
@@ -9,6 +9,7 @@
9#include "esp_netif.h" 9#include "esp_netif.h"
10#include "lwip/netif.h" 10#include "lwip/netif.h"
11#include "lwip/dns.h" 11#include "lwip/dns.h"
12#include "esp_sntp.h"
12#include "dhcpserver/dhcpserver.h" 13#include "dhcpserver/dhcpserver.h"
13#include "config.h" 14#include "config.h"
14#include "identity.h" 15#include "identity.h"
@@ -22,6 +23,7 @@
22#include "tollgate_client.h" 23#include "tollgate_client.h"
23#include "lightning_payout.h" 24#include "lightning_payout.h"
24#include "cvm_server.h" 25#include "cvm_server.h"
26#include "display.h"
25 27
26#define MAX_STA_RETRY 5 28#define MAX_STA_RETRY 5
27static const char *TAG = "tollgate_main"; 29static const char *TAG = "tollgate_main";
@@ -54,6 +56,7 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base,
54 tollgate_client_on_sta_disconnected(); 56 tollgate_client_on_sta_disconnected();
55 if (s_services_running) stop_services(); 57 if (s_services_running) stop_services();
56 if (s_retry_count < MAX_STA_RETRY) { 58 if (s_retry_count < MAX_STA_RETRY) {
59 vTaskDelay(pdMS_TO_TICKS(2000));
57 esp_wifi_connect(); 60 esp_wifi_connect();
58 } else { 61 } else {
59 wifi_config_t wifi_cfg; 62 wifi_config_t wifi_cfg;
@@ -94,6 +97,13 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base,
94 s_retry_count = 0; 97 s_retry_count = 0;
95 xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); 98 xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
96 99
100 esp_sntp_stop();
101 esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
102 esp_sntp_setservername(0, "pool.ntp.org");
103 esp_sntp_setservername(1, "time.google.com");
104 esp_sntp_init();
105 ESP_LOGI(TAG, "SNTP time sync started");
106
97 char gw_ip_str[16]; 107 char gw_ip_str[16];
98 snprintf(gw_ip_str, sizeof(gw_ip_str), IPSTR, IP2STR(&event->ip_info.gw)); 108 snprintf(gw_ip_str, sizeof(gw_ip_str), IPSTR, IP2STR(&event->ip_info.gw));
99 tollgate_client_on_sta_connected(gw_ip_str); 109 tollgate_client_on_sta_connected(gw_ip_str);
@@ -160,6 +170,11 @@ static void start_services(void)
160 s_services_running = true; 170 s_services_running = true;
161 if (s_services_mutex) xSemaphoreGive(s_services_mutex); 171 if (s_services_mutex) xSemaphoreGive(s_services_mutex);
162 ESP_LOGI(TAG, "=== TollGate services started ==="); 172 ESP_LOGI(TAG, "=== TollGate services started ===");
173
174 display_set_state(DISPLAY_READY);
175 char portal_url[128];
176 snprintf(portal_url, sizeof(portal_url), "http://%s/", cfg->ap_ip_str);
177 display_update(cfg->ap_ssid, 0, 0, portal_url);
163} 178}
164 179
165static void stop_services(void) 180static void stop_services(void)
@@ -240,6 +255,9 @@ void app_main(void)
240{ 255{
241 ESP_LOGI(TAG, "=== TollGate ESP32 Starting ==="); 256 ESP_LOGI(TAG, "=== TollGate ESP32 Starting ===");
242 257
258 display_init();
259 display_set_state(DISPLAY_BOOT);
260
243 esp_err_t ret = nvs_flash_init(); 261 esp_err_t ret = nvs_flash_init();
244 if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { 262 if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
245 ESP_ERROR_CHECK(nvs_flash_erase()); 263 ESP_ERROR_CHECK(nvs_flash_erase());
@@ -288,6 +306,9 @@ void app_main(void)
288 ESP_LOGI(TAG, "STA configured for SSID: %s", tcfg2->networks[tcfg2->current_network].ssid); 306 ESP_LOGI(TAG, "STA configured for SSID: %s", tcfg2->networks[tcfg2->current_network].ssid);
289 } 307 }
290 308
309 ESP_ERROR_CHECK(esp_wifi_set_country_code("DE", false));
310 ESP_LOGI(TAG, "WiFi country code set to DE (EU regulatory domain)");
311
291 ESP_ERROR_CHECK(esp_wifi_start()); 312 ESP_ERROR_CHECK(esp_wifi_start());
292 313
293 ESP_LOGI(TAG, "WiFi AP+STA started, waiting for connection..."); 314 ESP_LOGI(TAG, "WiFi AP+STA started, waiting for connection...");