diff --git a/README.md b/README.md index 7ad7052..75c887e 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,11 @@ Skills compose by adoption-list order (`10123`) and trigger tags carry runtime e Didactyl will support local inference, which is very privacy preserving. Remote inference does however have it's advantages, and in those cases Didactyl supports using Bitcoin Lightning and eCash inference providers. -## Current Status — v0.1.17 +## Current Status — v0.1.18 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.1.17 — Fix Cashu v4 token field encoding (i bytes, s text, c bytes) and rebuild nostr_core_lib integration +> Last release update: v0.1.18 — Auto-clean spent Cashu proofs in check_proofs, publish deletions, and add spent token events to kind-10000 ignore list - Connects to configured relays with auto-reconnect and relay state transition logging - Publishes configured startup events per relay as each relay becomes connected diff --git a/src/cashu_wallet.c b/src/cashu_wallet.c index 737e586..8d81e67 100644 --- a/src/cashu_wallet.c +++ b/src/cashu_wallet.c @@ -14,6 +14,7 @@ #include "debug.h" #include "nostr_handler.h" +#include "nostr_block_list.h" #include "../../nostr_core_lib/nostr_core/nostr_core.h" #define CASHU_WALLET_QUERY_TIMEOUT_MS 6000 @@ -1354,6 +1355,7 @@ int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) { if (!resolved) return -1; pthread_mutex_lock(&g_wallet.mutex); + int token_count_snapshot = g_wallet.token_count; int target_proofs = 0; for (int i = 0; i < g_wallet.token_count; i++) { if (!g_wallet.tokens[i].token.mint_url || strcmp(g_wallet.tokens[i].token.mint_url, resolved) != 0) continue; @@ -1371,6 +1373,9 @@ int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) { cJSON_AddNumberToObject(root, "proof_count", 0); cJSON_AddNumberToObject(root, "spent_count", 0); cJSON_AddNumberToObject(root, "unknown_count", 0); + cJSON_AddNumberToObject(root, "deleted_token_events", 0); + cJSON_AddNumberToObject(root, "ignored_token_events", 0); + cJSON_AddNumberToObject(root, "remaining_balance", 0); *out_json = root; pthread_mutex_unlock(&g_wallet.mutex); return 0; @@ -1378,9 +1383,15 @@ int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) { const char** ys = (const char**)calloc((size_t)target_proofs, sizeof(char*)); char (*y_hexes)[67] = (char(*)[67])calloc((size_t)target_proofs, sizeof(*y_hexes)); - if (!ys || !y_hexes) { + int* proof_to_token_idx = (int*)calloc((size_t)target_proofs, sizeof(int)); + int* token_total_counts = (int*)calloc((size_t)token_count_snapshot, sizeof(int)); + int* token_spent_counts = (int*)calloc((size_t)token_count_snapshot, sizeof(int)); + if (!ys || !y_hexes || !proof_to_token_idx || !token_total_counts || !token_spent_counts) { free(ys); free(y_hexes); + free(proof_to_token_idx); + free(token_total_counts); + free(token_spent_counts); pthread_mutex_unlock(&g_wallet.mutex); return -1; } @@ -1393,10 +1404,15 @@ int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) { if (!secret || cashu_secret_to_y_hex(secret, y_hexes[y_idx]) != 0) { free(ys); free(y_hexes); + free(proof_to_token_idx); + free(token_total_counts); + free(token_spent_counts); pthread_mutex_unlock(&g_wallet.mutex); return -1; } ys[y_idx] = y_hexes[y_idx]; + proof_to_token_idx[y_idx] = i; + token_total_counts[i]++; y_idx++; } } @@ -1406,6 +1422,9 @@ int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) { if (cashu_build_checkstate_request(ys, y_idx, &request_body) != 0 || !request_body) { free(ys); free(y_hexes); + free(proof_to_token_idx); + free(token_total_counts); + free(token_spent_counts); cJSON_Delete(request_body); return -1; } @@ -1416,6 +1435,9 @@ int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) { free(ys); free(y_hexes); if (rc != 0 || !response_body) { + free(proof_to_token_idx); + free(token_total_counts); + free(token_spent_counts); cJSON_Delete(response_body); return -1; } @@ -1425,22 +1447,71 @@ int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) { rc = cashu_parse_checkstate_response(response_body, &parsed); cJSON_Delete(response_body); if (rc != 0) { + free(proof_to_token_idx); + free(token_total_counts); + free(token_spent_counts); return -1; } int spent_count = 0; int unknown_count = 0; - for (int i = 0; i < parsed.state_count; i++) { + int state_n = parsed.state_count < y_idx ? parsed.state_count : y_idx; + for (int i = 0; i < state_n; i++) { if (strcasecmp(parsed.states[i].state, "SPENT") == 0) { spent_count++; + int token_idx = proof_to_token_idx[i]; + if (token_idx >= 0 && token_idx < token_count_snapshot) { + token_spent_counts[token_idx]++; + } } else if (strcasecmp(parsed.states[i].state, "UNSPENT") != 0) { unknown_count++; } } + int deleted_token_events = 0; + int ignored_token_events = 0; + uint64_t remaining = 0; + + pthread_mutex_lock(&g_wallet.mutex); + for (int i = 0; i < g_wallet.token_count && i < token_count_snapshot; i++) { + if (!g_wallet.tokens[i].token.mint_url || strcmp(g_wallet.tokens[i].token.mint_url, resolved) != 0) continue; + if (token_total_counts[i] <= 0) continue; + if (token_spent_counts[i] < token_total_counts[i]) continue; + + if (g_wallet.tokens[i].event_id[0] != '\0') { + if (publish_token_deletion_event(g_wallet.tokens[i].event_id) == 0) { + deleted_token_events++; + } + if (nostr_block_list_add("e", g_wallet.tokens[i].event_id, 0) == 0) { + ignored_token_events++; + } + } + + nostr_nip60_free_token_data(&g_wallet.tokens[i].token); + memset(&g_wallet.tokens[i], 0, sizeof(g_wallet.tokens[i])); + } + + int compact = 0; + for (int i = 0; i < g_wallet.token_count; i++) { + if (g_wallet.tokens[i].event_id[0] == '\0' && !g_wallet.tokens[i].token.mint_url) continue; + if (compact != i) g_wallet.tokens[compact] = g_wallet.tokens[i]; + compact++; + } + g_wallet.token_count = compact; + if (g_wallet.token_count == 0) { + free(g_wallet.tokens); + g_wallet.tokens = NULL; + } + + token_proofs_total_for_mint_locked(resolved, &remaining); + pthread_mutex_unlock(&g_wallet.mutex); + cJSON* root = cJSON_CreateObject(); if (!root) { cashu_mint_free_checkstate_response(&parsed); + free(proof_to_token_idx); + free(token_total_counts); + free(token_spent_counts); return -1; } @@ -1449,10 +1520,16 @@ int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) { cJSON_AddNumberToObject(root, "proof_count", y_idx); cJSON_AddNumberToObject(root, "spent_count", spent_count); cJSON_AddNumberToObject(root, "unknown_count", unknown_count); + cJSON_AddNumberToObject(root, "deleted_token_events", deleted_token_events); + cJSON_AddNumberToObject(root, "ignored_token_events", ignored_token_events); + cJSON_AddNumberToObject(root, "remaining_balance", (double)remaining); *out_json = root; cashu_mint_free_checkstate_response(&parsed); + free(proof_to_token_idx); + free(token_total_counts); + free(token_spent_counts); return 0; } diff --git a/src/main.h b/src/main.h index 04a560b..99ae5e3 100644 --- a/src/main.h +++ b/src/main.h @@ -12,8 +12,8 @@ // Using DIDACTYL_ prefix to avoid conflicts with nostr_core_lib VERSION macros #define DIDACTYL_VERSION_MAJOR 0 #define DIDACTYL_VERSION_MINOR 1 -#define DIDACTYL_VERSION_PATCH 17 -#define DIDACTYL_VERSION "v0.1.17" +#define DIDACTYL_VERSION_PATCH 18 +#define DIDACTYL_VERSION "v0.1.18" // Agent metadata #define DIDACTYL_NAME "Didactyl"