upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main/captive_portal.c
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-19 14:25:18 +0530
committerYour Name <you@example.com>2026-05-19 14:25:18 +0530
commite366ceb336550a72c76efea4c98a2a08cca27bce (patch)
tree4b45ac6f6e97b6763f81aa6d4a9b968d23e41235 /main/captive_portal.c
parent163b8badec9359373a8fc016c2b1fe9ee38e6406 (diff)
feat(mining): Bitcoin mining-for-bandwidth payment system
New modules: - mining_payment.c/h: hashprice calc (nbits->difficulty->sat/GH/s/day), share validation, client stats, allotment conversion (ms + bytes) - stratum_client.c/h: SV1 upstream pool connection (subscribe/authorize/submit) - stratum_proxy.c/h: Local SV1 TCP server for downstream miners, job broadcast - sw_miner.c/h: Software SHA256d miner (ESP32 CPU fallback) - asic_miner.c/h: ASIC detection stub (BM1366/BM1368 SPI) Config: - config.h/c: mining_payout_mode_t enum (auto/pool/upstream/proxy_only), stratum pool settings, mining port, hashprice override, sandbox mint access - Defaults fill nostr_seed_relays (8/8) and nostr_relays (4/4) with fast relays Integration into existing modules: - session.h/c: payment_method_t enum (CASHU/MINING/BYTES) - firewall.h/c: firewall_set_mining_port(), firewall_set_sandbox_mint_access() - tollgate_api.c: GET /mining/job, POST /mining/share, GET /mining/stats - tollgate_client.h/c: TG_CLIENT_MINING state, mining discovery tag parsing - tollgate_main.c: mining init in start_services(), stratum_client_tick() in loop - captive_portal.c: tabbed Cashu/Mine UI with live hashrate polling Unit tests (69 new assertions across 4 suites): - test_mining_payment (23 tests): nbits->difficulty, hashprice, client stats, allotment - test_stratum_proxy (21 tests): job set/get, stats, type validation - test_session_payment_method (12 tests): PAYMENT_METHOD enum, bytes/cashu methods - test_tollgate_client_mining (20 tests): mining tag parsing, discovery struct - test_firewall_sandbox (16 tests): client grant/revoke, max clients, setters Enhanced test stubs: - BaseType_t/pdPASS in freertos/task.h - lwip: sockets.h, etharp.h, prot/ip.h, prot/ip4.h, prot/tcp.h, netif.h - dns_server.h, esp_wifi_ap_get_sta_list.h Build fixes: - cvm_server.c: replace esp_timer_get_time() with xTaskGetTickCount(), fix process_relay_message() 3-arg call to 2-arg, add WS keepalive ping - stratum_proxy.c: widen task_name buffer 16->20 - sw_miner.c: add missing #include esp_random.h - nucula_src: save_proofs() moved to public in wallet.hpp Nostr relay updates: - nostr_seed_relays: +relay.anzenkodo.workers.dev, +nostr.koning-degraaf.nl, +knostr.neutrine.com, +nostr.einundzwanzig.space (8/8 slots) - nostr_relays: +relay.anzenkodo.workers.dev, +nostr.koning-degraaf.nl (4/4 slots) Squash-merge of feature/mining-payment (5 commits: c75230e..9d98ba1)
Diffstat (limited to 'main/captive_portal.c')
-rw-r--r--main/captive_portal.c133
1 files changed, 77 insertions, 56 deletions
diff --git a/main/captive_portal.c b/main/captive_portal.c
index c9bcf19..ea83906 100644
--- a/main/captive_portal.c
+++ b/main/captive_portal.c
@@ -2,7 +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 "mint_health.h" 5#include "mining_payment.h"
6#include "stratum_proxy.h"
6#include "esp_log.h" 7#include "esp_log.h"
7#include "esp_wifi.h" 8#include "esp_wifi.h"
8#include "cJSON.h" 9#include "cJSON.h"
@@ -32,6 +33,12 @@ static const char PORTAL_HTML_TEMPLATE[] = \
32"max-width:400px;width:100%;text-align:center}" 33"max-width:400px;width:100%;text-align:center}"
33"h1{font-size:28px;margin-bottom:8px;color:#f7931a}" 34"h1{font-size:28px;margin-bottom:8px;color:#f7931a}"
34".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}"
35".price{background:#252525;border-radius:12px;padding:16px;margin-bottom:16px}" 42".price{background:#252525;border-radius:12px;padding:16px;margin-bottom:16px}"
36".price-amount{font-size:36px;font-weight:bold;color:#f7931a}" 43".price-amount{font-size:36px;font-weight:bold;color:#f7931a}"
37".price-unit{color:#888;font-size:14px}" 44".price-unit{color:#888;font-size:14px}"
@@ -43,24 +50,26 @@ static const char PORTAL_HTML_TEMPLATE[] = \
43".btn:disabled{background:#333;color:#666;cursor:not-allowed}" 50".btn:disabled{background:#333;color:#666;cursor:not-allowed}"
44".mints{background:#252525;border-radius:12px;padding:12px;margin-top:16px;text-align:left}" 51".mints{background:#252525;border-radius:12px;padding:12px;margin-top:16px;text-align:left}"
45".mints-title{color:#888;font-size:12px;margin-bottom:8px}" 52".mints-title{color:#888;font-size:12px;margin-bottom:8px}"
46".mint-item{display:flex;align-items:center;padding:6px 8px;margin-bottom:4px;" 53".mint-url{font-family:monospace;font-size:11px;color:#f7931a;word-break:break-all;"
47"background:#1a1a1a;border-radius:6px;cursor:pointer}" 54"background:#1a1a1a;padding:8px;border-radius:6px;cursor:pointer}"
48".mint-item:active{opacity:0.7}" 55".mint-url:active{opacity:0.7}"
49".mint-dot{width:8px;height:8px;border-radius:50%;margin-right:8px;flex-shrink:0}"
50".mint-dot.green{background:#4caf50}"
51".mint-dot.grey{background:#666}"
52".mint-url{font-family:monospace;font-size:11px;color:#f7931a;word-break:break-all}"
53".mint-url.dim{color:#666}"
54".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}"
55"#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}"
56"#status.success{display:block;background:#1a472a;color:#4caf50}" 62"#status.success{display:block;background:#1a472a;color:#4caf50}"
57"#status.error{display:block;background:#471a1a;color:#f44336}" 63"#status.error{display:block;background:#471a1a;color:#f44336}"
58"#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}"
59"</style>" 66"</style>"
60"</head><body>" 67"</head><body>"
61"<div class='card'>" 68"<div class='card'>"
62"<h1>TollGate</h1>" 69"<h1>TollGate</h1>"
63"<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__'>"
64"<div class='price'>" 73"<div class='price'>"
65"<div class='price-amount'>__PRICE__</div>" 74"<div class='price-amount'>__PRICE__</div>"
66"<div class='price-unit'>sats per minute</div>" 75"<div class='price-unit'>sats per minute</div>"
@@ -69,21 +78,40 @@ static const char PORTAL_HTML_TEMPLATE[] = \
69"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>" 78"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>"
70"<div class='mints'>" 79"<div class='mints'>"
71"<div class='mints-title'>SUPPORTED MINTS</div>" 80"<div class='mints-title'>SUPPORTED MINTS</div>"
72"<div id='mintList'>__MINT_LIST__</div>" 81"<div class='mint-url' id='mintUrl' onclick='copyMint()'>__MINT_URL__</div>"
73"<div class='mint-hint'>Tap to copy &bull; Green = reachable</div>" 82"<div class='mint-hint'>Tap to copy &bull; Mint tokens at this URL before paying</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>"
74"</div>" 95"</div>"
75"<div id='status'></div>" 96"<div id='status'></div>"
76"</div>" 97"</div>"
77"<script>" 98"<script>"
78"const mintListEl=document.getElementById('mintList');" 99"const mintUrlEl=document.getElementById('mintUrl');"
100"const mintUrl=mintUrlEl.textContent;"
79"const statusEl=document.getElementById('status');" 101"const statusEl=document.getElementById('status');"
80"const payBtn=document.getElementById('payBtn');" 102"const payBtn=document.getElementById('payBtn');"
81"const tokenInput=document.getElementById('tokenInput');" 103"const tokenInput=document.getElementById('tokenInput');"
82"function copyMint(url){" 104"let miningActive=false;"
83"if(navigator.clipboard){navigator.clipboard.writeText(url);" 105"let miningInterval=null;"
84"const el=event.currentTarget;const u=el.querySelector('.mint-url');" 106"function switchTab(tab){"
85"const orig=u.textContent;u.textContent='Copied!';" 107"document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));"
86"setTimeout(()=>{u.textContent=orig;},1000);}" 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"}"
112"function copyMint(){"
113"if(navigator.clipboard){navigator.clipboard.writeText(mintUrl);"
114"mintUrlEl.textContent='Copied!';setTimeout(()=>{mintUrlEl.textContent=mintUrl;},1000);}"
87"}" 115"}"
88"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}" 116"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}"
89"function payToken(){" 117"function payToken(){"
@@ -100,20 +128,24 @@ static const char PORTAL_HTML_TEMPLATE[] = \
100"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;}"
101"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});" 129"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});"
102"}" 130"}"
103"function refreshMints(){" 131"function pollMiningStats(){"
104"fetch('http://__AP_IP__:2121/mints').then(r=>r.json()).then(data=>{" 132"fetch('http://__AP_IP__:2121/mining/stats').then(r=>r.json()).then(d=>{"
105"let html='';" 133"document.getElementById('hashrate').textContent=d.proxy.hashrate_ghs.toFixed(2)+' GH/s';"
106"for(const m of data){" 134"document.getElementById('shareCount').textContent=d.proxy.total_accepted;"
107"const cls=m.reachable?'green':'grey';" 135"document.getElementById('hashprice').textContent=d.proxy.hashprice.toFixed(2)+' sat/GH/s/day';"
108"const urlCls=m.reachable?'mint-url':'mint-url dim';"
109"html+='<div class=\"mint-item\" onclick=\"copyMint(\\''+m.url+'\\')\">';"
110"html+='<span class=\"mint-dot '+cls+'\"></span>';"
111"html+='<span class=\"'+urlCls+'\">'+m.url+'</span></div>';"
112"}"
113"if(html)mintListEl.innerHTML=html;"
114"}).catch(()=>{});" 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();"
115"}" 148"}"
116"setInterval(refreshMints,30000);"
117"</script>" 149"</script>"
118"</body></html>"; 150"</body></html>";
119 151
@@ -143,36 +175,25 @@ static esp_err_t portal_handler(httpd_req_t *req)
143 const char *tpl = PORTAL_HTML_TEMPLATE; 175 const char *tpl = PORTAL_HTML_TEMPLATE;
144 size_t tpl_len = strlen(tpl); 176 size_t tpl_len = strlen(tpl);
145 177
146 char mint_list_html[4096];
147 size_t mint_list_cap = sizeof(mint_list_html);
148 size_t mint_list_len = 0;
149 mint_list_html[0] = '\0';
150 int mint_count = 0;
151 const mint_status_t *mints = mint_health_get_all(&mint_count);
152 for (int i = 0; i < mint_count; i++) {
153 const char *cls = mints[i].reachable ? "green" : "grey";
154 const char *url_cls = mints[i].reachable ? "mint-url" : "mint-url dim";
155 int written = snprintf(mint_list_html + mint_list_len, mint_list_cap - mint_list_len,
156 "<div class='mint-item' onclick='copyMint(\"%s\")'>"
157 "<span class='mint-dot %s'></span>"
158 "<span class='%s'>%s</span></div>",
159 mints[i].url, cls, url_cls, mints[i].url);
160 if (written > 0 && (size_t)written < mint_list_cap - mint_list_len) {
161 mint_list_len += (size_t)written;
162 }
163 }
164 if (mint_count == 0) {
165 const tollgate_config_t *cfg = tollgate_config_get();
166 snprintf(mint_list_html, sizeof(mint_list_html),
167 "<div class='mint-item'><span class='mint-dot grey'></span>"
168 "<span class='mint-url dim'>%s</span></div>", cfg->mint_url);
169 }
170
171 struct { const char *key; const char *val; } subs[] = { 178 struct { const char *key; const char *val; } subs[] = {
172 { "__AP_IP__", s_ap_ip_str }, 179 { "__AP_IP__", s_ap_ip_str },
173 { "__PRICE__", price_str }, 180 { "__PRICE__", price_str },
174 { "__MINT_LIST__", mint_list_html }, 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__", "" },
175 }; 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 }
176 int nsubs = sizeof(subs) / sizeof(subs[0]); 197 int nsubs = sizeof(subs) / sizeof(subs[0]);
177 198
178 size_t extra = 0; 199 size_t extra = 0;