From edd125d0e3fe5fe7c0edf30c429723f3b0120c68 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 17 May 2026 04:50:07 +0530 Subject: feat(phase6): bytes-based billing - dual metric support - session_create_bytes() + session_add_bytes() for bytes-metric sessions - session_is_expired() dispatches on config metric (bytes vs milliseconds) - cashu_calculate_allotment() unified dispatcher for both metrics - tollgate_api discovery/usage/session_event use configured metric - config: metric field defaults to 'bytes', step_size_bytes=22020096 (21MB) - 14 new unit tests (148 total passing) - ASSERT_EQ_UINT64 macro added to test framework --- main/cashu.c | 8 ++++++++ main/cashu.h | 3 +++ main/config.c | 10 ++++++++++ main/config.h | 2 ++ main/session.c | 30 ++++++++++++++++++++++++++++++ main/session.h | 7 +++++++ main/tollgate_api.c | 36 +++++++++++++++++++++++++++--------- 7 files changed, 87 insertions(+), 9 deletions(-) (limited to 'main') diff --git a/main/cashu.c b/main/cashu.c index ba6d9ef..ec0566c 100644 --- a/main/cashu.c +++ b/main/cashu.c @@ -255,6 +255,14 @@ uint64_t cashu_calculate_allotment_ms(uint64_t token_amount, uint64_t price_per_ return (token_amount / price_per_step) * step_size_ms; } +uint64_t cashu_calculate_allotment(uint64_t token_amount, uint64_t price_per_step, + const char *metric, uint64_t step_size) +{ + if (price_per_step == 0) return 0; + (void)metric; + return (token_amount / price_per_step) * step_size; +} + bool cashu_is_mint_accepted(const char *mint_url) { if (!mint_url || mint_url[0] == '\0') return false; diff --git a/main/cashu.h b/main/cashu.h index 4c3d43b..76ad2eb 100644 --- a/main/cashu.h +++ b/main/cashu.h @@ -37,6 +37,9 @@ esp_err_t cashu_check_proof_states(const char *mint_url, const cashu_token_t *to uint64_t cashu_calculate_allotment_ms(uint64_t token_amount, uint64_t price_per_step, uint64_t step_size_ms); +uint64_t cashu_calculate_allotment(uint64_t token_amount, uint64_t price_per_step, + const char *metric, uint64_t step_size); + bool cashu_is_mint_accepted(const char *mint_url); #endif diff --git a/main/config.c b/main/config.c index 9257397..3e01efc 100644 --- a/main/config.c +++ b/main/config.c @@ -20,6 +20,8 @@ esp_err_t tollgate_config_init(void) g_config.ap_max_conn = 4; g_config.price_per_step = 21; g_config.step_size_ms = 60000; + g_config.step_size_bytes = 22020096; + strncpy(g_config.metric, "bytes", sizeof(g_config.metric) - 1); g_config.persist_threshold_sats = 1; g_config.nostr_publish_interval_s = 21600; g_config.client_enabled = false; @@ -136,6 +138,14 @@ esp_err_t tollgate_config_init(void) cJSON *step = cJSON_GetObjectItem(root, "step_size_ms"); if (step) g_config.step_size_ms = step->valueint; + cJSON *step_bytes = cJSON_GetObjectItem(root, "step_size_bytes"); + if (step_bytes) g_config.step_size_bytes = step_bytes->valueint; + + cJSON *metric = cJSON_GetObjectItem(root, "metric"); + if (metric && cJSON_IsString(metric)) { + strncpy(g_config.metric, metric->valuestring, sizeof(g_config.metric) - 1); + } + cJSON *persist = cJSON_GetObjectItem(root, "persist_threshold_sats"); if (persist) g_config.persist_threshold_sats = (uint64_t)persist->valuedouble; diff --git a/main/config.h b/main/config.h index de9f856..86b5e1a 100644 --- a/main/config.h +++ b/main/config.h @@ -43,6 +43,8 @@ typedef struct { char lnurl_url[256]; int price_per_step; int step_size_ms; + int step_size_bytes; + char metric[16]; uint64_t persist_threshold_sats; char nostr_geohash[16]; diff --git a/main/session.c b/main/session.c index 521b74a..4854163 100644 --- a/main/session.c +++ b/main/session.c @@ -1,6 +1,7 @@ #include "session.h" #include "firewall.h" #include "dns_server.h" +#include "config.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -103,6 +104,29 @@ session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, return NULL; } +session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes, + const char *spent_secrets[], int secret_count) +{ + session_t *s = session_create(client_ip, 0, spent_secrets, secret_count); + if (s) { + s->allotment_bytes = allotment_bytes; + s->bytes_consumed = 0; + s->allotment_ms = INT64_MAX; + esp_ip4_addr_t ip = { .addr = client_ip }; + ESP_LOGI(TAG, "Bytes session created: " IPSTR " allotment=%llu bytes", IP2STR(&ip), + (unsigned long long)allotment_bytes); + } + return s; +} + +void session_add_bytes(uint32_t client_ip, uint64_t bytes) +{ + session_t *s = session_find_by_ip(client_ip); + if (s && s->active) { + s->bytes_consumed += bytes; + } +} + session_t *session_find_by_ip(uint32_t client_ip) { for (int i = 0; i < SESSION_MAX_CLIENTS; i++) { @@ -136,6 +160,12 @@ void session_extend(session_t *session, uint64_t additional_ms) bool session_is_expired(const session_t *session) { if (!session || !session->active) return true; + + const tollgate_config_t *cfg = tollgate_config_get(); + if (cfg && strcmp(cfg->metric, "bytes") == 0) { + return session->bytes_consumed >= session->allotment_bytes; + } + int64_t elapsed = get_time_ms() - session->start_time_ms; return elapsed >= (int64_t)session->allotment_ms; } diff --git a/main/session.h b/main/session.h index 8e2d48d..6282f5a 100644 --- a/main/session.h +++ b/main/session.h @@ -13,6 +13,8 @@ typedef struct { char mac[SESSION_MAX_MAC_LEN]; uint64_t allotment_ms; int64_t start_time_ms; + uint64_t allotment_bytes; + uint64_t bytes_consumed; bool active; char spent_secrets[5][65]; int spent_secret_count; @@ -23,6 +25,11 @@ esp_err_t session_manager_init(void); session_t *session_create(uint32_t client_ip, uint64_t allotment_ms, const char *spent_secrets[], int secret_count); +session_t *session_create_bytes(uint32_t client_ip, uint64_t allotment_bytes, + const char *spent_secrets[], int secret_count); + +void session_add_bytes(uint32_t client_ip, uint64_t bytes); + session_t *session_find_by_ip(uint32_t client_ip); session_t *session_find_by_mac(const char *mac); diff --git a/main/tollgate_api.c b/main/tollgate_api.c index 72ed726..25e7dd2 100644 --- a/main/tollgate_api.c +++ b/main/tollgate_api.c @@ -78,7 +78,8 @@ static cJSON *create_session_event(uint32_t client_ip, uint64_t allotment_ms) cJSON *metric_tag = cJSON_CreateArray(); cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric")); - cJSON_AddItemToArray(metric_tag, cJSON_CreateString("milliseconds")); + const tollgate_config_t *mcfg = tollgate_config_get(); + cJSON_AddItemToArray(metric_tag, cJSON_CreateString(mcfg->metric[0] ? mcfg->metric : "milliseconds")); cJSON_AddItemToArray(tags, metric_tag); cJSON_AddItemToObject(root, "tags", tags); @@ -98,13 +99,14 @@ static esp_err_t api_get_discovery(httpd_req_t *req) cJSON *metric_tag = cJSON_CreateArray(); cJSON_AddItemToArray(metric_tag, cJSON_CreateString("metric")); - cJSON_AddItemToArray(metric_tag, cJSON_CreateString("milliseconds")); + cJSON_AddItemToArray(metric_tag, cJSON_CreateString(cfg->metric[0] ? cfg->metric : "milliseconds")); cJSON_AddItemToArray(tags, metric_tag); cJSON *step_tag = cJSON_CreateArray(); cJSON_AddItemToArray(step_tag, cJSON_CreateString("step_size")); char step_str[32]; - snprintf(step_str, sizeof(step_str), "%d", cfg->step_size_ms); + bool is_bytes = (strcmp(cfg->metric, "bytes") == 0); + snprintf(step_str, sizeof(step_str), "%d", is_bytes ? cfg->step_size_bytes : cfg->step_size_ms); cJSON_AddItemToArray(step_tag, cJSON_CreateString(step_str)); cJSON_AddItemToArray(tags, step_tag); @@ -280,7 +282,10 @@ static esp_err_t api_post_payment(httpd_req_t *req) } const tollgate_config_t *cfg = tollgate_config_get(); - uint64_t allotment = cashu_calculate_allotment_ms(token->total_amount, cfg->price_per_step, cfg->step_size_ms); + bool is_bytes = (strcmp(cfg->metric, "bytes") == 0); + uint64_t step_size = is_bytes ? (uint64_t)cfg->step_size_bytes : (uint64_t)cfg->step_size_ms; + uint64_t allotment = cashu_calculate_allotment(token->total_amount, cfg->price_per_step, + cfg->metric, step_size); if (allotment == 0) { free(states); free(token); @@ -299,7 +304,12 @@ static esp_err_t api_post_payment(httpd_req_t *req) for (int i = 0; i < secret_count; i++) { secrets[i] = token->proofs[i].secret; } - session_t *session = session_create(client_ip, allotment, secrets, secret_count); + session_t *session; + if (is_bytes) { + session = session_create_bytes(client_ip, allotment, secrets, secret_count); + } else { + session = session_create(client_ip, allotment, secrets, secret_count); + } if (!session) { free(states); free(token); @@ -339,12 +349,20 @@ static esp_err_t api_get_usage(httpd_req_t *req) return ESP_OK; } - int64_t elapsed = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS - session->start_time_ms; - int64_t remaining = session->allotment_ms - elapsed; - if (remaining < 0) remaining = 0; + const tollgate_config_t *cfg = tollgate_config_get(); + bool is_bytes = (strcmp(cfg->metric, "bytes") == 0); char resp[64]; - snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_ms); + if (is_bytes) { + int64_t remaining = (int64_t)session->allotment_bytes - (int64_t)session->bytes_consumed; + if (remaining < 0) remaining = 0; + snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_bytes); + } else { + int64_t elapsed = (int64_t)xTaskGetTickCount() * portTICK_PERIOD_MS - session->start_time_ms; + int64_t remaining = session->allotment_ms - elapsed; + if (remaining < 0) remaining = 0; + snprintf(resp, sizeof(resp), "%lld/%llu", (long long)remaining, (unsigned long long)session->allotment_ms); + } httpd_resp_set_type(req, "text/plain"); httpd_resp_send(req, resp, strlen(resp)); return ESP_OK; -- cgit v1.2.3