diff options
Diffstat (limited to 'main')
| -rw-r--r-- | main/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | main/config.c | 12 | ||||
| -rw-r--r-- | main/config.h | 3 | ||||
| -rw-r--r-- | main/cvm_server.c | 238 | ||||
| -rw-r--r-- | main/cvm_server.h | 10 | ||||
| -rw-r--r-- | main/mcp_handler.c | 175 | ||||
| -rw-r--r-- | main/mcp_handler.h | 36 | ||||
| -rw-r--r-- | main/nip04.c | 201 | ||||
| -rw-r--r-- | main/nip04.h | 13 | ||||
| -rw-r--r-- | main/tollgate_main.c | 8 |
10 files changed, 699 insertions, 0 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index c2aaeb2..91748f2 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt | |||
| @@ -13,6 +13,9 @@ idf_component_register(SRCS "tollgate_main.c" | |||
| 13 | "tollgate_client.c" | 13 | "tollgate_client.c" |
| 14 | "lnurl_pay.c" | 14 | "lnurl_pay.c" |
| 15 | "lightning_payout.c" | 15 | "lightning_payout.c" |
| 16 | "nip04.c" | ||
| 17 | "mcp_handler.c" | ||
| 18 | "cvm_server.c" | ||
| 16 | INCLUDE_DIRS "." | 19 | INCLUDE_DIRS "." |
| 17 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server | 20 | REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server |
| 18 | lwip json esp_http_client mbedtls esp-tls log spiffs | 21 | lwip json esp_http_client mbedtls esp-tls log spiffs |
diff --git a/main/config.c b/main/config.c index 3e01efc..2ee8637 100644 --- a/main/config.c +++ b/main/config.c | |||
| @@ -33,6 +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; | ||
| 37 | strncpy(g_config.cvm_relays, "wss://relay.damus.io", sizeof(g_config.cvm_relays) - 1); | ||
| 36 | 38 | ||
| 37 | esp_vfs_spiffs_conf_t conf = { | 39 | esp_vfs_spiffs_conf_t conf = { |
| 38 | .base_path = "/spiffs", | 40 | .base_path = "/spiffs", |
| @@ -235,6 +237,16 @@ esp_err_t tollgate_config_init(void) | |||
| 235 | } | 237 | } |
| 236 | } | 238 | } |
| 237 | 239 | ||
| 240 | cJSON *cvm = cJSON_GetObjectItem(root, "cvm"); | ||
| 241 | if (cvm && cJSON_IsObject(cvm)) { | ||
| 242 | cJSON *cvm_en = cJSON_GetObjectItem(cvm, "enabled"); | ||
| 243 | if (cvm_en && cJSON_IsBool(cvm_en)) g_config.cvm_enabled = cJSON_IsTrue(cvm_en); | ||
| 244 | cJSON *cvm_relays = cJSON_GetObjectItem(cvm, "relays"); | ||
| 245 | if (cvm_relays && cJSON_IsString(cvm_relays)) { | ||
| 246 | strncpy(g_config.cvm_relays, cvm_relays->valuestring, sizeof(g_config.cvm_relays) - 1); | ||
| 247 | } | ||
| 248 | } | ||
| 249 | |||
| 238 | if (g_config.payout.mint_count == 0 && g_config.mint_url[0] != '\0') { | 250 | if (g_config.payout.mint_count == 0 && g_config.mint_url[0] != '\0') { |
| 239 | strncpy(g_config.payout.mints[0].url, g_config.mint_url, | 251 | strncpy(g_config.payout.mints[0].url, g_config.mint_url, |
| 240 | sizeof(g_config.payout.mints[0].url) - 1); | 252 | sizeof(g_config.payout.mints[0].url) - 1); |
diff --git a/main/config.h b/main/config.h index 86b5e1a..fa4d95c 100644 --- a/main/config.h +++ b/main/config.h | |||
| @@ -60,6 +60,9 @@ typedef struct { | |||
| 60 | int client_retry_interval_ms; | 60 | int client_retry_interval_ms; |
| 61 | 61 | ||
| 62 | payout_config_t payout; | 62 | payout_config_t payout; |
| 63 | |||
| 64 | bool cvm_enabled; | ||
| 65 | char cvm_relays[256]; | ||
| 63 | } tollgate_config_t; | 66 | } tollgate_config_t; |
| 64 | 67 | ||
| 65 | void tollgate_config_derive_unique(tollgate_config_t *cfg); | 68 | void tollgate_config_derive_unique(tollgate_config_t *cfg); |
diff --git a/main/cvm_server.c b/main/cvm_server.c new file mode 100644 index 0000000..5addd88 --- /dev/null +++ b/main/cvm_server.c | |||
| @@ -0,0 +1,238 @@ | |||
| 1 | #include "cvm_server.h" | ||
| 2 | #include "mcp_handler.h" | ||
| 3 | #include "nip04.h" | ||
| 4 | #include "identity.h" | ||
| 5 | #include "config.h" | ||
| 6 | #include "nucula_wallet.h" | ||
| 7 | #include "cJSON.h" | ||
| 8 | #include "esp_log.h" | ||
| 9 | #include "esp_http_client.h" | ||
| 10 | #include "freertos/FreeRTOS.h" | ||
| 11 | #include "freertos/task.h" | ||
| 12 | #include <string.h> | ||
| 13 | #include <stdio.h> | ||
| 14 | |||
| 15 | static const char *TAG = "cvm_server"; | ||
| 16 | |||
| 17 | static bool g_running = false; | ||
| 18 | static TaskHandle_t g_task = NULL; | ||
| 19 | |||
| 20 | static const char *DEFAULT_RELAY = "wss://relay.damus.io"; | ||
| 21 | |||
| 22 | static char *fetch_relays(void) | ||
| 23 | { | ||
| 24 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 25 | if (cfg && cfg->cvm_relays[0]) { | ||
| 26 | return cfg->cvm_relays; | ||
| 27 | } | ||
| 28 | return (char *)DEFAULT_RELAY; | ||
| 29 | } | ||
| 30 | |||
| 31 | static char *http_get(const char *url, int timeout_ms) | ||
| 32 | { | ||
| 33 | char *buf = malloc(8192); | ||
| 34 | if (!buf) return NULL; | ||
| 35 | int total = 0; | ||
| 36 | |||
| 37 | esp_http_client_config_t config = { | ||
| 38 | .url = url, | ||
| 39 | .method = HTTP_METHOD_GET, | ||
| 40 | .timeout_ms = timeout_ms, | ||
| 41 | }; | ||
| 42 | esp_http_client_handle_t client = esp_http_client_init(&config); | ||
| 43 | if (!client) { free(buf); return NULL; } | ||
| 44 | |||
| 45 | esp_err_t err = esp_http_client_open(client, 0); | ||
| 46 | if (err != ESP_OK) { | ||
| 47 | esp_http_client_cleanup(client); | ||
| 48 | free(buf); | ||
| 49 | return NULL; | ||
| 50 | } | ||
| 51 | |||
| 52 | int content_length = esp_http_client_fetch_headers(client); | ||
| 53 | int max_read = content_length > 0 ? content_length : 8191; | ||
| 54 | |||
| 55 | while (total < max_read) { | ||
| 56 | int n = esp_http_client_read(client, buf + total, max_read - total); | ||
| 57 | if (n <= 0) break; | ||
| 58 | total += n; | ||
| 59 | } | ||
| 60 | buf[total] = '\0'; | ||
| 61 | esp_http_client_cleanup(client); | ||
| 62 | return buf; | ||
| 63 | } | ||
| 64 | |||
| 65 | static cJSON *build_filter(const char *npub) | ||
| 66 | { | ||
| 67 | cJSON *filter = cJSON_CreateObject(); | ||
| 68 | cJSON *kinds = cJSON_CreateArray(); | ||
| 69 | cJSON_AddItemToArray(kinds, cJSON_CreateNumber(4)); | ||
| 70 | cJSON_AddItemToObject(filter, "kinds", kinds); | ||
| 71 | cJSON_AddStringToObject(filter, "#p", npub); | ||
| 72 | cJSON_AddNumberToObject(filter, "limit", 10); | ||
| 73 | return filter; | ||
| 74 | } | ||
| 75 | |||
| 76 | static cJSON *build_subscription(const char *npub) | ||
| 77 | { | ||
| 78 | cJSON *sub = cJSON_CreateArray(); | ||
| 79 | cJSON_AddItemToArray(sub, cJSON_CreateString("REQ")); | ||
| 80 | cJSON_AddItemToArray(sub, cJSON_CreateString("cvm_sub_01")); | ||
| 81 | cJSON_AddItemToArray(sub, build_filter(npub)); | ||
| 82 | return sub; | ||
| 83 | } | ||
| 84 | |||
| 85 | static void process_dm(const char *sender_pubkey, const char *encrypted_content) | ||
| 86 | { | ||
| 87 | const tollgate_identity_t *id = identity_get(); | ||
| 88 | if (!id || !id->initialized) { | ||
| 89 | ESP_LOGE(TAG, "Identity not initialized"); | ||
| 90 | return; | ||
| 91 | } | ||
| 92 | |||
| 93 | uint8_t sender_pk[64]; | ||
| 94 | for (int i = 0; i < 64; i++) { | ||
| 95 | char hex[3] = { sender_pubkey[i*2], sender_pubkey[i*2+1], 0 }; | ||
| 96 | sender_pk[i] = (uint8_t)strtol(hex, NULL, 16); | ||
| 97 | } | ||
| 98 | |||
| 99 | char plaintext[2048]; | ||
| 100 | int pt_len = nip04_decrypt(id->nsec, sender_pk, encrypted_content, plaintext, sizeof(plaintext)); | ||
| 101 | if (pt_len < 0) { | ||
| 102 | ESP_LOGE(TAG, "Failed to decrypt DM from %.8s", sender_pubkey); | ||
| 103 | return; | ||
| 104 | } | ||
| 105 | |||
| 106 | ESP_LOGI(TAG, "Decrypted DM from %.8s: %s", sender_pubkey, plaintext); | ||
| 107 | |||
| 108 | cJSON *msg = cJSON_Parse(plaintext); | ||
| 109 | if (!msg) { | ||
| 110 | ESP_LOGE(TAG, "Invalid JSON in DM"); | ||
| 111 | return; | ||
| 112 | } | ||
| 113 | |||
| 114 | cJSON *method = cJSON_GetObjectItem(msg, "method"); | ||
| 115 | cJSON *params = cJSON_GetObjectItem(msg, "params"); | ||
| 116 | if (!method || !cJSON_IsString(method)) { | ||
| 117 | cJSON_Delete(msg); | ||
| 118 | ESP_LOGE(TAG, "Missing 'method' in CVM request"); | ||
| 119 | return; | ||
| 120 | } | ||
| 121 | |||
| 122 | mcp_request_t req = {0}; | ||
| 123 | req.tool = mcp_parse_tool(method->valuestring); | ||
| 124 | strncpy(req.method, method->valuestring, sizeof(req.method) - 1); | ||
| 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 | } | ||
| 132 | |||
| 133 | mcp_response_t resp = mcp_dispatch(&req); | ||
| 134 | 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 | } | ||
| 155 | |||
| 156 | static void parse_nostr_events(const char *data) | ||
| 157 | { | ||
| 158 | cJSON *arr = cJSON_Parse(data); | ||
| 159 | if (!arr || !cJSON_IsArray(arr)) { | ||
| 160 | if (arr) cJSON_Delete(arr); | ||
| 161 | return; | ||
| 162 | } | ||
| 163 | |||
| 164 | cJSON *item = NULL; | ||
| 165 | cJSON_ArrayForEach(item, arr) { | ||
| 166 | if (!cJSON_IsArray(item)) continue; | ||
| 167 | int arr_size = cJSON_GetArraySize(item); | ||
| 168 | if (arr_size < 3) continue; | ||
| 169 | |||
| 170 | cJSON *cmd = cJSON_GetArrayItem(item, 0); | ||
| 171 | if (!cmd || !cJSON_IsString(cmd) || strcmp(cmd->valuestring, "EVENT") != 0) continue; | ||
| 172 | |||
| 173 | cJSON *event = cJSON_GetArrayItem(item, 2); | ||
| 174 | if (!event) continue; | ||
| 175 | |||
| 176 | cJSON *kind = cJSON_GetObjectItem(event, "kind"); | ||
| 177 | if (!kind || kind->valueint != 4) continue; | ||
| 178 | |||
| 179 | cJSON *pubkey = cJSON_GetObjectItem(event, "pubkey"); | ||
| 180 | cJSON *content = cJSON_GetObjectItem(event, "content"); | ||
| 181 | if (pubkey && content) { | ||
| 182 | process_dm(pubkey->valuestring, content->valuestring); | ||
| 183 | } | ||
| 184 | } | ||
| 185 | cJSON_Delete(arr); | ||
| 186 | } | ||
| 187 | |||
| 188 | static void cvm_task(void *arg) | ||
| 189 | { | ||
| 190 | const tollgate_identity_t *id = identity_get(); | ||
| 191 | if (!id || !id->initialized) { | ||
| 192 | ESP_LOGE(TAG, "Cannot start: identity not initialized"); | ||
| 193 | vTaskDelete(NULL); | ||
| 194 | return; | ||
| 195 | } | ||
| 196 | |||
| 197 | char *relays = fetch_relays(); | ||
| 198 | ESP_LOGI(TAG, "CVM server started, relays: %s", relays); | ||
| 199 | |||
| 200 | while (g_running) { | ||
| 201 | ESP_LOGI(TAG, "Polling for DMs..."); | ||
| 202 | |||
| 203 | cJSON *sub = build_subscription(id->npub_hex); | ||
| 204 | char *sub_json = cJSON_PrintUnformatted(sub); | ||
| 205 | cJSON_Delete(sub); | ||
| 206 | |||
| 207 | char url[256]; | ||
| 208 | snprintf(url, sizeof(url), "%s/cvm_poll", relays); | ||
| 209 | free(sub_json); | ||
| 210 | |||
| 211 | vTaskDelay(pdMS_TO_TICKS(30000)); | ||
| 212 | } | ||
| 213 | |||
| 214 | ESP_LOGI(TAG, "CVM server stopped"); | ||
| 215 | vTaskDelete(NULL); | ||
| 216 | } | ||
| 217 | |||
| 218 | esp_err_t cvm_server_init(void) | ||
| 219 | { | ||
| 220 | ESP_LOGI(TAG, "CVM server initialized"); | ||
| 221 | return ESP_OK; | ||
| 222 | } | ||
| 223 | |||
| 224 | void cvm_server_start(void) | ||
| 225 | { | ||
| 226 | if (g_running) return; | ||
| 227 | g_running = true; | ||
| 228 | xTaskCreate(cvm_task, "cvm_server", 8192, NULL, 5, &g_task); | ||
| 229 | } | ||
| 230 | |||
| 231 | void cvm_server_stop(void) | ||
| 232 | { | ||
| 233 | g_running = false; | ||
| 234 | if (g_task) { | ||
| 235 | vTaskDelay(pdMS_TO_TICKS(500)); | ||
| 236 | g_task = NULL; | ||
| 237 | } | ||
| 238 | } | ||
diff --git a/main/cvm_server.h b/main/cvm_server.h new file mode 100644 index 0000000..d336514 --- /dev/null +++ b/main/cvm_server.h | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | #ifndef CVM_SERVER_H | ||
| 2 | #define CVM_SERVER_H | ||
| 3 | |||
| 4 | #include "esp_err.h" | ||
| 5 | |||
| 6 | esp_err_t cvm_server_init(void); | ||
| 7 | void cvm_server_start(void); | ||
| 8 | void cvm_server_stop(void); | ||
| 9 | |||
| 10 | #endif | ||
diff --git a/main/mcp_handler.c b/main/mcp_handler.c new file mode 100644 index 0000000..f40c1bd --- /dev/null +++ b/main/mcp_handler.c | |||
| @@ -0,0 +1,175 @@ | |||
| 1 | #include "mcp_handler.h" | ||
| 2 | #include "config.h" | ||
| 3 | #include "nucula_wallet.h" | ||
| 4 | #include "cJSON.h" | ||
| 5 | #include <string.h> | ||
| 6 | #include <stdio.h> | ||
| 7 | |||
| 8 | static const char *TAG = "mcp_handler"; | ||
| 9 | |||
| 10 | mcp_tool_t mcp_parse_tool(const char *method) | ||
| 11 | { | ||
| 12 | if (!method) return MCP_TOOL_UNKNOWN; | ||
| 13 | if (strcmp(method, "get_config") == 0) return MCP_TOOL_GET_CONFIG; | ||
| 14 | if (strcmp(method, "set_config") == 0) return MCP_TOOL_SET_CONFIG; | ||
| 15 | if (strcmp(method, "get_balance") == 0) return MCP_TOOL_GET_BALANCE; | ||
| 16 | if (strcmp(method, "wallet_send") == 0) return MCP_TOOL_WALLET_SEND; | ||
| 17 | return MCP_TOOL_UNKNOWN; | ||
| 18 | } | ||
| 19 | |||
| 20 | mcp_response_t mcp_handle_get_config(void) | ||
| 21 | { | ||
| 22 | mcp_response_t resp = {0}; | ||
| 23 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 24 | if (!cfg) { | ||
| 25 | resp.success = false; | ||
| 26 | snprintf(resp.error, sizeof(resp.error), "Config not loaded"); | ||
| 27 | return resp; | ||
| 28 | } | ||
| 29 | |||
| 30 | cJSON *root = cJSON_CreateObject(); | ||
| 31 | cJSON_AddStringToObject(root, "ssid", cfg->ap_ssid); | ||
| 32 | cJSON_AddStringToObject(root, "metric", cfg->metric); | ||
| 33 | cJSON_AddNumberToObject(root, "price_per_step", cfg->price_per_step); | ||
| 34 | cJSON_AddNumberToObject(root, "step_size_ms", cfg->step_size_ms); | ||
| 35 | cJSON_AddNumberToObject(root, "step_size_bytes", cfg->step_size_bytes); | ||
| 36 | cJSON_AddStringToObject(root, "mint_url", cfg->mint_url); | ||
| 37 | cJSON_AddBoolToObject(root, "client_enabled", cfg->client_enabled); | ||
| 38 | cJSON_AddBoolToObject(root, "payout_enabled", cfg->payout.enabled); | ||
| 39 | cJSON_AddStringToObject(root, "wifi_ssid", | ||
| 40 | cfg->network_count > 0 ? cfg->networks[0].ssid : ""); | ||
| 41 | |||
| 42 | char *json = cJSON_PrintUnformatted(root); | ||
| 43 | snprintf(resp.result_json, sizeof(resp.result_json), "%s", json); | ||
| 44 | cJSON_free(json); | ||
| 45 | cJSON_Delete(root); | ||
| 46 | resp.success = true; | ||
| 47 | return resp; | ||
| 48 | } | ||
| 49 | |||
| 50 | mcp_response_t mcp_handle_set_config(const char *params_json) | ||
| 51 | { | ||
| 52 | mcp_response_t resp = {0}; | ||
| 53 | cJSON *root = cJSON_Parse(params_json); | ||
| 54 | if (!root) { | ||
| 55 | resp.success = false; | ||
| 56 | snprintf(resp.error, sizeof(resp.error), "Invalid JSON params"); | ||
| 57 | return resp; | ||
| 58 | } | ||
| 59 | |||
| 60 | tollgate_config_t *cfg = (tollgate_config_t *)tollgate_config_get(); | ||
| 61 | if (!cfg) { | ||
| 62 | cJSON_Delete(root); | ||
| 63 | resp.success = false; | ||
| 64 | snprintf(resp.error, sizeof(resp.error), "Config not loaded"); | ||
| 65 | return resp; | ||
| 66 | } | ||
| 67 | |||
| 68 | cJSON *item; | ||
| 69 | item = cJSON_GetObjectItem(root, "price_per_step"); | ||
| 70 | if (item && cJSON_IsNumber(item)) cfg->price_per_step = item->valueint; | ||
| 71 | item = cJSON_GetObjectItem(root, "step_size_ms"); | ||
| 72 | if (item && cJSON_IsNumber(item)) cfg->step_size_ms = item->valueint; | ||
| 73 | item = cJSON_GetObjectItem(root, "step_size_bytes"); | ||
| 74 | if (item && cJSON_IsNumber(item)) cfg->step_size_bytes = item->valueint; | ||
| 75 | item = cJSON_GetObjectItem(root, "client_enabled"); | ||
| 76 | if (item && cJSON_IsBool(item)) cfg->client_enabled = cJSON_IsTrue(item); | ||
| 77 | item = cJSON_GetObjectItem(root, "payout_enabled"); | ||
| 78 | if (item && cJSON_IsBool(item)) cfg->payout.enabled = cJSON_IsTrue(item); | ||
| 79 | item = cJSON_GetObjectItem(root, "metric"); | ||
| 80 | if (item && cJSON_IsString(item)) { | ||
| 81 | strncpy(cfg->metric, item->valuestring, sizeof(cfg->metric) - 1); | ||
| 82 | } | ||
| 83 | |||
| 84 | cJSON_Delete(root); | ||
| 85 | resp.success = true; | ||
| 86 | snprintf(resp.result_json, sizeof(resp.result_json), "{\"status\":\"ok\"}"); | ||
| 87 | return resp; | ||
| 88 | } | ||
| 89 | |||
| 90 | mcp_response_t mcp_handle_get_balance(void) | ||
| 91 | { | ||
| 92 | mcp_response_t resp = {0}; | ||
| 93 | uint64_t balance = nucula_wallet_balance(); | ||
| 94 | int proof_count = nucula_wallet_proof_count(); | ||
| 95 | |||
| 96 | cJSON *root = cJSON_CreateObject(); | ||
| 97 | cJSON_AddNumberToObject(root, "balance_sats", (double)balance); | ||
| 98 | cJSON_AddNumberToObject(root, "proof_count", proof_count); | ||
| 99 | |||
| 100 | char *json = cJSON_PrintUnformatted(root); | ||
| 101 | snprintf(resp.result_json, sizeof(resp.result_json), "%s", json); | ||
| 102 | cJSON_free(json); | ||
| 103 | cJSON_Delete(root); | ||
| 104 | resp.success = true; | ||
| 105 | return resp; | ||
| 106 | } | ||
| 107 | |||
| 108 | mcp_response_t mcp_handle_wallet_send(const char *params_json) | ||
| 109 | { | ||
| 110 | mcp_response_t resp = {0}; | ||
| 111 | cJSON *root = cJSON_Parse(params_json); | ||
| 112 | if (!root) { | ||
| 113 | resp.success = false; | ||
| 114 | snprintf(resp.error, sizeof(resp.error), "Invalid JSON params"); | ||
| 115 | return resp; | ||
| 116 | } | ||
| 117 | |||
| 118 | cJSON *amount_item = cJSON_GetObjectItem(root, "amount"); | ||
| 119 | if (!amount_item || !cJSON_IsNumber(amount_item)) { | ||
| 120 | cJSON_Delete(root); | ||
| 121 | resp.success = false; | ||
| 122 | snprintf(resp.error, sizeof(resp.error), "Missing 'amount' field"); | ||
| 123 | return resp; | ||
| 124 | } | ||
| 125 | |||
| 126 | uint64_t amount = (uint64_t)amount_item->valuedouble; | ||
| 127 | char token_out[2048] = {0}; | ||
| 128 | int rc = nucula_wallet_send(amount, token_out, sizeof(token_out)); | ||
| 129 | |||
| 130 | if (rc != 0) { | ||
| 131 | cJSON_Delete(root); | ||
| 132 | resp.success = false; | ||
| 133 | snprintf(resp.error, sizeof(resp.error), "Send failed: %d", rc); | ||
| 134 | return resp; | ||
| 135 | } | ||
| 136 | |||
| 137 | cJSON *result = cJSON_CreateObject(); | ||
| 138 | cJSON_AddStringToObject(result, "token", token_out); | ||
| 139 | cJSON_AddNumberToObject(result, "amount", (double)amount); | ||
| 140 | char *json = cJSON_PrintUnformatted(result); | ||
| 141 | snprintf(resp.result_json, sizeof(resp.result_json), "%s", json); | ||
| 142 | cJSON_free(json); | ||
| 143 | cJSON_Delete(result); | ||
| 144 | cJSON_Delete(root); | ||
| 145 | resp.success = true; | ||
| 146 | return resp; | ||
| 147 | } | ||
| 148 | |||
| 149 | mcp_response_t mcp_dispatch(const mcp_request_t *req) | ||
| 150 | { | ||
| 151 | if (!req) { | ||
| 152 | mcp_response_t resp = {0}; | ||
| 153 | resp.success = false; | ||
| 154 | snprintf(resp.error, sizeof(resp.error), "NULL request"); | ||
| 155 | return resp; | ||
| 156 | } | ||
| 157 | |||
| 158 | switch (req->tool) { | ||
| 159 | case MCP_TOOL_GET_CONFIG: | ||
| 160 | return mcp_handle_get_config(); | ||
| 161 | case MCP_TOOL_SET_CONFIG: | ||
| 162 | return mcp_handle_set_config(req->params_json); | ||
| 163 | case MCP_TOOL_GET_BALANCE: | ||
| 164 | return mcp_handle_get_balance(); | ||
| 165 | case MCP_TOOL_WALLET_SEND: | ||
| 166 | return mcp_handle_wallet_send(req->params_json); | ||
| 167 | default: | ||
| 168 | break; | ||
| 169 | } | ||
| 170 | |||
| 171 | mcp_response_t resp = {0}; | ||
| 172 | resp.success = false; | ||
| 173 | snprintf(resp.error, sizeof(resp.error), "Unknown tool: %s", req->method); | ||
| 174 | return resp; | ||
| 175 | } | ||
diff --git a/main/mcp_handler.h b/main/mcp_handler.h new file mode 100644 index 0000000..e42b5ee --- /dev/null +++ b/main/mcp_handler.h | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | #ifndef MCP_HANDLER_H | ||
| 2 | #define MCP_HANDLER_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | |||
| 7 | typedef enum { | ||
| 8 | MCP_TOOL_GET_CONFIG = 0, | ||
| 9 | MCP_TOOL_SET_CONFIG = 1, | ||
| 10 | MCP_TOOL_GET_BALANCE = 2, | ||
| 11 | MCP_TOOL_WALLET_SEND = 3, | ||
| 12 | MCP_TOOL_UNKNOWN = 99 | ||
| 13 | } mcp_tool_t; | ||
| 14 | |||
| 15 | typedef struct { | ||
| 16 | mcp_tool_t tool; | ||
| 17 | char method[64]; | ||
| 18 | char params_json[1024]; | ||
| 19 | } mcp_request_t; | ||
| 20 | |||
| 21 | typedef struct { | ||
| 22 | bool success; | ||
| 23 | char result_json[2048]; | ||
| 24 | char error[256]; | ||
| 25 | } mcp_response_t; | ||
| 26 | |||
| 27 | mcp_tool_t mcp_parse_tool(const char *method); | ||
| 28 | |||
| 29 | mcp_response_t mcp_handle_get_config(void); | ||
| 30 | mcp_response_t mcp_handle_set_config(const char *params_json); | ||
| 31 | mcp_response_t mcp_handle_get_balance(void); | ||
| 32 | mcp_response_t mcp_handle_wallet_send(const char *params_json); | ||
| 33 | |||
| 34 | mcp_response_t mcp_dispatch(const mcp_request_t *req); | ||
| 35 | |||
| 36 | #endif | ||
diff --git a/main/nip04.c b/main/nip04.c new file mode 100644 index 0000000..5526d4f --- /dev/null +++ b/main/nip04.c | |||
| @@ -0,0 +1,201 @@ | |||
| 1 | #include "nip04.h" | ||
| 2 | #include "esp_log.h" | ||
| 3 | #include "esp_system.h" | ||
| 4 | #include "mbedtls/aes.h" | ||
| 5 | #include <string.h> | ||
| 6 | #include <stdlib.h> | ||
| 7 | |||
| 8 | #include <secp256k1.h> | ||
| 9 | #include <secp256k1_ecdh.h> | ||
| 10 | |||
| 11 | static const char *TAG = "nip04"; | ||
| 12 | |||
| 13 | static const unsigned char base64_table[] = | ||
| 14 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | ||
| 15 | |||
| 16 | static size_t base64_encode(const uint8_t *src, size_t len, char *dst) | ||
| 17 | { | ||
| 18 | size_t j = 0; | ||
| 19 | for (size_t i = 0; i < len; i += 3) { | ||
| 20 | uint32_t a = src[i]; | ||
| 21 | uint32_t b = (i + 1 < len) ? src[i + 1] : 0; | ||
| 22 | uint32_t c = (i + 2 < len) ? src[i + 2] : 0; | ||
| 23 | uint32_t triple = (a << 16) | (b << 8) | c; | ||
| 24 | dst[j++] = base64_table[(triple >> 18) & 0x3F]; | ||
| 25 | dst[j++] = base64_table[(triple >> 12) & 0x3F]; | ||
| 26 | dst[j++] = (i + 1 < len) ? base64_table[(triple >> 6) & 0x3F] : '='; | ||
| 27 | dst[j++] = (i + 2 < len) ? base64_table[triple & 0x3F] : '='; | ||
| 28 | } | ||
| 29 | dst[j] = '\0'; | ||
| 30 | return j; | ||
| 31 | } | ||
| 32 | |||
| 33 | static int base64_val(char c) | ||
| 34 | { | ||
| 35 | if (c >= 'A' && c <= 'Z') return c - 'A'; | ||
| 36 | if (c >= 'a' && c <= 'z') return c - 'a' + 26; | ||
| 37 | if (c >= '0' && c <= '9') return c - '0' + 52; | ||
| 38 | if (c == '+') return 62; | ||
| 39 | if (c == '/') return 63; | ||
| 40 | return -1; | ||
| 41 | } | ||
| 42 | |||
| 43 | static size_t base64_decode(const char *src, size_t len, uint8_t *dst) | ||
| 44 | { | ||
| 45 | size_t padding = 0; | ||
| 46 | if (len >= 1 && src[len - 1] == '=') padding++; | ||
| 47 | if (len >= 2 && src[len - 2] == '=') padding++; | ||
| 48 | size_t expected = (len / 4) * 3 - padding; | ||
| 49 | |||
| 50 | size_t j = 0; | ||
| 51 | for (size_t i = 0; i + 3 < len; i += 4) { | ||
| 52 | uint32_t a = (uint32_t)base64_val(src[i]); | ||
| 53 | uint32_t b = (uint32_t)base64_val(src[i + 1]); | ||
| 54 | uint32_t c = (src[i + 2] != '=') ? (uint32_t)base64_val(src[i + 2]) : 0; | ||
| 55 | uint32_t d = (src[i + 3] != '=') ? (uint32_t)base64_val(src[i + 3]) : 0; | ||
| 56 | uint32_t triple = (a << 18) | (b << 12) | (c << 6) | d; | ||
| 57 | if (j < expected) dst[j++] = (triple >> 16) & 0xFF; | ||
| 58 | if (j < expected) dst[j++] = (triple >> 8) & 0xFF; | ||
| 59 | if (j < expected) dst[j++] = triple & 0xFF; | ||
| 60 | } | ||
| 61 | return j; | ||
| 62 | } | ||
| 63 | |||
| 64 | static int ecdh_xonly_hash(unsigned char *output, const uint8_t *x32, const uint8_t *y32, void *data) | ||
| 65 | { | ||
| 66 | (void)y32; | ||
| 67 | (void)data; | ||
| 68 | memcpy(output, x32, 32); | ||
| 69 | return 1; | ||
| 70 | } | ||
| 71 | |||
| 72 | static void compute_shared_secret(const uint8_t *privkey, const uint8_t *pubkey32, | ||
| 73 | uint8_t shared_secret[32]) | ||
| 74 | { | ||
| 75 | if (!privkey || !pubkey32) { | ||
| 76 | memset(shared_secret, 0, 32); | ||
| 77 | return; | ||
| 78 | } | ||
| 79 | |||
| 80 | uint8_t compressed[33]; | ||
| 81 | compressed[0] = 0x02; | ||
| 82 | memcpy(compressed + 1, pubkey32, 32); | ||
| 83 | |||
| 84 | secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); | ||
| 85 | secp256k1_pubkey pk; | ||
| 86 | if (!secp256k1_ec_pubkey_parse(ctx, &pk, compressed, 33)) { | ||
| 87 | ESP_LOGE(TAG, "Failed to parse compressed pubkey"); | ||
| 88 | memset(shared_secret, 0, 32); | ||
| 89 | secp256k1_context_destroy(ctx); | ||
| 90 | return; | ||
| 91 | } | ||
| 92 | |||
| 93 | uint8_t shared[32]; | ||
| 94 | secp256k1_ecdh(ctx, shared, &pk, privkey, ecdh_xonly_hash, NULL); | ||
| 95 | memcpy(shared_secret, shared, 32); | ||
| 96 | |||
| 97 | ESP_LOGI(TAG, "Shared secret: %02x%02x%02x%02x... (pubkey32=%02x%02x%02x%02x...)", | ||
| 98 | shared[0], shared[1], shared[2], shared[3], | ||
| 99 | pubkey32[0], pubkey32[1], pubkey32[2], pubkey32[3]); | ||
| 100 | |||
| 101 | secp256k1_context_destroy(ctx); | ||
| 102 | } | ||
| 103 | |||
| 104 | static void pkcs7_pad(uint8_t *buf, size_t data_len, size_t block_size, size_t *padded_len) | ||
| 105 | { | ||
| 106 | size_t pad = block_size - (data_len % block_size); | ||
| 107 | for (size_t i = 0; i < pad; i++) { | ||
| 108 | buf[data_len + i] = (uint8_t)pad; | ||
| 109 | } | ||
| 110 | *padded_len = data_len + pad; | ||
| 111 | } | ||
| 112 | |||
| 113 | static size_t pkcs7_unpad(const uint8_t *buf, size_t len) | ||
| 114 | { | ||
| 115 | if (len == 0) return 0; | ||
| 116 | uint8_t pad = buf[len - 1]; | ||
| 117 | if (pad == 0 || pad > 16) return 0; | ||
| 118 | for (size_t i = len - pad; i < len; i++) { | ||
| 119 | if (buf[i] != pad) return 0; | ||
| 120 | } | ||
| 121 | return len - pad; | ||
| 122 | } | ||
| 123 | |||
| 124 | void nip04_encrypt(const uint8_t *sender_privkey, const uint8_t *recipient_pubkey, | ||
| 125 | const char *plaintext, uint8_t *ciphertext_base64, size_t *out_len) | ||
| 126 | { | ||
| 127 | uint8_t shared_secret[32]; | ||
| 128 | compute_shared_secret(sender_privkey, recipient_pubkey, shared_secret); | ||
| 129 | |||
| 130 | size_t pt_len = strlen(plaintext); | ||
| 131 | uint8_t iv[16]; | ||
| 132 | esp_fill_random(iv, 16); | ||
| 133 | uint8_t iv_copy[16]; | ||
| 134 | memcpy(iv_copy, iv, 16); | ||
| 135 | |||
| 136 | uint8_t padded[4096]; | ||
| 137 | memcpy(padded, plaintext, pt_len); | ||
| 138 | size_t padded_len; | ||
| 139 | pkcs7_pad(padded, pt_len, 16, &padded_len); | ||
| 140 | |||
| 141 | mbedtls_aes_context aes; | ||
| 142 | mbedtls_aes_init(&aes); | ||
| 143 | mbedtls_aes_setkey_enc(&aes, shared_secret, 256); | ||
| 144 | mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, padded_len, iv, padded, padded); | ||
| 145 | mbedtls_aes_free(&aes); | ||
| 146 | |||
| 147 | size_t total = 16 + padded_len; | ||
| 148 | uint8_t combined[4112]; | ||
| 149 | memcpy(combined, iv_copy, 16); | ||
| 150 | memcpy(combined + 16, padded, padded_len); | ||
| 151 | |||
| 152 | *out_len = base64_encode(combined, total, (char *)ciphertext_base64); | ||
| 153 | ((char *)ciphertext_base64)[*out_len] = '\0'; | ||
| 154 | |||
| 155 | ESP_LOGD(TAG, "Encrypted %zu bytes -> %zu base64", pt_len, *out_len); | ||
| 156 | } | ||
| 157 | |||
| 158 | int nip04_decrypt(const uint8_t *recipient_privkey, const uint8_t *sender_pubkey, | ||
| 159 | const char *ciphertext_base64, char *plaintext, size_t plaintext_max) | ||
| 160 | { | ||
| 161 | uint8_t shared_secret[32]; | ||
| 162 | compute_shared_secret(recipient_privkey, sender_pubkey, shared_secret); | ||
| 163 | |||
| 164 | size_t b64_len = strlen(ciphertext_base64); | ||
| 165 | uint8_t combined[4112]; | ||
| 166 | size_t combined_len = base64_decode(ciphertext_base64, b64_len, combined); | ||
| 167 | |||
| 168 | if (combined_len < 32) { | ||
| 169 | ESP_LOGE(TAG, "Ciphertext too short: %zu", combined_len); | ||
| 170 | return -1; | ||
| 171 | } | ||
| 172 | |||
| 173 | uint8_t iv[16]; | ||
| 174 | memcpy(iv, combined, 16); | ||
| 175 | |||
| 176 | size_t ct_len = combined_len - 16; | ||
| 177 | uint8_t ct[4096]; | ||
| 178 | memcpy(ct, combined + 16, ct_len); | ||
| 179 | |||
| 180 | mbedtls_aes_context aes; | ||
| 181 | mbedtls_aes_init(&aes); | ||
| 182 | mbedtls_aes_setkey_dec(&aes, shared_secret, 256); | ||
| 183 | mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, ct_len, iv, ct, ct); | ||
| 184 | mbedtls_aes_free(&aes); | ||
| 185 | |||
| 186 | size_t pt_len = pkcs7_unpad(ct, ct_len); | ||
| 187 | if (pt_len == 0 || pt_len >= plaintext_max) { | ||
| 188 | ESP_LOGE(TAG, "Invalid padding: pt_len=%zu ct_len=%zu last_byte=%d padded_bytes:", | ||
| 189 | pt_len, ct_len, ct_len > 0 ? ct[ct_len-1] : -1); | ||
| 190 | for (size_t i = ct_len > 4 ? ct_len - 4 : 0; i < ct_len; i++) { | ||
| 191 | ESP_LOGE(TAG, " ct[%zu]=%02x", i, ct[i]); | ||
| 192 | } | ||
| 193 | return -1; | ||
| 194 | } | ||
| 195 | |||
| 196 | memcpy(plaintext, ct, pt_len); | ||
| 197 | plaintext[pt_len] = '\0'; | ||
| 198 | |||
| 199 | ESP_LOGD(TAG, "Decrypted %zu base64 -> %zu bytes", b64_len, pt_len); | ||
| 200 | return (int)pt_len; | ||
| 201 | } | ||
diff --git a/main/nip04.h b/main/nip04.h new file mode 100644 index 0000000..f612a8e --- /dev/null +++ b/main/nip04.h | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | #ifndef NIP04_H | ||
| 2 | #define NIP04_H | ||
| 3 | |||
| 4 | #include <stdint.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | |||
| 7 | void nip04_encrypt(const uint8_t *sender_privkey, const uint8_t *recipient_pubkey, | ||
| 8 | const char *plaintext, uint8_t *ciphertext_base64, size_t *out_len); | ||
| 9 | |||
| 10 | int nip04_decrypt(const uint8_t *recipient_privkey, const uint8_t *sender_pubkey, | ||
| 11 | const char *ciphertext_base64, char *plaintext, size_t plaintext_max); | ||
| 12 | |||
| 13 | #endif | ||
diff --git a/main/tollgate_main.c b/main/tollgate_main.c index 3f83923..6adb0ec 100644 --- a/main/tollgate_main.c +++ b/main/tollgate_main.c | |||
| @@ -21,6 +21,7 @@ | |||
| 21 | #include "wifistr.h" | 21 | #include "wifistr.h" |
| 22 | #include "tollgate_client.h" | 22 | #include "tollgate_client.h" |
| 23 | #include "lightning_payout.h" | 23 | #include "lightning_payout.h" |
| 24 | #include "cvm_server.h" | ||
| 24 | 25 | ||
| 25 | #define MAX_STA_RETRY 5 | 26 | #define MAX_STA_RETRY 5 |
| 26 | static const char *TAG = "tollgate_main"; | 27 | static const char *TAG = "tollgate_main"; |
| @@ -146,6 +147,12 @@ static void start_services(void) | |||
| 146 | 147 | ||
| 147 | xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); | 148 | xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL); |
| 148 | 149 | ||
| 150 | const tollgate_config_t *cfg2 = tollgate_config_get(); | ||
| 151 | if (cfg2->cvm_enabled) { | ||
| 152 | cvm_server_init(); | ||
| 153 | cvm_server_start(); | ||
| 154 | } | ||
| 155 | |||
| 149 | s_services_running = true; | 156 | s_services_running = true; |
| 150 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); | 157 | if (s_services_mutex) xSemaphoreGive(s_services_mutex); |
| 151 | ESP_LOGI(TAG, "=== TollGate services started ==="); | 158 | ESP_LOGI(TAG, "=== TollGate services started ==="); |
| @@ -162,6 +169,7 @@ static void stop_services(void) | |||
| 162 | captive_portal_stop(); | 169 | captive_portal_stop(); |
| 163 | tollgate_api_stop(); | 170 | tollgate_api_stop(); |
| 164 | dns_server_stop(); | 171 | dns_server_stop(); |
| 172 | cvm_server_stop(); | ||
| 165 | firewall_disable_nat(); | 173 | firewall_disable_nat(); |
| 166 | firewall_revoke_all(); | 174 | firewall_revoke_all(); |
| 167 | s_services_running = false; | 175 | s_services_running = false; |