diff options
| author | Your Name <you@example.com> | 2026-05-16 23:55:05 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-16 23:55:05 +0530 |
| commit | 4c47ae188b288e7d24bd9566ab3e6a6805d9484f (patch) | |
| tree | 33b74b2090b4f3b7597841734a56a4006a86d73f /main | |
| parent | 133e40c82afb4d7659758b1fa57925ac57af4621 (diff) | |
Phase 3: Nostr identity derivation + wifistr service discovery
- Add identity.c/h: HMAC-SHA512 derivation from nsec → npub, STA/AP MAC, SSID, AP IP
- Add nostr_event.c/h: NIP-01 event serialization + Schnorr signing (BIP-340)
- Add geohash.c/h: lat/lon to geohash encoding
- Add wifistr.c/h: kind 38787 event builder + WebSocket publish to Nostr relays
- Update config.c/h: nsec-based identity, Nostr relay/geo config, remove static SSID/IP
- Replace custom mbedTLS wallet with nucula library (libsecp256k1)
- Remove wallet.c/h, wallet_persist.c/h (replaced by nucula_lib component)
- Verified on Board A: derived SSID, captive portal, payment, wallet, wifistr publish
Diffstat (limited to 'main')
| -rw-r--r-- | main/CMakeLists.txt | 9 | ||||
| -rw-r--r-- | main/config.c | 88 | ||||
| -rw-r--r-- | main/config.h | 14 | ||||
| -rw-r--r-- | main/geohash.c | 48 | ||||
| -rw-r--r-- | main/geohash.h | 8 | ||||
| -rw-r--r-- | main/identity.c | 124 | ||||
| -rw-r--r-- | main/identity.h | 29 | ||||
| -rw-r--r-- | main/nostr_event.c | 112 | ||||
| -rw-r--r-- | main/nostr_event.h | 25 | ||||
| -rw-r--r-- | main/tollgate_api.c | 63 | ||||
| -rw-r--r-- | main/tollgate_main.c | 31 | ||||
| -rw-r--r-- | main/wallet.c | 639 | ||||
| -rw-r--r-- | main/wallet.h | 53 | ||||
| -rw-r--r-- | main/wallet_persist.c | 147 | ||||
| -rw-r--r-- | main/wallet_persist.h | 9 | ||||
| -rw-r--r-- | main/wifistr.c | 252 | ||||
| -rw-r--r-- | main/wifistr.h | 10 |
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 | ||
| 152 | void tollgate_config_derive_unique(tollgate_config_t *cfg) | 185 | void 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 | ||
| 14 | typedef struct { | 15 | typedef 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 | ||
| 42 | void tollgate_config_derive_unique(tollgate_config_t *cfg); | 54 | void 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 | |||
| 4 | static const char BASE32[] = "0123456789bcdefghjkmnpqrstuvwxyz"; | ||
| 5 | |||
| 6 | void 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 | |||
| 6 | void 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 | |||
| 12 | static const char *TAG = "identity"; | ||
| 13 | static tollgate_identity_t s_identity; | ||
| 14 | |||
| 15 | static 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 | |||
| 26 | static 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 | |||
| 33 | static 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 | |||
| 53 | esp_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 | |||
| 121 | const 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 | |||
| 10 | typedef 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 | |||
| 25 | esp_err_t identity_init(const char *nsec_hex); | ||
| 26 | |||
| 27 | const 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 | |||
| 13 | static const char *TAG = "nostr_event"; | ||
| 14 | |||
| 15 | static 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 | |||
| 22 | esp_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 | |||
| 55 | esp_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 | |||
| 89 | esp_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 | |||
| 8 | typedef 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 | |||
| 18 | esp_err_t nostr_event_init(nostr_event_t *event, const char *npub_hex, | ||
| 19 | int kind, const char *tags_json, const char *content); | ||
| 20 | |||
| 21 | esp_err_t nostr_event_sign(nostr_event_t *event, const uint8_t nsec[32]); | ||
| 22 | |||
| 23 | esp_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 | ||
| 382 | static esp_err_t api_get_wallet(httpd_req_t *req) | 373 | static 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 | ||
| 407 | static esp_err_t api_post_wallet_swap(httpd_req_t *req) | 396 | static 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 |
| 22 | static const char *TAG = "tollgate_main"; | 24 | static const char *TAG = "tollgate_main"; |
| @@ -92,8 +94,16 @@ static void ip_event_handler(void *arg, esp_event_base_t event_base, | |||
| 92 | static void wallet_init_task(void *pvParameters) | 94 | static 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 | |||
| 101 | static 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 | |||
| 20 | static const char *TAG = "wallet"; | ||
| 21 | static wallet_t s_wallet; | ||
| 22 | |||
| 23 | static const char DOMAIN_SEPARATOR[] = "Secp256k1_HashToCurve_Cashu_"; | ||
| 24 | |||
| 25 | static mbedtls_ecp_group s_grp; | ||
| 26 | static mbedtls_mpi s_order; | ||
| 27 | static bool s_grp_loaded = false; | ||
| 28 | |||
| 29 | static 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 | |||
| 44 | static void random_bytes(uint8_t *buf, size_t len) | ||
| 45 | { | ||
| 46 | esp_fill_random(buf, len); | ||
| 47 | } | ||
| 48 | |||
| 49 | static 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 | |||
| 64 | static 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 | |||
| 112 | static 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 | |||
| 126 | static 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 | |||
| 136 | static 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 | |||
| 148 | static 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 | |||
| 156 | esp_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 | |||
| 166 | wallet_t *wallet_get(void) | ||
| 167 | { | ||
| 168 | return &s_wallet; | ||
| 169 | } | ||
| 170 | |||
| 171 | uint64_t wallet_balance(void) | ||
| 172 | { | ||
| 173 | return s_wallet.balance; | ||
| 174 | } | ||
| 175 | |||
| 176 | esp_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 | |||
| 194 | esp_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 | |||
| 207 | void wallet_clear(void) | ||
| 208 | { | ||
| 209 | s_wallet.balance = 0; | ||
| 210 | s_wallet.proof_count = 0; | ||
| 211 | wallet_persist_save(); | ||
| 212 | } | ||
| 213 | |||
| 214 | esp_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 | |||
| 283 | esp_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 | |||
| 551 | esp_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 | |||
| 622 | esp_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 | |||
| 628 | void 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 | |||
| 14 | typedef 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 | |||
| 21 | typedef 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 | |||
| 28 | typedef 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 | |||
| 36 | esp_err_t wallet_init(void); | ||
| 37 | wallet_t *wallet_get(void); | ||
| 38 | uint64_t wallet_balance(void); | ||
| 39 | |||
| 40 | esp_err_t wallet_add_proofs(const wallet_proof_t *proofs, int count); | ||
| 41 | esp_err_t wallet_remove_proof(int index); | ||
| 42 | void wallet_clear(void); | ||
| 43 | |||
| 44 | esp_err_t wallet_fetch_keysets(const char *mint_url); | ||
| 45 | esp_err_t wallet_swap_proofs(const char *mint_url, int start_index, int count); | ||
| 46 | |||
| 47 | esp_err_t wallet_create_token(char *out, size_t out_size, uint64_t amount, | ||
| 48 | const char *mint_url); | ||
| 49 | esp_err_t wallet_send(const char *mint_url, uint64_t amount, | ||
| 50 | char *token_out, size_t token_out_size); | ||
| 51 | |||
| 52 | void 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 | |||
| 10 | static const char *TAG = "wallet_persist"; | ||
| 11 | static const char *WALLET_FILE = "/spiffs/wallet.json"; | ||
| 12 | |||
| 13 | esp_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 | |||
| 67 | esp_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 | |||
| 6 | esp_err_t wallet_persist_save(void); | ||
| 7 | esp_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 | |||
| 15 | static const char *TAG = "wifistr"; | ||
| 16 | static TimerHandle_t s_publish_timer = NULL; | ||
| 17 | |||
| 18 | static 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 | |||
| 142 | static 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 | |||
| 209 | esp_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 | |||
| 232 | static void publish_task(void *pvParameters) | ||
| 233 | { | ||
| 234 | wifistr_publish(); | ||
| 235 | vTaskDelete(NULL); | ||
| 236 | } | ||
| 237 | |||
| 238 | static void timer_callback(TimerHandle_t timer) | ||
| 239 | { | ||
| 240 | xTaskCreate(publish_task, "wifistr_pub", 16384, NULL, 3, NULL); | ||
| 241 | } | ||
| 242 | |||
| 243 | void 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 | |||
| 6 | esp_err_t wifistr_publish(void); | ||
| 7 | |||
| 8 | void wifistr_start_periodic(int interval_s); | ||
| 9 | |||
| 10 | #endif | ||