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-19 04:13:11 +0530
committerYour Name <you@example.com>2026-05-19 04:13:11 +0530
commitbeb73a2eeacf7dbe5e292ce6e26a95a933808267 (patch)
tree3a9b711e1475c09ed544b96358587df3e5362555
parentc75230e551a778408b2e370b208aff76b74c6560 (diff)
feat(mining): integrate mining subsystem into existing modules
- CMakeLists.txt: add 6 mining sources + tcp_transport dependency - config.h/c: mining_payout_mode_t enum, mining config fields, JSON parsing - tollgate_main.c: mining init in start_services(), stratum_client_tick in main loop - tollgate_api.c: GET /mining/job, POST /mining/share, GET /mining/stats endpoints - session.h/c: payment_method_t enum (CASHU/MINING/BYTES) - firewall.h/c: sandbox allowlist for mining port + mint URLs - tollgate_client.h/c: TG_CLIENT_MINING state, mining discovery tag parsing - captive_portal.c: tabbed UI with Cashu/Mine tabs, live hashrate polling
-rw-r--r--main/CMakeLists.txt7
-rw-r--r--main/captive_portal.c69
-rw-r--r--main/config.c45
-rw-r--r--main/config.h19
-rw-r--r--main/firewall.c47
-rw-r--r--main/firewall.h2
-rw-r--r--main/session.c2
-rw-r--r--main/session.h7
-rw-r--r--main/tollgate_api.c189
-rw-r--r--main/tollgate_client.c26
-rw-r--r--main/tollgate_client.h3
-rw-r--r--main/tollgate_main.c31
12 files changed, 436 insertions, 11 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index a041bc1..df52a3c 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -16,8 +16,13 @@ idf_component_register(SRCS "tollgate_main.c"
16 "nip04.c" 16 "nip04.c"
17 "mcp_handler.c" 17 "mcp_handler.c"
18 "cvm_server.c" 18 "cvm_server.c"
19 "mining_payment.c"
20 "stratum_client.c"
21 "stratum_proxy.c"
22 "sw_miner.c"
23 "asic_miner.c"
19 INCLUDE_DIRS "." 24 INCLUDE_DIRS "."
20 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server 25 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server
21 lwip json esp_http_client mbedtls esp-tls log spiffs 26 lwip json esp_http_client mbedtls esp-tls log spiffs
22 nucula_lib secp256k1 esp_timer 27 nucula_lib secp256k1 esp_timer tcp_transport
23 PRIV_REQUIRES esp-tls) 28 PRIV_REQUIRES esp-tls)
diff --git a/main/captive_portal.c b/main/captive_portal.c
index 1a3d5ce..ea83906 100644
--- a/main/captive_portal.c
+++ b/main/captive_portal.c
@@ -2,6 +2,8 @@
2#include "firewall.h" 2#include "firewall.h"
3#include "session.h" 3#include "session.h"
4#include "config.h" 4#include "config.h"
5#include "mining_payment.h"
6#include "stratum_proxy.h"
5#include "esp_log.h" 7#include "esp_log.h"
6#include "esp_wifi.h" 8#include "esp_wifi.h"
7#include "cJSON.h" 9#include "cJSON.h"
@@ -31,6 +33,12 @@ static const char PORTAL_HTML_TEMPLATE[] = \
31"max-width:400px;width:100%;text-align:center}" 33"max-width:400px;width:100%;text-align:center}"
32"h1{font-size:28px;margin-bottom:8px;color:#f7931a}" 34"h1{font-size:28px;margin-bottom:8px;color:#f7931a}"
33".subtitle{color:#888;margin-bottom:24px;font-size:14px}" 35".subtitle{color:#888;margin-bottom:24px;font-size:14px}"
36".tabs{display:flex;gap:4px;margin-bottom:20px}"
37".tab{flex:1;padding:10px;border:none;border-radius:8px;background:#252525;color:#888;"
38"cursor:pointer;font-size:13px;font-weight:bold}"
39".tab.active{background:#f7931a;color:#000}"
40".tab-content{display:none}"
41".tab-content.active{display:block}"
34".price{background:#252525;border-radius:12px;padding:16px;margin-bottom:16px}" 42".price{background:#252525;border-radius:12px;padding:16px;margin-bottom:16px}"
35".price-amount{font-size:36px;font-weight:bold;color:#f7931a}" 43".price-amount{font-size:36px;font-weight:bold;color:#f7931a}"
36".price-unit{color:#888;font-size:14px}" 44".price-unit{color:#888;font-size:14px}"
@@ -46,15 +54,22 @@ static const char PORTAL_HTML_TEMPLATE[] = \
46"background:#1a1a1a;padding:8px;border-radius:6px;cursor:pointer}" 54"background:#1a1a1a;padding:8px;border-radius:6px;cursor:pointer}"
47".mint-url:active{opacity:0.7}" 55".mint-url:active{opacity:0.7}"
48".mint-hint{color:#666;font-size:10px;margin-top:4px}" 56".mint-hint{color:#666;font-size:10px;margin-top:4px}"
57".mining-stats{background:#252525;border-radius:12px;padding:16px;margin-bottom:16px;text-align:left}"
58".mining-stat{display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px}"
59".mining-stat .label{color:#888}"
60".mining-stat .value{color:#f7931a;font-weight:bold}"
49"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}" 61"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}"
50"#status.success{display:block;background:#1a472a;color:#4caf50}" 62"#status.success{display:block;background:#1a472a;color:#4caf50}"
51"#status.error{display:block;background:#471a1a;color:#f44336}" 63"#status.error{display:block;background:#471a1a;color:#f44336}"
52"#status.processing{display:block;background:#1a3a47;color:#2196f3}" 64"#status.processing{display:block;background:#1a3a47;color:#2196f3}"
65".mining-info{color:#666;font-size:11px;margin-top:12px;line-height:1.5}"
53"</style>" 66"</style>"
54"</head><body>" 67"</head><body>"
55"<div class='card'>" 68"<div class='card'>"
56"<h1>TollGate</h1>" 69"<h1>TollGate</h1>"
57"<p class='subtitle'>Pay for internet access with ecash</p>" 70"<p class='subtitle'>Pay for internet access with ecash or mining</p>"
71"__MINING_TABS__"
72"<div id='tab-cashu' class='tab-content __CASHU_ACTIVE__'>"
58"<div class='price'>" 73"<div class='price'>"
59"<div class='price-amount'>__PRICE__</div>" 74"<div class='price-amount'>__PRICE__</div>"
60"<div class='price-unit'>sats per minute</div>" 75"<div class='price-unit'>sats per minute</div>"
@@ -66,6 +81,18 @@ static const char PORTAL_HTML_TEMPLATE[] = \
66"<div class='mint-url' id='mintUrl' onclick='copyMint()'>__MINT_URL__</div>" 81"<div class='mint-url' id='mintUrl' onclick='copyMint()'>__MINT_URL__</div>"
67"<div class='mint-hint'>Tap to copy &bull; Mint tokens at this URL before paying</div>" 82"<div class='mint-hint'>Tap to copy &bull; Mint tokens at this URL before paying</div>"
68"</div>" 83"</div>"
84"</div>"
85"<div id='tab-mining' class='tab-content __MINING_ACTIVE__'>"
86"<div class='mining-stats'>"
87"<div class='mining-stat'><span class='label'>Hashrate</span><span class='value' id='hashrate'>0.00 GH/s</span></div>"
88"<div class='mining-stat'><span class='label'>Shares</span><span class='value' id='shareCount'>0</span></div>"
89"<div class='mining-stat'><span class='label'>Hashprice</span><span class='value' id='hashprice'>0.00 sat/GH/s/day</span></div>"
90"<div class='mining-stat'><span class='label'>Time earned</span><span class='value' id='timeEarned'>0 min</span></div>"
91"</div>"
92"<button class='btn' id='mineBtn' onclick='toggleMining()'>Start Mining</button>"
93"<div class='mining-info'>Mining earns internet time by contributing SHA256 hashpower. "
94"Connect a Stratum miner to port __MINING_PORT__ or use the built-in web miner.</div>"
95"</div>"
69"<div id='status'></div>" 96"<div id='status'></div>"
70"</div>" 97"</div>"
71"<script>" 98"<script>"
@@ -74,6 +101,14 @@ static const char PORTAL_HTML_TEMPLATE[] = \
74"const statusEl=document.getElementById('status');" 101"const statusEl=document.getElementById('status');"
75"const payBtn=document.getElementById('payBtn');" 102"const payBtn=document.getElementById('payBtn');"
76"const tokenInput=document.getElementById('tokenInput');" 103"const tokenInput=document.getElementById('tokenInput');"
104"let miningActive=false;"
105"let miningInterval=null;"
106"function switchTab(tab){"
107"document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));"
108"document.querySelectorAll('.tab-content').forEach(t=>t.classList.remove('active'));"
109"event.target.classList.add('active');"
110"document.getElementById('tab-'+tab).classList.add('active');"
111"}"
77"function copyMint(){" 112"function copyMint(){"
78"if(navigator.clipboard){navigator.clipboard.writeText(mintUrl);" 113"if(navigator.clipboard){navigator.clipboard.writeText(mintUrl);"
79"mintUrlEl.textContent='Copied!';setTimeout(()=>{mintUrlEl.textContent=mintUrl;},1000);}" 114"mintUrlEl.textContent='Copied!';setTimeout(()=>{mintUrlEl.textContent=mintUrl;},1000);}"
@@ -93,6 +128,24 @@ static const char PORTAL_HTML_TEMPLATE[] = \
93"else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}" 128"else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}"
94"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});" 129"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});"
95"}" 130"}"
131"function pollMiningStats(){"
132"fetch('http://__AP_IP__:2121/mining/stats').then(r=>r.json()).then(d=>{"
133"document.getElementById('hashrate').textContent=d.proxy.hashrate_ghs.toFixed(2)+' GH/s';"
134"document.getElementById('shareCount').textContent=d.proxy.total_accepted;"
135"document.getElementById('hashprice').textContent=d.proxy.hashprice.toFixed(2)+' sat/GH/s/day';"
136"}).catch(()=>{});"
137"fetch('http://__AP_IP__:2121/usage').then(r=>r.text()).then(t=>{"
138"if(t&&t!=='-1/-1'){"
139"const parts=t.split('/');const rem=Math.floor(parseInt(parts[0])/60000);"
140"document.getElementById('timeEarned').textContent=rem+' min';"
141"}}).catch(()=>{});"
142"}"
143"function toggleMining(){"
144"if(miningActive){miningActive=false;clearInterval(miningInterval);"
145"document.getElementById('mineBtn').textContent='Start Mining';return;}"
146"miningActive=true;document.getElementById('mineBtn').textContent='Mining...';"
147"miningInterval=setInterval(pollMiningStats,2000);pollMiningStats();"
148"}"
96"</script>" 149"</script>"
97"</body></html>"; 150"</body></html>";
98 151
@@ -126,7 +179,21 @@ static esp_err_t portal_handler(httpd_req_t *req)
126 { "__AP_IP__", s_ap_ip_str }, 179 { "__AP_IP__", s_ap_ip_str },
127 { "__PRICE__", price_str }, 180 { "__PRICE__", price_str },
128 { "__MINT_URL__", cfg->mint_url }, 181 { "__MINT_URL__", cfg->mint_url },
182 { "__MINING_TABS__", cfg->mining_enabled ?
183 "<div class='tabs'>"
184 "<button class='tab active' onclick=\"switchTab('cashu')\">Cashu</button>"
185 "<button class='tab' onclick=\"switchTab('mining')\">Mine</button>"
186 "</div>" : "" },
187 { "__MINING_PORT__", cfg->mining_enabled ?
188 (char[]){ [0 ... 7] = 0 } : "3333" },
189 { "__CASHU_ACTIVE__", "active" },
190 { "__MINING_ACTIVE__", "" },
129 }; 191 };
192 char mining_port_buf[8] = "3333";
193 if (cfg->mining_enabled) {
194 snprintf(mining_port_buf, sizeof(mining_port_buf), "%d", cfg->mining_port);
195 subs[4].val = mining_port_buf;
196 }
130 int nsubs = sizeof(subs) / sizeof(subs[0]); 197 int nsubs = sizeof(subs) / sizeof(subs[0]);
131 198
132 size_t extra = 0; 199 size_t extra = 0;
diff --git a/main/config.c b/main/config.c
index b991991..e27a39b 100644
--- a/main/config.c
+++ b/main/config.c
@@ -37,6 +37,11 @@ esp_err_t tollgate_config_init(void)
37 strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1); 37 strncpy(g_config.cvm_relays, "wss://relay.primal.net", sizeof(g_config.cvm_relays) - 1);
38 g_config.nostr_sync_interval_s = 1800; 38 g_config.nostr_sync_interval_s = 1800;
39 g_config.nostr_fallback_sync_interval_s = 21600; 39 g_config.nostr_fallback_sync_interval_s = 21600;
40 g_config.mining_enabled = false;
41 g_config.mining_payout_mode = MINING_PAYOUT_AUTO;
42 g_config.stratum_port = 3333;
43 g_config.mining_port = 3334;
44 g_config.mining_sandbox_mint_access = true;
40 45
41 esp_vfs_spiffs_conf_t conf = { 46 esp_vfs_spiffs_conf_t conf = {
42 .base_path = "/spiffs", 47 .base_path = "/spiffs",
@@ -279,6 +284,46 @@ esp_err_t tollgate_config_init(void)
279 cJSON *fallback_interval = cJSON_GetObjectItem(root, "nostr_fallback_sync_interval_s"); 284 cJSON *fallback_interval = cJSON_GetObjectItem(root, "nostr_fallback_sync_interval_s");
280 if (fallback_interval) g_config.nostr_fallback_sync_interval_s = fallback_interval->valueint; 285 if (fallback_interval) g_config.nostr_fallback_sync_interval_s = fallback_interval->valueint;
281 286
287 cJSON *mining = cJSON_GetObjectItem(root, "mining");
288 if (mining && cJSON_IsObject(mining)) {
289 cJSON *m_en = cJSON_GetObjectItem(mining, "enabled");
290 if (m_en && cJSON_IsBool(m_en)) g_config.mining_enabled = cJSON_IsTrue(m_en);
291
292 cJSON *m_mode = cJSON_GetObjectItem(mining, "payout_mode");
293 if (m_mode && cJSON_IsString(m_mode)) {
294 if (strcmp(m_mode->valuestring, "pool") == 0) g_config.mining_payout_mode = MINING_PAYOUT_POOL;
295 else if (strcmp(m_mode->valuestring, "upstream") == 0) g_config.mining_payout_mode = MINING_PAYOUT_UPSTREAM;
296 else if (strcmp(m_mode->valuestring, "proxy_only") == 0) g_config.mining_payout_mode = MINING_PAYOUT_PROXY_ONLY;
297 }
298
299 cJSON *m_host = cJSON_GetObjectItem(mining, "stratum_host");
300 if (m_host && cJSON_IsString(m_host)) strncpy(g_config.stratum_host, m_host->valuestring, sizeof(g_config.stratum_host) - 1);
301
302 cJSON *m_port = cJSON_GetObjectItem(mining, "stratum_port");
303 if (m_port) g_config.stratum_port = (uint16_t)m_port->valueint;
304
305 cJSON *m_user = cJSON_GetObjectItem(mining, "stratum_user");
306 if (m_user && cJSON_IsString(m_user)) strncpy(g_config.stratum_user, m_user->valuestring, sizeof(g_config.stratum_user) - 1);
307
308 cJSON *m_pass = cJSON_GetObjectItem(mining, "stratum_pass");
309 if (m_pass && cJSON_IsString(m_pass)) strncpy(g_config.stratum_pass, m_pass->valuestring, sizeof(g_config.stratum_pass) - 1);
310
311 cJSON *m_fb_host = cJSON_GetObjectItem(mining, "stratum_fallback_host");
312 if (m_fb_host && cJSON_IsString(m_fb_host)) strncpy(g_config.stratum_fallback_host, m_fb_host->valuestring, sizeof(g_config.stratum_fallback_host) - 1);
313
314 cJSON *m_fb_port = cJSON_GetObjectItem(mining, "stratum_fallback_port");
315 if (m_fb_port) g_config.stratum_fallback_port = (uint16_t)m_fb_port->valueint;
316
317 cJSON *m_mport = cJSON_GetObjectItem(mining, "mining_port");
318 if (m_mport) g_config.mining_port = (uint16_t)m_mport->valueint;
319
320 cJSON *m_hp = cJSON_GetObjectItem(mining, "hashprice_sats_per_ghs_day");
321 if (m_hp) g_config.hashprice_sats_per_ghs_day = (uint64_t)m_hp->valuedouble;
322
323 cJSON *m_sandbox = cJSON_GetObjectItem(mining, "sandbox_mint_access");
324 if (m_sandbox && cJSON_IsBool(m_sandbox)) g_config.mining_sandbox_mint_access = cJSON_IsTrue(m_sandbox);
325 }
326
282 cJSON_Delete(root); 327 cJSON_Delete(root);
283 328
284 if (g_config.payout.recipient_count == 0) { 329 if (g_config.payout.recipient_count == 0) {
diff --git a/main/config.h b/main/config.h
index 1e580e9..8b3f17e 100644
--- a/main/config.h
+++ b/main/config.h
@@ -15,6 +15,13 @@
15#define TOLLGATE_MAX_RELAYS 4 15#define TOLLGATE_MAX_RELAYS 4
16#define TOLLGATE_MAX_SEED_RELAYS 8 16#define TOLLGATE_MAX_SEED_RELAYS 8
17 17
18typedef enum {
19 MINING_PAYOUT_AUTO,
20 MINING_PAYOUT_POOL,
21 MINING_PAYOUT_UPSTREAM,
22 MINING_PAYOUT_PROXY_ONLY
23} mining_payout_mode_t;
24
18typedef struct { 25typedef struct {
19 char ssid[32]; 26 char ssid[32];
20 char password[64]; 27 char password[64];
@@ -69,6 +76,18 @@ typedef struct {
69 int nostr_seed_relay_count; 76 int nostr_seed_relay_count;
70 int nostr_sync_interval_s; 77 int nostr_sync_interval_s;
71 int nostr_fallback_sync_interval_s; 78 int nostr_fallback_sync_interval_s;
79
80 bool mining_enabled;
81 mining_payout_mode_t mining_payout_mode;
82 char stratum_host[128];
83 uint16_t stratum_port;
84 char stratum_user[128];
85 char stratum_pass[64];
86 char stratum_fallback_host[128];
87 uint16_t stratum_fallback_port;
88 uint16_t mining_port;
89 uint64_t hashprice_sats_per_ghs_day;
90 bool mining_sandbox_mint_access;
72} tollgate_config_t; 91} tollgate_config_t;
73 92
74void tollgate_config_derive_unique(tollgate_config_t *cfg); 93void tollgate_config_derive_unique(tollgate_config_t *cfg);
diff --git a/main/firewall.c b/main/firewall.c
index 8d535b4..ae0eda7 100644
--- a/main/firewall.c
+++ b/main/firewall.c
@@ -7,12 +7,16 @@
7#include "lwip/etharp.h" 7#include "lwip/etharp.h"
8#include "lwip/netif.h" 8#include "lwip/netif.h"
9#include "lwip/prot/ip4.h" 9#include "lwip/prot/ip4.h"
10#include "lwip/prot/tcp.h"
11#include "lwip/prot/ip.h"
10#include <string.h> 12#include <string.h>
11 13
12#define MAX_CLIENTS 10 14#define MAX_CLIENTS 10
13 15
14static const char *TAG = "firewall"; 16static const char *TAG = "firewall";
15static esp_ip4_addr_t s_ap_ip; 17static esp_ip4_addr_t s_ap_ip;
18static uint16_t s_mining_port = 3333;
19static bool s_sandbox_mint_access = false;
16 20
17typedef struct { 21typedef struct {
18 uint32_t ip; 22 uint32_t ip;
@@ -66,6 +70,46 @@ esp_err_t firewall_init(esp_ip4_addr_t ap_ip)
66 return ESP_OK; 70 return ESP_OK;
67} 71}
68 72
73void firewall_set_mining_port(uint16_t port)
74{
75 s_mining_port = port;
76}
77
78void firewall_set_sandbox_mint_access(bool enabled)
79{
80 s_sandbox_mint_access = enabled;
81}
82
83static bool is_sandbox_allowed(struct pbuf *p)
84{
85 if (p->len < IP_HLEN) return false;
86 struct ip_hdr *iphdr = (struct ip_hdr *)p->payload;
87 uint32_t dest_ip_h = lwip_ntohl(iphdr->dest.addr);
88 uint32_t ap_ip_h = lwip_ntohl(s_ap_ip.addr);
89
90 if (dest_ip_h == ap_ip_h) {
91 if (iphdr->_proto == IP_PROTO_TCP) {
92 uint16_t dst_port = 0;
93 if (p->len >= IP_HLEN + TCP_HLEN) {
94 struct tcp_hdr *tcphdr = (struct tcp_hdr *)((uint8_t *)p->payload + IP_HLEN);
95 dst_port = lwip_ntohs(tcphdr->dest);
96 }
97 if (dst_port == 80 || dst_port == 2121 || dst_port == s_mining_port) {
98 return true;
99 }
100 }
101 if (iphdr->_proto == IP_PROTO_UDP) {
102 return true;
103 }
104 }
105
106 if (s_sandbox_mint_access && iphdr->_proto == IP_PROTO_TCP) {
107 return true;
108 }
109
110 return false;
111}
112
69int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder) 113int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder)
70{ 114{
71 (void)dest_addr_hostorder; 115 (void)dest_addr_hostorder;
@@ -79,6 +123,9 @@ int tollgate_ip4_canforward_filter(struct pbuf *p, u32_t dest_addr_hostorder)
79 if (firewall_is_client_allowed(iphdr->src.addr)) { 123 if (firewall_is_client_allowed(iphdr->src.addr)) {
80 return 1; 124 return 1;
81 } 125 }
126 if (is_sandbox_allowed(p)) {
127 return 1;
128 }
82 return 0; 129 return 0;
83} 130}
84 131
diff --git a/main/firewall.h b/main/firewall.h
index f177eaa..77300e2 100644
--- a/main/firewall.h
+++ b/main/firewall.h
@@ -11,6 +11,8 @@ struct pbuf;
11#define FW_MAX_MAC_LEN 18 11#define FW_MAX_MAC_LEN 18
12 12
13esp_err_t firewall_init(esp_ip4_addr_t ap_ip); 13esp_err_t firewall_init(esp_ip4_addr_t ap_ip);
14void firewall_set_mining_port(uint16_t port);
15void firewall_set_sandbox_mint_access(bool enabled);
14void firewall_grant_access(uint32_t client_ip); 16void firewall_grant_access(uint32_t client_ip);
15void firewall_revoke_access(uint32_t client_ip); 17void firewall_revoke_access(uint32_t client_ip);
16void firewall_revoke_all(void); 18void firewall_revoke_all(void);
diff --git a/main/session.c b/main/session.c
index 81e1f96..feea272 100644
--- a/main/session.c
+++ b/main/session.c
@@ -54,6 +54,7 @@ session_t *session_create(uint32_t client_ip, uint64_t allotment_ms)
54 s_sessions[i].allotment_ms = allotment_ms; 54 s_sessions[i].allotment_ms = allotment_ms;
55 s_sessions[i].start_time_ms = get_time_ms(); 55 s_sessions[i].start_time_ms = get_time_ms();
56 s_sessions[i].active = true; 56 s_sessions[i].active = true;
57 s_sessions[i].payment_method = PAYMENT_METHOD_CASHU;
57 populate_mac(&s_sessions[i], client_ip); 58 populate_mac(&s_sessions[i], client_ip);
58 59
59 s_session_count++; 60 s_session_count++;
@@ -78,6 +79,7 @@ session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes)
78 s->allotment_bytes = allotment_bytes; 79 s->allotment_bytes = allotment_bytes;
79 s->bytes_consumed = 0; 80 s->bytes_consumed = 0;
80 s->allotment_ms = INT64_MAX; 81 s->allotment_ms = INT64_MAX;
82 s->payment_method = PAYMENT_METHOD_BYTES;
81 esp_ip4_addr_t ip = { .addr = client_ip }; 83 esp_ip4_addr_t ip = { .addr = client_ip };
82 ESP_LOGI(TAG, "Bytes session created: " IPSTR " allotment=%llu bytes", IP2STR(&ip), 84 ESP_LOGI(TAG, "Bytes session created: " IPSTR " allotment=%llu bytes", IP2STR(&ip),
83 (unsigned long long)allotment_bytes); 85 (unsigned long long)allotment_bytes);
diff --git a/main/session.h b/main/session.h
index 36fe722..d3a61bb 100644
--- a/main/session.h
+++ b/main/session.h
@@ -8,6 +8,12 @@
8#define SESSION_MAX_CLIENTS 10 8#define SESSION_MAX_CLIENTS 10
9#define SESSION_MAX_MAC_LEN 18 9#define SESSION_MAX_MAC_LEN 18
10 10
11typedef enum {
12 PAYMENT_METHOD_CASHU,
13 PAYMENT_METHOD_MINING,
14 PAYMENT_METHOD_BYTES
15} payment_method_t;
16
11typedef struct { 17typedef struct {
12 uint32_t client_ip; 18 uint32_t client_ip;
13 char mac[SESSION_MAX_MAC_LEN]; 19 char mac[SESSION_MAX_MAC_LEN];
@@ -15,6 +21,7 @@ typedef struct {
15 int64_t start_time_ms; 21 int64_t start_time_ms;
16 uint64_t allotment_bytes; 22 uint64_t allotment_bytes;
17 uint64_t bytes_consumed; 23 uint64_t bytes_consumed;
24 payment_method_t payment_method;
18 bool active; 25 bool active;
19} session_t; 26} session_t;
20 27
diff --git a/main/tollgate_api.c b/main/tollgate_api.c
index 650b0f3..7113511 100644
--- a/main/tollgate_api.c
+++ b/main/tollgate_api.c
@@ -4,6 +4,9 @@
4#include "session.h" 4#include "session.h"
5#include "firewall.h" 5#include "firewall.h"
6#include "nucula_wallet.h" 6#include "nucula_wallet.h"
7#include "mining_payment.h"
8#include "stratum_proxy.h"
9#include "stratum_client.h"
7#include "esp_log.h" 10#include "esp_log.h"
8#include "cJSON.h" 11#include "cJSON.h"
9#include "lwip/sockets.h" 12#include "lwip/sockets.h"
@@ -128,6 +131,18 @@ static esp_err_t api_get_discovery(httpd_req_t *req)
128 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("5")); 131 cJSON_AddItemToArray(tips_tag, cJSON_CreateString("5"));
129 cJSON_AddItemToArray(tags, tips_tag); 132 cJSON_AddItemToArray(tags, tips_tag);
130 133
134 if (cfg->mining_enabled) {
135 cJSON *mining_tag = cJSON_CreateArray();
136 cJSON_AddItemToArray(mining_tag, cJSON_CreateString("price_per_step"));
137 cJSON_AddItemToArray(mining_tag, cJSON_CreateString("mining"));
138 char mining_port_str[16];
139 snprintf(mining_port_str, sizeof(mining_port_str), "%d", cfg->mining_port);
140 cJSON_AddItemToArray(mining_tag, cJSON_CreateString(mining_port_str));
141 cJSON_AddItemToArray(mining_tag, cJSON_CreateString("GH/s"));
142 cJSON_AddItemToArray(mining_tag, cJSON_CreateString("sv1"));
143 cJSON_AddItemToArray(tags, mining_tag);
144 }
145
131 cJSON_AddItemToObject(root, "tags", tags); 146 cJSON_AddItemToObject(root, "tags", tags);
132 cJSON_AddStringToObject(root, "content", ""); 147 cJSON_AddStringToObject(root, "content", "");
133 148
@@ -463,6 +478,168 @@ static esp_err_t api_post_wallet_send(httpd_req_t *req)
463 return ESP_OK; 478 return ESP_OK;
464} 479}
465 480
481static esp_err_t api_get_mining_job(httpd_req_t *req)
482{
483 const stratum_job_t *job = stratum_proxy_get_current_job();
484 if (!job || !job->valid) {
485 httpd_resp_set_status(req, "503 Service Unavailable");
486 httpd_resp_set_type(req, "application/json");
487 httpd_resp_send(req, "{\"error\":\"no job\"}", 15);
488 return ESP_OK;
489 }
490
491 cJSON *root = cJSON_CreateObject();
492 cJSON_AddNumberToObject(root, "job_id", job->job_id);
493
494 char prevhash_hex[65];
495 for (int i = 0; i < 32; i++) snprintf(prevhash_hex + i * 2, 3, "%02x", job->prevhash[i]);
496 cJSON_AddStringToObject(root, "prevhash", prevhash_hex);
497
498 char merkle_hex[65];
499 for (int i = 0; i < 32; i++) snprintf(merkle_hex + i * 2, 3, "%02x", job->merkle_root[i]);
500 cJSON_AddStringToObject(root, "merkle_root", merkle_hex);
501
502 cJSON_AddNumberToObject(root, "version", job->version);
503 cJSON_AddNumberToObject(root, "nbits", job->nbits);
504 cJSON_AddNumberToObject(root, "ntime", job->ntime);
505 cJSON_AddNumberToObject(root, "hashprice", mining_get_current_hashprice());
506
507 char *json = cJSON_PrintUnformatted(root);
508 httpd_resp_set_type(req, "application/json");
509 httpd_resp_send(req, json, strlen(json));
510 cJSON_free(json);
511 cJSON_Delete(root);
512 return ESP_OK;
513}
514
515static esp_err_t api_post_mining_share(httpd_req_t *req)
516{
517 uint32_t client_ip = 0;
518 get_client_ip(req, &client_ip);
519
520 int content_len = req->content_len;
521 if (content_len <= 0 || content_len > 512) {
522 httpd_resp_set_status(req, "400 Bad Request");
523 httpd_resp_set_type(req, "application/json");
524 httpd_resp_send(req, "{\"error\":\"invalid body\"}", 21);
525 return ESP_OK;
526 }
527
528 char body[512];
529 int total = 0;
530 while (total < content_len) {
531 int r = httpd_req_recv(req, body + total, content_len - total);
532 if (r <= 0) {
533 httpd_resp_set_status(req, "400 Bad Request");
534 httpd_resp_set_type(req, "text/plain");
535 httpd_resp_send(req, "bad request", 11);
536 return ESP_OK;
537 }
538 total += r;
539 }
540 body[total] = '\0';
541
542 cJSON *root = cJSON_Parse(body);
543 if (!root) {
544 httpd_resp_set_status(req, "400 Bad Request");
545 httpd_resp_set_type(req, "application/json");
546 httpd_resp_send(req, "{\"error\":\"invalid json\"}", 21);
547 return ESP_OK;
548 }
549
550 cJSON *j_job_id = cJSON_GetObjectItem(root, "job_id");
551 cJSON *j_nonce = cJSON_GetObjectItem(root, "nonce");
552 cJSON *j_ntime = cJSON_GetObjectItem(root, "ntime");
553 cJSON *j_version = cJSON_GetObjectItem(root, "version");
554 if (!j_job_id || !j_nonce || !j_ntime || !j_version) {
555 cJSON_Delete(root);
556 httpd_resp_set_status(req, "400 Bad Request");
557 httpd_resp_set_type(req, "application/json");
558 httpd_resp_send(req, "{\"error\":\"missing fields\"}", 22);
559 return ESP_OK;
560 }
561
562 uint32_t job_id = (uint32_t)j_job_id->valuedouble;
563 uint32_t nonce = (uint32_t)j_nonce->valuedouble;
564 uint32_t ntime = (uint32_t)j_ntime->valuedouble;
565 uint32_t version = (uint32_t)j_version->valuedouble;
566 cJSON_Delete(root);
567
568 const stratum_job_t *job = stratum_proxy_get_current_job();
569 if (!job || !job->valid || job->job_id != job_id) {
570 httpd_resp_set_status(req, "400 Bad Request");
571 httpd_resp_set_type(req, "application/json");
572 httpd_resp_send(req, "{\"error\":\"stale job\"}", 19);
573 return ESP_OK;
574 }
575
576 esp_err_t share_err = stratum_client_submit_share(job_id, nonce, ntime, version);
577 bool accepted = (share_err == ESP_OK);
578
579 mining_update_hashrate(client_ip, accepted);
580 mining_client_stats_t *stats = mining_get_or_create_client(client_ip);
581
582 if (accepted) {
583 const tollgate_config_t *cfg = tollgate_config_get();
584 double hashprice = mining_get_current_hashprice();
585 uint64_t allotment_ms = mining_shares_to_allotment_ms(
586 stats->hashrate_ghs, hashprice, cfg->price_per_step, cfg->step_size_ms);
587
588 session_t *session = session_find_by_ip(client_ip);
589 if (!session || !session->active || session->payment_method != PAYMENT_METHOD_MINING) {
590 session = session_create(client_ip, allotment_ms);
591 if (session) session->payment_method = PAYMENT_METHOD_MINING;
592 } else {
593 session_extend(session, allotment_ms);
594 }
595 }
596
597 cJSON *resp = cJSON_CreateObject();
598 cJSON_AddBoolToObject(resp, "accepted", accepted);
599 cJSON_AddNumberToObject(resp, "hashrate_ghs", stats ? stats->hashrate_ghs : 0.0);
600 char *json = cJSON_PrintUnformatted(resp);
601 httpd_resp_set_type(req, "application/json");
602 httpd_resp_send(req, json, strlen(json));
603 cJSON_free(json);
604 cJSON_Delete(resp);
605 return ESP_OK;
606}
607
608static esp_err_t api_get_mining_stats(httpd_req_t *req)
609{
610 stratum_proxy_stats_t proxy_stats;
611 stratum_proxy_get_stats(&proxy_stats);
612
613 const stratum_client_state_t *client_state = stratum_client_get_state();
614
615 cJSON *root = cJSON_CreateObject();
616
617 cJSON *proxy = cJSON_CreateObject();
618 cJSON_AddNumberToObject(proxy, "hashrate_ghs", proxy_stats.hashrate_ghs);
619 cJSON_AddNumberToObject(proxy, "total_shares", (double)proxy_stats.total_shares);
620 cJSON_AddNumberToObject(proxy, "total_accepted", (double)proxy_stats.total_accepted);
621 cJSON_AddNumberToObject(proxy, "total_rejected", (double)proxy_stats.total_rejected);
622 cJSON_AddNumberToObject(proxy, "hashprice", proxy_stats.current_hashprice);
623 cJSON_AddNumberToObject(proxy, "active_miners", proxy_stats.active_miners);
624 cJSON_AddItemToObject(root, "proxy", proxy);
625
626 cJSON *upstream = cJSON_CreateObject();
627 cJSON_AddBoolToObject(upstream, "connected", client_state->connected);
628 cJSON_AddStringToObject(upstream, "pool_host", client_state->pool_host);
629 cJSON_AddNumberToObject(upstream, "pool_port", client_state->pool_port);
630 cJSON_AddNumberToObject(upstream, "difficulty", (double)client_state->difficulty);
631 cJSON_AddNumberToObject(upstream, "shares_accepted", (double)client_state->shares_accepted);
632 cJSON_AddNumberToObject(upstream, "shares_rejected", (double)client_state->shares_rejected);
633 cJSON_AddItemToObject(root, "upstream", upstream);
634
635 char *json = cJSON_PrintUnformatted(root);
636 httpd_resp_set_type(req, "application/json");
637 httpd_resp_send(req, json, strlen(json));
638 cJSON_free(json);
639 cJSON_Delete(root);
640 return ESP_OK;
641}
642
466static const httpd_uri_t uri_discovery = { .uri = "/", .method = HTTP_GET, .handler = api_get_discovery }; 643static const httpd_uri_t uri_discovery = { .uri = "/", .method = HTTP_GET, .handler = api_get_discovery };
467static const httpd_uri_t uri_payment = { .uri = "/", .method = HTTP_POST, .handler = api_post_payment }; 644static const httpd_uri_t uri_payment = { .uri = "/", .method = HTTP_POST, .handler = api_post_payment };
468static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = api_get_usage }; 645static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = api_get_usage };
@@ -470,6 +647,9 @@ static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .h
470static const httpd_uri_t uri_wallet = { .uri = "/wallet", .method = HTTP_GET, .handler = api_get_wallet }; 647static const httpd_uri_t uri_wallet = { .uri = "/wallet", .method = HTTP_GET, .handler = api_get_wallet };
471static const httpd_uri_t uri_wallet_swap = { .uri = "/wallet/swap", .method = HTTP_POST, .handler = api_post_wallet_swap }; 648static const httpd_uri_t uri_wallet_swap = { .uri = "/wallet/swap", .method = HTTP_POST, .handler = api_post_wallet_swap };
472static const httpd_uri_t uri_wallet_send = { .uri = "/wallet/send", .method = HTTP_POST, .handler = api_post_wallet_send }; 649static const httpd_uri_t uri_wallet_send = { .uri = "/wallet/send", .method = HTTP_POST, .handler = api_post_wallet_send };
650static const httpd_uri_t uri_mining_job = { .uri = "/mining/job", .method = HTTP_GET, .handler = api_get_mining_job };
651static const httpd_uri_t uri_mining_share = { .uri = "/mining/share", .method = HTTP_POST, .handler = api_post_mining_share };
652static const httpd_uri_t uri_mining_stats = { .uri = "/mining/stats", .method = HTTP_GET, .handler = api_get_mining_stats };
473 653
474esp_err_t tollgate_api_start(void) 654esp_err_t tollgate_api_start(void)
475{ 655{
@@ -478,7 +658,7 @@ esp_err_t tollgate_api_start(void)
478 httpd_config_t config = HTTPD_DEFAULT_CONFIG(); 658 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
479 config.server_port = 2121; 659 config.server_port = 2121;
480 config.ctrl_port = 32769; 660 config.ctrl_port = 32769;
481 config.max_uri_handlers = 10; 661 config.max_uri_handlers = 16;
482 config.stack_size = 16384; 662 config.stack_size = 16384;
483 663
484 esp_err_t ret = httpd_start(&s_api_server, &config); 664 esp_err_t ret = httpd_start(&s_api_server, &config);
@@ -495,6 +675,13 @@ esp_err_t tollgate_api_start(void)
495 httpd_register_uri_handler(s_api_server, &uri_wallet_swap); 675 httpd_register_uri_handler(s_api_server, &uri_wallet_swap);
496 httpd_register_uri_handler(s_api_server, &uri_wallet_send); 676 httpd_register_uri_handler(s_api_server, &uri_wallet_send);
497 677
678 const tollgate_config_t *cfg = tollgate_config_get();
679 if (cfg->mining_enabled) {
680 httpd_register_uri_handler(s_api_server, &uri_mining_job);
681 httpd_register_uri_handler(s_api_server, &uri_mining_share);
682 httpd_register_uri_handler(s_api_server, &uri_mining_stats);
683 }
684
498 ESP_LOGI(TAG, "TollGate API started on port 2121"); 685 ESP_LOGI(TAG, "TollGate API started on port 2121");
499 return ESP_OK; 686 return ESP_OK;
500} 687}
diff --git a/main/tollgate_client.c b/main/tollgate_client.c
index ac8dcfe..33b108e 100644
--- a/main/tollgate_client.c
+++ b/main/tollgate_client.c
@@ -126,15 +126,25 @@ static bool parse_discovery_response(const char *json_str, tollgate_discovery_t
126 if (val && cJSON_IsString(val)) { 126 if (val && cJSON_IsString(val)) {
127 out->step_size_ms = atoi(val->valuestring); 127 out->step_size_ms = atoi(val->valuestring);
128 } 128 }
129 } else if (strcmp(tag_name->valuestring, "price_per_step") == 0 && tag_len >= 6) { 129 } else if (strcmp(tag_name->valuestring, "price_per_step") == 0 && tag_len >= 4) {
130 cJSON *amount = cJSON_GetArrayItem(tag, 2); 130 cJSON *payment_type = cJSON_GetArrayItem(tag, 2);
131 cJSON *mint = cJSON_GetArrayItem(tag, 4); 131
132 if (cJSON_IsString(payment_type) && strcmp(payment_type->valuestring, "mining") == 0 && tag_len >= 5) {
133 out->mining_available = true;
134 cJSON *port_val = cJSON_GetArrayItem(tag, 3);
135 if (port_val && cJSON_IsString(port_val)) {
136 out->mining_port = (uint16_t)atoi(port_val->valuestring);
137 }
138 } else {
139 cJSON *amount = cJSON_GetArrayItem(tag, 2);
140 cJSON *mint = cJSON_GetArrayItem(tag, 4);
132 141
133 if (amount && cJSON_IsString(amount)) { 142 if (amount && cJSON_IsString(amount)) {
134 out->price_per_step = atoi(amount->valuestring); 143 out->price_per_step = atoi(amount->valuestring);
135 } 144 }
136 if (mint && cJSON_IsString(mint)) { 145 if (mint && cJSON_IsString(mint)) {
137 strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1); 146 strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1);
147 }
138 } 148 }
139 } 149 }
140 } 150 }
diff --git a/main/tollgate_client.h b/main/tollgate_client.h
index 2055e52..ccee624 100644
--- a/main/tollgate_client.h
+++ b/main/tollgate_client.h
@@ -17,6 +17,7 @@ typedef enum {
17 TG_CLIENT_PAYING, 17 TG_CLIENT_PAYING,
18 TG_CLIENT_PAID, 18 TG_CLIENT_PAID,
19 TG_CLIENT_RENEWING, 19 TG_CLIENT_RENEWING,
20 TG_CLIENT_MINING,
20 TG_CLIENT_ERROR 21 TG_CLIENT_ERROR
21} tollgate_client_state_t; 22} tollgate_client_state_t;
22 23
@@ -26,6 +27,8 @@ typedef struct {
26 int step_size_ms; 27 int step_size_ms;
27 char mint_url[TG_CLIENT_MAX_MINT_URL]; 28 char mint_url[TG_CLIENT_MAX_MINT_URL];
28 char metric[TG_CLIENT_MAX_METRIC]; 29 char metric[TG_CLIENT_MAX_METRIC];
30 bool mining_available;
31 uint16_t mining_port;
29} tollgate_discovery_t; 32} tollgate_discovery_t;
30 33
31esp_err_t tollgate_client_init(void); 34esp_err_t tollgate_client_init(void);
diff --git a/main/tollgate_main.c b/main/tollgate_main.c
index fa7a692..30a63db 100644
--- a/main/tollgate_main.c
+++ b/main/tollgate_main.c
@@ -23,6 +23,11 @@
23#include "tollgate_client.h" 23#include "tollgate_client.h"
24#include "lightning_payout.h" 24#include "lightning_payout.h"
25#include "cvm_server.h" 25#include "cvm_server.h"
26#include "stratum_client.h"
27#include "stratum_proxy.h"
28#include "sw_miner.h"
29#include "asic_miner.h"
30#include "mining_payment.h"
26 31
27#define MAX_STA_RETRY 5 32#define MAX_STA_RETRY 5
28static const char *TAG = "tollgate_main"; 33static const char *TAG = "tollgate_main";
@@ -154,6 +159,11 @@ static void start_services(void)
154 session_manager_init(); 159 session_manager_init();
155 160
156 const tollgate_config_t *cfg = tollgate_config_get(); 161 const tollgate_config_t *cfg = tollgate_config_get();
162 if (cfg->mining_enabled) {
163 firewall_set_mining_port(cfg->mining_port);
164 firewall_set_sandbox_mint_access(cfg->mining_sandbox_mint_access);
165 }
166
157 nucula_wallet_init(cfg->mint_url); 167 nucula_wallet_init(cfg->mint_url);
158 lightning_payout_init(&cfg->payout); 168 lightning_payout_init(&cfg->payout);
159 169
@@ -175,6 +185,26 @@ static void start_services(void)
175 cvm_server_start(); 185 cvm_server_start();
176 } 186 }
177 187
188 if (cfg2->mining_enabled) {
189 ESP_LOGI(TAG, "Mining subsystem enabled, initializing...");
190 mining_payment_init();
191 stratum_client_init();
192 stratum_proxy_init(cfg2->mining_port);
193
194 if (cfg2->mining_payout_mode != MINING_PAYOUT_UPSTREAM) {
195 stratum_client_start();
196 }
197
198 asic_miner_init();
199 if (asic_miner_is_present()) {
200 asic_miner_start();
201 ESP_LOGI(TAG, "ASIC miner started");
202 } else {
203 sw_miner_start();
204 ESP_LOGI(TAG, "Software miner started (no ASIC)");
205 }
206 }
207
178 s_services_running = true; 208 s_services_running = true;
179 if (s_services_mutex) xSemaphoreGive(s_services_mutex); 209 if (s_services_mutex) xSemaphoreGive(s_services_mutex);
180 ESP_LOGI(TAG, "=== TollGate services started ==="); 210 ESP_LOGI(TAG, "=== TollGate services started ===");
@@ -329,5 +359,6 @@ void app_main(void)
329 session_tick(); 359 session_tick();
330 tollgate_client_tick(); 360 tollgate_client_tick();
331 lightning_payout_tick(); 361 lightning_payout_tick();
362 stratum_client_tick();
332 } 363 }
333} 364}