upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-23 15:26:52 +0530
committerYour Name <you@example.com>2026-05-23 15:26:52 +0530
commitbe4d490e96de3d20cc6c9921f09534cde0ca0765 (patch)
tree624cdbaff079e14a26fc22f7741a1452b54555b9
parent4ce93d0470e8ab2402486c893f50bfdf216752e6 (diff)
Phase 8c-8f: Extract portal, stratum_client, mint_health, client into tollgate_core
New component modules: - tollgate_core_portal.c/h: template rendering, usage calc, captive URI detection - tollgate_core_stratum_client.c/h: Stratum V1 JSON protocol parsing, message building - tollgate_core_mint_health.c/h: mint health state tracking, recovery threshold logic - tollgate_core_client.c/h: discovery/session/usage JSON parsing, renewal check, price calc Main shims updated to delegate pure logic to component: - captive_portal.c: uses tollgate_core_portal_render(), format_usage(), is_captive_uri() - stratum_client.c: uses tollgate_core_stratum_parse_notify/build_*() - mint_health.c: uses tollgate_core_mint_health_update/is_reachable/mark_unreachable() - tollgate_client.c: uses tollgate_core_client_parse_*/should_renew/calc_price_per_min() 4 new unit tests (test_portal, test_stratum_client, test_mint_health_core, test_client_core) All 25 unit tests pass. ESP-IDF build passes (68% partition free).
-rw-r--r--components/tollgate_core/CMakeLists.txt4
-rw-r--r--components/tollgate_core/src/tollgate_core_client.c146
-rw-r--r--components/tollgate_core/src/tollgate_core_client.h51
-rw-r--r--components/tollgate_core/src/tollgate_core_mint_health.c91
-rw-r--r--components/tollgate_core/src/tollgate_core_mint_health.h50
-rw-r--r--components/tollgate_core/src/tollgate_core_portal.c149
-rw-r--r--components/tollgate_core/src/tollgate_core_portal.h34
-rw-r--r--components/tollgate_core/src/tollgate_core_stratum_client.c91
-rw-r--r--components/tollgate_core/src/tollgate_core_stratum_client.h50
-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
-rw-r--r--tests/unit/Makefile26
-rw-r--r--tests/unit/test_client_core.c189
-rw-r--r--tests/unit/test_mint_health_core.c130
-rw-r--r--tests/unit/test_portal.c153
-rw-r--r--tests/unit/test_stratum_client.c125
19 files changed, 1393 insertions, 391 deletions
diff --git a/components/tollgate_core/CMakeLists.txt b/components/tollgate_core/CMakeLists.txt
index 3cd5e85..502a276 100644
--- a/components/tollgate_core/CMakeLists.txt
+++ b/components/tollgate_core/CMakeLists.txt
@@ -8,6 +8,10 @@ idf_component_register(
8 "src/tollgate_core_stratum_proxy.c" 8 "src/tollgate_core_stratum_proxy.c"
9 "src/tollgate_core_beacon.c" 9 "src/tollgate_core_beacon.c"
10 "src/tollgate_core_market.c" 10 "src/tollgate_core_market.c"
11 "src/tollgate_core_portal.c"
12 "src/tollgate_core_stratum_client.c"
13 "src/tollgate_core_mint_health.c"
14 "src/tollgate_core_client.c"
11 INCLUDE_DIRS "include" "src" 15 INCLUDE_DIRS "include" "src"
12 REQUIRES lwip json esp_http_client mbedtls log esp_netif 16 REQUIRES lwip json esp_http_client mbedtls log esp_netif
13 PRIV_REQUIRES esp_wifi) 17 PRIV_REQUIRES esp_wifi)
diff --git a/components/tollgate_core/src/tollgate_core_client.c b/components/tollgate_core/src/tollgate_core_client.c
new file mode 100644
index 0000000..9e8508a
--- /dev/null
+++ b/components/tollgate_core/src/tollgate_core_client.c
@@ -0,0 +1,146 @@
1#include "tollgate_core_client.h"
2#include "cJSON.h"
3#include <string.h>
4#include <stdlib.h>
5
6bool tollgate_core_client_parse_discovery(const char *json_str, tollgate_discovery_t *out)
7{
8 cJSON *root = cJSON_Parse(json_str);
9 if (!root) return false;
10
11 cJSON *kind = cJSON_GetObjectItemCaseSensitive(root, "kind");
12 if (!kind || !cJSON_IsNumber(kind) || kind->valueint != 10021) {
13 cJSON_Delete(root);
14 return false;
15 }
16
17 memset(out, 0, sizeof(tollgate_discovery_t));
18 out->is_tollgate = true;
19
20 cJSON *tags = cJSON_GetObjectItemCaseSensitive(root, "tags");
21 if (!tags || !cJSON_IsArray(tags)) {
22 cJSON_Delete(root);
23 return true;
24 }
25
26 int tag_count = cJSON_GetArraySize(tags);
27 for (int i = 0; i < tag_count; i++) {
28 cJSON *tag = cJSON_GetArrayItem(tags, i);
29 if (!tag || !cJSON_IsArray(tag)) continue;
30
31 int tag_len = cJSON_GetArraySize(tag);
32 if (tag_len < 2) continue;
33
34 cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
35 if (!tag_name || !cJSON_IsString(tag_name)) continue;
36
37 if (strcmp(tag_name->valuestring, "metric") == 0) {
38 cJSON *val = cJSON_GetArrayItem(tag, 1);
39 if (val && cJSON_IsString(val)) {
40 strncpy(out->metric, val->valuestring, sizeof(out->metric) - 1);
41 }
42 } else if (strcmp(tag_name->valuestring, "step_size") == 0) {
43 cJSON *val = cJSON_GetArrayItem(tag, 1);
44 if (val && cJSON_IsString(val)) {
45 out->step_size_ms = atoi(val->valuestring);
46 }
47 } else if (strcmp(tag_name->valuestring, "price_per_step") == 0 && tag_len >= 4) {
48 cJSON *payment_type = cJSON_GetArrayItem(tag, 2);
49
50 if (cJSON_IsString(payment_type) && strcmp(payment_type->valuestring, "mining") == 0 && tag_len >= 5) {
51 out->mining_available = true;
52 cJSON *port_val = cJSON_GetArrayItem(tag, 3);
53 if (port_val && cJSON_IsString(port_val)) {
54 out->mining_port = (uint16_t)atoi(port_val->valuestring);
55 }
56 } else {
57 cJSON *amount = cJSON_GetArrayItem(tag, 2);
58 cJSON *mint = cJSON_GetArrayItem(tag, 4);
59
60 if (amount && cJSON_IsString(amount)) {
61 out->price_per_step = atoi(amount->valuestring);
62 }
63 if (mint && cJSON_IsString(mint)) {
64 strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1);
65 }
66 }
67 }
68 }
69
70 cJSON_Delete(root);
71 return true;
72}
73
74bool tollgate_core_client_parse_session(const char *json_str, int64_t *allotment_ms_out)
75{
76 cJSON *root = cJSON_Parse(json_str);
77 if (!root) return false;
78
79 cJSON *kind = cJSON_GetObjectItemCaseSensitive(root, "kind");
80 if (!kind || !cJSON_IsNumber(kind)) {
81 cJSON_Delete(root);
82 return false;
83 }
84
85 if (kind->valueint != 1022) {
86 cJSON_Delete(root);
87 return false;
88 }
89
90 cJSON *tags = cJSON_GetObjectItemCaseSensitive(root, "tags");
91 if (tags && cJSON_IsArray(tags)) {
92 int tag_count = cJSON_GetArraySize(tags);
93 for (int i = 0; i < tag_count; i++) {
94 cJSON *tag = cJSON_GetArrayItem(tags, i);
95 if (!tag || !cJSON_IsArray(tag)) continue;
96 cJSON *tag_name = cJSON_GetArrayItem(tag, 0);
97 if (tag_name && cJSON_IsString(tag_name) && strcmp(tag_name->valuestring, "allotment") == 0) {
98 cJSON *val = cJSON_GetArrayItem(tag, 1);
99 if (val && cJSON_IsString(val)) {
100 *allotment_ms_out = atoll(val->valuestring);
101 }
102 }
103 }
104 }
105
106 cJSON_Delete(root);
107 return true;
108}
109
110bool tollgate_core_client_parse_usage(const char *resp, int64_t *remaining_out, int64_t *total_out)
111{
112 char remaining_str[32] = {0};
113 char total_str[32] = {0};
114 const char *slash = strchr(resp, '/');
115 if (!slash) return false;
116
117 size_t rlen = slash - resp;
118 if (rlen >= sizeof(remaining_str)) return false;
119 memcpy(remaining_str, resp, rlen);
120 strncpy(total_str, slash + 1, sizeof(total_str) - 1);
121
122 *remaining_out = atoll(remaining_str);
123 *total_out = atoll(total_str);
124 return true;
125}
126
127bool tollgate_core_client_should_renew(int64_t remaining_ms, int64_t allotment_ms, int threshold_pct)
128{
129 if (threshold_pct <= 0) threshold_pct = 20;
130 if (allotment_ms <= 0 || remaining_ms < 0) return false;
131
132 int remaining_pct = (int)((remaining_ms * 100) / allotment_ms);
133 return remaining_pct <= threshold_pct;
134}
135
136int tollgate_core_client_calc_price_per_min(int price_per_step, int step_size_ms)
137{
138 if (step_size_ms <= 0) step_size_ms = 1;
139 return price_per_step * 60000 / step_size_ms;
140}
141
142int tollgate_core_client_calc_steps(int steps_to_buy, int price_per_step, int discovery_price)
143{
144 if (steps_to_buy <= 0) steps_to_buy = 1;
145 return steps_to_buy * discovery_price;
146}
diff --git a/components/tollgate_core/src/tollgate_core_client.h b/components/tollgate_core/src/tollgate_core_client.h
new file mode 100644
index 0000000..32b90e6
--- /dev/null
+++ b/components/tollgate_core/src/tollgate_core_client.h
@@ -0,0 +1,51 @@
1#ifndef TOLLGATE_CORE_CLIENT_H
2#define TOLLGATE_CORE_CLIENT_H
3
4#include <stdint.h>
5#include <stdbool.h>
6
7#define TG_CLIENT_MAX_MINT_URL 256
8#define TG_CLIENT_MAX_METRIC 32
9#define TG_CLIENT_MAX_GW_IP_LEN 16
10
11typedef enum {
12 TG_CLIENT_IDLE,
13 TG_CLIENT_DETECTING,
14 TG_CLIENT_NO_TOLLGATE,
15 TG_CLIENT_NEEDS_PAY,
16 TG_CLIENT_PAYING,
17 TG_CLIENT_PAID,
18 TG_CLIENT_RENEWING,
19 TG_CLIENT_MINING,
20 TG_CLIENT_ERROR
21} tollgate_client_state_t;
22
23typedef struct {
24 bool is_tollgate;
25 int price_per_step;
26 int step_size_ms;
27 char mint_url[TG_CLIENT_MAX_MINT_URL];
28 char metric[TG_CLIENT_MAX_METRIC];
29 bool mining_available;
30 uint16_t mining_port;
31} tollgate_discovery_t;
32
33typedef struct {
34 const char *ssid;
35 int price_per_step;
36 uint32_t step_size;
37} tollgate_client_market_entry_t;
38
39bool tollgate_core_client_parse_discovery(const char *json_str, tollgate_discovery_t *out);
40
41bool tollgate_core_client_parse_session(const char *json_str, int64_t *allotment_ms_out);
42
43bool tollgate_core_client_parse_usage(const char *resp, int64_t *remaining_out, int64_t *total_out);
44
45bool tollgate_core_client_should_renew(int64_t remaining_ms, int64_t allotment_ms, int threshold_pct);
46
47int tollgate_core_client_calc_price_per_min(int price_per_step, int step_size_ms);
48
49int tollgate_core_client_calc_steps(int steps_to_buy, int price_per_step, int discovery_price);
50
51#endif
diff --git a/components/tollgate_core/src/tollgate_core_mint_health.c b/components/tollgate_core/src/tollgate_core_mint_health.c
new file mode 100644
index 0000000..c810003
--- /dev/null
+++ b/components/tollgate_core/src/tollgate_core_mint_health.c
@@ -0,0 +1,91 @@
1#include "tollgate_core_mint_health.h"
2#include <string.h>
3
4void tollgate_core_mint_health_init(const char urls[][256], int count)
5{
6}
7
8void tollgate_core_mint_health_update(tollgate_mint_health_t *state,
9 int mint_index,
10 bool probe_ok,
11 int http_status,
12 int err_code,
13 int64_t probe_time_ms)
14{
15 if (!state || mint_index < 0 || mint_index >= state->count) return;
16
17 tollgate_mint_status_t *m = &state->mints[mint_index];
18 m->last_probe_ms = probe_time_ms;
19 m->last_http_status = probe_ok ? http_status : 0;
20 m->last_err = probe_ok ? 0 : err_code;
21
22 if (probe_ok) {
23 m->consecutive_successes++;
24 if (m->consecutive_successes >= TG_MINT_HEALTH_RECOVERY_THRESHOLD) {
25 m->reachable = true;
26 }
27 } else {
28 m->reachable = false;
29 m->consecutive_successes = 0;
30 }
31}
32
33void tollgate_core_mint_health_update_initial(tollgate_mint_health_t *state,
34 int mint_index,
35 bool probe_ok,
36 int http_status,
37 int err_code,
38 int64_t probe_time_ms)
39{
40 if (!state || mint_index < 0 || mint_index >= state->count) return;
41
42 tollgate_mint_status_t *m = &state->mints[mint_index];
43 m->last_probe_ms = probe_time_ms;
44 m->last_http_status = probe_ok ? http_status : 0;
45 m->last_err = probe_ok ? 0 : err_code;
46
47 if (probe_ok) {
48 m->consecutive_successes = TG_MINT_HEALTH_RECOVERY_THRESHOLD;
49 m->reachable = true;
50 } else {
51 m->consecutive_successes = 0;
52 m->reachable = false;
53 }
54}
55
56bool tollgate_core_mint_health_is_reachable(const tollgate_mint_health_t *state,
57 const char *url)
58{
59 if (!state || !url) return false;
60 for (int i = 0; i < state->count; i++) {
61 if (strcmp(state->mints[i].url, url) == 0 || strstr(url, state->mints[i].url) != NULL) {
62 return state->mints[i].reachable;
63 }
64 }
65 return false;
66}
67
68void tollgate_core_mint_health_mark_unreachable(tollgate_mint_health_t *state,
69 const char *url)
70{
71 if (!state || !url) return;
72 for (int i = 0; i < state->count; i++) {
73 if (strcmp(state->mints[i].url, url) == 0 || strstr(url, state->mints[i].url) != NULL) {
74 if (state->mints[i].reachable) {
75 state->mints[i].reachable = false;
76 state->mints[i].consecutive_successes = 0;
77 }
78 break;
79 }
80 }
81}
82
83int tollgate_core_mint_health_count_reachable(const tollgate_mint_health_t *state)
84{
85 if (!state) return 0;
86 int count = 0;
87 for (int i = 0; i < state->count; i++) {
88 if (state->mints[i].reachable) count++;
89 }
90 return count;
91}
diff --git a/components/tollgate_core/src/tollgate_core_mint_health.h b/components/tollgate_core/src/tollgate_core_mint_health.h
new file mode 100644
index 0000000..4435a14
--- /dev/null
+++ b/components/tollgate_core/src/tollgate_core_mint_health.h
@@ -0,0 +1,50 @@
1#ifndef TOLLGATE_CORE_MINT_HEALTH_H
2#define TOLLGATE_CORE_MINT_HEALTH_H
3
4#include <stdint.h>
5#include <stdbool.h>
6
7#define TG_MINT_HEALTH_MAX 8
8#define TG_MINT_HEALTH_RECOVERY_THRESHOLD 3
9
10typedef struct {
11 char url[256];
12 bool reachable;
13 uint8_t consecutive_successes;
14 int64_t last_probe_ms;
15 int last_http_status;
16 int last_err;
17} tollgate_mint_status_t;
18
19typedef struct {
20 tollgate_mint_status_t mints[TG_MINT_HEALTH_MAX];
21 int count;
22} tollgate_mint_health_t;
23
24typedef void (*tollgate_mint_health_changed_fn)(int old_reachable, int new_reachable);
25
26void tollgate_core_mint_health_init(const char urls[][256], int count);
27
28void tollgate_core_mint_health_update(tollgate_mint_health_t *state,
29 int mint_index,
30 bool probe_ok,
31 int http_status,
32 int err_code,
33 int64_t probe_time_ms);
34
35void tollgate_core_mint_health_update_initial(tollgate_mint_health_t *state,
36 int mint_index,
37 bool probe_ok,
38 int http_status,
39 int err_code,
40 int64_t probe_time_ms);
41
42bool tollgate_core_mint_health_is_reachable(const tollgate_mint_health_t *state,
43 const char *url);
44
45void tollgate_core_mint_health_mark_unreachable(tollgate_mint_health_t *state,
46 const char *url);
47
48int tollgate_core_mint_health_count_reachable(const tollgate_mint_health_t *state);
49
50#endif
diff --git a/components/tollgate_core/src/tollgate_core_portal.c b/components/tollgate_core/src/tollgate_core_portal.c
new file mode 100644
index 0000000..ef2ae1e
--- /dev/null
+++ b/components/tollgate_core/src/tollgate_core_portal.c
@@ -0,0 +1,149 @@
1#include "tollgate_core_portal.h"
2#include <string.h>
3#include <stdio.h>
4#include <stdlib.h>
5
6static const char *CAPTIVE_URIS[] = {
7 "/generate_204",
8 "/hotspot-detect.html",
9 "/canonical.html",
10 "/success.txt",
11 "/ncsi.txt",
12 "/connecttest.txt",
13 "/wpad.dat",
14 NULL
15};
16
17char *tollgate_core_portal_template_replace(const char *tpl, const char *key, const char *val)
18{
19 const char *p;
20 size_t klen = strlen(key);
21 size_t vlen = strlen(val);
22 size_t tlen = strlen(tpl);
23 size_t extra = 0;
24
25 p = tpl;
26 while ((p = strstr(p, key)) != NULL) {
27 extra += vlen - klen;
28 p += klen;
29 }
30
31 size_t out_size = tlen + extra + 1;
32 char *out = malloc(out_size);
33 if (!out) return NULL;
34
35 char *dst = out;
36 p = tpl;
37 while (*p) {
38 const char *found = strstr(p, key);
39 if (found) {
40 memcpy(dst, p, found - p);
41 dst += found - p;
42 memcpy(dst, val, vlen);
43 dst += vlen;
44 p = found + klen;
45 } else {
46 strcpy(dst, p);
47 dst += strlen(p);
48 break;
49 }
50 }
51 *dst = '\0';
52 return out;
53}
54
55char *tollgate_core_portal_render(const char *tpl,
56 const tollgate_portal_sub_t *subs,
57 int nsubs)
58{
59 size_t tpl_len = strlen(tpl);
60
61 size_t extra = 0;
62 for (int i = 0; i < nsubs; i++) {
63 const char *p = tpl;
64 size_t klen = strlen(subs[i].key);
65 while ((p = strstr(p, subs[i].key)) != NULL) {
66 extra += strlen(subs[i].val) - klen;
67 p += klen;
68 }
69 }
70
71 size_t out_size = tpl_len + extra + 1;
72 char *html = malloc(out_size);
73 if (!html) return NULL;
74
75 char *out = html;
76 const char *src = tpl;
77 while (*src) {
78 const char *earliest = NULL;
79 int ei = -1;
80 for (int i = 0; i < nsubs; i++) {
81 const char *found = strstr(src, subs[i].key);
82 if (found && (earliest == NULL || found < earliest)) {
83 earliest = found;
84 ei = i;
85 }
86 }
87 if (earliest) {
88 size_t vlen = strlen(subs[ei].val);
89 memcpy(out, src, earliest - src);
90 out += earliest - src;
91 memcpy(out, subs[ei].val, vlen);
92 out += vlen;
93 src = earliest + strlen(subs[ei].key);
94 } else {
95 strcpy(out, src);
96 out += strlen(src);
97 break;
98 }
99 }
100 *out = '\0';
101 return html;
102}
103
104bool tollgate_core_portal_calc_usage(const tg_session_t *session,
105 const char *metric,
106 int64_t now_ms,
107 int64_t *remaining_out,
108 int64_t *total_out)
109{
110 if (!session || !session->active) return false;
111
112 bool is_bytes = (strcmp(metric, "bytes") == 0);
113
114 if (is_bytes) {
115 int64_t remaining = (int64_t)session->allotment_bytes - (int64_t)session->bytes_consumed;
116 if (remaining < 0) remaining = 0;
117 *remaining_out = remaining;
118 *total_out = (int64_t)session->allotment_bytes;
119 } else {
120 int64_t elapsed = now_ms - session->start_time_ms;
121 int64_t remaining = (int64_t)session->allotment_ms - elapsed;
122 if (remaining < 0) remaining = 0;
123 *remaining_out = remaining;
124 *total_out = (int64_t)session->allotment_ms;
125 }
126 return true;
127}
128
129int tollgate_core_portal_format_usage(const tg_session_t *session,
130 const char *metric,
131 int64_t now_ms,
132 char *buf,
133 size_t buf_size)
134{
135 int64_t remaining = 0, total = 0;
136 if (!tollgate_core_portal_calc_usage(session, metric, now_ms, &remaining, &total)) {
137 return snprintf(buf, buf_size, "-1/-1");
138 }
139 return snprintf(buf, buf_size, "%lld/%llu", (long long)remaining, (unsigned long long)total);
140}
141
142bool tollgate_core_portal_is_captive_uri(const char *uri)
143{
144 if (!uri) return false;
145 for (int i = 0; CAPTIVE_URIS[i] != NULL; i++) {
146 if (strcmp(uri, CAPTIVE_URIS[i]) == 0) return true;
147 }
148 return false;
149}
diff --git a/components/tollgate_core/src/tollgate_core_portal.h b/components/tollgate_core/src/tollgate_core_portal.h
new file mode 100644
index 0000000..20c0552
--- /dev/null
+++ b/components/tollgate_core/src/tollgate_core_portal.h
@@ -0,0 +1,34 @@
1#ifndef TOLLGATE_CORE_PORTAL_H
2#define TOLLGATE_CORE_PORTAL_H
3
4#include "tollgate_core_session.h"
5#include <stdint.h>
6#include <stddef.h>
7#include <stdbool.h>
8
9typedef struct {
10 const char *key;
11 const char *val;
12} tollgate_portal_sub_t;
13
14char *tollgate_core_portal_template_replace(const char *tpl, const char *key, const char *val);
15
16char *tollgate_core_portal_render(const char *tpl,
17 const tollgate_portal_sub_t *subs,
18 int nsubs);
19
20bool tollgate_core_portal_calc_usage(const tg_session_t *session,
21 const char *metric,
22 int64_t now_ms,
23 int64_t *remaining_out,
24 int64_t *total_out);
25
26int tollgate_core_portal_format_usage(const tg_session_t *session,
27 const char *metric,
28 int64_t now_ms,
29 char *buf,
30 size_t buf_size);
31
32bool tollgate_core_portal_is_captive_uri(const char *uri);
33
34#endif
diff --git a/components/tollgate_core/src/tollgate_core_stratum_client.c b/components/tollgate_core/src/tollgate_core_stratum_client.c
new file mode 100644
index 0000000..8cf22da
--- /dev/null
+++ b/components/tollgate_core/src/tollgate_core_stratum_client.c
@@ -0,0 +1,91 @@
1#include "tollgate_core_stratum_client.h"
2#include "cJSON.h"
3#include <string.h>
4#include <stdio.h>
5#include <stdlib.h>
6
7void tollgate_core_stratum_hex_to_bytes(const char *hex, uint8_t *out, int len)
8{
9 for (int i = 0; i < len && hex[i * 2] && hex[i * 2 + 1]; i++) {
10 char byte[3] = {hex[i * 2], hex[i * 2 + 1], 0};
11 out[i] = (uint8_t)strtoul(byte, NULL, 16);
12 }
13}
14
15bool tollgate_core_stratum_parse_notify(const void *params_json,
16 tollgate_stratum_job_t *job,
17 uint32_t *out_nbits)
18{
19 const cJSON *params = (const cJSON *)params_json;
20 if (!params || !cJSON_IsArray(params) || cJSON_GetArraySize(params) < 6) return false;
21
22 const cJSON *p_job_id = cJSON_GetArrayItem(params, 0);
23 const cJSON *p_prevhash = cJSON_GetArrayItem(params, 1);
24 const cJSON *p_version = cJSON_GetArrayItem(params, 5);
25 const cJSON *p_nbits = cJSON_GetArrayItem(params, 6);
26 const cJSON *p_ntime = cJSON_GetArrayItem(params, 7);
27
28 if (!p_job_id || !p_prevhash || !p_nbits) return false;
29
30 memset(job, 0, sizeof(*job));
31 job->job_id = (uint32_t)atoi(p_job_id->valuestring);
32 job->valid = true;
33
34 tollgate_core_stratum_hex_to_bytes(p_prevhash->valuestring, job->prevhash, 32);
35
36 if (p_version && cJSON_IsString(p_version)) {
37 job->version = (uint32_t)strtoul(p_version->valuestring, NULL, 16);
38 }
39 if (p_nbits && cJSON_IsString(p_nbits)) {
40 job->nbits = (uint32_t)strtoul(p_nbits->valuestring, NULL, 16);
41 if (out_nbits) *out_nbits = job->nbits;
42 }
43 if (p_ntime && cJSON_IsString(p_ntime)) {
44 job->ntime = (uint32_t)strtoul(p_ntime->valuestring, NULL, 16);
45 }
46
47 memset(job->target, 0xFF, 32);
48 job->target_len = 32;
49
50 return true;
51}
52
53bool tollgate_core_stratum_parse_difficulty(const void *params_json,
54 uint64_t *difficulty_out)
55{
56 const cJSON *params = (const cJSON *)params_json;
57 if (!params || !cJSON_IsArray(params) || cJSON_GetArraySize(params) < 1) return false;
58
59 const cJSON *diff = cJSON_GetArrayItem(params, 0);
60 if (diff && cJSON_IsNumber(diff)) {
61 *difficulty_out = (uint64_t)diff->valuedouble;
62 return true;
63 }
64 return false;
65}
66
67int tollgate_core_stratum_build_subscribe(char *buf, size_t buf_size, uint32_t req_id)
68{
69 return snprintf(buf, buf_size,
70 "{\"id\":%lu,\"method\":\"mining.subscribe\",\"params\":[\"TollGate/1.0\"]}\n",
71 (unsigned long)req_id);
72}
73
74int tollgate_core_stratum_build_authorize(char *buf, size_t buf_size, uint32_t req_id,
75 const char *user, const char *pass)
76{
77 return snprintf(buf, buf_size,
78 "{\"id\":%lu,\"method\":\"mining.authorize\",\"params\":[\"%s\",\"%s\"]}\n",
79 (unsigned long)req_id, user, pass);
80}
81
82int tollgate_core_stratum_build_submit(char *buf, size_t buf_size, uint32_t req_id,
83 const char *user, uint32_t job_id,
84 uint32_t ntime, uint32_t nonce, uint32_t version)
85{
86 return snprintf(buf, buf_size,
87 "{\"id\":%lu,\"method\":\"mining.submit\",\"params\":[\"%s\",\"%lu\",\"%08lx\",\"%08lx\",\"%08lx\"]}\n",
88 (unsigned long)req_id, user,
89 (unsigned long)job_id, (unsigned long)ntime,
90 (unsigned long)nonce, (unsigned long)version);
91}
diff --git a/components/tollgate_core/src/tollgate_core_stratum_client.h b/components/tollgate_core/src/tollgate_core_stratum_client.h
new file mode 100644
index 0000000..fdcce1c
--- /dev/null
+++ b/components/tollgate_core/src/tollgate_core_stratum_client.h
@@ -0,0 +1,50 @@
1#ifndef TOLLGATE_CORE_STRATUM_CLIENT_H
2#define TOLLGATE_CORE_STRATUM_CLIENT_H
3
4#include <stdint.h>
5#include <stddef.h>
6#include <stdbool.h>
7
8#define TG_STRATUM_MAX_JOB_ID_LEN 32
9
10typedef struct {
11 uint32_t job_id;
12 uint8_t prevhash[32];
13 uint8_t merkle_root[32];
14 uint32_t ntime;
15 uint32_t nbits;
16 uint32_t version;
17 uint8_t target[32];
18 int target_len;
19 bool valid;
20} tollgate_stratum_job_t;
21
22typedef struct {
23 bool connected;
24 char pool_host[128];
25 uint16_t pool_port;
26 uint32_t nbits;
27 uint64_t difficulty;
28 uint64_t shares_accepted;
29 uint64_t shares_rejected;
30} tollgate_stratum_client_state_t;
31
32void tollgate_core_stratum_hex_to_bytes(const char *hex, uint8_t *out, int len);
33
34bool tollgate_core_stratum_parse_notify(const void *params_json,
35 tollgate_stratum_job_t *job,
36 uint32_t *out_nbits);
37
38bool tollgate_core_stratum_parse_difficulty(const void *params_json,
39 uint64_t *difficulty_out);
40
41int tollgate_core_stratum_build_subscribe(char *buf, size_t buf_size, uint32_t req_id);
42
43int tollgate_core_stratum_build_authorize(char *buf, size_t buf_size, uint32_t req_id,
44 const char *user, const char *pass);
45
46int tollgate_core_stratum_build_submit(char *buf, size_t buf_size, uint32_t req_id,
47 const char *user, uint32_t job_id,
48 uint32_t ntime, uint32_t nonce, uint32_t version);
49
50#endif
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);
diff --git a/tests/unit/Makefile b/tests/unit/Makefile
index 9347516..591ec0f 100644
--- a/tests/unit/Makefile
+++ b/tests/unit/Makefile
@@ -26,7 +26,7 @@ LDFLAGS := -lmbedcrypto -lcjson -lm
26 26
27SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o 27SECP256K1_OBJ := secp256k1.o precomputed_ecmult.o precomputed_ecmult_gen.o
28 28
29TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 test_cvm_server test_beacon_price test_market test_mint_health test_mining_payment test_stratum_proxy test_session_payment_method test_tollgate_client_mining test_firewall_sandbox test_relay_selector test_relay_validator 29TESTS := test_geohash test_identity test_nostr_event test_cashu test_session test_tollgate_client test_lnurl_pay test_lightning_payout test_mcp_handler test_nip04 test_cvm_server test_beacon_price test_market test_mint_health test_mining_payment test_stratum_proxy test_session_payment_method test_tollgate_client_mining test_firewall_sandbox test_relay_selector test_relay_validator test_portal test_stratum_client test_mint_health_core test_client_core
30 30
31.PHONY: all test clean $(TESTS) 31.PHONY: all test clean $(TESTS)
32 32
@@ -69,8 +69,8 @@ TG_CORE_OBJS_CFLAGS := -include stubs/mint_health.h -include stubs/esp_netif.h -
69test_session: test_session.c $(TG_CORE_SRC)/tollgate_core_cashu.c $(TG_CORE_SRC)/tollgate_core_session.c $(TG_CORE_SRC)/tollgate_core_firewall.c $(TG_CORE_SRC)/tollgate_core.c $(TG_CORE_SRC)/tollgate_core_mining.c $(TG_CORE_SRC)/tollgate_core_dns.c $(TG_CORE_SRC)/tollgate_core_stratum_proxy.c 69test_session: test_session.c $(TG_CORE_SRC)/tollgate_core_cashu.c $(TG_CORE_SRC)/tollgate_core_session.c $(TG_CORE_SRC)/tollgate_core_firewall.c $(TG_CORE_SRC)/tollgate_core.c $(TG_CORE_SRC)/tollgate_core_mining.c $(TG_CORE_SRC)/tollgate_core_dns.c $(TG_CORE_SRC)/tollgate_core_stratum_proxy.c
70 $(CC) $(CFLAGS) $(TG_CORE_OBJS_CFLAGS) $^ -o $@ $(LDFLAGS) 70 $(CC) $(CFLAGS) $(TG_CORE_OBJS_CFLAGS) $^ -o $@ $(LDFLAGS)
71 71
72test_tollgate_client: test_tollgate_client.c $(REPO_ROOT)/main/market.c $(REPO_ROOT)/main/beacon_price.c $(TG_CORE_SRC)/tollgate_core_beacon.c $(TG_CORE_SRC)/tollgate_core_market.c 72test_tollgate_client: test_tollgate_client.c $(REPO_ROOT)/main/market.c $(REPO_ROOT)/main/beacon_price.c $(TG_CORE_SRC)/tollgate_core_beacon.c $(TG_CORE_SRC)/tollgate_core_market.c $(TG_CORE_SRC)/tollgate_core_client.c
73 $(CC) $(CFLAGS) -I $(REPO_ROOT)/main $< $(REPO_ROOT)/main/market.c $(REPO_ROOT)/main/beacon_price.c $(TG_CORE_SRC)/tollgate_core_beacon.c $(TG_CORE_SRC)/tollgate_core_market.c -o $@ $(LDFLAGS) 73 $(CC) $(CFLAGS) -I $(REPO_ROOT)/main $< $(REPO_ROOT)/main/market.c $(REPO_ROOT)/main/beacon_price.c $(TG_CORE_SRC)/tollgate_core_beacon.c $(TG_CORE_SRC)/tollgate_core_market.c $(TG_CORE_SRC)/tollgate_core_client.c -o $@ $(LDFLAGS)
74 74
75test_lnurl_pay: test_lnurl_pay.c 75test_lnurl_pay: test_lnurl_pay.c
76 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) 76 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
@@ -87,8 +87,8 @@ test_nip04: test_nip04.c $(REPO_ROOT)/main/nip04.c $(SECP256K1_OBJ)
87test_cvm_server: test_cvm_server.c 87test_cvm_server: test_cvm_server.c
88 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) 88 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
89 89
90test_mint_health: test_mint_health.c $(REPO_ROOT)/main/mint_health.c 90test_mint_health: test_mint_health.c $(REPO_ROOT)/main/mint_health.c $(TG_CORE_SRC)/tollgate_core_mint_health.c
91 $(CC) -Wall -Wextra -Wno-unused-parameter -Wno-unused-function -Wno-sign-compare -std=gnu17 -g -O0 -DTEST_HOST -include stubs/esp_err.h -I $(REPO_ROOT)/main -I stubs -I $(SECP256K1_INC) -I $(SECP256K1_CFG) -I /usr/include/cjson $< $(REPO_ROOT)/main/mint_health.c -o $@ $(LDFLAGS) 91 $(CC) -Wall -Wextra -Wno-unused-parameter -Wno-unused-function -Wno-sign-compare -std=gnu17 -g -O0 -DTEST_HOST -include stubs/esp_err.h -I $(REPO_ROOT)/main -I stubs -I $(SECP256K1_INC) -I $(SECP256K1_CFG) -I /usr/include/cjson -I $(TG_CORE_SRC) -I $(TG_CORE_INC) $^ -o $@ $(LDFLAGS)
92 92
93test_beacon_price: test_beacon_price.c $(REPO_ROOT)/main/beacon_price.c $(TG_CORE_SRC)/tollgate_core_beacon.c 93test_beacon_price: test_beacon_price.c $(REPO_ROOT)/main/beacon_price.c $(TG_CORE_SRC)/tollgate_core_beacon.c
94 $(CC) $(CFLAGS) -I $(REPO_ROOT)/main $< $(REPO_ROOT)/main/beacon_price.c $(TG_CORE_SRC)/tollgate_core_beacon.c -o $@ $(LDFLAGS) 94 $(CC) $(CFLAGS) -I $(REPO_ROOT)/main $< $(REPO_ROOT)/main/beacon_price.c $(TG_CORE_SRC)/tollgate_core_beacon.c -o $@ $(LDFLAGS)
@@ -105,8 +105,8 @@ test_stratum_proxy: test_stratum_proxy.c $(REPO_ROOT)/main/stratum_proxy.c $(TG_
105test_session_payment_method: test_session_payment_method.c $(TG_CORE_SRC)/tollgate_core_cashu.c $(TG_CORE_SRC)/tollgate_core_session.c $(TG_CORE_SRC)/tollgate_core_firewall.c $(TG_CORE_SRC)/tollgate_core.c $(TG_CORE_SRC)/tollgate_core_mining.c $(TG_CORE_SRC)/tollgate_core_dns.c $(TG_CORE_SRC)/tollgate_core_stratum_proxy.c 105test_session_payment_method: test_session_payment_method.c $(TG_CORE_SRC)/tollgate_core_cashu.c $(TG_CORE_SRC)/tollgate_core_session.c $(TG_CORE_SRC)/tollgate_core_firewall.c $(TG_CORE_SRC)/tollgate_core.c $(TG_CORE_SRC)/tollgate_core_mining.c $(TG_CORE_SRC)/tollgate_core_dns.c $(TG_CORE_SRC)/tollgate_core_stratum_proxy.c
106 $(CC) $(CFLAGS) $(TG_CORE_OBJS_CFLAGS) $^ -o $@ $(LDFLAGS) 106 $(CC) $(CFLAGS) $(TG_CORE_OBJS_CFLAGS) $^ -o $@ $(LDFLAGS)
107 107
108test_tollgate_client_mining: test_tollgate_client_mining.c 108test_tollgate_client_mining: test_tollgate_client_mining.c $(TG_CORE_SRC)/tollgate_core_client.c
109 $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) 109 $(CC) $(CFLAGS) -I $(REPO_ROOT)/main $< $(TG_CORE_SRC)/tollgate_core_client.c -o $@ $(LDFLAGS)
110 110
111test_firewall_sandbox: test_firewall_sandbox.c $(TG_CORE_SRC)/tollgate_core_firewall.c $(TG_CORE_SRC)/tollgate_core.c $(TG_CORE_SRC)/tollgate_core_session.c $(TG_CORE_SRC)/tollgate_core_cashu.c $(TG_CORE_SRC)/tollgate_core_mining.c $(TG_CORE_SRC)/tollgate_core_dns.c $(TG_CORE_SRC)/tollgate_core_stratum_proxy.c 111test_firewall_sandbox: test_firewall_sandbox.c $(TG_CORE_SRC)/tollgate_core_firewall.c $(TG_CORE_SRC)/tollgate_core.c $(TG_CORE_SRC)/tollgate_core_session.c $(TG_CORE_SRC)/tollgate_core_cashu.c $(TG_CORE_SRC)/tollgate_core_mining.c $(TG_CORE_SRC)/tollgate_core_dns.c $(TG_CORE_SRC)/tollgate_core_stratum_proxy.c
112 $(CC) $(CFLAGS) -include stubs/dns_server.h $(TG_CORE_OBJS_CFLAGS) $^ -o $@ $(LDFLAGS) 112 $(CC) $(CFLAGS) -include stubs/dns_server.h $(TG_CORE_OBJS_CFLAGS) $^ -o $@ $(LDFLAGS)
@@ -117,5 +117,17 @@ test_relay_selector: test_relay_selector.c
117test_relay_validator: test_relay_validator.c $(REPO_ROOT)/main/nostr_event.c $(REPO_ROOT)/main/identity.c $(REPO_ROOT)/components/wisp_relay/relay_validator.c $(SECP256K1_OBJ) 117test_relay_validator: test_relay_validator.c $(REPO_ROOT)/main/nostr_event.c $(REPO_ROOT)/main/identity.c $(REPO_ROOT)/components/wisp_relay/relay_validator.c $(SECP256K1_OBJ)
118 $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) -I $(REPO_ROOT)/main -I $(REPO_ROOT)/components/wisp_relay $< $(REPO_ROOT)/main/nostr_event.c $(REPO_ROOT)/main/identity.c $(REPO_ROOT)/components/wisp_relay/relay_validator.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS) 118 $(CC) $(CFLAGS) -I $(SECP256K1_PRIV_INC) -I $(REPO_ROOT)/main -I $(REPO_ROOT)/components/wisp_relay $< $(REPO_ROOT)/main/nostr_event.c $(REPO_ROOT)/main/identity.c $(REPO_ROOT)/components/wisp_relay/relay_validator.c $(SECP256K1_OBJ) -o $@ $(LDFLAGS)
119 119
120test_portal: test_portal.c $(TG_CORE_SRC)/tollgate_core_portal.c
121 $(CC) $(CFLAGS) $(TG_CORE_OBJS_CFLAGS) $^ -o $@ $(LDFLAGS)
122
123test_stratum_client: test_stratum_client.c $(TG_CORE_SRC)/tollgate_core_stratum_client.c
124 $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
125
126test_mint_health_core: test_mint_health_core.c $(TG_CORE_SRC)/tollgate_core_mint_health.c
127 $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
128
129test_client_core: test_client_core.c $(TG_CORE_SRC)/tollgate_core_client.c
130 $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
131
120clean: 132clean:
121 rm -f $(TESTS) $(SECP256K1_OBJ) 133 rm -f $(TESTS) $(SECP256K1_OBJ)
diff --git a/tests/unit/test_client_core.c b/tests/unit/test_client_core.c
new file mode 100644
index 0000000..7cc6cc7
--- /dev/null
+++ b/tests/unit/test_client_core.c
@@ -0,0 +1,189 @@
1#include "test_framework.h"
2#include "../../components/tollgate_core/src/tollgate_core_client.h"
3#include <cjson/cJSON.h>
4#include <string.h>
5#include <stdio.h>
6
7int main(void)
8{
9 printf("=== test_client_core ===\n");
10
11 printf("\n--- parse_discovery valid ---\n");
12 {
13 const char *json = "{\"kind\":10021,\"content\":\"discovery\","
14 "\"tags\":["
15 "[\"metric\",\"milliseconds\"],"
16 "[\"step_size\",\"60000\"],"
17 "[\"price_per_step\",\"21\",\"21\",\"sha256\",\"https://test.mint\"]"
18 "]}";
19 tollgate_discovery_t disc = {0};
20 bool ok = tollgate_core_client_parse_discovery(json, &disc);
21 ASSERT(ok, "parse_discovery returns true");
22 ASSERT(disc.is_tollgate, "is_tollgate set");
23 ASSERT_EQ_INT(21, disc.price_per_step, "price_per_step");
24 ASSERT_EQ_INT(60000, disc.step_size_ms, "step_size_ms");
25 ASSERT(strcmp(disc.metric, "milliseconds") == 0, "metric");
26 ASSERT(strcmp(disc.mint_url, "https://test.mint") == 0, "mint_url");
27 }
28
29 printf("\n--- parse_discovery wrong kind ---\n");
30 {
31 const char *json = "{\"kind\":9999}";
32 tollgate_discovery_t disc = {0};
33 bool ok = tollgate_core_client_parse_discovery(json, &disc);
34 ASSERT(!ok, "wrong kind returns false");
35 }
36
37 printf("\n--- parse_discovery mining payment ---\n");
38 {
39 const char *json = "{\"kind\":10021,\"content\":\"\","
40 "\"tags\":["
41 "[\"metric\",\"milliseconds\"],"
42 "[\"step_size\",\"60000\"],"
43 "[\"price_per_step\",\"0\",\"mining\",\"3333\",\"extra\"]"
44 "]}";
45 tollgate_discovery_t disc = {0};
46 bool ok = tollgate_core_client_parse_discovery(json, &disc);
47 ASSERT(ok, "parse_discovery mining returns true");
48 ASSERT(disc.mining_available, "mining_available set");
49 ASSERT_EQ_INT(3333, (int)disc.mining_port, "mining_port");
50 }
51
52 printf("\n--- parse_discovery empty tags ---\n");
53 {
54 const char *json = "{\"kind\":10021,\"tags\":[]}";
55 tollgate_discovery_t disc = {0};
56 bool ok = tollgate_core_client_parse_discovery(json, &disc);
57 ASSERT(ok, "empty tags returns true");
58 ASSERT(disc.is_tollgate, "is_tollgate set");
59 }
60
61 printf("\n--- parse_discovery no tags ---\n");
62 {
63 const char *json = "{\"kind\":10021}";
64 tollgate_discovery_t disc = {0};
65 bool ok = tollgate_core_client_parse_discovery(json, &disc);
66 ASSERT(ok, "no tags returns true");
67 }
68
69 printf("\n--- parse_discovery invalid JSON ---\n");
70 {
71 tollgate_discovery_t disc = {0};
72 bool ok = tollgate_core_client_parse_discovery("not json", &disc);
73 ASSERT(!ok, "invalid JSON returns false");
74 }
75
76 printf("\n--- parse_session valid ---\n");
77 {
78 const char *json = "{\"kind\":1022,\"content\":\"session\","
79 "\"tags\":[[\"allotment\",\"120000\"]]}";
80 int64_t allotment = 0;
81 bool ok = tollgate_core_client_parse_session(json, &allotment);
82 ASSERT(ok, "parse_session returns true");
83 ASSERT_EQ_UINT64(120000, (unsigned long long)allotment, "allotment_ms");
84 }
85
86 printf("\n--- parse_session wrong kind ---\n");
87 {
88 const char *json = "{\"kind\":10021}";
89 int64_t allotment = 0;
90 bool ok = tollgate_core_client_parse_session(json, &allotment);
91 ASSERT(!ok, "wrong kind returns false");
92 }
93
94 printf("\n--- parse_session no allotment tag ---\n");
95 {
96 const char *json = "{\"kind\":1022,\"tags\":[]}";
97 int64_t allotment = 999;
98 bool ok = tollgate_core_client_parse_session(json, &allotment);
99 ASSERT(ok, "returns true");
100 ASSERT_EQ_UINT64(999, (unsigned long long)allotment, "allotment unchanged when no tag");
101 }
102
103 printf("\n--- parse_usage valid ---\n");
104 {
105 int64_t remaining = 0, total = 0;
106 bool ok = tollgate_core_client_parse_usage("45000/60000", &remaining, &total);
107 ASSERT(ok, "parse_usage returns true");
108 ASSERT_EQ_UINT64(45000, (unsigned long long)remaining, "remaining");
109 ASSERT_EQ_UINT64(60000, (unsigned long long)total, "total");
110 }
111
112 printf("\n--- parse_usage no slash ---\n");
113 {
114 int64_t remaining = 0, total = 0;
115 bool ok = tollgate_core_client_parse_usage("45000", &remaining, &total);
116 ASSERT(!ok, "no slash returns false");
117 }
118
119 printf("\n--- parse_usage zero values ---\n");
120 {
121 int64_t remaining = 0, total = 0;
122 bool ok = tollgate_core_client_parse_usage("0/0", &remaining, &total);
123 ASSERT(ok, "parse_usage zero values");
124 ASSERT_EQ_UINT64(0, (unsigned long long)remaining, "remaining is 0");
125 ASSERT_EQ_UINT64(0, (unsigned long long)total, "total is 0");
126 }
127
128 printf("\n--- should_renew ---\n");
129 {
130 ASSERT(tollgate_core_client_should_renew(1000, 60000, 20),
131 "1.6% remaining → should renew");
132 ASSERT(tollgate_core_client_should_renew(0, 60000, 20),
133 "0% remaining → should renew");
134 ASSERT(!tollgate_core_client_should_renew(30000, 60000, 20),
135 "50% remaining → should not renew");
136 ASSERT(!tollgate_core_client_should_renew(60000, 60000, 20),
137 "100% remaining → should not renew");
138 ASSERT(tollgate_core_client_should_renew(11999, 60000, 20),
139 "19.99% → should renew");
140 ASSERT(tollgate_core_client_should_renew(12000, 60000, 20),
141 "20% exactly at threshold → should renew");
142 }
143
144 printf("\n--- should_renew default threshold ---\n");
145 {
146 ASSERT(tollgate_core_client_should_renew(1000, 60000, 0),
147 "threshold=0 defaults to 20%");
148 ASSERT(tollgate_core_client_should_renew(1000, 60000, -1),
149 "negative threshold defaults to 20%");
150 }
151
152 printf("\n--- should_renew edge cases ---\n");
153 {
154 ASSERT(!tollgate_core_client_should_renew(-1, 60000, 20),
155 "negative remaining → false");
156 ASSERT(!tollgate_core_client_should_renew(1000, 0, 20),
157 "zero allotment → false");
158 }
159
160 printf("\n--- calc_price_per_min ---\n");
161 {
162 ASSERT_EQ_INT(21, tollgate_core_client_calc_price_per_min(21, 60000),
163 "21 sats/60s = 21 sats/min");
164 ASSERT_EQ_INT(42, tollgate_core_client_calc_price_per_min(21, 30000),
165 "21 sats/30s = 42 sats/min");
166 ASSERT_EQ_INT(21000, tollgate_core_client_calc_price_per_min(21, 60),
167 "21 sats/60ms effective");
168 }
169
170 printf("\n--- calc_price_per_min zero step ---\n");
171 {
172 int result = tollgate_core_client_calc_price_per_min(21, 0);
173 ASSERT(result > 0, "zero step_size_ms doesn't crash");
174 }
175
176 printf("\n--- calc_steps ---\n");
177 {
178 ASSERT_EQ_INT(21, tollgate_core_client_calc_steps(1, 21, 21),
179 "1 step * 21 = 21");
180 ASSERT_EQ_INT(42, tollgate_core_client_calc_steps(2, 21, 21),
181 "2 steps * 21 = 42");
182 ASSERT_EQ_INT(21, tollgate_core_client_calc_steps(0, 21, 21),
183 "0 steps defaults to 1");
184 ASSERT_EQ_INT(21, tollgate_core_client_calc_steps(-1, 21, 21),
185 "negative steps defaults to 1");
186 }
187
188 TEST_SUMMARY();
189}
diff --git a/tests/unit/test_mint_health_core.c b/tests/unit/test_mint_health_core.c
new file mode 100644
index 0000000..9727edb
--- /dev/null
+++ b/tests/unit/test_mint_health_core.c
@@ -0,0 +1,130 @@
1#include "test_framework.h"
2#include "../../components/tollgate_core/src/tollgate_core_mint_health.h"
3#include <string.h>
4#include <stdio.h>
5
6int main(void)
7{
8 printf("=== test_mint_health_core ===\n");
9
10 printf("\n--- update: probe success ---\n");
11 {
12 tollgate_mint_health_t state = {0};
13 strncpy(state.mints[0].url, "https://test.mint", sizeof(state.mints[0].url) - 1);
14 state.count = 1;
15
16 tollgate_core_mint_health_update(&state, 0, true, 200, 0, 1000);
17 ASSERT_EQ_INT(1, state.mints[0].consecutive_successes, "1st success");
18 ASSERT(!state.mints[0].reachable, "not yet reachable (threshold=3)");
19
20 tollgate_core_mint_health_update(&state, 0, true, 200, 0, 2000);
21 ASSERT_EQ_INT(2, state.mints[0].consecutive_successes, "2nd success");
22 ASSERT(!state.mints[0].reachable, "still not reachable");
23
24 tollgate_core_mint_health_update(&state, 0, true, 200, 0, 3000);
25 ASSERT_EQ_INT(3, state.mints[0].consecutive_successes, "3rd success");
26 ASSERT(state.mints[0].reachable, "now reachable after threshold");
27 }
28
29 printf("\n--- update: failure resets ---\n");
30 {
31 tollgate_mint_health_t state = {0};
32 strncpy(state.mints[0].url, "https://test.mint", sizeof(state.mints[0].url) - 1);
33 state.count = 1;
34 state.mints[0].reachable = true;
35 state.mints[0].consecutive_successes = 5;
36
37 tollgate_core_mint_health_update(&state, 0, false, 0, -1, 1000);
38 ASSERT(!state.mints[0].reachable, "failure marks unreachable");
39 ASSERT_EQ_INT(0, state.mints[0].consecutive_successes, "successes reset to 0");
40 ASSERT_EQ_INT(-1, state.mints[0].last_err, "error code recorded");
41 }
42
43 printf("\n--- update_initial: success ---\n");
44 {
45 tollgate_mint_health_t state = {0};
46 strncpy(state.mints[0].url, "https://test.mint", sizeof(state.mints[0].url) - 1);
47 state.count = 1;
48
49 tollgate_core_mint_health_update_initial(&state, 0, true, 200, 0, 1000);
50 ASSERT(state.mints[0].reachable, "immediately reachable on initial success");
51 ASSERT_EQ_INT(TG_MINT_HEALTH_RECOVERY_THRESHOLD,
52 state.mints[0].consecutive_successes, "successes set to threshold");
53 }
54
55 printf("\n--- update_initial: failure ---\n");
56 {
57 tollgate_mint_health_t state = {0};
58 strncpy(state.mints[0].url, "https://test.mint", sizeof(state.mints[0].url) - 1);
59 state.count = 1;
60
61 tollgate_core_mint_health_update_initial(&state, 0, false, 0, -2, 1000);
62 ASSERT(!state.mints[0].reachable, "not reachable on initial failure");
63 ASSERT_EQ_INT(0, state.mints[0].consecutive_successes, "successes is 0");
64 }
65
66 printf("\n--- is_reachable ---\n");
67 {
68 tollgate_mint_health_t state = {0};
69 strncpy(state.mints[0].url, "https://test.mint", sizeof(state.mints[0].url) - 1);
70 state.count = 1;
71 state.mints[0].reachable = true;
72
73 ASSERT(tollgate_core_mint_health_is_reachable(&state, "https://test.mint"),
74 "exact URL match");
75 ASSERT(tollgate_core_mint_health_is_reachable(&state, "https://test.mint/v1/info"),
76 "substring match");
77 ASSERT(!tollgate_core_mint_health_is_reachable(&state, "https://other.mint"),
78 "no match for other URL");
79 ASSERT(!tollgate_core_mint_health_is_reachable(NULL, "https://test.mint"),
80 "NULL state");
81 ASSERT(!tollgate_core_mint_health_is_reachable(&state, NULL),
82 "NULL url");
83 }
84
85 printf("\n--- mark_unreachable ---\n");
86 {
87 tollgate_mint_health_t state = {0};
88 strncpy(state.mints[0].url, "https://test.mint", sizeof(state.mints[0].url) - 1);
89 state.count = 1;
90 state.mints[0].reachable = true;
91 state.mints[0].consecutive_successes = 5;
92
93 tollgate_core_mint_health_mark_unreachable(&state, "https://test.mint");
94 ASSERT(!state.mints[0].reachable, "marked unreachable");
95 ASSERT_EQ_INT(0, state.mints[0].consecutive_successes, "successes cleared");
96
97 tollgate_core_mint_health_mark_unreachable(&state, "https://test.mint");
98 ASSERT(!state.mints[0].reachable, "idempotent");
99 }
100
101 printf("\n--- count_reachable ---\n");
102 {
103 tollgate_mint_health_t state = {0};
104 state.count = 3;
105 strncpy(state.mints[0].url, "https://a.mint", sizeof(state.mints[0].url) - 1);
106 strncpy(state.mints[1].url, "https://b.mint", sizeof(state.mints[1].url) - 1);
107 strncpy(state.mints[2].url, "https://c.mint", sizeof(state.mints[2].url) - 1);
108 state.mints[0].reachable = true;
109 state.mints[1].reachable = false;
110 state.mints[2].reachable = true;
111
112 ASSERT_EQ_INT(2, tollgate_core_mint_health_count_reachable(&state),
113 "2 out of 3 reachable");
114 ASSERT_EQ_INT(0, tollgate_core_mint_health_count_reachable(NULL),
115 "NULL returns 0");
116 }
117
118 printf("\n--- update with invalid index ---\n");
119 {
120 tollgate_mint_health_t state = {0};
121 state.count = 1;
122
123 tollgate_core_mint_health_update(&state, -1, true, 200, 0, 1000);
124 tollgate_core_mint_health_update(&state, 5, true, 200, 0, 1000);
125 tollgate_core_mint_health_update(NULL, 0, true, 200, 0, 1000);
126 ASSERT(true, "no crash on invalid inputs");
127 }
128
129 TEST_SUMMARY();
130}
diff --git a/tests/unit/test_portal.c b/tests/unit/test_portal.c
new file mode 100644
index 0000000..575921e
--- /dev/null
+++ b/tests/unit/test_portal.c
@@ -0,0 +1,153 @@
1#include "test_framework.h"
2#include "../../components/tollgate_core/src/tollgate_core_portal.h"
3#include "../../components/tollgate_core/src/tollgate_core_session.h"
4#include <string.h>
5#include <stdio.h>
6#include <stdlib.h>
7
8int main(void)
9{
10 printf("=== test_portal ===\n");
11
12 printf("\n--- template_replace single key ---\n");
13 {
14 char *result = tollgate_core_portal_template_replace(
15 "Hello __NAME__!", "__NAME__", "World");
16 ASSERT(result != NULL, "result not NULL");
17 ASSERT(strcmp(result, "Hello World!") == 0, "single substitution works");
18 free(result);
19 }
20
21 printf("\n--- template_replace no match ---\n");
22 {
23 char *result = tollgate_core_portal_template_replace(
24 "Hello World!", "__NAME__", "Test");
25 ASSERT(result != NULL, "result not NULL");
26 ASSERT(strcmp(result, "Hello World!") == 0, "no match returns original");
27 free(result);
28 }
29
30 printf("\n--- template_replace multiple occurrences ---\n");
31 {
32 char *result = tollgate_core_portal_template_replace(
33 "__IP__:2121 and __IP__:80", "__IP__", "10.0.0.1");
34 ASSERT(result != NULL, "result not NULL");
35 ASSERT(strcmp(result, "10.0.0.1:2121 and 10.0.0.1:80") == 0,
36 "multiple occurrences replaced");
37 free(result);
38 }
39
40 printf("\n--- template_replace key longer than value ---\n");
41 {
42 char *result = tollgate_core_portal_template_replace(
43 "A __LONGKEY__ B", "__LONGKEY__", "X");
44 ASSERT(result != NULL, "result not NULL");
45 ASSERT(strcmp(result, "A X B") == 0, "shrinks correctly");
46 free(result);
47 }
48
49 printf("\n--- render multi-key substitution ---\n");
50 {
51 tollgate_portal_sub_t subs[] = {
52 { "__AP_IP__", "10.0.0.1" },
53 { "__PRICE__", "21" },
54 { "__MINT_URL__", "https://test.mint" },
55 };
56 char *result = tollgate_core_portal_render(
57 "IP=__AP_IP__ price=__PRICE__ mint=__MINT_URL__", subs, 3);
58 ASSERT(result != NULL, "result not NULL");
59 ASSERT(strcmp(result, "IP=10.0.0.1 price=21 mint=https://test.mint") == 0,
60 "multi-key render works");
61 free(result);
62 }
63
64 printf("\n--- render with no matches ---\n");
65 {
66 tollgate_portal_sub_t subs[] = {
67 { "__AP_IP__", "10.0.0.1" },
68 };
69 char *result = tollgate_core_portal_render("no placeholders", subs, 1);
70 ASSERT(result != NULL, "result not NULL");
71 ASSERT(strcmp(result, "no placeholders") == 0, "no matches returns original");
72 free(result);
73 }
74
75 printf("\n--- calc_usage time metric ---\n");
76 {
77 tg_session_t session = {0};
78 session.active = true;
79 session.allotment_ms = 60000;
80 session.start_time_ms = 10000;
81
82 int64_t remaining = 0, total = 0;
83 bool ok = tollgate_core_portal_calc_usage(&session, "milliseconds", 30000, &remaining, &total);
84 ASSERT(ok, "calc_usage returns true for active session");
85 ASSERT_EQ_UINT64(40000, (unsigned long long)remaining, "remaining = 60000 - 20000");
86 ASSERT_EQ_UINT64(60000, (unsigned long long)total, "total = allotment_ms");
87 }
88
89 printf("\n--- calc_usage bytes metric ---\n");
90 {
91 tg_session_t session = {0};
92 session.active = true;
93 session.allotment_bytes = 1000000;
94 session.bytes_consumed = 300000;
95
96 int64_t remaining = 0, total = 0;
97 bool ok = tollgate_core_portal_calc_usage(&session, "bytes", 0, &remaining, &total);
98 ASSERT(ok, "calc_usage returns true for bytes");
99 ASSERT_EQ_UINT64(700000, (unsigned long long)remaining, "remaining = 1000000 - 300000");
100 ASSERT_EQ_UINT64(1000000, (unsigned long long)total, "total = allotment_bytes");
101 }
102
103 printf("\n--- calc_usage expired time ---\n");
104 {
105 tg_session_t session = {0};
106 session.active = true;
107 session.allotment_ms = 60000;
108 session.start_time_ms = 0;
109
110 int64_t remaining = 0, total = 0;
111 tollgate_core_portal_calc_usage(&session, "milliseconds", 120000, &remaining, &total);
112 ASSERT_EQ_UINT64(0, (unsigned long long)remaining, "remaining clamped to 0 when expired");
113 }
114
115 printf("\n--- calc_usage inactive session ---\n");
116 {
117 tg_session_t session = {0};
118 session.active = false;
119
120 int64_t remaining = 0, total = 0;
121 bool ok = tollgate_core_portal_calc_usage(&session, "milliseconds", 0, &remaining, &total);
122 ASSERT(!ok, "inactive session returns false");
123 }
124
125 printf("\n--- format_usage ---\n");
126 {
127 tg_session_t session = {0};
128 session.active = true;
129 session.allotment_ms = 60000;
130 session.start_time_ms = 10000;
131
132 char buf[64];
133 int len = tollgate_core_portal_format_usage(&session, "milliseconds", 30000, buf, sizeof(buf));
134 ASSERT(len > 0, "format_usage returns positive length");
135 ASSERT(strstr(buf, "/") != NULL, "contains separator");
136 ASSERT(strstr(buf, "60000") != NULL, "contains total");
137 }
138
139 printf("\n--- is_captive_uri ---\n");
140 {
141 ASSERT(tollgate_core_portal_is_captive_uri("/generate_204"), "generate_204 is captive");
142 ASSERT(tollgate_core_portal_is_captive_uri("/hotspot-detect.html"), "hotspot-detect is captive");
143 ASSERT(tollgate_core_portal_is_captive_uri("/success.txt"), "success.txt is captive");
144 ASSERT(tollgate_core_portal_is_captive_uri("/ncsi.txt"), "ncsi.txt is captive");
145 ASSERT(tollgate_core_portal_is_captive_uri("/connecttest.txt"), "connecttest is captive");
146 ASSERT(tollgate_core_portal_is_captive_uri("/wpad.dat"), "wpad.dat is captive");
147 ASSERT(!tollgate_core_portal_is_captive_uri("/"), "root is not captive");
148 ASSERT(!tollgate_core_portal_is_captive_uri("/setup"), "setup is not captive");
149 ASSERT(!tollgate_core_portal_is_captive_uri(NULL), "NULL is not captive");
150 }
151
152 TEST_SUMMARY();
153}
diff --git a/tests/unit/test_stratum_client.c b/tests/unit/test_stratum_client.c
new file mode 100644
index 0000000..302fcba
--- /dev/null
+++ b/tests/unit/test_stratum_client.c
@@ -0,0 +1,125 @@
1#include "test_framework.h"
2#include "../../components/tollgate_core/src/tollgate_core_stratum_client.h"
3#include <cjson/cJSON.h>
4#include <string.h>
5#include <stdio.h>
6#include <stdlib.h>
7
8int main(void)
9{
10 printf("=== test_stratum_client ===\n");
11
12 printf("\n--- hex_to_bytes ---\n");
13 {
14 const char *hex = "0a1b2c3d";
15 uint8_t out[4] = {0};
16 tollgate_core_stratum_hex_to_bytes(hex, out, 4);
17 ASSERT_EQ_INT(0x0a, (int)out[0], "byte 0");
18 ASSERT_EQ_INT(0x1b, (int)out[1], "byte 1");
19 ASSERT_EQ_INT(0x2c, (int)out[2], "byte 2");
20 ASSERT_EQ_INT(0x3d, (int)out[3], "byte 3");
21 }
22
23 printf("\n--- hex_to_bytes zeros ---\n");
24 {
25 const char *hex = "00000000";
26 uint8_t out[4] = {0xff, 0xff, 0xff, 0xff};
27 tollgate_core_stratum_hex_to_bytes(hex, out, 4);
28 ASSERT_EQ_INT(0, (int)out[0], "zero byte 0");
29 ASSERT_EQ_INT(0, (int)out[1], "zero byte 1");
30 }
31
32 printf("\n--- parse_notify valid ---\n");
33 {
34 cJSON *params = cJSON_Parse("[\"123\",\"0000000000000000000000000000000000000000000000000000000000000000\","
35 "\"\""
36 ",\"\""
37 ",\"\""
38 ",\"20000000\""
39 ",\"1705e3c4\""
40 ",\"65a3b2c1\"]");
41 tollgate_stratum_job_t job = {0};
42 uint32_t nbits = 0;
43 bool ok = tollgate_core_stratum_parse_notify(params, &job, &nbits);
44 ASSERT(ok, "parse_notify returns true");
45 ASSERT_EQ_INT(123, (int)job.job_id, "job_id parsed");
46 ASSERT(job.valid, "job is valid");
47 ASSERT_EQ_INT(0x20000000, (int)job.version, "version parsed");
48 ASSERT_EQ_INT(0x1705e3c4, (int)job.nbits, "nbits parsed");
49 ASSERT_EQ_INT(0x65a3b2c1, (int)job.ntime, "ntime parsed");
50 ASSERT_EQ_INT(0x1705e3c4, (int)nbits, "out_nbits set");
51 ASSERT_EQ_INT(32, job.target_len, "target_len is 32");
52 cJSON_Delete(params);
53 }
54
55 printf("\n--- parse_notify too few params ---\n");
56 {
57 cJSON *params = cJSON_Parse("[\"123\",\"abcd\"]");
58 tollgate_stratum_job_t job = {0};
59 bool ok = tollgate_core_stratum_parse_notify(params, &job, NULL);
60 ASSERT(!ok, "parse_notify returns false for insufficient params");
61 cJSON_Delete(params);
62 }
63
64 printf("\n--- parse_notify NULL ---\n");
65 {
66 tollgate_stratum_job_t job = {0};
67 bool ok = tollgate_core_stratum_parse_notify(NULL, &job, NULL);
68 ASSERT(!ok, "parse_notify returns false for NULL");
69 }
70
71 printf("\n--- parse_difficulty valid ---\n");
72 {
73 cJSON *params = cJSON_Parse("[512]");
74 uint64_t diff = 0;
75 bool ok = tollgate_core_stratum_parse_difficulty(params, &diff);
76 ASSERT(ok, "parse_difficulty returns true");
77 ASSERT_EQ_UINT64(512, (unsigned long long)diff, "difficulty parsed");
78 cJSON_Delete(params);
79 }
80
81 printf("\n--- parse_difficulty invalid ---\n");
82 {
83 cJSON *params = cJSON_Parse("[]");
84 uint64_t diff = 0;
85 bool ok = tollgate_core_stratum_parse_difficulty(params, &diff);
86 ASSERT(!ok, "parse_difficulty returns false for empty array");
87 cJSON_Delete(params);
88 }
89
90 printf("\n--- build_subscribe ---\n");
91 {
92 char buf[256];
93 int len = tollgate_core_stratum_build_subscribe(buf, sizeof(buf), 1);
94 ASSERT(len > 0, "build_subscribe returns positive length");
95 ASSERT(strstr(buf, "mining.subscribe") != NULL, "contains method");
96 ASSERT(strstr(buf, "\"id\":1") != NULL, "contains id");
97 ASSERT(strstr(buf, "TollGate/1.0") != NULL, "contains user agent");
98 ASSERT(buf[len - 1] == '\n', "ends with newline");
99 }
100
101 printf("\n--- build_authorize ---\n");
102 {
103 char buf[512];
104 int len = tollgate_core_stratum_build_authorize(buf, sizeof(buf), 2, "user1", "pass1");
105 ASSERT(len > 0, "build_authorize returns positive length");
106 ASSERT(strstr(buf, "mining.authorize") != NULL, "contains method");
107 ASSERT(strstr(buf, "user1") != NULL, "contains user");
108 ASSERT(strstr(buf, "pass1") != NULL, "contains pass");
109 }
110
111 printf("\n--- build_submit ---\n");
112 {
113 char buf[512];
114 int len = tollgate_core_stratum_build_submit(buf, sizeof(buf), 3, "worker1",
115 100, 0x65a3b2c1, 0x12345678, 0x20000000);
116 ASSERT(len > 0, "build_submit returns positive length");
117 ASSERT(strstr(buf, "mining.submit") != NULL, "contains method");
118 ASSERT(strstr(buf, "worker1") != NULL, "contains worker");
119 ASSERT(strstr(buf, "100") != NULL, "contains job_id");
120 ASSERT(strstr(buf, "65a3b2c1") != NULL, "contains ntime");
121 ASSERT(strstr(buf, "12345678") != NULL, "contains nonce");
122 }
123
124 TEST_SUMMARY();
125}