upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
Diffstat (limited to 'main')
-rw-r--r--main/CMakeLists.txt9
-rw-r--r--main/config.c88
-rw-r--r--main/config.h14
-rw-r--r--main/geohash.c48
-rw-r--r--main/geohash.h8
-rw-r--r--main/identity.c124
-rw-r--r--main/identity.h29
-rw-r--r--main/nostr_event.c112
-rw-r--r--main/nostr_event.h25
-rw-r--r--main/tollgate_api.c63
-rw-r--r--main/tollgate_main.c31
-rw-r--r--main/wallet.c639
-rw-r--r--main/wallet.h53
-rw-r--r--main/wallet_persist.c147
-rw-r--r--main/wallet_persist.h9
-rw-r--r--main/wifistr.c252
-rw-r--r--main/wifistr.h10
17 files changed, 734 insertions, 927 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 2eef030..df69283 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -6,9 +6,12 @@ idf_component_register(SRCS "tollgate_main.c"
6 "cashu.c" 6 "cashu.c"
7 "session.c" 7 "session.c"
8 "tollgate_api.c" 8 "tollgate_api.c"
9 "wallet.c" 9 "identity.c"
10 "wallet_persist.c" 10 "nostr_event.c"
11 INCLUDE_DIRS "." "${IDF_PATH}/components/lwip/include/apps" 11 "geohash.c"
12 "wifistr.c"
13 INCLUDE_DIRS "."
12 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server 14 REQUIRES esp_wifi esp_event esp_netif nvs_flash esp_http_server
13 lwip json esp_http_client mbedtls esp-tls log spiffs 15 lwip json esp_http_client mbedtls esp-tls log spiffs
16 nucula_lib secp256k1
14 PRIV_REQUIRES esp-tls) 17 PRIV_REQUIRES esp-tls)
diff --git a/main/config.c b/main/config.c
index 7e8a14c..47d631f 100644
--- a/main/config.c
+++ b/main/config.c
@@ -1,4 +1,5 @@
1#include "config.h" 1#include "config.h"
2#include "identity.h"
2#include "esp_log.h" 3#include "esp_log.h"
3#include "esp_spiffs.h" 4#include "esp_spiffs.h"
4#include "esp_system.h" 5#include "esp_system.h"
@@ -20,6 +21,7 @@ esp_err_t tollgate_config_init(void)
20 g_config.price_per_step = 21; 21 g_config.price_per_step = 21;
21 g_config.step_size_ms = 60000; 22 g_config.step_size_ms = 60000;
22 g_config.persist_threshold_sats = 1; 23 g_config.persist_threshold_sats = 1;
24 g_config.nostr_publish_interval_s = 21600;
23 25
24 esp_vfs_spiffs_conf_t conf = { 26 esp_vfs_spiffs_conf_t conf = {
25 .base_path = "/spiffs", 27 .base_path = "/spiffs",
@@ -37,16 +39,17 @@ esp_err_t tollgate_config_init(void)
37 if (!f) { 39 if (!f) {
38 ESP_LOGW(TAG, "No config.json found, generating default"); 40 ESP_LOGW(TAG, "No config.json found, generating default");
39 const char *default_json = "{" 41 const char *default_json = "{"
42 "\"nsec\":\"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\","
40 "\"wifi_networks\":[" 43 "\"wifi_networks\":["
41 "{\"ssid\":\"EnterSSID-2.4GHz\",\"password\":\"c03rad0r123!\"}" 44 "{\"ssid\":\"EnterSSID-2.4GHz\",\"password\":\"c03rad0r123!\"}"
42 "]," 45 "],"
43 "\"ap_ssid\":\"TollGate\","
44 "\"ap_password\":\"\"," 46 "\"ap_password\":\"\","
45 "\"ap_channel\":1,"
46 "\"mint_url\":\"https://testnut.cashu.space\"," 47 "\"mint_url\":\"https://testnut.cashu.space\","
47 "\"lnurl_url\":\"https://redeem.cashu.me/.well-known/lnurlp/tollgate\","
48 "\"price_per_step\":21," 48 "\"price_per_step\":21,"
49 "\"step_size_ms\":60000" 49 "\"step_size_ms\":60000,"
50 "\"nostr_geohash\":\"u281w0dfz\","
51 "\"nostr_relays\":[\"wss://relay.damus.io\",\"wss://nos.lol\"],"
52 "\"nostr_publish_interval_s\":21600"
50 "}"; 53 "}";
51 f = fopen("/spiffs/config.json", "w"); 54 f = fopen("/spiffs/config.json", "w");
52 if (f) { 55 if (f) {
@@ -80,6 +83,15 @@ esp_err_t tollgate_config_init(void)
80 return ESP_FAIL; 83 return ESP_FAIL;
81 } 84 }
82 85
86 cJSON *nsec = cJSON_GetObjectItem(root, "nsec");
87 if (nsec && cJSON_IsString(nsec)) {
88 strncpy(g_config.nsec, nsec->valuestring, sizeof(g_config.nsec) - 1);
89 } else {
90 ESP_LOGE(TAG, "Missing 'nsec' in config.json");
91 cJSON_Delete(root);
92 return ESP_FAIL;
93 }
94
83 cJSON *networks = cJSON_GetObjectItem(root, "wifi_networks"); 95 cJSON *networks = cJSON_GetObjectItem(root, "wifi_networks");
84 if (networks && cJSON_IsArray(networks)) { 96 if (networks && cJSON_IsArray(networks)) {
85 int count = cJSON_GetArraySize(networks); 97 int count = cJSON_GetArraySize(networks);
@@ -96,16 +108,9 @@ esp_err_t tollgate_config_init(void)
96 } 108 }
97 } 109 }
98 110
99 cJSON *ap_ssid = cJSON_GetObjectItem(root, "ap_ssid");
100 if (ap_ssid) strncpy(g_config.ap_ssid, ap_ssid->valuestring, sizeof(g_config.ap_ssid) - 1);
101 else strncpy(g_config.ap_ssid, "TollGate", sizeof(g_config.ap_ssid) - 1);
102
103 cJSON *ap_pass = cJSON_GetObjectItem(root, "ap_password"); 111 cJSON *ap_pass = cJSON_GetObjectItem(root, "ap_password");
104 if (ap_pass) strncpy(g_config.ap_password, ap_pass->valuestring, sizeof(g_config.ap_password) - 1); 112 if (ap_pass) strncpy(g_config.ap_password, ap_pass->valuestring, sizeof(g_config.ap_password) - 1);
105 113
106 cJSON *ap_ch = cJSON_GetObjectItem(root, "ap_channel");
107 if (ap_ch) g_config.ap_channel = ap_ch->valueint;
108
109 cJSON *mint = cJSON_GetObjectItem(root, "mint_url"); 114 cJSON *mint = cJSON_GetObjectItem(root, "mint_url");
110 if (mint) strncpy(g_config.mint_url, mint->valuestring, sizeof(g_config.mint_url) - 1); 115 if (mint) strncpy(g_config.mint_url, mint->valuestring, sizeof(g_config.mint_url) - 1);
111 116
@@ -121,9 +126,37 @@ esp_err_t tollgate_config_init(void)
121 cJSON *persist = cJSON_GetObjectItem(root, "persist_threshold_sats"); 126 cJSON *persist = cJSON_GetObjectItem(root, "persist_threshold_sats");
122 if (persist) g_config.persist_threshold_sats = (uint64_t)persist->valuedouble; 127 if (persist) g_config.persist_threshold_sats = (uint64_t)persist->valuedouble;
123 128
129 cJSON *geohash = cJSON_GetObjectItem(root, "nostr_geohash");
130 if (geohash) strncpy(g_config.nostr_geohash, geohash->valuestring, sizeof(g_config.nostr_geohash) - 1);
131 else strncpy(g_config.nostr_geohash, "u281w0dfz", sizeof(g_config.nostr_geohash) - 1);
132
133 cJSON *relays = cJSON_GetObjectItem(root, "nostr_relays");
134 if (relays && cJSON_IsArray(relays)) {
135 int rcount = cJSON_GetArraySize(relays);
136 if (rcount > TOLLGATE_MAX_RELAYS) rcount = TOLLGATE_MAX_RELAYS;
137 for (int i = 0; i < rcount; i++) {
138 cJSON *r = cJSON_GetArrayItem(relays, i);
139 if (r && cJSON_IsString(r)) {
140 strncpy(g_config.nostr_relays[i], r->valuestring, sizeof(g_config.nostr_relays[i]) - 1);
141 g_config.nostr_relay_count++;
142 }
143 }
144 }
145
146 cJSON *pub_interval = cJSON_GetObjectItem(root, "nostr_publish_interval_s");
147 if (pub_interval) g_config.nostr_publish_interval_s = pub_interval->valueint;
148
124 cJSON_Delete(root); 149 cJSON_Delete(root);
125 ESP_LOGI(TAG, "Config loaded: AP='%s', %d WiFi networks, price=%d sats/%dms", 150
126 g_config.ap_ssid, g_config.network_count, g_config.price_per_step, g_config.step_size_ms); 151 if (g_config.nostr_relay_count == 0) {
152 strncpy(g_config.nostr_relays[0], "wss://relay.damus.io", sizeof(g_config.nostr_relays[0]) - 1);
153 strncpy(g_config.nostr_relays[1], "wss://nos.lol", sizeof(g_config.nostr_relays[1]) - 1);
154 g_config.nostr_relay_count = 2;
155 }
156
157 ESP_LOGI(TAG, "Config loaded: nsec=%s...%s, %d WiFi networks, price=%d sats/%dms",
158 g_config.nsec, g_config.nsec + 60, g_config.network_count,
159 g_config.price_per_step, g_config.step_size_ms);
127 return ESP_OK; 160 return ESP_OK;
128} 161}
129 162
@@ -151,22 +184,23 @@ esp_err_t tollgate_config_get_next_wifi(wifi_config_t *wifi_config)
151 184
152void tollgate_config_derive_unique(tollgate_config_t *cfg) 185void tollgate_config_derive_unique(tollgate_config_t *cfg)
153{ 186{
154 if (cfg->unique_derived) return; 187 if (cfg->identity_initialized) return;
155
156 uint8_t mac[6];
157 esp_read_mac(mac, ESP_MAC_WIFI_STA);
158 188
159 snprintf(cfg->ap_ssid + strlen(cfg->ap_ssid), 189 const tollgate_identity_t *id = identity_get();
160 TOLLGATE_MAX_AP_SSID_LEN - strlen(cfg->ap_ssid), 190 if (!id || !id->initialized) {
161 "-%02X%02X", mac[4], mac[5]); 191 ESP_LOGE(TAG, "Cannot derive unique config: identity not initialized");
192 return;
193 }
162 194
163 uint8_t b5 = mac[4]; 195 strncpy(cfg->ap_ssid, id->ap_ssid, sizeof(cfg->ap_ssid) - 1);
164 uint8_t b6 = mac[5]; 196 memcpy(cfg->sta_mac, id->sta_mac, 6);
165 uint8_t subnet = (b5 ^ b6) % 200 + 10; 197 memcpy(cfg->ap_mac, id->ap_mac, 6);
166 IP4_ADDR(&cfg->ap_ip, 10, b5, subnet, 1); 198 cfg->ap_ip = id->ap_ip;
167 snprintf(cfg->ap_ip_str, sizeof(cfg->ap_ip_str), IPSTR, IP2STR(&cfg->ap_ip)); 199 strncpy(cfg->ap_ip_str, id->ap_ip_str, sizeof(cfg->ap_ip_str) - 1);
200 strncpy(cfg->npub, id->npub_hex, sizeof(cfg->npub) - 1);
168 201
169 cfg->unique_derived = true; 202 cfg->identity_initialized = true;
170 203
171 ESP_LOGI(TAG, "Unique config: SSID='%s', AP_IP=%s", cfg->ap_ssid, cfg->ap_ip_str); 204 ESP_LOGI(TAG, "Unique config derived from nsec: SSID='%s', AP_IP=%s",
205 cfg->ap_ssid, cfg->ap_ip_str);
172} 206}
diff --git a/main/config.h b/main/config.h
index 2bcd400..8254a62 100644
--- a/main/config.h
+++ b/main/config.h
@@ -10,6 +10,7 @@
10#define TOLLGATE_MAX_MINT_URLS 3 10#define TOLLGATE_MAX_MINT_URLS 3
11#define TOLLGATE_MAX_AP_SSID_LEN 32 11#define TOLLGATE_MAX_AP_SSID_LEN 32
12#define TOLLGATE_MAX_AP_PASS_LEN 64 12#define TOLLGATE_MAX_AP_PASS_LEN 64
13#define TOLLGATE_MAX_RELAYS 4
13 14
14typedef struct { 15typedef struct {
15 char ssid[32]; 16 char ssid[32];
@@ -22,11 +23,17 @@ typedef struct {
22 int current_network; 23 int current_network;
23 int max_retry; 24 int max_retry;
24 25
26 char nsec[65];
27 char npub[65];
28
25 char ap_ssid[TOLLGATE_MAX_AP_SSID_LEN]; 29 char ap_ssid[TOLLGATE_MAX_AP_SSID_LEN];
26 char ap_password[TOLLGATE_MAX_AP_PASS_LEN]; 30 char ap_password[TOLLGATE_MAX_AP_PASS_LEN];
27 uint8_t ap_channel; 31 uint8_t ap_channel;
28 uint8_t ap_max_conn; 32 uint8_t ap_max_conn;
29 33
34 uint8_t sta_mac[6];
35 uint8_t ap_mac[6];
36
30 esp_ip4_addr_t ap_ip; 37 esp_ip4_addr_t ap_ip;
31 char ap_ip_str[16]; 38 char ap_ip_str[16];
32 39
@@ -36,7 +43,12 @@ typedef struct {
36 int step_size_ms; 43 int step_size_ms;
37 uint64_t persist_threshold_sats; 44 uint64_t persist_threshold_sats;
38 45
39 bool unique_derived; 46 char nostr_geohash[16];
47 char nostr_relays[TOLLGATE_MAX_RELAYS][128];
48 int nostr_relay_count;
49 int nostr_publish_interval_s;
50
51 bool identity_initialized;
40} tollgate_config_t; 52} tollgate_config_t;
41 53
42void tollgate_config_derive_unique(tollgate_config_t *cfg); 54void tollgate_config_derive_unique(tollgate_config_t *cfg);
diff --git a/main/geohash.c b/main/geohash.c
new file mode 100644
index 0000000..f649824
--- /dev/null
+++ b/main/geohash.c
@@ -0,0 +1,48 @@
1#include "geohash.h"
2#include <string.h>
3
4static const char BASE32[] = "0123456789bcdefghjkmnpqrstuvwxyz";
5
6void geohash_encode(double lat, double lon, int precision, char *out)
7{
8 double lat_range[2] = { -90.0, 90.0 };
9 double lon_range[2] = { -180.0, 180.0 };
10 uint8_t hash_bytes[16];
11 int bit_count = precision * 5;
12 int byte_count = (bit_count + 7) / 8;
13 memset(hash_bytes, 0, sizeof(hash_bytes));
14
15 for (int i = 0; i < bit_count; i++) {
16 int byte_idx = i / 8;
17 int bit_idx = 7 - (i % 8);
18
19 if (i % 2 == 0) {
20 double mid = (lon_range[0] + lon_range[1]) / 2.0;
21 if (lon >= mid) {
22 hash_bytes[byte_idx] |= (1 << bit_idx);
23 lon_range[0] = mid;
24 } else {
25 lon_range[1] = mid;
26 }
27 } else {
28 double mid = (lat_range[0] + lat_range[1]) / 2.0;
29 if (lat >= mid) {
30 hash_bytes[byte_idx] |= (1 << bit_idx);
31 lat_range[0] = mid;
32 } else {
33 lat_range[1] = mid;
34 }
35 }
36 }
37
38 for (int i = 0; i < precision; i++) {
39 int byte_idx = (i * 5) / 8;
40 int bit_offset = (i * 5) % 8;
41 uint16_t val = (hash_bytes[byte_idx] << 8);
42 if (byte_idx + 1 < (int)sizeof(hash_bytes))
43 val |= hash_bytes[byte_idx + 1];
44 val = (val >> (16 - 5 - bit_offset)) & 0x1F;
45 out[i] = BASE32[val];
46 }
47 out[precision] = '\0';
48}
diff --git a/main/geohash.h b/main/geohash.h
new file mode 100644
index 0000000..f8eb69d
--- /dev/null
+++ b/main/geohash.h
@@ -0,0 +1,8 @@
1#ifndef GEOHASH_H
2#define GEOHASH_H
3
4#include <stddef.h>
5
6void geohash_encode(double lat, double lon, int precision, char *out);
7
8#endif
diff --git a/main/identity.c b/main/identity.c
new file mode 100644
index 0000000..1dab415
--- /dev/null
+++ b/main/identity.c
@@ -0,0 +1,124 @@
1#include "identity.h"
2#include "config.h"
3#include "esp_log.h"
4#include "lwip/ip4_addr.h"
5#include "mbedtls/md.h"
6#include "secp256k1.h"
7#include "secp256k1_extrakeys.h"
8#include <string.h>
9#include <stdio.h>
10#include <stdlib.h>
11
12static const char *TAG = "identity";
13static tollgate_identity_t s_identity;
14
15static int hex_to_bytes(const char *hex, uint8_t *out, size_t out_len)
16{
17 if (strlen(hex) != out_len * 2) return 0;
18 for (size_t i = 0; i < out_len; i++) {
19 unsigned int byte;
20 if (sscanf(hex + i * 2, "%02x", &byte) != 1) return 0;
21 out[i] = (uint8_t)byte;
22 }
23 return 1;
24}
25
26static void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex)
27{
28 for (size_t i = 0; i < len; i++)
29 sprintf(hex + i * 2, "%02x", bytes[i]);
30 hex[len * 2] = '\0';
31}
32
33static void tollgate_derive(const uint8_t nsec[32], const char *label,
34 uint32_t index, uint8_t *out, size_t out_len)
35{
36 size_t label_len = strlen(label);
37 size_t msg_len = label_len + 4;
38 uint8_t *msg = (uint8_t *)malloc(msg_len);
39 memcpy(msg, label, label_len);
40 msg[label_len] = (uint8_t)(index & 0xff);
41 msg[label_len + 1] = (uint8_t)((index >> 8) & 0xff);
42 msg[label_len + 2] = (uint8_t)((index >> 16) & 0xff);
43 msg[label_len + 3] = (uint8_t)((index >> 24) & 0xff);
44
45 uint8_t hmac[64];
46 mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA512),
47 nsec, 32, msg, msg_len, hmac);
48 free(msg);
49
50 memcpy(out, hmac, out_len);
51}
52
53esp_err_t identity_init(const char *nsec_hex)
54{
55 memset(&s_identity, 0, sizeof(s_identity));
56
57 if (!nsec_hex || strlen(nsec_hex) != 64) {
58 ESP_LOGE(TAG, "Invalid nsec: must be 64 hex chars");
59 return ESP_ERR_INVALID_ARG;
60 }
61
62 strncpy(s_identity.nsec_hex, nsec_hex, sizeof(s_identity.nsec_hex) - 1);
63
64 if (!hex_to_bytes(nsec_hex, s_identity.nsec, 32)) {
65 ESP_LOGE(TAG, "Failed to parse nsec hex");
66 return ESP_ERR_INVALID_ARG;
67 }
68
69 secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
70 if (!ctx) {
71 ESP_LOGE(TAG, "Failed to create secp256k1 context");
72 return ESP_ERR_NO_MEM;
73 }
74
75 secp256k1_pubkey pubkey;
76 if (!secp256k1_ec_pubkey_create(ctx, &pubkey, s_identity.nsec)) {
77 ESP_LOGE(TAG, "Invalid nsec: secp256k1 key creation failed");
78 secp256k1_context_destroy(ctx);
79 return ESP_ERR_INVALID_ARG;
80 }
81
82 secp256k1_xonly_pubkey xonly;
83 secp256k1_xonly_pubkey_from_pubkey(ctx, &xonly, NULL, &pubkey);
84 uint8_t npub_bytes[32];
85 secp256k1_xonly_pubkey_serialize(ctx, npub_bytes, &xonly);
86 bytes_to_hex(npub_bytes, 32, s_identity.npub_hex);
87
88 tollgate_derive(s_identity.nsec, "sta-mac", 0, s_identity.sta_mac, 6);
89 s_identity.sta_mac[0] = (s_identity.sta_mac[0] | 0x02) & 0xFE;
90
91 tollgate_derive(s_identity.nsec, "ap-mac", 0, s_identity.ap_mac, 6);
92 s_identity.ap_mac[0] = (s_identity.ap_mac[0] | 0x02) & 0xFE;
93
94 snprintf(s_identity.ap_ssid, sizeof(s_identity.ap_ssid),
95 "TollGate-%02X%02X%02X",
96 s_identity.ap_mac[3], s_identity.ap_mac[4], s_identity.ap_mac[5]);
97
98 uint8_t b3 = s_identity.ap_mac[3];
99 uint8_t b4 = s_identity.ap_mac[4];
100 uint8_t b5 = s_identity.ap_mac[5];
101 uint8_t subnet = (b4 ^ b5) % 200 + 10;
102 IP4_ADDR(&s_identity.ap_ip, 10, b3, subnet, 1);
103 snprintf(s_identity.ap_ip_str, sizeof(s_identity.ap_ip_str),
104 IPSTR, IP2STR(&s_identity.ap_ip));
105
106 secp256k1_context_destroy(ctx);
107 s_identity.initialized = true;
108
109 ESP_LOGI(TAG, "Identity: npub=%s", s_identity.npub_hex);
110 ESP_LOGI(TAG, " STA MAC: %02X:%02X:%02X:%02X:%02X:%02X",
111 s_identity.sta_mac[0], s_identity.sta_mac[1], s_identity.sta_mac[2],
112 s_identity.sta_mac[3], s_identity.sta_mac[4], s_identity.sta_mac[5]);
113 ESP_LOGI(TAG, " AP MAC: %02X:%02X:%02X:%02X:%02X:%02X",
114 s_identity.ap_mac[0], s_identity.ap_mac[1], s_identity.ap_mac[2],
115 s_identity.ap_mac[3], s_identity.ap_mac[4], s_identity.ap_mac[5]);
116 ESP_LOGI(TAG, " SSID: %s, AP IP: %s", s_identity.ap_ssid, s_identity.ap_ip_str);
117
118 return ESP_OK;
119}
120
121const tollgate_identity_t *identity_get(void)
122{
123 return &s_identity;
124}
diff --git a/main/identity.h b/main/identity.h
new file mode 100644
index 0000000..2990455
--- /dev/null
+++ b/main/identity.h
@@ -0,0 +1,29 @@
1#ifndef IDENTITY_H
2#define IDENTITY_H
3
4#include "esp_err.h"
5#include "esp_wifi.h"
6#include "esp_netif.h"
7#include <stdint.h>
8#include <stdbool.h>
9
10typedef struct {
11 uint8_t nsec[32];
12 char nsec_hex[65];
13 char npub_hex[65];
14
15 uint8_t sta_mac[6];
16 uint8_t ap_mac[6];
17
18 char ap_ssid[32];
19 esp_ip4_addr_t ap_ip;
20 char ap_ip_str[16];
21
22 bool initialized;
23} tollgate_identity_t;
24
25esp_err_t identity_init(const char *nsec_hex);
26
27const tollgate_identity_t *identity_get(void);
28
29#endif
diff --git a/main/nostr_event.c b/main/nostr_event.c
new file mode 100644
index 0000000..b55c47d
--- /dev/null
+++ b/main/nostr_event.c
@@ -0,0 +1,112 @@
1#include "nostr_event.h"
2#include "esp_log.h"
3#include "esp_err.h"
4#include "mbedtls/sha256.h"
5#include "secp256k1.h"
6#include "secp256k1_extrakeys.h"
7#include "secp256k1_schnorrsig.h"
8#include "cJSON.h"
9#include <string.h>
10#include <stdio.h>
11#include <sys/time.h>
12
13static const char *TAG = "nostr_event";
14
15static void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex)
16{
17 for (size_t i = 0; i < len; i++)
18 sprintf(hex + i * 2, "%02x", bytes[i]);
19 hex[len * 2] = '\0';
20}
21
22esp_err_t nostr_event_init(nostr_event_t *event, const char *npub_hex,
23 int kind, const char *tags_json, const char *content)
24{
25 memset(event, 0, sizeof(*event));
26 strncpy(event->pubkey, npub_hex, sizeof(event->pubkey) - 1);
27 event->kind = kind;
28 event->tags_json = tags_json ? tags_json : "[]";
29 event->content = content ? content : "";
30
31 struct timeval tv;
32 gettimeofday(&tv, NULL);
33 event->created_at = (uint64_t)tv.tv_sec;
34
35 cJSON *serial = cJSON_CreateArray();
36 cJSON_AddItemToArray(serial, cJSON_CreateNumber(0));
37 cJSON_AddItemToArray(serial, cJSON_CreateString(event->pubkey));
38 cJSON_AddItemToArray(serial, cJSON_CreateNumber((double)event->created_at));
39 cJSON_AddItemToArray(serial, cJSON_CreateNumber(event->kind));
40 cJSON_AddItemToArray(serial, cJSON_Parse(event->tags_json));
41 cJSON_AddItemToArray(serial, cJSON_CreateString(event->content));
42
43 char *serialized = cJSON_PrintUnformatted(serial);
44 cJSON_Delete(serial);
45
46 uint8_t hash[32];
47 mbedtls_sha256((const unsigned char *)serialized, strlen(serialized),
48 hash, 0);
49 free(serialized);
50
51 bytes_to_hex(hash, 32, event->id);
52 return ESP_OK;
53}
54
55esp_err_t nostr_event_sign(nostr_event_t *event, const uint8_t nsec[32])
56{
57 uint8_t msg[32];
58 for (size_t i = 0; i < 32; i++) {
59 unsigned int byte;
60 sscanf(event->id + i * 2, "%02x", &byte);
61 msg[i] = (uint8_t)byte;
62 }
63
64 secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
65 if (!ctx) {
66 ESP_LOGE(TAG, "Failed to create secp256k1 context");
67 return ESP_ERR_NO_MEM;
68 }
69
70 secp256k1_keypair keypair;
71 if (!secp256k1_keypair_create(ctx, &keypair, nsec)) {
72 ESP_LOGE(TAG, "Invalid nsec for signing");
73 secp256k1_context_destroy(ctx);
74 return ESP_ERR_INVALID_ARG;
75 }
76
77 uint8_t sig[64];
78 if (!secp256k1_schnorrsig_sign32(ctx, sig, msg, &keypair, NULL)) {
79 ESP_LOGE(TAG, "Schnorr signing failed");
80 secp256k1_context_destroy(ctx);
81 return ESP_FAIL;
82 }
83
84 bytes_to_hex(sig, 64, event->sig);
85 secp256k1_context_destroy(ctx);
86 return ESP_OK;
87}
88
89esp_err_t nostr_event_to_json(const nostr_event_t *event, char *buf, size_t buf_len)
90{
91 cJSON *root = cJSON_CreateObject();
92 cJSON_AddStringToObject(root, "id", event->id);
93 cJSON_AddStringToObject(root, "pubkey", event->pubkey);
94 cJSON_AddNumberToObject(root, "created_at", (double)event->created_at);
95 cJSON_AddNumberToObject(root, "kind", event->kind);
96 cJSON_AddItemToObject(root, "tags", cJSON_Parse(event->tags_json));
97 cJSON_AddStringToObject(root, "content", event->content);
98 cJSON_AddStringToObject(root, "sig", event->sig);
99
100 char *json = cJSON_PrintUnformatted(root);
101 cJSON_Delete(root);
102
103 if (!json) return ESP_FAIL;
104 size_t len = strlen(json);
105 if (len >= buf_len) {
106 free(json);
107 return ESP_ERR_NO_MEM;
108 }
109 memcpy(buf, json, len + 1);
110 free(json);
111 return ESP_OK;
112}
diff --git a/main/nostr_event.h b/main/nostr_event.h
new file mode 100644
index 0000000..ce15900
--- /dev/null
+++ b/main/nostr_event.h
@@ -0,0 +1,25 @@
1#ifndef NOSTR_EVENT_H
2#define NOSTR_EVENT_H
3
4#include "esp_err.h"
5#include <stdint.h>
6#include <stddef.h>
7
8typedef struct {
9 char pubkey[65];
10 uint64_t created_at;
11 int kind;
12 const char *tags_json;
13 const char *content;
14 char id[65];
15 char sig[129];
16} nostr_event_t;
17
18esp_err_t nostr_event_init(nostr_event_t *event, const char *npub_hex,
19 int kind, const char *tags_json, const char *content);
20
21esp_err_t nostr_event_sign(nostr_event_t *event, const uint8_t nsec[32]);
22
23esp_err_t nostr_event_to_json(const nostr_event_t *event, char *buf, size_t buf_len);
24
25#endif
diff --git a/main/tollgate_api.c b/main/tollgate_api.c
index e6880e0..72ed726 100644
--- a/main/tollgate_api.c
+++ b/main/tollgate_api.c
@@ -3,7 +3,7 @@
3#include "config.h" 3#include "config.h"
4#include "session.h" 4#include "session.h"
5#include "firewall.h" 5#include "firewall.h"
6#include "wallet.h" 6#include "nucula_wallet.h"
7#include "esp_log.h" 7#include "esp_log.h"
8#include "cJSON.h" 8#include "cJSON.h"
9#include "lwip/sockets.h" 9#include "lwip/sockets.h"
@@ -194,6 +194,7 @@ static esp_err_t api_post_payment(httpd_req_t *req)
194 return ESP_OK; 194 return ESP_OK;
195 } 195 }
196 esp_err_t err = cashu_decode_token(body, token); 196 esp_err_t err = cashu_decode_token(body, token);
197 char *body_copy = strdup(body);
197 free(body); 198 free(body);
198 199
199 if (err != ESP_OK) { 200 if (err != ESP_OK) {
@@ -319,17 +320,7 @@ static esp_err_t api_post_payment(httpd_req_t *req)
319 cJSON_free(json); 320 cJSON_free(json);
320 cJSON_Delete(session_event); 321 cJSON_Delete(session_event);
321 322
322 { 323 nucula_wallet_receive(body_copy);
323 wallet_proof_t wproofs[CASHU_MAX_PROOFS];
324 int wcount = token->proof_count > CASHU_MAX_PROOFS ? CASHU_MAX_PROOFS : token->proof_count;
325 for (int i = 0; i < wcount; i++) {
326 wproofs[i].amount = token->proofs[i].amount;
327 strncpy(wproofs[i].id, token->proofs[i].id, WALLET_KEYSET_ID_LEN - 1);
328 strncpy(wproofs[i].secret, token->proofs[i].secret, WALLET_SECRET_LEN - 1);
329 strncpy(wproofs[i].c, token->proofs[i].c, WALLET_SIG_LEN - 1);
330 }
331 wallet_add_proofs(wproofs, wcount);
332 }
333 324
334 free(states); 325 free(states);
335 free(token); 326 free(token);
@@ -381,20 +372,18 @@ static esp_err_t api_get_whoami(httpd_req_t *req)
381 372
382static esp_err_t api_get_wallet(httpd_req_t *req) 373static esp_err_t api_get_wallet(httpd_req_t *req)
383{ 374{
384 wallet_t *w = wallet_get();
385 cJSON *root = cJSON_CreateObject(); 375 cJSON *root = cJSON_CreateObject();
386 cJSON_AddNumberToObject(root, "balance", (double)w->balance); 376 cJSON_AddNumberToObject(root, "balance", (double)nucula_wallet_balance());
387 cJSON_AddNumberToObject(root, "proof_count", w->proof_count); 377 cJSON_AddNumberToObject(root, "proof_count", nucula_wallet_proof_count());
388 cJSON_AddNumberToObject(root, "keyset_count", w->keyset_count); 378
389 379 char *proofs_json = nucula_wallet_proofs_json();
390 cJSON *proofs = cJSON_CreateArray(); 380 if (proofs_json) {
391 for (int i = 0; i < w->proof_count; i++) { 381 cJSON *proofs = cJSON_Parse(proofs_json);
392 cJSON *p = cJSON_CreateObject(); 382 free(proofs_json);
393 cJSON_AddNumberToObject(p, "amount", (double)w->proofs[i].amount); 383 cJSON_AddItemToObject(root, "proofs", proofs);
394 cJSON_AddStringToObject(p, "id", w->proofs[i].id); 384 } else {
395 cJSON_AddItemToArray(proofs, p); 385 cJSON_AddItemToObject(root, "proofs", cJSON_CreateArray());
396 } 386 }
397 cJSON_AddItemToObject(root, "proofs", proofs);
398 387
399 char *json = cJSON_PrintUnformatted(root); 388 char *json = cJSON_PrintUnformatted(root);
400 httpd_resp_set_type(req, "application/json"); 389 httpd_resp_set_type(req, "application/json");
@@ -406,27 +395,16 @@ static esp_err_t api_get_wallet(httpd_req_t *req)
406 395
407static esp_err_t api_post_wallet_swap(httpd_req_t *req) 396static esp_err_t api_post_wallet_swap(httpd_req_t *req)
408{ 397{
409 const tollgate_config_t *cfg = tollgate_config_get(); 398 if (nucula_wallet_balance() == 0) {
410
411 if (wallet_balance() == 0) {
412 httpd_resp_set_status(req, "400 Bad Request"); 399 httpd_resp_set_status(req, "400 Bad Request");
413 httpd_resp_set_type(req, "application/json"); 400 httpd_resp_set_type(req, "application/json");
414 httpd_resp_send(req, "{\"error\":\"no proofs to swap\"}", 27); 401 httpd_resp_send(req, "{\"error\":\"no proofs to swap\"}", 27);
415 return ESP_OK; 402 return ESP_OK;
416 } 403 }
417 404
418 wallet_print_status(); 405 nucula_wallet_print_status();
419
420 esp_err_t err = wallet_fetch_keysets(cfg->mint_url);
421 if (err != ESP_OK) {
422 httpd_resp_set_status(req, "502 Bad Gateway");
423 httpd_resp_set_type(req, "application/json");
424 httpd_resp_send(req, "{\"error\":\"keyset fetch failed\"}", 29);
425 return ESP_OK;
426 }
427 406
428 wallet_t *w = wallet_get(); 407 esp_err_t err = nucula_wallet_swap_all();
429 err = wallet_swap_proofs(cfg->mint_url, 0, w->proof_count);
430 if (err != ESP_OK) { 408 if (err != ESP_OK) {
431 httpd_resp_set_status(req, "502 Bad Gateway"); 409 httpd_resp_set_status(req, "502 Bad Gateway");
432 httpd_resp_set_type(req, "application/json"); 410 httpd_resp_set_type(req, "application/json");
@@ -434,11 +412,11 @@ static esp_err_t api_post_wallet_swap(httpd_req_t *req)
434 return ESP_OK; 412 return ESP_OK;
435 } 413 }
436 414
437 wallet_print_status(); 415 nucula_wallet_print_status();
438 416
439 cJSON *root = cJSON_CreateObject(); 417 cJSON *root = cJSON_CreateObject();
440 cJSON_AddNumberToObject(root, "balance", (double)wallet_balance()); 418 cJSON_AddNumberToObject(root, "balance", (double)nucula_wallet_balance());
441 cJSON_AddNumberToObject(root, "proof_count", wallet_get()->proof_count); 419 cJSON_AddNumberToObject(root, "proof_count", nucula_wallet_proof_count());
442 char *json = cJSON_PrintUnformatted(root); 420 char *json = cJSON_PrintUnformatted(root);
443 httpd_resp_set_type(req, "application/json"); 421 httpd_resp_set_type(req, "application/json");
444 httpd_resp_send(req, json, strlen(json)); 422 httpd_resp_send(req, json, strlen(json));
@@ -472,9 +450,8 @@ static esp_err_t api_post_wallet_send(httpd_req_t *req)
472 return ESP_OK; 450 return ESP_OK;
473 } 451 }
474 452
475 const tollgate_config_t *cfg = tollgate_config_get();
476 char token[4096]; 453 char token[4096];
477 esp_err_t err = wallet_send(cfg->mint_url, amount, token, sizeof(token)); 454 esp_err_t err = nucula_wallet_send(amount, token, sizeof(token));
478 if (err != ESP_OK) { 455 if (err != ESP_OK) {
479 httpd_resp_set_status(req, "402 Payment Required"); 456 httpd_resp_set_status(req, "402 Payment Required");
480 httpd_resp_set_type(req, "text/plain"); 457 httpd_resp_set_type(req, "text/plain");
diff --git a/main/tollgate_main.c b/main/tollgate_main.c
index d4b29bc..7fa1be1 100644
--- a/main/tollgate_main.c
+++ b/main/tollgate_main.c
@@ -11,12 +11,14 @@
11#include "lwip/dns.h" 11#include "lwip/dns.h"
12#include "dhcpserver/dhcpserver.h" 12#include "dhcpserver/dhcpserver.h"
13#include "config.h" 13#include "config.h"
14#include "identity.h"
14#include "dns_server.h" 15#include "dns_server.h"
15#include "captive_portal.h" 16#include "captive_portal.h"
16#include "firewall.h" 17#include "firewall.h"
17#include "session.h" 18#include "session.h"
18#include "tollgate_api.h" 19#include "tollgate_api.h"
19#include "wallet.h" 20#include "nucula_wallet.h"
21#include "wifistr.h"
20 22
21#define MAX_STA_RETRY 5 23#define MAX_STA_RETRY 5
22static const char *TAG = "tollgate_main"; 24static const char *TAG = "tollgate_main";
@@ -92,8 +94,16 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base,
92static void wallet_init_task(void *pvParameters) 94static void wallet_init_task(void *pvParameters)
93{ 95{
94 const tollgate_config_t *cfg = tollgate_config_get(); 96 const tollgate_config_t *cfg = tollgate_config_get();
95 wallet_init(); 97 nucula_wallet_init(cfg->mint_url);
96 wallet_fetch_keysets(cfg->mint_url); 98 vTaskDelete(NULL);
99}
100
101static void publish_wifistr_task(void *pvParameters)
102{
103 vTaskDelay(pdMS_TO_TICKS(5000));
104 wifistr_publish();
105 const tollgate_config_t *cfg = tollgate_config_get();
106 wifistr_start_periodic(cfg->nostr_publish_interval_s);
97 vTaskDelete(NULL); 107 vTaskDelete(NULL);
98} 108}
99 109
@@ -123,6 +133,8 @@ static void start_services(void)
123 captive_portal_start(cfg->ap_ip_str); 133 captive_portal_start(cfg->ap_ip_str);
124 tollgate_api_start(); 134 tollgate_api_start();
125 135
136 xTaskCreate(publish_wifistr_task, "wifistr_init", 16384, NULL, 3, NULL);
137
126 s_services_running = true; 138 s_services_running = true;
127 if (s_services_mutex) xSemaphoreGive(s_services_mutex); 139 if (s_services_mutex) xSemaphoreGive(s_services_mutex);
128 ESP_LOGI(TAG, "=== TollGate services started ==="); 140 ESP_LOGI(TAG, "=== TollGate services started ===");
@@ -214,7 +226,11 @@ void app_main(void)
214 ESP_ERROR_CHECK(ret); 226 ESP_ERROR_CHECK(ret);
215 227
216 ESP_ERROR_CHECK(tollgate_config_init()); 228 ESP_ERROR_CHECK(tollgate_config_init());
229
230 ESP_ERROR_CHECK(identity_init(tollgate_config_get()->nsec));
231
217 tollgate_config_derive_unique((tollgate_config_t *)tollgate_config_get()); 232 tollgate_config_derive_unique((tollgate_config_t *)tollgate_config_get());
233
218 ESP_ERROR_CHECK(esp_netif_init()); 234 ESP_ERROR_CHECK(esp_netif_init());
219 ESP_ERROR_CHECK(esp_event_loop_create_default()); 235 ESP_ERROR_CHECK(esp_event_loop_create_default());
220 236
@@ -227,6 +243,11 @@ void app_main(void)
227 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); 243 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
228 ESP_ERROR_CHECK(esp_wifi_init(&cfg)); 244 ESP_ERROR_CHECK(esp_wifi_init(&cfg));
229 245
246 const tollgate_config_t *tcfg = tollgate_config_get();
247 ESP_ERROR_CHECK(esp_wifi_set_mac(WIFI_IF_STA, tcfg->sta_mac));
248 ESP_ERROR_CHECK(esp_wifi_set_mac(WIFI_IF_AP, tcfg->ap_mac));
249 ESP_LOGI(TAG, "MACs set from identity");
250
230 ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, 251 ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
231 &wifi_event_handler, NULL, NULL)); 252 &wifi_event_handler, NULL, NULL));
232 ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, 253 ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
@@ -241,8 +262,8 @@ void app_main(void)
241 wifi_config_t sta_config; 262 wifi_config_t sta_config;
242 if (tollgate_config_get_wifi(&sta_config) == ESP_OK) { 263 if (tollgate_config_get_wifi(&sta_config) == ESP_OK) {
243 ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); 264 ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config));
244 const tollgate_config_t *tcfg = tollgate_config_get(); 265 const tollgate_config_t *tcfg2 = tollgate_config_get();
245 ESP_LOGI(TAG, "STA configured for SSID: %s", tcfg->networks[tcfg->current_network].ssid); 266 ESP_LOGI(TAG, "STA configured for SSID: %s", tcfg2->networks[tcfg2->current_network].ssid);
246 } 267 }
247 268
248 ESP_ERROR_CHECK(esp_wifi_start()); 269 ESP_ERROR_CHECK(esp_wifi_start());
diff --git a/main/wallet.c b/main/wallet.c
deleted file mode 100644
index 3f65220..0000000
--- a/main/wallet.c
+++ /dev/null
@@ -1,639 +0,0 @@
1#include "wallet.h"
2#include "wallet_persist.h"
3#include "config.h"
4#include "esp_log.h"
5#include "esp_random.h"
6#include "esp_http_client.h"
7#include "esp_crt_bundle.h"
8#include "cJSON.h"
9#include "mbedtls/ecp.h"
10#include "mbedtls/bignum.h"
11#include "mbedtls/sha256.h"
12#include "mbedtls/base64.h"
13#include "freertos/FreeRTOS.h"
14#include "freertos/task.h"
15#include "freertos/semphr.h"
16#include "esp_heap_caps.h"
17#include <string.h>
18#include <stdio.h>
19
20static const char *TAG = "wallet";
21static wallet_t s_wallet;
22
23static const char DOMAIN_SEPARATOR[] = "Secp256k1_HashToCurve_Cashu_";
24
25static mbedtls_ecp_group s_grp;
26static mbedtls_mpi s_order;
27static bool s_grp_loaded = false;
28
29static esp_err_t init_ecp_group(void)
30{
31 if (s_grp_loaded) return ESP_OK;
32 mbedtls_ecp_group_init(&s_grp);
33 mbedtls_mpi_init(&s_order);
34 int ret = mbedtls_ecp_group_load(&s_grp, MBEDTLS_ECP_DP_SECP256K1);
35 if (ret != 0) {
36 ESP_LOGE(TAG, "Failed to load secp256k1 group: -0x%x", -ret);
37 return ESP_FAIL;
38 }
39 mbedtls_mpi_copy(&s_order, &s_grp.N);
40 s_grp_loaded = true;
41 return ESP_OK;
42}
43
44static void random_bytes(uint8_t *buf, size_t len)
45{
46 esp_fill_random(buf, len);
47}
48
49static esp_err_t random_scalar(mbedtls_mpi *r)
50{
51 uint8_t buf[32];
52 random_bytes(buf, 32);
53 mbedtls_mpi_init(r);
54 int ret = mbedtls_mpi_read_binary(r, buf, 32);
55 if (ret != 0) return ESP_FAIL;
56 ret = mbedtls_mpi_mod_mpi(r, r, &s_order);
57 if (ret != 0) return ESP_FAIL;
58 if (mbedtls_mpi_cmp_int(r, 1) < 0) {
59 mbedtls_mpi_add_int(r, r, 1);
60 }
61 return ESP_OK;
62}
63
64static esp_err_t hash_to_curve(const uint8_t *msg, size_t msg_len, mbedtls_ecp_point *Y)
65{
66 uint8_t msg_hash[32];
67 size_t ds_len = strlen(DOMAIN_SEPARATOR);
68 uint8_t *hash_input = malloc(ds_len + msg_len);
69 if (!hash_input) return ESP_FAIL;
70 memcpy(hash_input, DOMAIN_SEPARATOR, ds_len);
71 memcpy(hash_input + ds_len, msg, msg_len);
72 mbedtls_sha256(hash_input, ds_len + msg_len, msg_hash, 0);
73 free(hash_input);
74
75 mbedtls_ecp_point_init(Y);
76 for (uint32_t counter = 0; counter < 256; counter++) {
77 uint8_t counter_bytes[4];
78 counter_bytes[0] = counter & 0xFF;
79 counter_bytes[1] = (counter >> 8) & 0xFF;
80 counter_bytes[2] = (counter >> 16) & 0xFF;
81 counter_bytes[3] = (counter >> 24) & 0xFF;
82
83 uint8_t to_hash[32 + 4 + 1];
84 memcpy(to_hash, msg_hash, 32);
85 memcpy(to_hash + 32, counter_bytes, 4);
86
87 uint8_t point_hash[32];
88 mbedtls_sha256(to_hash, 36, point_hash, 0);
89
90 uint8_t compressed[33];
91 compressed[0] = 0x02;
92 memcpy(compressed + 1, point_hash, 32);
93
94 int ret = mbedtls_ecp_point_read_binary(&s_grp, Y, compressed, 33);
95 if (ret == 0) {
96 ret = mbedtls_ecp_check_pubkey(&s_grp, Y);
97 if (ret == 0) return ESP_OK;
98 }
99
100 compressed[0] = 0x03;
101 ret = mbedtls_ecp_point_read_binary(&s_grp, Y, compressed, 33);
102 if (ret == 0) {
103 ret = mbedtls_ecp_check_pubkey(&s_grp, Y);
104 if (ret == 0) return ESP_OK;
105 }
106 }
107
108 ESP_LOGE(TAG, "hash_to_curve failed after 256 attempts");
109 return ESP_FAIL;
110}
111
112static esp_err_t point_add(const mbedtls_ecp_point *A, const mbedtls_ecp_point *B,
113 mbedtls_ecp_point *R)
114{
115 mbedtls_mpi one;
116 mbedtls_mpi_init(&one);
117 mbedtls_mpi_lset(&one, 1);
118 int ret = mbedtls_ecp_muladd(&s_grp, R, &one, A, &one, B);
119 if (ret != 0) {
120 ESP_LOGE(TAG, "point_add failed: -0x%x", -ret);
121 }
122 mbedtls_mpi_free(&one);
123 return (ret == 0) ? ESP_OK : ESP_FAIL;
124}
125
126static esp_err_t scalar_mul(const mbedtls_mpi *m, const mbedtls_ecp_point *P,
127 mbedtls_ecp_point *R)
128{
129 int ret = mbedtls_ecp_mul(&s_grp, R, m, P, NULL, NULL);
130 if (ret != 0) {
131 ESP_LOGE(TAG, "scalar_mul failed: -0x%x", -ret);
132 }
133 return (ret == 0) ? ESP_OK : ESP_FAIL;
134}
135
136static int hex_to_bytes(const char *hex, uint8_t *bytes, size_t bytes_len)
137{
138 size_t hex_len = strlen(hex);
139 if (hex_len / 2 > bytes_len) return -1;
140 for (size_t i = 0; i < hex_len / 2; i++) {
141 unsigned int b;
142 sscanf(hex + i * 2, "%02x", &b);
143 bytes[i] = (uint8_t)b;
144 }
145 return hex_len / 2;
146}
147
148static void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex)
149{
150 for (size_t i = 0; i < len; i++) {
151 sprintf(hex + i * 2, "%02x", bytes[i]);
152 }
153 hex[len * 2] = '\0';
154}
155
156esp_err_t wallet_init(void)
157{
158 memset(&s_wallet, 0, sizeof(s_wallet));
159 esp_err_t err = init_ecp_group();
160 if (err != ESP_OK) return err;
161 wallet_persist_load();
162 ESP_LOGI(TAG, "Wallet initialized (secp256k1 loaded)");
163 return ESP_OK;
164}
165
166wallet_t *wallet_get(void)
167{
168 return &s_wallet;
169}
170
171uint64_t wallet_balance(void)
172{
173 return s_wallet.balance;
174}
175
176esp_err_t wallet_add_proofs(const wallet_proof_t *proofs, int count)
177{
178 for (int i = 0; i < count; i++) {
179 if (s_wallet.proof_count >= WALLET_MAX_PROOFS) {
180 ESP_LOGW(TAG, "Wallet full, cannot add more proofs");
181 return ESP_ERR_NO_MEM;
182 }
183 memcpy(&s_wallet.proofs[s_wallet.proof_count], &proofs[i], sizeof(wallet_proof_t));
184 s_wallet.balance += proofs[i].amount;
185 s_wallet.proof_count++;
186 ESP_LOGI(TAG, "Added proof: amount=%llu, total_balance=%llu",
187 (unsigned long long)proofs[i].amount,
188 (unsigned long long)s_wallet.balance);
189 }
190 wallet_persist_save();
191 return ESP_OK;
192}
193
194esp_err_t wallet_remove_proof(int index)
195{
196 if (index < 0 || index >= s_wallet.proof_count) return ESP_ERR_INVALID_ARG;
197 s_wallet.balance -= s_wallet.proofs[index].amount;
198 for (int i = index; i < s_wallet.proof_count - 1; i++) {
199 memcpy(&s_wallet.proofs[i], &s_wallet.proofs[i + 1], sizeof(wallet_proof_t));
200 }
201 memset(&s_wallet.proofs[s_wallet.proof_count - 1], 0, sizeof(wallet_proof_t));
202 s_wallet.proof_count--;
203 wallet_persist_save();
204 return ESP_OK;
205}
206
207void wallet_clear(void)
208{
209 s_wallet.balance = 0;
210 s_wallet.proof_count = 0;
211 wallet_persist_save();
212}
213
214esp_err_t wallet_fetch_keysets(const char *mint_url)
215{
216 char url[512];
217 snprintf(url, sizeof(url), "%s/v1/keysets", mint_url);
218
219 char *resp_buf = malloc(8192);
220 if (!resp_buf) return ESP_ERR_NO_MEM;
221
222 esp_http_client_config_t config = {
223 .url = url,
224 .method = HTTP_METHOD_GET,
225 .timeout_ms = 10000,
226 .crt_bundle_attach = esp_crt_bundle_attach,
227 };
228 esp_http_client_handle_t client = esp_http_client_init(&config);
229 if (!client) { free(resp_buf); return ESP_FAIL; }
230
231 esp_err_t err = esp_http_client_open(client, 0);
232 if (err != ESP_OK) {
233 ESP_LOGE(TAG, "Keyset fetch open failed: %s", esp_err_to_name(err));
234 esp_http_client_cleanup(client);
235 free(resp_buf);
236 return err;
237 }
238
239 int content_length = esp_http_client_fetch_headers(client);
240 int status = esp_http_client_get_status_code(client);
241 ESP_LOGI(TAG, "Keyset fetch: status=%d content_length=%d", status, content_length);
242
243 int resp_len = esp_http_client_read(client, resp_buf, 8191);
244 ESP_LOGI(TAG, "Keyset fetch: read %d bytes", resp_len);
245 esp_http_client_cleanup(client);
246
247 if (status != 200 || resp_len <= 0) {
248 ESP_LOGE(TAG, "Keyset fetch failed: status=%d len=%d", status, resp_len);
249 free(resp_buf);
250 return ESP_FAIL;
251 }
252 resp_buf[resp_len] = '\0';
253
254 cJSON *root = cJSON_Parse(resp_buf);
255 free(resp_buf);
256 if (!root) return ESP_FAIL;
257
258 cJSON *keysets = cJSON_GetObjectItemCaseSensitive(root, "keysets");
259 if (!keysets || !cJSON_IsArray(keysets)) {
260 cJSON_Delete(root);
261 return ESP_FAIL;
262 }
263
264 s_wallet.keyset_count = 0;
265 int n = cJSON_GetArraySize(keysets);
266 for (int i = 0; i < n && i < WALLET_MAX_KEYSETS; i++) {
267 cJSON *ks = cJSON_GetArrayItem(keysets, i);
268 cJSON *id = cJSON_GetObjectItemCaseSensitive(ks, "id");
269 if (id && cJSON_IsString(id)) {
270 strncpy(s_wallet.keysets[s_wallet.keyset_count].id, id->valuestring,
271 WALLET_KEYSET_ID_LEN - 1);
272 cJSON *fee = cJSON_GetObjectItemCaseSensitive(ks, "input_fee_ppk");
273 s_wallet.keysets[s_wallet.keyset_count].input_fee_ppk = fee ? fee->valueint : 0;
274 s_wallet.keyset_count++;
275 }
276 }
277
278 cJSON_Delete(root);
279 ESP_LOGI(TAG, "Fetched %d keysets from %s", s_wallet.keyset_count, mint_url);
280 return ESP_OK;
281}
282
283esp_err_t wallet_swap_proofs(const char *mint_url, int start_index, int count)
284{
285 ESP_LOGI(TAG, "wallet_swap_proofs called: start=%d count=%d keysets=%d proofs=%d",
286 start_index, count, s_wallet.keyset_count, s_wallet.proof_count);
287
288 if (s_wallet.keyset_count == 0) {
289 ESP_LOGE(TAG, "No keysets loaded, fetch first");
290 return ESP_FAIL;
291 }
292 if (start_index < 0 || start_index + count > s_wallet.proof_count) {
293 return ESP_ERR_INVALID_ARG;
294 }
295
296 wallet_proof_t *old_proofs = &s_wallet.proofs[start_index];
297 int n = count;
298
299 uint64_t total_input = 0;
300 for (int i = 0; i < n; i++) total_input += old_proofs[i].amount;
301
302 int fee_ppk = s_wallet.keysets[0].input_fee_ppk;
303 uint64_t fee_sats = (total_input * fee_ppk + 999) / 1000;
304 uint64_t total_output = total_input - fee_sats;
305 ESP_LOGI(TAG, "Swap: total_input=%llu fee_ppk=%d fee=%llu total_output=%llu",
306 (unsigned long long)total_input, fee_ppk,
307 (unsigned long long)fee_sats, (unsigned long long)total_output);
308
309 cJSON *inputs = cJSON_CreateArray();
310 for (int i = 0; i < n; i++) {
311 cJSON *p = cJSON_CreateObject();
312 cJSON_AddNumberToObject(p, "amount", (double)old_proofs[i].amount);
313 cJSON_AddStringToObject(p, "id", old_proofs[i].id);
314 cJSON_AddStringToObject(p, "secret", old_proofs[i].secret);
315 cJSON_AddStringToObject(p, "C", old_proofs[i].c);
316 cJSON_AddItemToArray(inputs, p);
317 }
318
319 typedef struct {
320 uint8_t secret[32];
321 mbedtls_mpi r;
322 mbedtls_ecp_point Y;
323 } swap_output_t;
324
325 swap_output_t *outputs = heap_caps_malloc(n * sizeof(swap_output_t), MALLOC_CAP_SPIRAM);
326 if (!outputs) { cJSON_Delete(inputs); return ESP_ERR_NO_MEM; }
327
328 cJSON *blinded_msgs = cJSON_CreateArray();
329 for (int i = 0; i < n; i++) {
330 random_bytes(outputs[i].secret, 32);
331 mbedtls_ecp_point_init(&outputs[i].Y);
332 esp_err_t htc_ret = hash_to_curve(outputs[i].secret, 32, &outputs[i].Y);
333 if (htc_ret != ESP_OK) {
334 ESP_LOGE(TAG, "hash_to_curve failed for output %d", i);
335 }
336 mbedtls_mpi_init(&outputs[i].r);
337 random_scalar(&outputs[i].r);
338
339 mbedtls_ecp_point rG, B_;
340 mbedtls_ecp_point_init(&rG);
341 mbedtls_ecp_point_init(&B_);
342
343 esp_err_t sm_ret = scalar_mul(&outputs[i].r, &s_grp.G, &rG);
344 if (sm_ret != ESP_OK) {
345 ESP_LOGE(TAG, "scalar_mul failed for output %d", i);
346 }
347 esp_err_t pa_ret = point_add(&outputs[i].Y, &rG, &B_);
348 if (pa_ret != ESP_OK) {
349 ESP_LOGE(TAG, "point_add failed for output %d", i);
350 }
351
352 uint8_t b_bytes[33];
353 size_t olen = 0;
354 int wret = mbedtls_ecp_point_write_binary(&s_grp, &B_, MBEDTLS_ECP_PF_COMPRESSED, &olen, b_bytes, 33);
355 if (wret != 0 || olen == 0) {
356 ESP_LOGE(TAG, "Blinded point write failed: ret=-0x%x olen=%zu", -wret, olen);
357 olen = 1;
358 b_bytes[0] = 0x00;
359 }
360 char b_hex[67];
361 bytes_to_hex(b_bytes, olen, b_hex);
362
363 uint64_t out_amount = old_proofs[i].amount;
364 if (i == n - 1) {
365 uint64_t running = 0;
366 for (int j = 0; j < n - 1; j++) running += old_proofs[j].amount;
367 out_amount = total_output - running;
368 }
369
370 cJSON *bm = cJSON_CreateObject();
371 cJSON_AddNumberToObject(bm, "amount", (double)out_amount);
372 cJSON_AddStringToObject(bm, "id", s_wallet.keysets[0].id);
373 cJSON_AddStringToObject(bm, "B_", b_hex);
374 cJSON_AddItemToArray(blinded_msgs, bm);
375
376 mbedtls_ecp_point_free(&rG);
377 mbedtls_ecp_point_free(&B_);
378 }
379
380 cJSON *body = cJSON_CreateObject();
381 cJSON_AddItemToObject(body, "inputs", inputs);
382 cJSON_AddItemToObject(body, "outputs", blinded_msgs);
383 char *body_str = cJSON_PrintUnformatted(body);
384 cJSON_Delete(body);
385
386 ESP_LOGI(TAG, "Swap request body (%zu bytes): %s", strlen(body_str), body_str);
387
388 char url[512];
389 snprintf(url, sizeof(url), "%s/v1/swap", mint_url);
390
391 char *resp_buf = malloc(8192);
392 if (!resp_buf) {
393 free(body_str);
394 for (int i = 0; i < n; i++) {
395 mbedtls_mpi_free(&outputs[i].r);
396 mbedtls_ecp_point_free(&outputs[i].Y);
397 }
398 free(outputs);
399 return ESP_ERR_NO_MEM;
400 }
401
402 esp_http_client_config_t config = {
403 .url = url,
404 .method = HTTP_METHOD_POST,
405 .timeout_ms = 15000,
406 .crt_bundle_attach = esp_crt_bundle_attach,
407 };
408 esp_http_client_handle_t client = esp_http_client_init(&config);
409 if (!client) {
410 free(body_str);
411 free(resp_buf);
412 for (int i = 0; i < n; i++) {
413 mbedtls_mpi_free(&outputs[i].r);
414 mbedtls_ecp_point_free(&outputs[i].Y);
415 }
416 free(outputs);
417 return ESP_FAIL;
418 }
419
420 esp_http_client_set_header(client, "Content-Type", "application/json");
421 esp_http_client_open(client, strlen(body_str));
422 esp_http_client_write(client, body_str, strlen(body_str));
423 free(body_str);
424
425 esp_http_client_fetch_headers(client);
426 int resp_len = esp_http_client_read(client, resp_buf, 8191);
427 int status = esp_http_client_get_status_code(client);
428 esp_http_client_cleanup(client);
429
430 if (status != 200 || resp_len <= 0) {
431 if (resp_len > 0) {
432 resp_buf[resp_len] = '\0';
433 ESP_LOGE(TAG, "Swap failed: status=%d body=%s", status, resp_buf);
434 } else {
435 ESP_LOGE(TAG, "Swap failed: status=%d len=%d", status, resp_len);
436 }
437 free(resp_buf);
438 for (int i = 0; i < n; i++) {
439 mbedtls_mpi_free(&outputs[i].r);
440 mbedtls_ecp_point_free(&outputs[i].Y);
441 }
442 free(outputs);
443 return ESP_FAIL;
444 }
445 resp_buf[resp_len] = '\0';
446
447 cJSON *root = cJSON_Parse(resp_buf);
448 free(resp_buf);
449 if (!root) {
450 for (int i = 0; i < n; i++) {
451 mbedtls_mpi_free(&outputs[i].r);
452 mbedtls_ecp_point_free(&outputs[i].Y);
453 }
454 free(outputs);
455 return ESP_FAIL;
456 }
457
458 cJSON *signatures = cJSON_GetObjectItemCaseSensitive(root, "signatures");
459 if (!signatures || !cJSON_IsArray(signatures)) {
460 ESP_LOGE(TAG, "No signatures in swap response");
461 cJSON_Delete(root);
462 for (int i = 0; i < n; i++) {
463 mbedtls_mpi_free(&outputs[i].r);
464 mbedtls_ecp_point_free(&outputs[i].Y);
465 }
466 free(outputs);
467 return ESP_FAIL;
468 }
469
470 for (int i = start_index; i < start_index + n; i++) {
471 s_wallet.balance -= s_wallet.proofs[i].amount;
472 }
473
474 int sig_count = cJSON_GetArraySize(signatures);
475 for (int i = 0; i < sig_count && i < n; i++) {
476 cJSON *sig = cJSON_GetArrayItem(signatures, i);
477 cJSON *c_ = cJSON_GetObjectItemCaseSensitive(sig, "C_");
478 cJSON *amt = cJSON_GetObjectItemCaseSensitive(sig, "amount");
479 cJSON *id = cJSON_GetObjectItemCaseSensitive(sig, "id");
480
481 if (!c_ || !cJSON_IsString(c_)) continue;
482
483 uint8_t c_bytes[33];
484 int c_len = hex_to_bytes(c_->valuestring, c_bytes, 33);
485
486 mbedtls_ecp_point C_;
487 mbedtls_ecp_point_init(&C_);
488 mbedtls_ecp_point_read_binary(&s_grp, &C_, c_bytes, c_len);
489
490 char ks_id[WALLET_KEYSET_ID_LEN] = {0};
491 if (id && cJSON_IsString(id)) {
492 strncpy(ks_id, id->valuestring, WALLET_KEYSET_ID_LEN - 1);
493 }
494
495 mbedtls_mpi neg_r;
496 mbedtls_mpi_init(&neg_r);
497 mbedtls_mpi_sub_mpi(&neg_r, &s_order, &outputs[i].r);
498
499 mbedtls_ecp_point neg_rG;
500 mbedtls_ecp_point_init(&neg_rG);
501 scalar_mul(&neg_r, &s_grp.G, &neg_rG);
502
503 mbedtls_ecp_point C;
504 mbedtls_ecp_point_init(&C);
505 point_add(&C_, &neg_rG, &C);
506
507 uint8_t c_final[33];
508 size_t c_final_len;
509 mbedtls_ecp_point_write_binary(&s_grp, &C, MBEDTLS_ECP_PF_COMPRESSED,
510 &c_final_len, c_final, 33);
511
512 if (s_wallet.proof_count < WALLET_MAX_PROOFS) {
513 wallet_proof_t *wp = &s_wallet.proofs[s_wallet.proof_count];
514 if (amt && cJSON_IsNumber(amt)) {
515 wp->amount = (uint64_t)amt->valuedouble;
516 }
517 strncpy(wp->id, ks_id, WALLET_KEYSET_ID_LEN - 1);
518 bytes_to_hex(outputs[i].secret, 32, wp->secret);
519 bytes_to_hex(c_final, c_final_len, wp->c);
520 s_wallet.balance += wp->amount;
521 s_wallet.proof_count++;
522 }
523
524 mbedtls_mpi_free(&neg_r);
525 mbedtls_ecp_point_free(&C_);
526 mbedtls_ecp_point_free(&neg_rG);
527 mbedtls_ecp_point_free(&C);
528 }
529
530 for (int i = 0; i < n; i++) {
531 int idx = start_index;
532 for (int j = idx; j < s_wallet.proof_count - 1; j++) {
533 memcpy(&s_wallet.proofs[j], &s_wallet.proofs[j + 1], sizeof(wallet_proof_t));
534 }
535 s_wallet.proof_count--;
536 }
537
538 for (int i = 0; i < n; i++) {
539 mbedtls_mpi_free(&outputs[i].r);
540 mbedtls_ecp_point_free(&outputs[i].Y);
541 }
542 free(outputs);
543 cJSON_Delete(root);
544
545 ESP_LOGI(TAG, "Swap complete: %d proofs swapped, balance=%llu",
546 n, (unsigned long long)s_wallet.balance);
547 wallet_persist_save();
548 return ESP_OK;
549}
550
551esp_err_t wallet_create_token(char *out, size_t out_size, uint64_t amount,
552 const char *mint_url)
553{
554 if (s_wallet.proof_count == 0 || s_wallet.balance < amount) {
555 ESP_LOGE(TAG, "Insufficient balance: have=%llu need=%llu",
556 (unsigned long long)s_wallet.balance, (unsigned long long)amount);
557 return ESP_FAIL;
558 }
559
560 cJSON *proofs_arr = cJSON_CreateArray();
561 uint64_t remaining = amount;
562 int indices_to_remove[10];
563 int remove_count = 0;
564
565 for (int i = 0; i < s_wallet.proof_count && remaining > 0 && remove_count < 10; i++) {
566 if (s_wallet.proofs[i].amount <= remaining) {
567 cJSON *p = cJSON_CreateObject();
568 cJSON_AddNumberToObject(p, "amount", (double)s_wallet.proofs[i].amount);
569 cJSON_AddStringToObject(p, "id", s_wallet.proofs[i].id);
570 cJSON_AddStringToObject(p, "secret", s_wallet.proofs[i].secret);
571 cJSON_AddStringToObject(p, "C", s_wallet.proofs[i].c);
572 cJSON_AddItemToArray(proofs_arr, p);
573 remaining -= s_wallet.proofs[i].amount;
574 indices_to_remove[remove_count++] = i;
575 }
576 }
577
578 if (remaining > 0) {
579 cJSON_Delete(proofs_arr);
580 ESP_LOGE(TAG, "Cannot make exact amount: %llu remaining", (unsigned long long)remaining);
581 return ESP_FAIL;
582 }
583
584 cJSON *token_obj = cJSON_CreateObject();
585 cJSON *token_arr = cJSON_CreateArray();
586 cJSON *mint_proofs = cJSON_CreateObject();
587 cJSON_AddStringToObject(mint_proofs, "mint", mint_url);
588 cJSON_AddItemToObject(mint_proofs, "proofs", proofs_arr);
589 cJSON_AddItemToArray(token_arr, mint_proofs);
590 cJSON_AddItemToObject(token_obj, "token", token_arr);
591
592 char *json_str = cJSON_PrintUnformatted(token_obj);
593 cJSON_Delete(token_obj);
594
595 size_t b64_len;
596 mbedtls_base64_encode((unsigned char *)out + 6, out_size - 6, &b64_len,
597 (const unsigned char *)json_str, strlen(json_str));
598 free(json_str);
599
600 memcpy(out, "cashuA", 6);
601 for (size_t i = 0; i < b64_len; i++) {
602 if (out[6 + i] == '+') out[6 + i] = '-';
603 else if (out[6 + i] == '/') out[6 + i] = '_';
604 else if (out[6 + i] == '=') { out[6 + i] = '\0'; break; }
605 }
606 out[6 + b64_len] = '\0';
607
608 for (int i = remove_count - 1; i >= 0; i--) {
609 s_wallet.balance -= s_wallet.proofs[indices_to_remove[i]].amount;
610 for (int j = indices_to_remove[i]; j < s_wallet.proof_count - 1; j++) {
611 memcpy(&s_wallet.proofs[j], &s_wallet.proofs[j + 1], sizeof(wallet_proof_t));
612 }
613 s_wallet.proof_count--;
614 }
615
616 ESP_LOGI(TAG, "Created token for %llu sats, remaining balance=%llu",
617 (unsigned long long)amount, (unsigned long long)s_wallet.balance);
618 wallet_persist_save();
619 return ESP_OK;
620}
621
622esp_err_t wallet_send(const char *mint_url, uint64_t amount,
623 char *token_out, size_t token_out_size)
624{
625 return wallet_create_token(token_out, token_out_size, amount, mint_url);
626}
627
628void wallet_print_status(void)
629{
630 ESP_LOGI(TAG, "Wallet: %d proofs, balance=%llu sats, %d keysets",
631 s_wallet.proof_count,
632 (unsigned long long)s_wallet.balance,
633 s_wallet.keyset_count);
634 for (int i = 0; i < s_wallet.proof_count; i++) {
635 ESP_LOGI(TAG, " [%d] amount=%llu id=%s", i,
636 (unsigned long long)s_wallet.proofs[i].amount,
637 s_wallet.proofs[i].id);
638 }
639}
diff --git a/main/wallet.h b/main/wallet.h
deleted file mode 100644
index 5089f93..0000000
--- a/main/wallet.h
+++ /dev/null
@@ -1,53 +0,0 @@
1#ifndef WALLET_H
2#define WALLET_H
3
4#include "esp_err.h"
5#include <stdint.h>
6#include <stdbool.h>
7
8#define WALLET_MAX_PROOFS 50
9#define WALLET_MAX_KEYSETS 5
10#define WALLET_KEYSET_ID_LEN 68
11#define WALLET_SECRET_LEN 65
12#define WALLET_SIG_LEN 67
13
14typedef struct {
15 uint64_t amount;
16 char id[WALLET_KEYSET_ID_LEN];
17 char secret[WALLET_SECRET_LEN];
18 char c[WALLET_SIG_LEN];
19} wallet_proof_t;
20
21typedef struct {
22 char id[WALLET_KEYSET_ID_LEN];
23 char public_key_33[67];
24 uint64_t amount;
25 int input_fee_ppk;
26} wallet_keyset_t;
27
28typedef struct {
29 wallet_proof_t proofs[WALLET_MAX_PROOFS];
30 int proof_count;
31 wallet_keyset_t keysets[WALLET_MAX_KEYSETS];
32 int keyset_count;
33 uint64_t balance;
34} wallet_t;
35
36esp_err_t wallet_init(void);
37wallet_t *wallet_get(void);
38uint64_t wallet_balance(void);
39
40esp_err_t wallet_add_proofs(const wallet_proof_t *proofs, int count);
41esp_err_t wallet_remove_proof(int index);
42void wallet_clear(void);
43
44esp_err_t wallet_fetch_keysets(const char *mint_url);
45esp_err_t wallet_swap_proofs(const char *mint_url, int start_index, int count);
46
47esp_err_t wallet_create_token(char *out, size_t out_size, uint64_t amount,
48 const char *mint_url);
49esp_err_t wallet_send(const char *mint_url, uint64_t amount,
50 char *token_out, size_t token_out_size);
51
52void wallet_print_status(void);
53#endif
diff --git a/main/wallet_persist.c b/main/wallet_persist.c
deleted file mode 100644
index 45c932f..0000000
--- a/main/wallet_persist.c
+++ /dev/null
@@ -1,147 +0,0 @@
1#include "wallet_persist.h"
2#include "wallet.h"
3#include "config.h"
4#include "esp_log.h"
5#include "cJSON.h"
6#include <string.h>
7#include <stdio.h>
8#include <unistd.h>
9
10static const char *TAG = "wallet_persist";
11static const char *WALLET_FILE = "/spiffs/wallet.json";
12
13esp_err_t wallet_persist_save(void)
14{
15 const tollgate_config_t *cfg = tollgate_config_get();
16 wallet_t *w = wallet_get();
17
18 if (w->balance < cfg->persist_threshold_sats) {
19 if (w->proof_count == 0) {
20 unlink(WALLET_FILE);
21 ESP_LOGI(TAG, "Wallet empty, removed persist file");
22 }
23 return ESP_OK;
24 }
25
26 cJSON *root = cJSON_CreateObject();
27 cJSON_AddNumberToObject(root, "balance", (double)w->balance);
28
29 cJSON *proofs = cJSON_CreateArray();
30 for (int i = 0; i < w->proof_count; i++) {
31 cJSON *p = cJSON_CreateObject();
32 cJSON_AddNumberToObject(p, "amount", (double)w->proofs[i].amount);
33 cJSON_AddStringToObject(p, "id", w->proofs[i].id);
34 cJSON_AddStringToObject(p, "secret", w->proofs[i].secret);
35 cJSON_AddStringToObject(p, "C", w->proofs[i].c);
36 cJSON_AddItemToArray(proofs, p);
37 }
38 cJSON_AddItemToObject(root, "proofs", proofs);
39
40 cJSON *keysets = cJSON_CreateArray();
41 for (int i = 0; i < w->keyset_count; i++) {
42 cJSON *ks = cJSON_CreateObject();
43 cJSON_AddStringToObject(ks, "id", w->keysets[i].id);
44 cJSON_AddItemToArray(keysets, ks);
45 }
46 cJSON_AddItemToObject(root, "keysets", keysets);
47
48 char *json_str = cJSON_PrintUnformatted(root);
49 cJSON_Delete(root);
50
51 FILE *f = fopen(WALLET_FILE, "w");
52 if (!f) {
53 ESP_LOGE(TAG, "Failed to open %s for writing", WALLET_FILE);
54 cJSON_free(json_str);
55 return ESP_FAIL;
56 }
57
58 size_t written = fwrite(json_str, 1, strlen(json_str), f);
59 fclose(f);
60 cJSON_free(json_str);
61
62 ESP_LOGI(TAG, "Wallet persisted: %d proofs, balance=%llu (%zu bytes)",
63 w->proof_count, (unsigned long long)w->balance, written);
64 return ESP_OK;
65}
66
67esp_err_t wallet_persist_load(void)
68{
69 wallet_t *w = wallet_get();
70
71 FILE *f = fopen(WALLET_FILE, "r");
72 if (!f) {
73 ESP_LOGI(TAG, "No persisted wallet found, starting fresh");
74 return ESP_OK;
75 }
76
77 fseek(f, 0, SEEK_END);
78 long fsize = ftell(f);
79 fseek(f, 0, SEEK_SET);
80
81 if (fsize <= 0 || fsize > 65536) {
82 fclose(f);
83 ESP_LOGW(TAG, "Wallet file size invalid: %ld", fsize);
84 return ESP_FAIL;
85 }
86
87 char *buf = malloc(fsize + 1);
88 if (!buf) {
89 fclose(f);
90 return ESP_ERR_NO_MEM;
91 }
92
93 fread(buf, 1, fsize, f);
94 buf[fsize] = '\0';
95 fclose(f);
96
97 cJSON *root = cJSON_Parse(buf);
98 free(buf);
99 if (!root) {
100 ESP_LOGE(TAG, "Failed to parse wallet.json");
101 return ESP_FAIL;
102 }
103
104 cJSON *balance_j = cJSON_GetObjectItemCaseSensitive(root, "balance");
105 if (balance_j && cJSON_IsNumber(balance_j)) {
106 w->balance = (uint64_t)balance_j->valuedouble;
107 }
108
109 cJSON *proofs = cJSON_GetObjectItemCaseSensitive(root, "proofs");
110 if (proofs && cJSON_IsArray(proofs)) {
111 int count = cJSON_GetArraySize(proofs);
112 if (count > WALLET_MAX_PROOFS) count = WALLET_MAX_PROOFS;
113 for (int i = 0; i < count; i++) {
114 cJSON *p = cJSON_GetArrayItem(proofs, i);
115 cJSON *amt = cJSON_GetObjectItemCaseSensitive(p, "amount");
116 cJSON *id = cJSON_GetObjectItemCaseSensitive(p, "id");
117 cJSON *secret = cJSON_GetObjectItemCaseSensitive(p, "secret");
118 cJSON *c = cJSON_GetObjectItemCaseSensitive(p, "C");
119 if (amt) w->proofs[i].amount = (uint64_t)amt->valuedouble;
120 if (id && cJSON_IsString(id))
121 strncpy(w->proofs[i].id, id->valuestring, WALLET_KEYSET_ID_LEN - 1);
122 if (secret && cJSON_IsString(secret))
123 strncpy(w->proofs[i].secret, secret->valuestring, WALLET_SECRET_LEN - 1);
124 if (c && cJSON_IsString(c))
125 strncpy(w->proofs[i].c, c->valuestring, WALLET_SIG_LEN - 1);
126 w->proof_count++;
127 }
128 }
129
130 cJSON *keysets = cJSON_GetObjectItemCaseSensitive(root, "keysets");
131 if (keysets && cJSON_IsArray(keysets)) {
132 int count = cJSON_GetArraySize(keysets);
133 if (count > WALLET_MAX_KEYSETS) count = WALLET_MAX_KEYSETS;
134 for (int i = 0; i < count; i++) {
135 cJSON *ks = cJSON_GetArrayItem(keysets, i);
136 cJSON *id = cJSON_GetObjectItemCaseSensitive(ks, "id");
137 if (id && cJSON_IsString(id))
138 strncpy(w->keysets[i].id, id->valuestring, WALLET_KEYSET_ID_LEN - 1);
139 w->keyset_count++;
140 }
141 }
142
143 cJSON_Delete(root);
144 ESP_LOGI(TAG, "Wallet loaded: %d proofs, %d keysets, balance=%llu",
145 w->proof_count, w->keyset_count, (unsigned long long)w->balance);
146 return ESP_OK;
147}
diff --git a/main/wallet_persist.h b/main/wallet_persist.h
deleted file mode 100644
index 4dfcbfc..0000000
--- a/main/wallet_persist.h
+++ /dev/null
@@ -1,9 +0,0 @@
1#ifndef WALLET_PERSIST_H
2#define WALLET_PERSIST_H
3
4#include "esp_err.h"
5
6esp_err_t wallet_persist_save(void);
7esp_err_t wallet_persist_load(void);
8
9#endif
diff --git a/main/wifistr.c b/main/wifistr.c
new file mode 100644
index 0000000..bf03b4d
--- /dev/null
+++ b/main/wifistr.c
@@ -0,0 +1,252 @@
1#include "wifistr.h"
2#include "identity.h"
3#include "nostr_event.h"
4#include "config.h"
5#include "esp_log.h"
6#include "esp_tls.h"
7#include "esp_crt_bundle.h"
8#include "cJSON.h"
9#include "freertos/task.h"
10#include "freertos/timers.h"
11#include <string.h>
12#include <stdio.h>
13#include <stdlib.h>
14
15static const char *TAG = "wifistr";
16static TimerHandle_t s_publish_timer = NULL;
17
18static esp_err_t ws_send_to_relay(const char *relay_url, const char *event_json)
19{
20 char host[128] = {0};
21 int port = 443;
22 char path[128] = "/";
23
24 if (strncmp(relay_url, "wss://", 6) != 0) {
25 ESP_LOGW(TAG, "Unsupported relay URL: %s", relay_url);
26 return ESP_ERR_INVALID_ARG;
27 }
28
29 const char *url_start = relay_url + 6;
30 const char *path_ptr = strchr(url_start, '/');
31 if (path_ptr) {
32 size_t host_len = path_ptr - url_start;
33 if (host_len >= sizeof(host)) host_len = sizeof(host) - 1;
34 memcpy(host, url_start, host_len);
35 host[host_len] = '\0';
36 strncpy(path, path_ptr, sizeof(path) - 1);
37 } else {
38 strncpy(host, url_start, sizeof(host) - 1);
39 }
40
41 char *colon = strchr(host, ':');
42 if (colon) {
43 *colon = '\0';
44 port = atoi(colon + 1);
45 }
46
47 ESP_LOGI(TAG, "Connecting to %s:%d%s", host, port, path);
48
49 esp_tls_cfg_t tls_cfg = {
50 .crt_bundle_attach = esp_crt_bundle_attach,
51 };
52
53 esp_tls_t *tls = esp_tls_init();
54 if (!tls) {
55 ESP_LOGE(TAG, "Failed to allocate TLS handle");
56 return ESP_ERR_NO_MEM;
57 }
58
59 int ret = esp_tls_conn_new_sync(host, strlen(host), port, &tls_cfg, tls);
60 if (ret < 0) {
61 ESP_LOGE(TAG, "TLS connect failed to %s", host);
62 esp_tls_conn_destroy(tls);
63 return ESP_FAIL;
64 }
65
66 char upgrade[512];
67 snprintf(upgrade, sizeof(upgrade),
68 "GET %s HTTP/1.1\r\n"
69 "Host: %s\r\n"
70 "Upgrade: websocket\r\n"
71 "Connection: Upgrade\r\n"
72 "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
73 "Sec-WebSocket-Version: 13\r\n"
74 "\r\n",
75 path, host);
76
77 int written = esp_tls_conn_write(tls, (const unsigned char *)upgrade, strlen(upgrade));
78 if (written < 0) {
79 ESP_LOGE(TAG, "Failed to send upgrade request");
80 esp_tls_conn_destroy(tls);
81 return ESP_FAIL;
82 }
83
84 char resp[1024];
85 int rlen = esp_tls_conn_read(tls, (unsigned char *)resp, sizeof(resp) - 1);
86 if (rlen <= 0 || !strstr(resp, "101")) {
87 ESP_LOGE(TAG, "WebSocket upgrade failed (read %d bytes)", rlen);
88 esp_tls_conn_destroy(tls);
89 return ESP_FAIL;
90 }
91
92 cJSON *arr = cJSON_CreateArray();
93 cJSON_AddItemToArray(arr, cJSON_CreateString("EVENT"));
94 cJSON_AddItemToArray(arr, cJSON_Parse(event_json));
95 char *msg = cJSON_PrintUnformatted(arr);
96 cJSON_Delete(arr);
97
98 size_t msg_len = strlen(msg);
99 uint8_t ws_header[10];
100 int header_len = 0;
101 ws_header[0] = 0x81;
102 if (msg_len <= 125) {
103 ws_header[1] = (uint8_t)msg_len;
104 header_len = 2;
105 } else if (msg_len <= 65535) {
106 ws_header[1] = 126;
107 ws_header[2] = (uint8_t)((msg_len >> 8) & 0xff);
108 ws_header[3] = (uint8_t)(msg_len & 0xff);
109 header_len = 4;
110 } else {
111 ws_header[1] = 127;
112 for (int i = 0; i < 8; i++)
113 ws_header[2 + i] = (uint8_t)((msg_len >> (56 - i * 8)) & 0xff);
114 header_len = 10;
115 }
116
117 esp_tls_conn_write(tls, ws_header, header_len);
118 esp_tls_conn_write(tls, (const unsigned char *)msg, msg_len);
119
120 free(msg);
121
122 uint8_t resp_buf[256];
123 int resp_len = esp_tls_conn_read(tls, resp_buf, sizeof(resp_buf) - 1);
124 if (resp_len > 0) {
125 resp_buf[resp_len] = '\0';
126 int mask_len = (resp_buf[1] & 0x80) ? 4 : 0;
127 int payload_offset = 2 + mask_len;
128 if (resp_len > payload_offset) {
129 ESP_LOGI(TAG, "Relay response: %.*s", resp_len - payload_offset,
130 (char *)resp_buf + payload_offset);
131 }
132 }
133
134 uint8_t close_frame[2] = {0x88, 0x00};
135 esp_tls_conn_write(tls, close_frame, 2);
136 esp_tls_conn_destroy(tls);
137
138 ESP_LOGI(TAG, "Published to %s", host);
139 return ESP_OK;
140}
141
142static char *build_wifistr_event(void)
143{
144 const tollgate_identity_t *id = identity_get();
145 if (!id || !id->initialized) {
146 ESP_LOGE(TAG, "Identity not initialized");
147 return NULL;
148 }
149
150 const tollgate_config_t *cfg = tollgate_config_get();
151
152 cJSON *tags = cJSON_CreateArray();
153
154 cJSON *d_tag = cJSON_CreateArray();
155 cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
156 cJSON_AddItemToArray(d_tag, cJSON_CreateString(id->npub_hex));
157 cJSON_AddItemToArray(tags, d_tag);
158
159 cJSON *ssid_tag = cJSON_CreateArray();
160 cJSON_AddItemToArray(ssid_tag, cJSON_CreateString("ssid"));
161 cJSON_AddItemToArray(ssid_tag, cJSON_CreateString(id->ap_ssid));
162 cJSON_AddItemToArray(tags, ssid_tag);
163
164 cJSON *h_tag = cJSON_CreateArray();
165 cJSON_AddItemToArray(h_tag, cJSON_CreateString("h"));
166 cJSON_AddItemToArray(h_tag, cJSON_CreateString("cashu-testnut"));
167 cJSON_AddItemToArray(tags, h_tag);
168
169 cJSON *sec_tag = cJSON_CreateArray();
170 cJSON_AddItemToArray(sec_tag, cJSON_CreateString("security"));
171 cJSON_AddItemToArray(sec_tag, cJSON_CreateString("open"));
172 cJSON_AddItemToArray(tags, sec_tag);
173
174 cJSON *g_tag = cJSON_CreateArray();
175 cJSON_AddItemToArray(g_tag, cJSON_CreateString("g"));
176 cJSON_AddItemToArray(g_tag, cJSON_CreateString(cfg->nostr_geohash));
177 cJSON_AddItemToArray(tags, g_tag);
178
179 cJSON *c_tag = cJSON_CreateArray();
180 cJSON_AddItemToArray(c_tag, cJSON_CreateString("c"));
181 cJSON_AddItemToArray(c_tag, cJSON_CreateString("cashu"));
182 cJSON_AddItemToArray(tags, c_tag);
183
184 char content[512];
185 snprintf(content, sizeof(content),
186 "TollGate WiFi hotspot: %s | Price: %d sats/%dms | Mint: %s",
187 id->ap_ssid, cfg->price_per_step, cfg->step_size_ms, cfg->mint_url);
188
189 char *tags_str = cJSON_PrintUnformatted(tags);
190 cJSON_Delete(tags);
191
192 nostr_event_t event;
193 nostr_event_init(&event, id->npub_hex, 38787, tags_str, content);
194 nostr_event_sign(&event, id->nsec);
195 free(tags_str);
196
197 char *event_json = malloc(2048);
198 if (!event_json) return NULL;
199
200 esp_err_t ret = nostr_event_to_json(&event, event_json, 2048);
201 if (ret != ESP_OK) {
202 free(event_json);
203 return NULL;
204 }
205
206 return event_json;
207}
208
209esp_err_t wifistr_publish(void)
210{
211 char *event_json = build_wifistr_event();
212 if (!event_json) {
213 ESP_LOGE(TAG, "Failed to build wifistr event");
214 return ESP_FAIL;
215 }
216
217 ESP_LOGI(TAG, "Wifistr event: %s", event_json);
218
219 const tollgate_config_t *cfg = tollgate_config_get();
220 esp_err_t last_err = ESP_FAIL;
221
222 for (int i = 0; i < cfg->nostr_relay_count; i++) {
223 esp_err_t err = ws_send_to_relay(cfg->nostr_relays[i], event_json);
224 if (err == ESP_OK) last_err = ESP_OK;
225 vTaskDelay(pdMS_TO_TICKS(500));
226 }
227
228 free(event_json);
229 return last_err;
230}
231
232static void publish_task(void *pvParameters)
233{
234 wifistr_publish();
235 vTaskDelete(NULL);
236}
237
238static void timer_callback(TimerHandle_t timer)
239{
240 xTaskCreate(publish_task, "wifistr_pub", 16384, NULL, 3, NULL);
241}
242
243void wifistr_start_periodic(int interval_s)
244{
245 if (s_publish_timer) return;
246 s_publish_timer = xTimerCreate("wifistr", pdMS_TO_TICKS(interval_s * 1000),
247 pdTRUE, NULL, timer_callback);
248 if (s_publish_timer) {
249 xTimerStart(s_publish_timer, 0);
250 ESP_LOGI(TAG, "Periodic publish every %ds", interval_s);
251 }
252}
diff --git a/main/wifistr.h b/main/wifistr.h
new file mode 100644
index 0000000..843b6be
--- /dev/null
+++ b/main/wifistr.h
@@ -0,0 +1,10 @@
1#ifndef WIFISTR_H
2#define WIFISTR_H
3
4#include "esp_err.h"
5
6esp_err_t wifistr_publish(void);
7
8void wifistr_start_periodic(int interval_s);
9
10#endif