diff options
| author | Your Name <you@example.com> | 2026-05-18 03:37:27 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-18 03:37:27 +0530 |
| commit | 8a2f7a6c9423e0c00fae3c1233bee9e0bb3ae239 (patch) | |
| tree | 8f8d2ede379b7e3cc0da82d472bcf0eeedcbf03b /main | |
| parent | fe7c3be2fd9d464dbc837d1913409d2691bd50f5 (diff) | |
feat: ContextVM (MCP over Nostr) server with WS masking fix
- Full CVM server: persistent WS relay listener, kind 25910 subscription
- MCP protocol handlers: initialize, tools/list, tools/call, ping
- 10 MCP tools: get_config, set_config, get_balance, wallet_send,
get_sessions, get_usage, set_payout, set_metric, set_price, wallet_melt
- CEP-6 announcements via WS (kinds 11316, 11317, 10002)
- Auth check: owner npub only
- Fix: WebSocket client-to-server frame masking (RFC 6455 requirement)
- Fix: Raw event JSON in EVENT wrapper (no re-parsing that breaks sig)
- SNTP init after STA gets IP
- 282 unit tests passing (61 CVM + 60 MCP handler + 161 existing)
- Integration test scaffold: tests/integration/test-cvm.mjs
Diffstat (limited to 'main')
| -rw-r--r-- | main/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | main/config.c | 4 | ||||
| -rw-r--r-- | main/cvm_server.c | 727 | ||||
| -rw-r--r-- | main/cvm_server.h | 4 | ||||
| -rw-r--r-- | main/mcp_handler.c | 236 | ||||
| -rw-r--r-- | main/mcp_handler.h | 12 | ||||
| -rw-r--r-- | main/session.c | 10 | ||||
| -rw-r--r-- | main/session.h | 3 | ||||
| -rw-r--r-- | main/tollgate_main.c | 17 |
9 files changed, 884 insertions, 133 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..7c8ebed 100644 --- a/main/config.c +++ b/main/config.c | |||
| @@ -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", |
diff --git a/main/cvm_server.c b/main/cvm_server.c index 5addd88..cf052df 100644 --- a/main/cvm_server.c +++ b/main/cvm_server.c | |||
| @@ -2,217 +2,679 @@ | |||
| 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" | ||
| 10 | #include "freertos/FreeRTOS.h" | 14 | #include "freertos/FreeRTOS.h" |
| 11 | #include "freertos/task.h" | 15 | #include "freertos/task.h" |
| 12 | #include <string.h> | 16 | #include <string.h> |
| 13 | #include <stdio.h> | 17 | #include <stdio.h> |
| 18 | #include <stdlib.h> | ||
| 14 | 19 | ||
| 15 | static const char *TAG = "cvm_server"; | 20 | static const char *TAG = "cvm_server"; |
| 16 | 21 | ||
| 17 | static bool g_running = false; | 22 | static bool g_running = false; |
| 18 | static TaskHandle_t g_task = NULL; | 23 | static TaskHandle_t g_task = NULL; |
| 19 | 24 | ||
| 20 | static const char *DEFAULT_RELAY = "wss://relay.damus.io"; | 25 | static void publish_announcements_via_ws(esp_tls_t *tls); |
| 21 | 26 | ||
| 22 | static char *fetch_relays(void) | 27 | #define CVM_VERSION "2025-07-02" |
| 28 | #define CVM_SERVER_NAME "TollGate" | ||
| 29 | #define CVM_SERVER_VERSION "1.0.0" | ||
| 30 | #define CVM_WS_BUF_SIZE 8192 | ||
| 31 | #define CVM_MAX_RESPONSE_SIZE 4096 | ||
| 32 | #define CVM_RECONNECT_DELAY_MS 5000 | ||
| 33 | |||
| 34 | static char *parse_ws_text_frame(const uint8_t *buf, int len) | ||
| 23 | { | 35 | { |
| 24 | const tollgate_config_t *cfg = tollgate_config_get(); | 36 | if (len < 2) return NULL; |
| 25 | if (cfg && cfg->cvm_relays[0]) { | 37 | bool masked = (buf[1] & 0x80) != 0; |
| 26 | return cfg->cvm_relays; | 38 | uint64_t payload_len = buf[1] & 0x7F; |
| 39 | int offset = 2; | ||
| 40 | |||
| 41 | if (payload_len == 126) { | ||
| 42 | if (len < 4) return NULL; | ||
| 43 | payload_len = ((uint64_t)buf[2] << 8) | buf[3]; | ||
| 44 | offset = 4; | ||
| 45 | } else if (payload_len == 127) { | ||
| 46 | if (len < 10) return NULL; | ||
| 47 | payload_len = 0; | ||
| 48 | for (int i = 0; i < 8; i++) | ||
| 49 | payload_len = (payload_len << 8) | buf[2 + i]; | ||
| 50 | offset = 10; | ||
| 51 | } | ||
| 52 | |||
| 53 | if (masked) offset += 4; | ||
| 54 | if (offset + payload_len > (uint64_t)len) return NULL; | ||
| 55 | |||
| 56 | char *text = malloc((size_t)payload_len + 1); | ||
| 57 | if (!text) return NULL; | ||
| 58 | |||
| 59 | if (masked) { | ||
| 60 | uint8_t mask[4] = { buf[offset - 4], buf[offset - 3], buf[offset - 2], buf[offset - 1] }; | ||
| 61 | for (uint64_t i = 0; i < payload_len; i++) | ||
| 62 | text[i] = buf[offset + i] ^ mask[i & 3]; | ||
| 63 | } else { | ||
| 64 | memcpy(text, buf + offset, (size_t)payload_len); | ||
| 27 | } | 65 | } |
| 28 | return (char *)DEFAULT_RELAY; | 66 | text[payload_len] = '\0'; |
| 67 | return text; | ||
| 29 | } | 68 | } |
| 30 | 69 | ||
| 31 | static char *http_get(const char *url, int timeout_ms) | 70 | static int ws_send_text(esp_tls_t *tls, const char *msg) |
| 32 | { | 71 | { |
| 33 | char *buf = malloc(8192); | 72 | size_t len = strlen(msg); |
| 34 | if (!buf) return NULL; | 73 | uint8_t mask[4]; |
| 35 | int total = 0; | 74 | esp_fill_random(mask, 4); |
| 75 | |||
| 76 | size_t frame_len = 6 + len; | ||
| 77 | if (len > 125) frame_len += 2; | ||
| 78 | if (len > 65535) frame_len += 6; | ||
| 79 | |||
| 80 | uint8_t *frame = malloc(frame_len + len); | ||
| 81 | if (!frame) return -1; | ||
| 82 | |||
| 83 | int pos = 0; | ||
| 84 | frame[pos++] = 0x81; | ||
| 85 | if (len <= 125) { | ||
| 86 | frame[pos++] = (uint8_t)(0x80 | len); | ||
| 87 | } else if (len <= 65535) { | ||
| 88 | frame[pos++] = 0x80 | 126; | ||
| 89 | frame[pos++] = (uint8_t)((len >> 8) & 0xff); | ||
| 90 | frame[pos++] = (uint8_t)(len & 0xff); | ||
| 91 | } else { | ||
| 92 | frame[pos++] = 0x80 | 127; | ||
| 93 | for (int i = 0; i < 8; i++) | ||
| 94 | frame[pos++] = (uint8_t)((len >> (56 - i * 8)) & 0xff); | ||
| 95 | } | ||
| 96 | memcpy(frame + pos, mask, 4); | ||
| 97 | pos += 4; | ||
| 98 | |||
| 99 | for (size_t i = 0; i < len; i++) | ||
| 100 | frame[pos + i] = (uint8_t)msg[i] ^ mask[i & 3]; | ||
| 101 | pos += len; | ||
| 36 | 102 | ||
| 37 | esp_http_client_config_t config = { | 103 | int w = esp_tls_conn_write(tls, frame, pos); |
| 38 | .url = url, | 104 | free(frame); |
| 39 | .method = HTTP_METHOD_GET, | 105 | return w > 0 ? 0 : -1; |
| 40 | .timeout_ms = timeout_ms, | 106 | } |
| 107 | |||
| 108 | static esp_err_t ws_connect(const char *relay_url, esp_tls_t **tls_out) | ||
| 109 | { | ||
| 110 | char host[128] = {0}; | ||
| 111 | int port = 443; | ||
| 112 | char path[128] = "/"; | ||
| 113 | |||
| 114 | if (strncmp(relay_url, "wss://", 6) != 0) return ESP_ERR_INVALID_ARG; | ||
| 115 | |||
| 116 | const char *url_start = relay_url + 6; | ||
| 117 | const char *path_ptr = strchr(url_start, '/'); | ||
| 118 | if (path_ptr) { | ||
| 119 | size_t host_len = path_ptr - url_start; | ||
| 120 | if (host_len >= sizeof(host)) host_len = sizeof(host) - 1; | ||
| 121 | memcpy(host, url_start, host_len); | ||
| 122 | host[host_len] = '\0'; | ||
| 123 | strncpy(path, path_ptr, sizeof(path) - 1); | ||
| 124 | } else { | ||
| 125 | strncpy(host, url_start, sizeof(host) - 1); | ||
| 126 | } | ||
| 127 | |||
| 128 | char *colon = strchr(host, ':'); | ||
| 129 | if (colon) { | ||
| 130 | *colon = '\0'; | ||
| 131 | port = atoi(colon + 1); | ||
| 132 | } | ||
| 133 | |||
| 134 | esp_tls_cfg_t tls_cfg = { | ||
| 135 | .crt_bundle_attach = esp_crt_bundle_attach, | ||
| 136 | .timeout_ms = 15000, | ||
| 41 | }; | 137 | }; |
| 42 | esp_http_client_handle_t client = esp_http_client_init(&config); | 138 | esp_tls_t *tls = esp_tls_init(); |
| 43 | if (!client) { free(buf); return NULL; } | 139 | if (!tls) return ESP_ERR_NO_MEM; |
| 44 | 140 | ||
| 45 | esp_err_t err = esp_http_client_open(client, 0); | 141 | int ret = esp_tls_conn_new_sync(host, strlen(host), port, &tls_cfg, tls); |
| 46 | if (err != ESP_OK) { | 142 | if (ret < 0) { |
| 47 | esp_http_client_cleanup(client); | 143 | esp_tls_conn_destroy(tls); |
| 48 | free(buf); | 144 | return ESP_FAIL; |
| 49 | return NULL; | ||
| 50 | } | 145 | } |
| 51 | 146 | ||
| 52 | int content_length = esp_http_client_fetch_headers(client); | 147 | char upgrade[512]; |
| 53 | int max_read = content_length > 0 ? content_length : 8191; | 148 | snprintf(upgrade, sizeof(upgrade), |
| 149 | "GET %s HTTP/1.1\r\n" | ||
| 150 | "Host: %s\r\n" | ||
| 151 | "Upgrade: websocket\r\n" | ||
| 152 | "Connection: Upgrade\r\n" | ||
| 153 | "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" | ||
| 154 | "Sec-WebSocket-Version: 13\r\n" | ||
| 155 | "\r\n", | ||
| 156 | path, host); | ||
| 157 | |||
| 158 | int written = esp_tls_conn_write(tls, (const unsigned char *)upgrade, strlen(upgrade)); | ||
| 159 | if (written < 0) { | ||
| 160 | esp_tls_conn_destroy(tls); | ||
| 161 | return ESP_FAIL; | ||
| 162 | } | ||
| 54 | 163 | ||
| 55 | while (total < max_read) { | 164 | char resp[1024]; |
| 56 | int n = esp_http_client_read(client, buf + total, max_read - total); | 165 | int rlen = esp_tls_conn_read(tls, (unsigned char *)resp, sizeof(resp) - 1); |
| 57 | if (n <= 0) break; | 166 | if (rlen <= 0 || !strstr(resp, "101")) { |
| 58 | total += n; | 167 | ESP_LOGE(TAG, "WS upgrade failed to %s (read %d)", host, rlen); |
| 168 | esp_tls_conn_destroy(tls); | ||
| 169 | return ESP_FAIL; | ||
| 59 | } | 170 | } |
| 60 | buf[total] = '\0'; | 171 | |
| 61 | esp_http_client_cleanup(client); | 172 | *tls_out = tls; |
| 62 | return buf; | 173 | ESP_LOGI(TAG, "Connected to %s", host); |
| 174 | return ESP_OK; | ||
| 63 | } | 175 | } |
| 64 | 176 | ||
| 65 | static cJSON *build_filter(const char *npub) | 177 | static cJSON *build_tools_list(void) |
| 66 | { | 178 | { |
| 67 | cJSON *filter = cJSON_CreateObject(); | 179 | cJSON *tools = cJSON_CreateArray(); |
| 68 | cJSON *kinds = cJSON_CreateArray(); | 180 | |
| 69 | cJSON_AddItemToArray(kinds, cJSON_CreateNumber(4)); | 181 | const char *tool_defs[][3] = { |
| 70 | cJSON_AddItemToObject(filter, "kinds", kinds); | 182 | {"get_config", "Get current device configuration", "{\"type\":\"object\",\"properties\":{},\"required\":[]}"}, |
| 71 | cJSON_AddStringToObject(filter, "#p", npub); | 183 | {"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); | 184 | {"get_balance", "Get wallet balance and proof count", "{\"type\":\"object\",\"properties\":{},\"required\":[]}"}, |
| 73 | return filter; | 185 | {"wallet_send", "Send e-cash tokens from wallet", "{\"type\":\"object\",\"properties\":{\"amount\":{\"type\":\"integer\",\"description\":\"Amount in sats\"}},\"required\":[\"amount\"]}"}, |
| 186 | {"get_sessions","Get active client sessions", "{\"type\":\"object\",\"properties\":{},\"required\":[]}"}, | ||
| 187 | {"get_usage", "Get current billing usage info", "{\"type\":\"object\",\"properties\":{},\"required\":[]}"}, | ||
| 188 | {"set_payout", "Configure payout recipients", "{\"type\":\"object\",\"properties\":{\"enabled\":{\"type\":\"boolean\"},\"recipients\":{\"type\":\"array\"}}}"}, | ||
| 189 | {"set_metric", "Set billing metric", "{\"type\":\"object\",\"properties\":{\"metric\":{\"type\":\"string\",\"enum\":[\"bytes\",\"milliseconds\"]}},\"required\":[\"metric\"]}"}, | ||
| 190 | {"set_price", "Set price per step", "{\"type\":\"object\",\"properties\":{\"price_per_step\":{\"type\":\"integer\",\"minimum\":1}},\"required\":[\"price_per_step\"]}"}, | ||
| 191 | {"wallet_melt", "Melt tokens for lightning payment", "{\"type\":\"object\",\"properties\":{\"bolt11\":{\"type\":\"string\"},\"max_fee_sats\":{\"type\":\"integer\"}},\"required\":[\"bolt11\"]}"}, | ||
| 192 | }; | ||
| 193 | |||
| 194 | for (int i = 0; i < 10; i++) { | ||
| 195 | cJSON *tool = cJSON_CreateObject(); | ||
| 196 | cJSON_AddStringToObject(tool, "name", tool_defs[i][0]); | ||
| 197 | cJSON_AddStringToObject(tool, "description", tool_defs[i][1]); | ||
| 198 | cJSON *schema = cJSON_Parse(tool_defs[i][2]); | ||
| 199 | if (schema) cJSON_AddItemToObject(tool, "inputSchema", schema); | ||
| 200 | cJSON_AddItemToArray(tools, tool); | ||
| 201 | } | ||
| 202 | |||
| 203 | return tools; | ||
| 74 | } | 204 | } |
| 75 | 205 | ||
| 76 | static cJSON *build_subscription(const char *npub) | 206 | static char *build_initialize_response(const char *request_id_str, const char *client_pubkey) |
| 77 | { | 207 | { |
| 78 | cJSON *sub = cJSON_CreateArray(); | 208 | cJSON *response = cJSON_CreateObject(); |
| 79 | cJSON_AddItemToArray(sub, cJSON_CreateString("REQ")); | 209 | cJSON_AddStringToObject(response, "jsonrpc", "2.0"); |
| 80 | cJSON_AddItemToArray(sub, cJSON_CreateString("cvm_sub_01")); | 210 | cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 0); |
| 81 | cJSON_AddItemToArray(sub, build_filter(npub)); | 211 | |
| 82 | return sub; | 212 | cJSON *result = cJSON_CreateObject(); |
| 213 | cJSON_AddStringToObject(result, "protocolVersion", CVM_VERSION); | ||
| 214 | |||
| 215 | cJSON *capabilities = cJSON_CreateObject(); | ||
| 216 | cJSON_AddItemToObject(capabilities, "tools", cJSON_CreateObject()); | ||
| 217 | cJSON_AddItemToObject(result, "capabilities", capabilities); | ||
| 218 | |||
| 219 | cJSON *serverInfo = cJSON_CreateObject(); | ||
| 220 | cJSON_AddStringToObject(serverInfo, "name", CVM_SERVER_NAME); | ||
| 221 | cJSON_AddStringToObject(serverInfo, "version", CVM_SERVER_VERSION); | ||
| 222 | cJSON_AddItemToObject(result, "serverInfo", serverInfo); | ||
| 223 | |||
| 224 | cJSON_AddItemToObject(response, "result", result); | ||
| 225 | |||
| 226 | char *json = cJSON_PrintUnformatted(response); | ||
| 227 | cJSON_Delete(response); | ||
| 228 | return json; | ||
| 83 | } | 229 | } |
| 84 | 230 | ||
| 85 | static void process_dm(const char *sender_pubkey, const char *encrypted_content) | 231 | static char *build_tools_list_response(const char *request_id_str) |
| 232 | { | ||
| 233 | cJSON *response = cJSON_CreateObject(); | ||
| 234 | cJSON_AddStringToObject(response, "jsonrpc", "2.0"); | ||
| 235 | cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 1); | ||
| 236 | |||
| 237 | cJSON *result = cJSON_CreateObject(); | ||
| 238 | cJSON *tools = build_tools_list(); | ||
| 239 | cJSON_AddItemToObject(result, "tools", tools); | ||
| 240 | cJSON_AddItemToObject(response, "result", result); | ||
| 241 | |||
| 242 | char *json = cJSON_PrintUnformatted(response); | ||
| 243 | cJSON_Delete(response); | ||
| 244 | return json; | ||
| 245 | } | ||
| 246 | |||
| 247 | static char *build_tool_call_response(const char *request_id_str, const mcp_response_t *mcp_resp) | ||
| 248 | { | ||
| 249 | cJSON *response = cJSON_CreateObject(); | ||
| 250 | cJSON_AddStringToObject(response, "jsonrpc", "2.0"); | ||
| 251 | cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 2); | ||
| 252 | |||
| 253 | if (mcp_resp->success) { | ||
| 254 | cJSON *result = cJSON_CreateObject(); | ||
| 255 | cJSON_AddItemToObject(result, "content", cJSON_CreateArray()); | ||
| 256 | cJSON *content_arr = cJSON_GetObjectItem(result, "content"); | ||
| 257 | cJSON *text_item = cJSON_CreateObject(); | ||
| 258 | cJSON_AddStringToObject(text_item, "type", "text"); | ||
| 259 | cJSON_AddStringToObject(text_item, "text", mcp_resp->result_json); | ||
| 260 | cJSON_AddItemToArray(content_arr, text_item); | ||
| 261 | cJSON_AddBoolToObject(result, "isError", false); | ||
| 262 | cJSON_AddItemToObject(response, "result", result); | ||
| 263 | } else { | ||
| 264 | cJSON *error = cJSON_CreateObject(); | ||
| 265 | cJSON_AddNumberToObject(error, "code", -32603); | ||
| 266 | cJSON_AddStringToObject(error, "message", mcp_resp->error); | ||
| 267 | cJSON_AddItemToObject(response, "error", error); | ||
| 268 | } | ||
| 269 | |||
| 270 | char *json = cJSON_PrintUnformatted(response); | ||
| 271 | cJSON_Delete(response); | ||
| 272 | return json; | ||
| 273 | } | ||
| 274 | |||
| 275 | static char *build_ping_response(const char *request_id_str) | ||
| 276 | { | ||
| 277 | cJSON *response = cJSON_CreateObject(); | ||
| 278 | cJSON_AddStringToObject(response, "jsonrpc", "2.0"); | ||
| 279 | cJSON_AddNumberToObject(response, "id", request_id_str ? atof(request_id_str) : 0); | ||
| 280 | cJSON *result = cJSON_CreateObject(); | ||
| 281 | cJSON_AddItemToObject(response, "result", result); | ||
| 282 | char *json = cJSON_PrintUnformatted(response); | ||
| 283 | cJSON_Delete(response); | ||
| 284 | return json; | ||
| 285 | } | ||
| 286 | |||
| 287 | static esp_err_t publish_event_to_relay(const char *relay_url, const char *event_json) | ||
| 288 | { | ||
| 289 | esp_tls_t *tls = NULL; | ||
| 290 | esp_err_t err = ws_connect(relay_url, &tls); | ||
| 291 | if (err != ESP_OK) return err; | ||
| 292 | |||
| 293 | char *msg; | ||
| 294 | size_t event_len2 = strlen(event_json); | ||
| 295 | size_t msg_len2 = 8 + event_len2 + 1; | ||
| 296 | msg = malloc(msg_len2); | ||
| 297 | snprintf(msg, msg_len2, "[\"EVENT\",%s]", event_json); | ||
| 298 | |||
| 299 | ws_send_text(tls, msg); | ||
| 300 | free(msg); | ||
| 301 | |||
| 302 | uint8_t resp_buf[256]; | ||
| 303 | esp_tls_conn_read(tls, resp_buf, sizeof(resp_buf) - 1); | ||
| 304 | |||
| 305 | uint8_t close_frame[2] = {0x88, 0x00}; | ||
| 306 | esp_tls_conn_write(tls, close_frame, 2); | ||
| 307 | esp_tls_conn_destroy(tls); | ||
| 308 | return ESP_OK; | ||
| 309 | } | ||
| 310 | |||
| 311 | static esp_err_t publish_kind_25910_response(const char *relay_url, | ||
| 312 | const char *content_json, | ||
| 313 | const char *request_event_id) | ||
| 86 | { | 314 | { |
| 87 | const tollgate_identity_t *id = identity_get(); | 315 | const tollgate_identity_t *id = identity_get(); |
| 88 | if (!id || !id->initialized) { | 316 | if (!id || !id->initialized) return ESP_FAIL; |
| 89 | ESP_LOGE(TAG, "Identity not initialized"); | 317 | |
| 318 | cJSON *tags = cJSON_CreateArray(); | ||
| 319 | cJSON *e_tag = cJSON_CreateArray(); | ||
| 320 | cJSON_AddItemToArray(e_tag, cJSON_CreateString("e")); | ||
| 321 | cJSON_AddItemToArray(e_tag, cJSON_CreateString(request_event_id)); | ||
| 322 | cJSON_AddItemToArray(tags, e_tag); | ||
| 323 | |||
| 324 | char *tags_str = cJSON_PrintUnformatted(tags); | ||
| 325 | cJSON_Delete(tags); | ||
| 326 | |||
| 327 | nostr_event_t event; | ||
| 328 | nostr_event_init(&event, id->npub_hex, 25910, tags_str, content_json); | ||
| 329 | nostr_event_sign(&event, id->nsec); | ||
| 330 | free(tags_str); | ||
| 331 | |||
| 332 | char *event_json = malloc(8192); | ||
| 333 | if (!event_json) return ESP_ERR_NO_MEM; | ||
| 334 | |||
| 335 | esp_err_t ret = nostr_event_to_json(&event, event_json, 8192); | ||
| 336 | if (ret != ESP_OK) { | ||
| 337 | free(event_json); | ||
| 338 | return ret; | ||
| 339 | } | ||
| 340 | |||
| 341 | ret = publish_event_to_relay(relay_url, event_json); | ||
| 342 | free(event_json); | ||
| 343 | return ret; | ||
| 344 | } | ||
| 345 | |||
| 346 | static bool is_owner_pubkey(const char *pubkey_hex) | ||
| 347 | { | ||
| 348 | const tollgate_identity_t *id = identity_get(); | ||
| 349 | if (!id || !id->initialized) return false; | ||
| 350 | if (!pubkey_hex) return false; | ||
| 351 | return strcmp(id->npub_hex, pubkey_hex) == 0; | ||
| 352 | } | ||
| 353 | |||
| 354 | static void handle_mcp_message(const char *relay_url, const char *sender_pubkey, | ||
| 355 | const char *event_id, const char *content) | ||
| 356 | { | ||
| 357 | cJSON *msg = cJSON_Parse(content); | ||
| 358 | if (!msg) { | ||
| 359 | ESP_LOGW(TAG, "Invalid JSON in kind 25910 content"); | ||
| 90 | return; | 360 | return; |
| 91 | } | 361 | } |
| 92 | 362 | ||
| 93 | uint8_t sender_pk[64]; | 363 | cJSON *method = cJSON_GetObjectItem(msg, "method"); |
| 94 | for (int i = 0; i < 64; i++) { | 364 | cJSON *id_field = cJSON_GetObjectItem(msg, "id"); |
| 95 | char hex[3] = { sender_pubkey[i*2], sender_pubkey[i*2+1], 0 }; | 365 | const char *id_str = (id_field && cJSON_IsNumber(id_field)) |
| 96 | sender_pk[i] = (uint8_t)strtol(hex, NULL, 16); | 366 | ? cJSON_PrintUnformatted(id_field) : "0"; |
| 367 | |||
| 368 | if (method && cJSON_IsString(method)) { | ||
| 369 | const char *m = method->valuestring; | ||
| 370 | |||
| 371 | if (strcmp(m, "initialize") == 0) { | ||
| 372 | ESP_LOGI(TAG, "MCP initialize from %s", sender_pubkey); | ||
| 373 | char *resp = build_initialize_response(id_str, sender_pubkey); | ||
| 374 | publish_kind_25910_response(relay_url, resp, event_id); | ||
| 375 | free(resp); | ||
| 376 | } else if (strcmp(m, "notifications/initialized") == 0) { | ||
| 377 | ESP_LOGI(TAG, "Client initialized: %s", sender_pubkey); | ||
| 378 | } else if (strcmp(m, "tools/list") == 0) { | ||
| 379 | ESP_LOGI(TAG, "tools/list from %s", sender_pubkey); | ||
| 380 | char *resp = build_tools_list_response(id_str); | ||
| 381 | publish_kind_25910_response(relay_url, resp, event_id); | ||
| 382 | free(resp); | ||
| 383 | } else if (strcmp(m, "tools/call") == 0) { | ||
| 384 | cJSON *params = cJSON_GetObjectItem(msg, "params"); | ||
| 385 | cJSON *name = params ? cJSON_GetObjectItem(params, "name") : NULL; | ||
| 386 | cJSON *arguments = params ? cJSON_GetObjectItem(params, "arguments") : NULL; | ||
| 387 | |||
| 388 | if (name && cJSON_IsString(name)) { | ||
| 389 | ESP_LOGI(TAG, "tools/call %s from %s", name->valuestring, sender_pubkey); | ||
| 390 | |||
| 391 | mcp_request_t req = {0}; | ||
| 392 | req.tool = mcp_parse_tool(name->valuestring); | ||
| 393 | strncpy(req.method, name->valuestring, sizeof(req.method) - 1); | ||
| 394 | if (arguments) { | ||
| 395 | char *ajson = cJSON_PrintUnformatted(arguments); | ||
| 396 | strncpy(req.params_json, ajson, sizeof(req.params_json) - 1); | ||
| 397 | cJSON_free(ajson); | ||
| 398 | } | ||
| 399 | |||
| 400 | mcp_response_t mcp_resp = mcp_dispatch(&req); | ||
| 401 | char *resp = build_tool_call_response(id_str, &mcp_resp); | ||
| 402 | publish_kind_25910_response(relay_url, resp, event_id); | ||
| 403 | free(resp); | ||
| 404 | } | ||
| 405 | } else if (strcmp(m, "ping") == 0) { | ||
| 406 | char *resp = build_ping_response(id_str); | ||
| 407 | publish_kind_25910_response(relay_url, resp, event_id); | ||
| 408 | free(resp); | ||
| 409 | } else { | ||
| 410 | ESP_LOGW(TAG, "Unknown MCP method: %s", m); | ||
| 411 | } | ||
| 412 | } | ||
| 413 | |||
| 414 | if (id_field && cJSON_IsNumber(id_field) && id_str[0] != '0') { | ||
| 415 | free((void *)id_str); | ||
| 416 | } else if (id_str[0] != '0') { | ||
| 97 | } | 417 | } |
| 418 | cJSON_Delete(msg); | ||
| 419 | } | ||
| 98 | 420 | ||
| 99 | char plaintext[2048]; | 421 | static void process_relay_message(const char *relay_url, const char *msg_str) |
| 100 | int pt_len = nip04_decrypt(id->nsec, sender_pk, encrypted_content, plaintext, sizeof(plaintext)); | 422 | { |
| 101 | if (pt_len < 0) { | 423 | cJSON *arr = cJSON_Parse(msg_str); |
| 102 | ESP_LOGE(TAG, "Failed to decrypt DM from %.8s", sender_pubkey); | 424 | if (!arr || !cJSON_IsArray(arr)) { |
| 425 | if (arr) cJSON_Delete(arr); | ||
| 103 | return; | 426 | return; |
| 104 | } | 427 | } |
| 105 | 428 | ||
| 106 | ESP_LOGI(TAG, "Decrypted DM from %.8s: %s", sender_pubkey, plaintext); | 429 | cJSON *cmd = cJSON_GetArrayItem(arr, 0); |
| 430 | if (!cmd || !cJSON_IsString(cmd)) { | ||
| 431 | cJSON_Delete(arr); | ||
| 432 | return; | ||
| 433 | } | ||
| 107 | 434 | ||
| 108 | cJSON *msg = cJSON_Parse(plaintext); | 435 | if (strcmp(cmd->valuestring, "OK") == 0) { |
| 109 | if (!msg) { | 436 | cJSON *ev_id = cJSON_GetArrayItem(arr, 1); |
| 110 | ESP_LOGE(TAG, "Invalid JSON in DM"); | 437 | cJSON *ok = cJSON_GetArrayItem(arr, 2); |
| 438 | cJSON *reason = cJSON_GetArrayItem(arr, 3); | ||
| 439 | ESP_LOGI(TAG, "Relay OK: id=%.16s success=%s reason=%s", | ||
| 440 | ev_id ? ev_id->valuestring : "?", | ||
| 441 | ok ? (cJSON_IsTrue(ok) ? "true" : "FALSE") : "?", | ||
| 442 | reason ? reason->valuestring : ""); | ||
| 443 | cJSON_Delete(arr); | ||
| 111 | return; | 444 | return; |
| 112 | } | 445 | } |
| 113 | 446 | ||
| 114 | cJSON *method = cJSON_GetObjectItem(msg, "method"); | 447 | if (strcmp(cmd->valuestring, "EVENT") != 0) { |
| 115 | cJSON *params = cJSON_GetObjectItem(msg, "params"); | 448 | ESP_LOGI(TAG, "Relay msg: %.100s", msg_str); |
| 116 | if (!method || !cJSON_IsString(method)) { | 449 | cJSON_Delete(arr); |
| 117 | cJSON_Delete(msg); | ||
| 118 | ESP_LOGE(TAG, "Missing 'method' in CVM request"); | ||
| 119 | return; | 450 | return; |
| 120 | } | 451 | } |
| 121 | 452 | ||
| 122 | mcp_request_t req = {0}; | 453 | cJSON *event = cJSON_GetArrayItem(arr, 2); |
| 123 | req.tool = mcp_parse_tool(method->valuestring); | 454 | if (!event) { |
| 124 | strncpy(req.method, method->valuestring, sizeof(req.method) - 1); | 455 | cJSON_Delete(arr); |
| 125 | if (params && cJSON_IsString(params)) { | 456 | return; |
| 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 | } | 457 | } |
| 132 | 458 | ||
| 133 | mcp_response_t resp = mcp_dispatch(&req); | 459 | cJSON *kind = cJSON_GetObjectItem(event, "kind"); |
| 134 | cJSON_Delete(msg); | 460 | if (!kind || kind->valueint != 25910) { |
| 461 | cJSON_Delete(arr); | ||
| 462 | return; | ||
| 463 | } | ||
| 135 | 464 | ||
| 136 | cJSON *response_msg = cJSON_CreateObject(); | 465 | cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey"); |
| 137 | if (resp.success) { | 466 | cJSON *event_id = cJSON_GetObjectItem(event, "id"); |
| 138 | cJSON_AddStringToObject(response_msg, "status", "ok"); | 467 | cJSON *content = cJSON_GetObjectItem(event, "content"); |
| 139 | cJSON_AddItemToObject(response_msg, "result", cJSON_Parse(resp.result_json)); | 468 | |
| 140 | } else { | 469 | if (!pubkey || !content || !event_id) { |
| 141 | cJSON_AddStringToObject(response_msg, "status", "error"); | 470 | cJSON_Delete(arr); |
| 142 | cJSON_AddStringToObject(response_msg, "error", resp.error); | 471 | return; |
| 472 | } | ||
| 473 | |||
| 474 | if (!is_owner_pubkey(pubkey->valuestring)) { | ||
| 475 | ESP_LOGW(TAG, "Ignoring request from non-owner: %.16s...", pubkey->valuestring); | ||
| 476 | cJSON_Delete(arr); | ||
| 477 | return; | ||
| 143 | } | 478 | } |
| 144 | 479 | ||
| 145 | char *response_str = cJSON_PrintUnformatted(response_msg); | 480 | handle_mcp_message(relay_url, pubkey->valuestring, event_id->valuestring, content->valuestring); |
| 146 | cJSON_Delete(response_msg); | 481 | cJSON_Delete(arr); |
| 482 | } | ||
| 483 | |||
| 484 | static esp_err_t subscribe_to_relay(esp_tls_t *tls, const char *npub) | ||
| 485 | { | ||
| 486 | cJSON *sub = cJSON_CreateArray(); | ||
| 487 | cJSON_AddItemToArray(sub, cJSON_CreateString("REQ")); | ||
| 488 | cJSON_AddItemToArray(sub, cJSON_CreateString("cvm_sub")); | ||
| 489 | cJSON *filter = cJSON_CreateObject(); | ||
| 490 | cJSON *kinds = cJSON_CreateArray(); | ||
| 491 | cJSON_AddItemToArray(kinds, cJSON_CreateNumber(25910)); | ||
| 492 | cJSON_AddItemToObject(filter, "kinds", kinds); | ||
| 493 | cJSON_AddStringToObject(filter, "#p", npub); | ||
| 494 | cJSON_AddNumberToObject(filter, "limit", 100); | ||
| 495 | cJSON_AddItemToArray(sub, filter); | ||
| 147 | 496 | ||
| 148 | uint8_t response_ct[4096]; | 497 | char *msg = cJSON_PrintUnformatted(sub); |
| 149 | size_t ct_len = 0; | 498 | cJSON_Delete(sub); |
| 150 | nip04_encrypt(id->nsec, sender_pk, response_str, response_ct, &ct_len); | ||
| 151 | free(response_str); | ||
| 152 | 499 | ||
| 153 | ESP_LOGI(TAG, "CVM response prepared (%zu bytes encrypted), would send to %.8s", ct_len, sender_pubkey); | 500 | int rc = ws_send_text(tls, msg); |
| 501 | free(msg); | ||
| 502 | return rc == 0 ? ESP_OK : ESP_FAIL; | ||
| 154 | } | 503 | } |
| 155 | 504 | ||
| 156 | static void parse_nostr_events(const char *data) | 505 | static void cvm_relay_task(void *arg) |
| 157 | { | 506 | { |
| 158 | cJSON *arr = cJSON_Parse(data); | 507 | const char *relay_url = (const char *)arg; |
| 159 | if (!arr || !cJSON_IsArray(arr)) { | 508 | const tollgate_identity_t *id = identity_get(); |
| 160 | if (arr) cJSON_Delete(arr); | 509 | if (!id || !id->initialized) { |
| 510 | ESP_LOGE(TAG, "Identity not initialized"); | ||
| 511 | vTaskDelete(NULL); | ||
| 161 | return; | 512 | return; |
| 162 | } | 513 | } |
| 163 | 514 | ||
| 164 | cJSON *item = NULL; | 515 | while (g_running) { |
| 165 | cJSON_ArrayForEach(item, arr) { | 516 | esp_tls_t *tls = NULL; |
| 166 | if (!cJSON_IsArray(item)) continue; | 517 | esp_err_t err = ws_connect(relay_url, &tls); |
| 167 | int arr_size = cJSON_GetArraySize(item); | 518 | if (err != ESP_OK) { |
| 168 | if (arr_size < 3) continue; | 519 | ESP_LOGW(TAG, "Connect failed to %s, retrying", relay_url); |
| 520 | vTaskDelay(pdMS_TO_TICKS(CVM_RECONNECT_DELAY_MS)); | ||
| 521 | continue; | ||
| 522 | } | ||
| 169 | 523 | ||
| 170 | cJSON *cmd = cJSON_GetArrayItem(item, 0); | 524 | err = subscribe_to_relay(tls, id->npub_hex); |
| 171 | if (!cmd || !cJSON_IsString(cmd) || strcmp(cmd->valuestring, "EVENT") != 0) continue; | 525 | if (err != ESP_OK) { |
| 526 | esp_tls_conn_destroy(tls); | ||
| 527 | vTaskDelay(pdMS_TO_TICKS(CVM_RECONNECT_DELAY_MS)); | ||
| 528 | continue; | ||
| 529 | } | ||
| 172 | 530 | ||
| 173 | cJSON *event = cJSON_GetArrayItem(item, 2); | 531 | ESP_LOGI(TAG, "Listening on %s for kind 25910 events", relay_url); |
| 174 | if (!event) continue; | 532 | publish_announcements_via_ws(tls); |
| 175 | 533 | ||
| 176 | cJSON *kind = cJSON_GetObjectItem(event, "kind"); | 534 | uint8_t *buf = malloc(CVM_WS_BUF_SIZE); |
| 177 | if (!kind || kind->valueint != 4) continue; | 535 | if (!buf) { |
| 536 | esp_tls_conn_destroy(tls); | ||
| 537 | vTaskDelete(NULL); | ||
| 538 | return; | ||
| 539 | } | ||
| 178 | 540 | ||
| 179 | cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey"); | 541 | while (g_running) { |
| 180 | cJSON *content = cJSON_GetObjectItem(event, "content"); | 542 | int rlen = esp_tls_conn_read(tls, buf, CVM_WS_BUF_SIZE - 1); |
| 181 | if (pubkey && content) { | 543 | if (rlen < 0) { |
| 182 | process_dm(pubkey->valuestring, content->valuestring); | 544 | ESP_LOGW(TAG, "Read error on %s (rlen=%d)", relay_url, rlen); |
| 545 | break; | ||
| 546 | } | ||
| 547 | if (rlen == 0) { | ||
| 548 | break; | ||
| 549 | } | ||
| 550 | |||
| 551 | if ((buf[0] & 0x0F) == 0x01) { | ||
| 552 | char *text = parse_ws_text_frame(buf, rlen); | ||
| 553 | if (text) { | ||
| 554 | if (strlen(text) > 0) { | ||
| 555 | process_relay_message(relay_url, text); | ||
| 556 | } | ||
| 557 | free(text); | ||
| 558 | } | ||
| 559 | } | ||
| 183 | } | 560 | } |
| 561 | |||
| 562 | free(buf); | ||
| 563 | uint8_t close_frame[2] = {0x88, 0x00}; | ||
| 564 | esp_tls_conn_write(tls, close_frame, 2); | ||
| 565 | esp_tls_conn_destroy(tls); | ||
| 566 | ESP_LOGW(TAG, "Disconnected from %s, reconnecting", relay_url); | ||
| 567 | vTaskDelay(pdMS_TO_TICKS(CVM_RECONNECT_DELAY_MS)); | ||
| 184 | } | 568 | } |
| 185 | cJSON_Delete(arr); | 569 | |
| 570 | vTaskDelete(NULL); | ||
| 186 | } | 571 | } |
| 187 | 572 | ||
| 188 | static void cvm_task(void *arg) | 573 | static esp_err_t publish_event_via_ws(esp_tls_t *tls, int kind, |
| 574 | const char *content, const char *tags_json) | ||
| 189 | { | 575 | { |
| 190 | const tollgate_identity_t *id = identity_get(); | 576 | const tollgate_identity_t *id = identity_get(); |
| 191 | if (!id || !id->initialized) { | 577 | if (!id || !id->initialized) return ESP_FAIL; |
| 192 | ESP_LOGE(TAG, "Cannot start: identity not initialized"); | ||
| 193 | vTaskDelete(NULL); | ||
| 194 | return; | ||
| 195 | } | ||
| 196 | 578 | ||
| 197 | char *relays = fetch_relays(); | 579 | nostr_event_t event; |
| 198 | ESP_LOGI(TAG, "CVM server started, relays: %s", relays); | 580 | nostr_event_init(&event, id->npub_hex, kind, tags_json, content); |
| 581 | nostr_event_sign(&event, id->nsec); | ||
| 199 | 582 | ||
| 200 | while (g_running) { | 583 | char *event_json = malloc(4096); |
| 201 | ESP_LOGI(TAG, "Polling for DMs..."); | 584 | if (!event_json) return ESP_ERR_NO_MEM; |
| 585 | |||
| 586 | esp_err_t ret = nostr_event_to_json(&event, event_json, 4096); | ||
| 587 | if (ret != ESP_OK) { | ||
| 588 | free(event_json); | ||
| 589 | return ret; | ||
| 590 | } | ||
| 202 | 591 | ||
| 203 | cJSON *sub = build_subscription(id->npub_hex); | 592 | char *msg; |
| 204 | char *sub_json = cJSON_PrintUnformatted(sub); | 593 | size_t event_len = strlen(event_json); |
| 205 | cJSON_Delete(sub); | 594 | size_t msg_len = 8 + event_len + 1; |
| 595 | msg = malloc(msg_len); | ||
| 596 | snprintf(msg, msg_len, "[\"EVENT\",%s]", event_json); | ||
| 206 | 597 | ||
| 207 | char url[256]; | 598 | ws_send_text(tls, msg); |
| 208 | snprintf(url, sizeof(url), "%s/cvm_poll", relays); | 599 | ESP_LOGI(TAG, "Published kind %d event (%d bytes)", kind, (int)strlen(event_json)); |
| 209 | free(sub_json); | 600 | free(msg); |
| 601 | free(event_json); | ||
| 602 | return ESP_OK; | ||
| 603 | } | ||
| 210 | 604 | ||
| 211 | vTaskDelay(pdMS_TO_TICKS(30000)); | 605 | static void publish_announcements_via_ws(esp_tls_t *tls) |
| 606 | { | ||
| 607 | const tollgate_identity_t *id = identity_get(); | ||
| 608 | if (!id || !id->initialized) return; | ||
| 609 | |||
| 610 | ESP_LOGI(TAG, "Publishing CEP-6 announcements via active WS"); | ||
| 611 | |||
| 612 | cJSON *ann_content = cJSON_CreateObject(); | ||
| 613 | cJSON_AddStringToObject(ann_content, "protocolVersion", CVM_VERSION); | ||
| 614 | cJSON *capabilities = cJSON_CreateObject(); | ||
| 615 | cJSON *tools_cap = cJSON_CreateObject(); | ||
| 616 | cJSON_AddBoolToObject(tools_cap, "listChanged", true); | ||
| 617 | cJSON_AddItemToObject(capabilities, "tools", tools_cap); | ||
| 618 | cJSON_AddItemToObject(ann_content, "capabilities", capabilities); | ||
| 619 | cJSON *serverInfo = cJSON_CreateObject(); | ||
| 620 | cJSON_AddStringToObject(serverInfo, "name", CVM_SERVER_NAME); | ||
| 621 | cJSON_AddStringToObject(serverInfo, "version", CVM_SERVER_VERSION); | ||
| 622 | cJSON_AddItemToObject(ann_content, "serverInfo", serverInfo); | ||
| 623 | char *ann_str = cJSON_PrintUnformatted(ann_content); | ||
| 624 | cJSON_Delete(ann_content); | ||
| 625 | |||
| 626 | cJSON *ann_tags = cJSON_CreateArray(); | ||
| 627 | cJSON *name_tag = cJSON_CreateArray(); | ||
| 628 | cJSON_AddItemToArray(name_tag, cJSON_CreateString("name")); | ||
| 629 | cJSON_AddItemToArray(name_tag, cJSON_CreateString(CVM_SERVER_NAME)); | ||
| 630 | cJSON_AddItemToArray(ann_tags, name_tag); | ||
| 631 | cJSON *about_tag = cJSON_CreateArray(); | ||
| 632 | cJSON_AddItemToArray(about_tag, cJSON_CreateString("about")); | ||
| 633 | cJSON_AddItemToArray(about_tag, cJSON_CreateString("ESP32 TollGate WiFi hotspot with Cashu e-cash payments")); | ||
| 634 | cJSON_AddItemToArray(ann_tags, about_tag); | ||
| 635 | char *ann_tags_str = cJSON_PrintUnformatted(ann_tags); | ||
| 636 | cJSON_Delete(ann_tags); | ||
| 637 | |||
| 638 | publish_event_via_ws(tls, 11316, ann_str, ann_tags_str); | ||
| 639 | free(ann_str); | ||
| 640 | free(ann_tags_str); | ||
| 641 | |||
| 642 | cJSON *tools = build_tools_list(); | ||
| 643 | cJSON *tools_content = cJSON_CreateObject(); | ||
| 644 | cJSON_AddItemToObject(tools_content, "tools", tools); | ||
| 645 | char *tools_str = cJSON_PrintUnformatted(tools_content); | ||
| 646 | cJSON_Delete(tools_content); | ||
| 647 | |||
| 648 | publish_event_via_ws(tls, 11317, tools_str, "[]"); | ||
| 649 | free(tools_str); | ||
| 650 | |||
| 651 | cJSON *relay_tags = cJSON_CreateArray(); | ||
| 652 | const char *relays[] = {"wss://relay.primal.net", "wss://nostr-pub.wellorder.net", NULL}; | ||
| 653 | for (int i = 0; relays[i]; i++) { | ||
| 654 | cJSON *r_tag = cJSON_CreateArray(); | ||
| 655 | cJSON_AddItemToArray(r_tag, cJSON_CreateString("r")); | ||
| 656 | cJSON_AddItemToArray(r_tag, cJSON_CreateString(relays[i])); | ||
| 657 | cJSON_AddItemToArray(relay_tags, r_tag); | ||
| 212 | } | 658 | } |
| 659 | char *relay_tags_str = cJSON_PrintUnformatted(relay_tags); | ||
| 660 | cJSON_Delete(relay_tags); | ||
| 213 | 661 | ||
| 214 | ESP_LOGI(TAG, "CVM server stopped"); | 662 | publish_event_via_ws(tls, 10002, "", relay_tags_str); |
| 215 | vTaskDelete(NULL); | 663 | free(relay_tags_str); |
| 664 | |||
| 665 | ESP_LOGI(TAG, "CEP-6 announcements published (kinds 11316, 11317, 10002)"); | ||
| 666 | } | ||
| 667 | |||
| 668 | esp_err_t cvm_publish_announcements(void) | ||
| 669 | { | ||
| 670 | return ESP_OK; | ||
| 671 | } | ||
| 672 | |||
| 673 | const char *cvm_get_pubkey_hex(void) | ||
| 674 | { | ||
| 675 | const tollgate_identity_t *id = identity_get(); | ||
| 676 | if (!id || !id->initialized) return NULL; | ||
| 677 | return id->npub_hex; | ||
| 216 | } | 678 | } |
| 217 | 679 | ||
| 218 | esp_err_t cvm_server_init(void) | 680 | esp_err_t cvm_server_init(void) |
| @@ -225,7 +687,12 @@ void cvm_server_start(void) | |||
| 225 | { | 687 | { |
| 226 | if (g_running) return; | 688 | if (g_running) return; |
| 227 | g_running = true; | 689 | g_running = true; |
| 228 | xTaskCreate(cvm_task, "cvm_server", 8192, NULL, 5, &g_task); | 690 | |
| 691 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 692 | const char *relay = (cfg->cvm_relays[0]) ? cfg->cvm_relays : "wss://relay.primal.net"; | ||
| 693 | |||
| 694 | char *relay_copy = strdup(relay); | ||
| 695 | xTaskCreate(cvm_relay_task, "cvm_relay", 16384, relay_copy, 5, &g_task); | ||
| 229 | } | 696 | } |
| 230 | 697 | ||
| 231 | void cvm_server_stop(void) | 698 | void 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); | |||
| 7 | void cvm_server_start(void); | 7 | void cvm_server_start(void); |
| 8 | void cvm_server_stop(void); | 8 | void cvm_server_stop(void); |
| 9 | 9 | ||
| 10 | esp_err_t cvm_publish_announcements(void); | ||
| 11 | |||
| 12 | const 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 | ||
| 157 | mcp_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 | |||
| 193 | mcp_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 | |||
| 213 | mcp_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 | |||
| 259 | mcp_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 | |||
| 301 | mcp_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 | |||
| 336 | mcp_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 | |||
| 149 | mcp_response_t mcp_dispatch(const mcp_request_t *req) | 373 | mcp_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); | |||
| 30 | mcp_response_t mcp_handle_set_config(const char *params_json); | 36 | mcp_response_t mcp_handle_set_config(const char *params_json); |
| 31 | mcp_response_t mcp_handle_get_balance(void); | 37 | mcp_response_t mcp_handle_get_balance(void); |
| 32 | mcp_response_t mcp_handle_wallet_send(const char *params_json); | 38 | mcp_response_t mcp_handle_wallet_send(const char *params_json); |
| 39 | mcp_response_t mcp_handle_get_sessions(void); | ||
| 40 | mcp_response_t mcp_handle_get_usage(void); | ||
| 41 | mcp_response_t mcp_handle_set_payout(const char *params_json); | ||
| 42 | mcp_response_t mcp_handle_set_metric(const char *params_json); | ||
| 43 | mcp_response_t mcp_handle_set_price(const char *params_json); | ||
| 44 | mcp_response_t mcp_handle_wallet_melt(const char *params_json); | ||
| 33 | 45 | ||
| 34 | mcp_response_t mcp_dispatch(const mcp_request_t *req); | 46 | mcp_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 | |||
| 182 | session_t *cvm_get_sessions_array(void) | ||
| 183 | { | ||
| 184 | return s_sessions; | ||
| 185 | } | ||
| 186 | |||
| 187 | int 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 | ||
| 44 | void session_tick(void); | 44 | void session_tick(void); |
| 45 | 45 | ||
| 46 | session_t *cvm_get_sessions_array(void); | ||
| 47 | int cvm_get_sessions_count(void); | ||
| 48 | |||
| 46 | #endif | 49 | #endif |
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 1350d70..c0ff65f 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 |
| 27 | static const char *TAG = "tollgate_main"; | 29 | static const char *TAG = "tollgate_main"; |
| @@ -94,6 +96,13 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base, | |||
| 94 | s_retry_count = 0; | 96 | s_retry_count = 0; |
| 95 | xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); | 97 | xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); |
| 96 | 98 | ||
| 99 | esp_sntp_stop(); | ||
| 100 | esp_sntp_setoperatingmode(SNTP_OPMODE_POLL); | ||
| 101 | esp_sntp_setservername(0, "pool.ntp.org"); | ||
| 102 | esp_sntp_setservername(1, "time.google.com"); | ||
| 103 | esp_sntp_init(); | ||
| 104 | ESP_LOGI(TAG, "SNTP time sync started"); | ||
| 105 | |||
| 97 | char gw_ip_str[16]; | 106 | char gw_ip_str[16]; |
| 98 | snprintf(gw_ip_str, sizeof(gw_ip_str), IPSTR, IP2STR(&event->ip_info.gw)); | 107 | snprintf(gw_ip_str, sizeof(gw_ip_str), IPSTR, IP2STR(&event->ip_info.gw)); |
| 99 | tollgate_client_on_sta_connected(gw_ip_str); | 108 | tollgate_client_on_sta_connected(gw_ip_str); |
| @@ -160,6 +169,11 @@ static void start_services(void) | |||
| 160 | s_services_running = true; | 169 | s_services_running = true; |
| 161 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); | 170 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); |
| 162 | ESP_LOGI(TAG, "=== TollGate services started ==="); | 171 | ESP_LOGI(TAG, "=== TollGate services started ==="); |
| 172 | |||
| 173 | display_set_state(DISPLAY_READY); | ||
| 174 | char portal_url[128]; | ||
| 175 | snprintf(portal_url, sizeof(portal_url), "http://%s/", cfg->ap_ip_str); | ||
| 176 | display_update(cfg->ap_ssid, 0, 0, portal_url); | ||
| 163 | } | 177 | } |
| 164 | 178 | ||
| 165 | static void stop_services(void) | 179 | static void stop_services(void) |
| @@ -240,6 +254,9 @@ void app_main(void) | |||
| 240 | { | 254 | { |
| 241 | ESP_LOGI(TAG, "=== TollGate ESP32 Starting ==="); | 255 | ESP_LOGI(TAG, "=== TollGate ESP32 Starting ==="); |
| 242 | 256 | ||
| 257 | display_init(); | ||
| 258 | display_set_state(DISPLAY_BOOT); | ||
| 259 | |||
| 243 | esp_err_t ret = nvs_flash_init(); | 260 | esp_err_t ret = nvs_flash_init(); |
| 244 | if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { | 261 | if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { |
| 245 | ESP_ERROR_CHECK(nvs_flash_erase()); | 262 | ESP_ERROR_CHECK(nvs_flash_erase()); |