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/captive_portal.c108
-rw-r--r--main/mint_health.c94
-rw-r--r--main/stratum_client.c95
-rw-r--r--main/tollgate_client.c171
-rw-r--r--main/tollgate_client.h27
5 files changed, 111 insertions, 384 deletions
diff --git a/main/captive_portal.c b/main/captive_portal.c
index f95a791..8113856 100644
--- a/main/captive_portal.c
+++ b/main/captive_portal.c
@@ -2,6 +2,7 @@
2#include "tollgate_core.h" 2#include "tollgate_core.h"
3#include "tollgate_core_firewall.h" 3#include "tollgate_core_firewall.h"
4#include "tollgate_core_session.h" 4#include "tollgate_core_session.h"
5#include "tollgate_core_portal.h"
5#include "config.h" 6#include "config.h"
6#include "stratum_proxy.h" 7#include "stratum_proxy.h"
7#include "esp_log.h" 8#include "esp_log.h"
@@ -176,10 +177,12 @@ static esp_err_t portal_handler(httpd_req_t *req)
176 char price_str[16]; 177 char price_str[16];
177 snprintf(price_str, sizeof(price_str), "%d", cfg->price_per_step); 178 snprintf(price_str, sizeof(price_str), "%d", cfg->price_per_step);
178 179
179 const char *tpl = PORTAL_HTML_TEMPLATE; 180 char mining_port_buf[8] = "3333";
180 size_t tpl_len = strlen(tpl); 181 if (cfg->mining_enabled) {
182 snprintf(mining_port_buf, sizeof(mining_port_buf), "%d", cfg->mining_port);
183 }
181 184
182 struct { const char *key; const char *val; } subs[] = { 185 tollgate_portal_sub_t subs[] = {
183 { "__AP_IP__", s_ap_ip_str }, 186 { "__AP_IP__", s_ap_ip_str },
184 { "__PRICE__", price_str }, 187 { "__PRICE__", price_str },
185 { "__MINT_URL__", cfg->mint_url }, 188 { "__MINT_URL__", cfg->mint_url },
@@ -188,63 +191,19 @@ static esp_err_t portal_handler(httpd_req_t *req)
188 "<button class='tab active' onclick=\"switchTab('cashu')\">Cashu</button>" 191 "<button class='tab active' onclick=\"switchTab('cashu')\">Cashu</button>"
189 "<button class='tab' onclick=\"switchTab('mining')\">Mine</button>" 192 "<button class='tab' onclick=\"switchTab('mining')\">Mine</button>"
190 "</div>" : "" }, 193 "</div>" : "" },
191 { "__MINING_PORT__", cfg->mining_enabled ? 194 { "__MINING_PORT__", mining_port_buf },
192 (char[]){ [0 ... 7] = 0 } : "3333" },
193 { "__CASHU_ACTIVE__", "active" }, 195 { "__CASHU_ACTIVE__", "active" },
194 { "__MINING_ACTIVE__", "" }, 196 { "__MINING_ACTIVE__", "" },
195 }; 197 };
196 char mining_port_buf[8] = "3333";
197 if (cfg->mining_enabled) {
198 snprintf(mining_port_buf, sizeof(mining_port_buf), "%d", cfg->mining_port);
199 subs[4].val = mining_port_buf;
200 }
201 int nsubs = sizeof(subs) / sizeof(subs[0]); 198 int nsubs = sizeof(subs) / sizeof(subs[0]);
202 199
203 size_t extra = 0; 200 char *html = tollgate_core_portal_render(PORTAL_HTML_TEMPLATE, subs, nsubs);
204 for (int i = 0; i < nsubs; i++) {
205 const char *p = tpl;
206 size_t klen = strlen(subs[i].key);
207 while ((p = strstr(p, subs[i].key)) != NULL) {
208 extra += strlen(subs[i].val) - klen;
209 p += klen;
210 }
211 }
212
213 size_t out_size = tpl_len + extra + 1;
214 char *html = malloc(out_size);
215 if (!html) { 201 if (!html) {
216 httpd_resp_send_500(req); 202 httpd_resp_send_500(req);
217 return ESP_OK; 203 return ESP_OK;
218 } 204 }
219 205
220 char *out = html; 206 httpd_resp_send(req, html, strlen(html));
221 const char *src = tpl;
222 while (*src) {
223 const char *earliest = NULL;
224 int ei = -1;
225 for (int i = 0; i < nsubs; i++) {
226 const char *found = strstr(src, subs[i].key);
227 if (found && (earliest == NULL || found < earliest)) {
228 earliest = found;
229 ei = i;
230 }
231 }
232 if (earliest) {
233 size_t vlen = strlen(subs[ei].val);
234 memcpy(out, src, earliest - src);
235 out += earliest - src;
236 memcpy(out, subs[ei].val, vlen);
237 out += vlen;
238 src = earliest + strlen(subs[ei].key);
239 } else {
240 strcpy(out, src);
241 out += strlen(src);
242 break;
243 }
244 }
245 *out = '\0';
246
247 httpd_resp_send(req, html, out - html);
248 free(html); 207 free(html);
249 return ESP_OK; 208 return ESP_OK;
250} 209}
@@ -312,19 +271,10 @@ static esp_err_t usage_handler(httpd_req_t *req)
312 } 271 }
313 272
314 const tollgate_config_t *cfg = tollgate_config_get(); 273 const tollgate_config_t *cfg = tollgate_config_get();
315 bool is_bytes = (strcmp(cfg->metric, "bytes") == 0); 274 int64_t now_ms = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
316 275
317 char resp[64]; 276 char resp[64];
318 if (is_bytes) { 277 tollgate_core_portal_format_usage(session, cfg->metric, now_ms, resp, sizeof(resp));
319 int64_t remaining = (int64_t)session->allotment_bytes - (int64_t)session->bytes_consumed;
320 if (remaining < 0) remaining = 0;
321 snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_bytes);
322 } else {
323 int64_t elapsed = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS - session->start_time_ms;
324 int64_t remaining = session->allotment_ms - elapsed;
325 if (remaining < 0) remaining = 0;
326 snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_ms);
327 }
328 httpd_resp_set_type(req, "text/plain"); 278 httpd_resp_set_type(req, "text/plain");
329 httpd_resp_send(req, resp, strlen(resp)); 279 httpd_resp_send(req, resp, strlen(resp));
330 return ESP_OK; 280 return ESP_OK;
@@ -342,7 +292,9 @@ static esp_err_t reset_auth_handler(httpd_req_t *req)
342 292
343static esp_err_t redirect_to_portal_handler(httpd_req_t *req) 293static esp_err_t redirect_to_portal_handler(httpd_req_t *req)
344{ 294{
345 ESP_LOGI(TAG, "Captive detect: GET %s → 200 portal HTML", req->uri); 295 if (tollgate_core_portal_is_captive_uri(req->uri)) {
296 ESP_LOGI(TAG, "Captive detect: GET %s → 200 portal HTML", req->uri);
297 }
346 return portal_handler(req); 298 return portal_handler(req);
347} 299}
348 300
@@ -493,37 +445,7 @@ static const char SETUP_HTML_TEMPLATE[] = \
493"</body></html>"; 445"</body></html>";
494 446
495static char *template_replace(const char *tpl, const char *key, const char *val) { 447static char *template_replace(const char *tpl, const char *key, const char *val) {
496 const char *p; 448 return tollgate_core_portal_template_replace(tpl, key, val);
497 size_t klen = strlen(key);
498 size_t vlen = strlen(val);
499 size_t tlen = strlen(tpl);
500 size_t extra = 0;
501 p = tpl;
502 while ((p = strstr(p, key)) != NULL) {
503 extra += vlen - klen;
504 p += klen;
505 }
506 size_t out_size = tlen + extra + 1;
507 char *out = malloc(out_size);
508 if (!out) return NULL;
509 char *dst = out;
510 p = tpl;
511 while (*p) {
512 const char *found = strstr(p, key);
513 if (found) {
514 memcpy(dst, p, found - p);
515 dst += found - p;
516 memcpy(dst, val, vlen);
517 dst += vlen;
518 p = found + klen;
519 } else {
520 strcpy(dst, p);
521 dst += strlen(p);
522 break;
523 }
524 }
525 *dst = '\0';
526 return out;
527} 449}
528 450
529static bool is_setup_available(void) { 451static bool is_setup_available(void) {
diff --git a/main/mint_health.c b/main/mint_health.c
index d309731..48a4333 100644
--- a/main/mint_health.c
+++ b/main/mint_health.c
@@ -1,5 +1,6 @@
1#include "mint_health.h" 1#include "mint_health.h"
2#include "tls_worker.h" 2#include "tls_worker.h"
3#include "tollgate_core_mint_health.h"
3#include "esp_log.h" 4#include "esp_log.h"
4#include "esp_http_client.h" 5#include "esp_http_client.h"
5#include "esp_crt_bundle.h" 6#include "esp_crt_bundle.h"
@@ -18,8 +19,7 @@ static QueueHandle_t s_wallet_queue = NULL;
18 19
19static int s_last_probe_err = 0; 20static int s_last_probe_err = 0;
20 21
21static mint_status_t s_mints[MINT_HEALTH_MAX]; 22static tollgate_mint_health_t s_health_state;
22static int s_mint_count = 0;
23static bool s_running = false; 23static bool s_running = false;
24static TaskHandle_t s_task_handle = NULL; 24static TaskHandle_t s_task_handle = NULL;
25static SemaphoreHandle_t s_mutex = NULL; 25static SemaphoreHandle_t s_mutex = NULL;
@@ -38,18 +38,18 @@ static void fire_callbacks(void)
38esp_err_t mint_health_init(const char urls[][256], int count) 38esp_err_t mint_health_init(const char urls[][256], int count)
39{ 39{
40 if (count > MINT_HEALTH_MAX) count = MINT_HEALTH_MAX; 40 if (count > MINT_HEALTH_MAX) count = MINT_HEALTH_MAX;
41 s_mint_count = count; 41 s_health_state.count = count;
42 s_callback_count = 0; 42 s_callback_count = 0;
43 43
44 if (!s_mutex) s_mutex = xSemaphoreCreateMutex(); 44 if (!s_mutex) s_mutex = xSemaphoreCreateMutex();
45 45
46 memset(s_mints, 0, sizeof(s_mints)); 46 memset(s_health_state.mints, 0, sizeof(s_health_state.mints));
47 for (int i = 0; i < count; i++) { 47 for (int i = 0; i < count; i++) {
48 strncpy(s_mints[i].url, urls[i], sizeof(s_mints[i].url) - 1); 48 strncpy(s_health_state.mints[i].url, urls[i], sizeof(s_health_state.mints[i].url) - 1);
49 s_mints[i].reachable = false; 49 s_health_state.mints[i].reachable = false;
50 s_mints[i].consecutive_successes = 0; 50 s_health_state.mints[i].consecutive_successes = 0;
51 s_mints[i].last_probe_ms = 0; 51 s_health_state.mints[i].last_probe_ms = 0;
52 s_mints[i].last_http_status = 0; 52 s_health_state.mints[i].last_http_status = 0;
53 } 53 }
54 54
55 ESP_LOGI(TAG, "Initialized with %d mints", count); 55 ESP_LOGI(TAG, "Initialized with %d mints", count);
@@ -106,35 +106,17 @@ static void run_probes(void)
106 106
107 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) return; 107 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) return;
108 108
109 for (int i = 0; i < s_mint_count; i++) { 109 old_reachable = tollgate_core_mint_health_count_reachable(&s_health_state);
110 if (s_mints[i].reachable) old_reachable++;
111 }
112
113 for (int i = 0; i < s_mint_count; i++) {
114 bool ok = probe_mint(s_mints[i].url);
115 s_mints[i].last_probe_ms = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
116 s_mints[i].last_http_status = ok ? 200 : 0;
117 s_mints[i].last_err = ok ? 0 : s_last_probe_err;
118 110
119 if (ok) { 111 for (int i = 0; i < s_health_state.count; i++) {
120 s_mints[i].consecutive_successes++; 112 bool ok = probe_mint(s_health_state.mints[i].url);
121 if (s_mints[i].consecutive_successes >= MINT_HEALTH_RECOVERY_THRESHOLD) { 113 int64_t probe_time = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
122 if (!s_mints[i].reachable) {
123 ESP_LOGI(TAG, "Mint RECOVERED: %s", s_mints[i].url);
124 }
125 s_mints[i].reachable = true;
126 }
127 } else {
128 if (s_mints[i].reachable) {
129 ESP_LOGW(TAG, "Mint UNREACHABLE: %s err=0x%x", s_mints[i].url, s_last_probe_err);
130 }
131 s_mints[i].reachable = false;
132 s_mints[i].consecutive_successes = 0;
133 }
134 114
135 if (s_mints[i].reachable) new_reachable++; 115 tollgate_core_mint_health_update(&s_health_state, i, ok, ok ? 200 : 0,
116 ok ? 0 : s_last_probe_err, probe_time);
136 } 117 }
137 118
119 new_reachable = tollgate_core_mint_health_count_reachable(&s_health_state);
138 bool changed = (old_reachable != new_reachable); 120 bool changed = (old_reachable != new_reachable);
139 xSemaphoreGive(s_mutex); 121 xSemaphoreGive(s_mutex);
140 122
@@ -148,20 +130,17 @@ static void run_initial_probes(void)
148{ 130{
149 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) return; 131 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) return;
150 132
151 for (int i = 0; i < s_mint_count; i++) { 133 for (int i = 0; i < s_health_state.count; i++) {
152 bool ok = probe_mint(s_mints[i].url); 134 bool ok = probe_mint(s_health_state.mints[i].url);
153 s_mints[i].last_probe_ms = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS; 135 int64_t probe_time = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS;
154 s_mints[i].last_http_status = ok ? 200 : 0; 136
155 s_mints[i].last_err = ok ? 0 : s_last_probe_err; 137 tollgate_core_mint_health_update_initial(&s_health_state, i, ok, ok ? 200 : 0,
138 ok ? 0 : s_last_probe_err, probe_time);
156 139
157 if (ok) { 140 if (ok) {
158 s_mints[i].consecutive_successes = MINT_HEALTH_RECOVERY_THRESHOLD; 141 ESP_LOGI(TAG, "Initial probe OK: %s (reachable)", s_health_state.mints[i].url);
159 s_mints[i].reachable = true;
160 ESP_LOGI(TAG, "Initial probe OK: %s (reachable)", s_mints[i].url);
161 } else { 142 } else {
162 s_mints[i].consecutive_successes = 0; 143 ESP_LOGW(TAG, "Initial probe FAIL: %s (unreachable)", s_health_state.mints[i].url);
163 s_mints[i].reachable = false;
164 ESP_LOGW(TAG, "Initial probe FAIL: %s (unreachable)", s_mints[i].url);
165 } 144 }
166 } 145 }
167 146
@@ -246,24 +225,18 @@ const mint_status_t *mint_health_get_all(int *out_count)
246{ 225{
247 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) { 226 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) {
248 *out_count = 0; 227 *out_count = 0;
249 return s_mints; 228 return (const mint_status_t *)s_health_state.mints;
250 } 229 }
251 *out_count = s_mint_count; 230 *out_count = s_health_state.count;
252 xSemaphoreGive(s_mutex); 231 xSemaphoreGive(s_mutex);
253 return s_mints; 232 return (const mint_status_t *)s_health_state.mints;
254} 233}
255 234
256bool mint_health_is_reachable(const char *url) 235bool mint_health_is_reachable(const char *url)
257{ 236{
258 if (!url) return false; 237 if (!url) return false;
259 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return false; 238 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return false;
260 bool result = false; 239 bool result = tollgate_core_mint_health_is_reachable(&s_health_state, url);
261 for (int i = 0; i < s_mint_count; i++) {
262 if (strcmp(s_mints[i].url, url) == 0 || strstr(url, s_mints[i].url) != NULL) {
263 result = s_mints[i].reachable;
264 break;
265 }
266 }
267 xSemaphoreGive(s_mutex); 240 xSemaphoreGive(s_mutex);
268 return result; 241 return result;
269} 242}
@@ -272,16 +245,7 @@ void mint_health_mark_unreachable(const char *url)
272{ 245{
273 if (!url) return; 246 if (!url) return;
274 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return; 247 if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(1000)) != pdTRUE) return;
275 for (int i = 0; i < s_mint_count; i++) { 248 tollgate_core_mint_health_mark_unreachable(&s_health_state, url);
276 if (strcmp(s_mints[i].url, url) == 0 || strstr(url, s_mints[i].url) != NULL) {
277 if (s_mints[i].reachable) {
278 s_mints[i].reachable = false;
279 s_mints[i].consecutive_successes = 0;
280 ESP_LOGW(TAG, "Reactively marked unreachable: %s", url);
281 }
282 break;
283 }
284 }
285 xSemaphoreGive(s_mutex); 249 xSemaphoreGive(s_mutex);
286} 250}
287 251
diff --git a/main/stratum_client.c b/main/stratum_client.c
index a308a5b..14f0880 100644
--- a/main/stratum_client.c
+++ b/main/stratum_client.c
@@ -1,6 +1,7 @@
1#include "stratum_client.h" 1#include "stratum_client.h"
2#include "stratum_proxy.h" 2#include "stratum_proxy.h"
3#include "tollgate_core_mining.h" 3#include "tollgate_core_mining.h"
4#include "tollgate_core_stratum_client.h"
4#include "config.h" 5#include "config.h"
5#include "esp_log.h" 6#include "esp_log.h"
6#include "esp_transport.h" 7#include "esp_transport.h"
@@ -66,76 +67,53 @@ static esp_err_t stratum_connect(const char *host, uint16_t port)
66static void send_subscribe(void) 67static void send_subscribe(void)
67{ 68{
68 char subscribe[256]; 69 char subscribe[256];
69 snprintf(subscribe, sizeof(subscribe), 70 int len = tollgate_core_stratum_build_subscribe(subscribe, sizeof(subscribe), s_req_id++);
70 "{\"id\":%lu,\"method\":\"mining.subscribe\",\"params\":[\"TollGate/1.0\"]}\n", 71 if (len > 0) {
71 (unsigned long)s_req_id++); 72 esp_transport_write(s_transport, subscribe, len, 5000);
72 esp_transport_write(s_transport, subscribe, strlen(subscribe), 5000); 73 ESP_LOGI(TAG, "Sent mining.subscribe");
73 ESP_LOGI(TAG, "Sent mining.subscribe"); 74 }
74} 75}
75 76
76static void send_authorize(void) 77static void send_authorize(void)
77{ 78{
78 const tollgate_config_t *cfg = tollgate_config_get(); 79 const tollgate_config_t *cfg = tollgate_config_get();
79 char authorize[512]; 80 char authorize[512];
80 snprintf(authorize, sizeof(authorize), 81 int len = tollgate_core_stratum_build_authorize(authorize, sizeof(authorize),
81 "{\"id\":%lu,\"method\":\"mining.authorize\",\"params\":[\"%s\",\"%s\"]}\n", 82 s_req_id++, cfg->stratum_user, cfg->stratum_pass);
82 (unsigned long)s_req_id++, cfg->stratum_user, cfg->stratum_pass); 83 if (len > 0) {
83 esp_transport_write(s_transport, authorize, strlen(authorize), 5000); 84 esp_transport_write(s_transport, authorize, len, 5000);
84 ESP_LOGI(TAG, "Sent mining.authorize for user=%s", cfg->stratum_user); 85 ESP_LOGI(TAG, "Sent mining.authorize for user=%s", cfg->stratum_user);
85}
86
87static void hex_to_bytes(const char *hex, uint8_t *out, int len)
88{
89 for (int i = 0; i < len && hex[i * 2] && hex[i * 2 + 1]; i++) {
90 char byte[3] = {hex[i * 2], hex[i * 2 + 1], 0};
91 out[i] = (uint8_t)strtoul(byte, NULL, 16);
92 } 86 }
93} 87}
94 88
95static void handle_mining_notify(cJSON *params) 89static void handle_mining_notify(cJSON *params)
96{ 90{
97 if (!params || !cJSON_IsArray(params) || cJSON_GetArraySize(params) < 6) return; 91 tollgate_stratum_job_t job = {0};
98 92 uint32_t nbits = 0;
99 cJSON *p_job_id = cJSON_GetArrayItem(params, 0); 93 if (!tollgate_core_stratum_parse_notify(params, &job, &nbits)) return;
100 cJSON *p_prevhash = cJSON_GetArrayItem(params, 1); 94
101 cJSON *p_version = cJSON_GetArrayItem(params, 5); 95 stratum_job_t proxy_job = {0};
102 cJSON *p_nbits = cJSON_GetArrayItem(params, 6); 96 proxy_job.job_id = job.job_id;
103 cJSON *p_ntime = cJSON_GetArrayItem(params, 7); 97 memcpy(proxy_job.prevhash, job.prevhash, 32);
104 98 proxy_job.version = job.version;
105 if (!p_job_id || !p_prevhash || !p_nbits) return; 99 proxy_job.nbits = job.nbits;
106 100 proxy_job.ntime = job.ntime;
107 stratum_job_t job = {0}; 101 memcpy(proxy_job.target, job.target, 32);
108 job.job_id = (uint32_t)atoi(p_job_id->valuestring); 102 proxy_job.target_len = job.target_len;
109 job.valid = true; 103 proxy_job.valid = job.valid;
110 104
111 hex_to_bytes(p_prevhash->valuestring, job.prevhash, 32); 105 tollgate_core_mining_set_current_nbits(proxy_job.nbits);
112 106 stratum_proxy_set_job(&proxy_job);
113 if (p_version && cJSON_IsString(p_version)) { 107
114 job.version = (uint32_t)strtoul(p_version->valuestring, NULL, 16); 108 if (nbits) s_state.nbits = nbits;
115 }
116 if (p_nbits && cJSON_IsString(p_nbits)) {
117 job.nbits = (uint32_t)strtoul(p_nbits->valuestring, NULL, 16);
118 s_state.nbits = job.nbits;
119 }
120 if (p_ntime && cJSON_IsString(p_ntime)) {
121 job.ntime = (uint32_t)strtoul(p_ntime->valuestring, NULL, 16);
122 }
123
124 memset(job.target, 0xFF, 32);
125 job.target_len = 32;
126
127 tollgate_core_mining_set_current_nbits(job.nbits);
128 stratum_proxy_set_job(&job);
129
130 ESP_LOGI(TAG, "New mining job: id=%lu, nbits=0x%08lx", (unsigned long)job.job_id, (unsigned long)job.nbits); 109 ESP_LOGI(TAG, "New mining job: id=%lu, nbits=0x%08lx", (unsigned long)job.job_id, (unsigned long)job.nbits);
131} 110}
132 111
133static void handle_mining_set_difficulty(cJSON *params) 112static void handle_mining_set_difficulty(cJSON *params)
134{ 113{
135 if (!params || !cJSON_IsArray(params) || cJSON_GetArraySize(params) < 1) return; 114 uint64_t diff = 0;
136 cJSON *diff = cJSON_GetArrayItem(params, 0); 115 if (tollgate_core_stratum_parse_difficulty(params, &diff)) {
137 if (diff && cJSON_IsNumber(diff)) { 116 s_state.difficulty = diff;
138 s_state.difficulty = (uint64_t)diff->valuedouble;
139 ESP_LOGI(TAG, "Pool set difficulty: %llu", (unsigned long long)s_state.difficulty); 117 ESP_LOGI(TAG, "Pool set difficulty: %llu", (unsigned long long)s_state.difficulty);
140 } 118 }
141} 119}
@@ -243,12 +221,11 @@ esp_err_t stratum_client_submit_share(uint32_t job_id, uint32_t nonce, uint32_t
243 const tollgate_config_t *cfg = tollgate_config_get(); 221 const tollgate_config_t *cfg = tollgate_config_get();
244 222
245 char submit[512]; 223 char submit[512];
246 snprintf(submit, sizeof(submit), 224 int len = tollgate_core_stratum_build_submit(submit, sizeof(submit), s_req_id++,
247 "{\"id\":%lu,\"method\":\"mining.submit\",\"params\":[\"%s\",\"%lu\",\"%08lx\",\"%08lx\",\"%08lx\"]}\n", 225 cfg->stratum_user, job_id, ntime, nonce, version);
248 (unsigned long)s_req_id++, cfg->stratum_user, 226 if (len <= 0) return ESP_FAIL;
249 (unsigned long)job_id, (unsigned long)ntime, (unsigned long)nonce, (unsigned long)version);
250 227
251 int written = esp_transport_write(s_transport, submit, strlen(submit), 5000); 228 int written = esp_transport_write(s_transport, submit, len, 5000);
252 if (written < 0) { 229 if (written < 0) {
253 ESP_LOGW(TAG, "Failed to submit share"); 230 ESP_LOGW(TAG, "Failed to submit share");
254 s_state.shares_rejected++; 231 s_state.shares_rejected++;
diff --git a/main/tollgate_client.c b/main/tollgate_client.c
index 73c8370..70a25c2 100644
--- a/main/tollgate_client.c
+++ b/main/tollgate_client.c
@@ -88,123 +88,17 @@ static esp_err_t http_post_text(const char *url, const char *body, char *resp_bu
88 88
89static bool parse_discovery_response(const char *json_str, tollgate_discovery_t *out) 89static bool parse_discovery_response(const char *json_str, tollgate_discovery_t *out)
90{ 90{
91 cJSON *root = cJSON_Parse(json_str); 91 return tollgate_core_client_parse_discovery(json_str, out);
92 if (!root) return false;
93
94 cJSON *kind = cJSON_GetObjectItemCaseSensitive(root, "kind");
95 if (!kind || !cJSON_IsNumber(kind) || kind->valueint != 10021) {
96 cJSON_Delete(root);
97 return false;
98 }
99
100 memset(out, 0, sizeof(tollgate_discovery_t));
101 out->is_tollgate = true;
102
103 cJSON *tags = cJSON_GetObjectItemCaseSensitive(root, "tags");
104 if (!tags || !cJSON_IsArray(tags)) {
105 cJSON_Delete(root);
106 return true;
107 }
108
109 int tag_count = cJSON_GetArraySize(tags);
110 for (int i = 0; i < tag_count; i++) {
111 cJSON *tag = cJSON_GetArrayItem(tags, i);
112 if (!tag || !cJSON_IsArray(tag)) continue;
113
114 int tag_len = cJSON_GetArraySize(tag);
115 if (tag_len < 2) continue;
116
117 cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
118 if (!tag_name || !cJSON_IsString(tag_name)) continue;
119
120 if (strcmp(tag_name->valuestring, "metric") == 0) {
121 cJSON *val = cJSON_GetArrayItem(tag, 1);
122 if (val && cJSON_IsString(val)) {
123 strncpy(out->metric, val->valuestring, sizeof(out->metric) - 1);
124 }
125 } else if (strcmp(tag_name->valuestring, "step_size") == 0) {
126 cJSON *val = cJSON_GetArrayItem(tag, 1);
127 if (val && cJSON_IsString(val)) {
128 out->step_size_ms = atoi(val->valuestring);
129 }
130 } else if (strcmp(tag_name->valuestring, "price_per_step") == 0 && tag_len >= 4) {
131 cJSON *payment_type = cJSON_GetArrayItem(tag, 2);
132
133 if (cJSON_IsString(payment_type) && strcmp(payment_type->valuestring, "mining") == 0 && tag_len >= 5) {
134 out->mining_available = true;
135 cJSON *port_val = cJSON_GetArrayItem(tag, 3);
136 if (port_val && cJSON_IsString(port_val)) {
137 out->mining_port = (uint16_t)atoi(port_val->valuestring);
138 }
139 } else {
140 cJSON *amount = cJSON_GetArrayItem(tag, 2);
141 cJSON *mint = cJSON_GetArrayItem(tag, 4);
142
143 if (amount && cJSON_IsString(amount)) {
144 out->price_per_step = atoi(amount->valuestring);
145 }
146 if (mint && cJSON_IsString(mint)) {
147 strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1);
148 }
149 }
150 }
151 }
152
153 cJSON_Delete(root);
154 return true;
155} 92}
156 93
157static bool parse_session_response(const char *json_str, int64_t *allotment_ms_out) 94static bool parse_session_response(const char *json_str, int64_t *allotment_ms_out)
158{ 95{
159 cJSON *root = cJSON_Parse(json_str); 96 return tollgate_core_client_parse_session(json_str, allotment_ms_out);
160 if (!root) return false;
161
162 cJSON *kind = cJSON_GetObjectItemCaseSensitive(root, "kind");
163 if (!kind || !cJSON_IsNumber(kind)) {
164 cJSON_Delete(root);
165 return false;
166 }
167
168 if (kind->valueint != 1022) {
169 cJSON_Delete(root);
170 return false;
171 }
172
173 cJSON *tags = cJSON_GetObjectItemCaseSensitive(root, "tags");
174 if (tags && cJSON_IsArray(tags)) {
175 int tag_count = cJSON_GetArraySize(tags);
176 for (int i = 0; i < tag_count; i++) {
177 cJSON *tag = cJSON_GetArrayItem(tags, i);
178 if (!tag || !cJSON_IsArray(tag)) continue;
179 cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
180 if (tag_name && cJSON_IsString(tag_name) && strcmp(tag_name->valuestring, "allotment") == 0) {
181 cJSON *val = cJSON_GetArrayItem(tag, 1);
182 if (val && cJSON_IsString(val)) {
183 *allotment_ms_out = atoll(val->valuestring);
184 }
185 }
186 }
187 }
188
189 cJSON_Delete(root);
190 return true;
191} 97}
192 98
193static bool parse_usage_response(const char *resp, int64_t *remaining_out, int64_t *total_out) 99static bool parse_usage_response(const char *resp, int64_t *remaining_out, int64_t *total_out)
194{ 100{
195 char remaining_str[32] = {0}; 101 return tollgate_core_client_parse_usage(resp, remaining_out, total_out);
196 char total_str[32] = {0};
197 const char *slash = strchr(resp, '/');
198 if (!slash) return false;
199
200 size_t rlen = slash - resp;
201 if (rlen >= sizeof(remaining_str)) return false;
202 memcpy(remaining_str, resp, rlen);
203 strncpy(total_str, slash + 1, sizeof(total_str) - 1);
204
205 *remaining_out = atoll(remaining_str);
206 *total_out = atoll(total_str);
207 return true;
208} 102}
209 103
210esp_err_t tollgate_client_detect(const char *gw_ip, tollgate_discovery_t *discovery) 104esp_err_t tollgate_client_detect(const char *gw_ip, tollgate_discovery_t *discovery)
@@ -358,13 +252,11 @@ esp_err_t tollgate_client_on_sta_connected(const char *gw_ip_str)
358 const market_t *mkt = market_get(); 252 const market_t *mkt = market_get();
359 int cheapest = market_find_cheapest(); 253 int cheapest = market_find_cheapest();
360 if (cheapest >= 0 && mkt->entries[cheapest].valid && mkt->entries[cheapest].ssid[0] != '\0') { 254 if (cheapest >= 0 && mkt->entries[cheapest].valid && mkt->entries[cheapest].ssid[0] != '\0') {
361 uint32_t upstream_step = s_discovery.step_size_ms > 0 ? s_discovery.step_size_ms : 1; 255 int upstream_eff = tollgate_core_client_calc_price_per_min(s_discovery.price_per_step, s_discovery.step_size_ms);
362 uint32_t upstream_eff = (uint32_t)s_discovery.price_per_step * 60000 / upstream_step; 256 int cheap_eff = tollgate_core_client_calc_price_per_min(mkt->entries[cheapest].price_per_step, mkt->entries[cheapest].step_size);
363 uint32_t cheap_step = mkt->entries[cheapest].step_size > 0 ? mkt->entries[cheapest].step_size : 1;
364 uint32_t cheap_eff = (uint32_t)mkt->entries[cheapest].price_per_step * 60000 / cheap_step;
365 if (cheap_eff < upstream_eff) { 257 if (cheap_eff < upstream_eff) {
366 ESP_LOGW(TAG, "CHEAPER TOLLGATE AVAILABLE: %s at %lu sats/min vs upstream %lu sats/min", 258 ESP_LOGW(TAG, "CHEAPER TOLLGATE AVAILABLE: %s at %d sats/min vs upstream %d sats/min",
367 mkt->entries[cheapest].ssid, (unsigned long)cheap_eff, (unsigned long)upstream_eff); 259 mkt->entries[cheapest].ssid, cheap_eff, upstream_eff);
368 } 260 }
369 } 261 }
370 return ESP_OK; 262 return ESP_OK;
@@ -430,32 +322,29 @@ void tollgate_client_tick(void)
430 322
431 const tollgate_config_t *cfg = tollgate_config_get(); 323 const tollgate_config_t *cfg = tollgate_config_get();
432 int threshold_pct = cfg->client_renewal_threshold_pct; 324 int threshold_pct = cfg->client_renewal_threshold_pct;
433 if (threshold_pct <= 0) threshold_pct = 20; 325
434 326 if (tollgate_core_client_should_renew(s_remaining_ms, s_allotment_ms, threshold_pct)) {
435 if (s_allotment_ms > 0 && s_remaining_ms >= 0) { 327 ESP_LOGI(TAG, "session nearing expiry (%lld/%lldms, %d%%), renewing",
436 int remaining_pct = (int)((s_remaining_ms * 100) / s_allotment_ms); 328 (long long)s_remaining_ms, (long long)s_allotment_ms,
437 if (remaining_pct <= threshold_pct) { 329 (int)((s_remaining_ms * 100) / s_allotment_ms));
438 ESP_LOGI(TAG, "session nearing expiry (%lld/%lldms, %d%%), renewing", 330
439 (long long)s_remaining_ms, (long long)s_allotment_ms, remaining_pct); 331 s_state = TG_CLIENT_RENEWING;
440 332 int steps = cfg->client_steps_to_buy;
441 s_state = TG_CLIENT_RENEWING; 333 if (steps <= 0) steps = 1;
442 int steps = cfg->client_steps_to_buy; 334 int amount_sats = steps * s_discovery.price_per_step;
443 if (steps <= 0) steps = 1; 335
444 int amount_sats = steps * s_discovery.price_per_step; 336 int64_t allotment = 0;
445 337 err = tollgate_client_pay(s_gw_ip, amount_sats, &allotment);
446 int64_t allotment = 0; 338 if (err == ESP_OK) {
447 err = tollgate_client_pay(s_gw_ip, amount_sats, &allotment); 339 s_allotment_ms = allotment;
448 if (err == ESP_OK) { 340 s_remaining_ms = allotment;
449 s_allotment_ms = allotment; 341 s_last_pay_time_ms = get_time_ms();
450 s_remaining_ms = allotment; 342 s_state = TG_CLIENT_PAID;
451 s_last_pay_time_ms = get_time_ms(); 343 ESP_LOGI(TAG, "renewal succeeded: %lldms", (long long)allotment);
452 s_state = TG_CLIENT_PAID; 344 } else {
453 ESP_LOGI(TAG, "renewal succeeded: %lldms", (long long)allotment); 345 s_state = TG_CLIENT_ERROR;
454 } else { 346 s_last_pay_time_ms = get_time_ms();
455 s_state = TG_CLIENT_ERROR; 347 ESP_LOGE(TAG, "renewal payment failed");
456 s_last_pay_time_ms = get_time_ms();
457 ESP_LOGE(TAG, "renewal payment failed");
458 }
459 } 348 }
460 } 349 }
461} 350}
diff --git a/main/tollgate_client.h b/main/tollgate_client.h
index ccee624..463cf63 100644
--- a/main/tollgate_client.h
+++ b/main/tollgate_client.h
@@ -1,36 +1,11 @@
1#ifndef TOLLGATE_CLIENT_H 1#ifndef TOLLGATE_CLIENT_H
2#define TOLLGATE_CLIENT_H 2#define TOLLGATE_CLIENT_H
3 3
4#include "tollgate_core_client.h"
4#include "esp_err.h" 5#include "esp_err.h"
5#include <stdint.h> 6#include <stdint.h>
6#include <stdbool.h> 7#include <stdbool.h>
7 8
8#define TG_CLIENT_MAX_GW_IP_LEN 16
9#define TG_CLIENT_MAX_MINT_URL 256
10#define TG_CLIENT_MAX_METRIC 32
11
12typedef enum {
13 TG_CLIENT_IDLE,
14 TG_CLIENT_DETECTING,
15 TG_CLIENT_NO_TOLLGATE,
16 TG_CLIENT_NEEDS_PAY,
17 TG_CLIENT_PAYING,
18 TG_CLIENT_PAID,
19 TG_CLIENT_RENEWING,
20 TG_CLIENT_MINING,
21 TG_CLIENT_ERROR
22} tollgate_client_state_t;
23
24typedef struct {
25 bool is_tollgate;
26 int price_per_step;
27 int step_size_ms;
28 char mint_url[TG_CLIENT_MAX_MINT_URL];
29 char metric[TG_CLIENT_MAX_METRIC];
30 bool mining_available;
31 uint16_t mining_port;
32} tollgate_discovery_t;
33
34esp_err_t tollgate_client_init(void); 9esp_err_t tollgate_client_init(void);
35 10
36esp_err_t tollgate_client_on_sta_connected(const char *gw_ip_str); 11esp_err_t tollgate_client_on_sta_connected(const char *gw_ip_str);