diff options
| author | Your Name <you@example.com> | 2026-05-19 14:25:18 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-19 14:25:18 +0530 |
| commit | e366ceb336550a72c76efea4c98a2a08cca27bce (patch) | |
| tree | 4b45ac6f6e97b6763f81aa6d4a9b968d23e41235 /main/captive_portal.c | |
| parent | 163b8badec9359373a8fc016c2b1fe9ee38e6406 (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.c | 133 |
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 • Green = reachable</div>" | 82 | "<div class='mint-hint'>Tap to copy • 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; |