From a2b884d447fa31611e91ff8ec773d7ec2a24bd57 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 20 Mar 2026 09:45:39 -0400 Subject: [PATCH] v0.1.7 - Improve Cashu receive diagnostics: preserve mint error JSON on non-2xx, add step-level DEBUG_WARN logging in cashu_wallet_receive_token, and return structured error details in wallet tool responses --- README.md | 4 +- src/cashu_wallet.c | 202 +++++++++++++++++++++++++++++----- src/main.h | 4 +- src/tools/tool_cashu_wallet.c | 18 ++- 4 files changed, 196 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 69a1aea..0422a3d 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.6 +## Current Status — v0.1.7 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.1.6 — Security: ignore genesis.jsonc, add safe genesis.jsonc.example, purge genesis history, and restore local ignored genesis file +> Last release update: v0.1.7 — Improve Cashu receive diagnostics: preserve mint error JSON on non-2xx, add step-level DEBUG_WARN logging in cashu_wallet_receive_token, and return structured error details in wallet tool responses - 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 7c5cb05..8094580 100644 --- a/src/cashu_wallet.c +++ b/src/cashu_wallet.c @@ -1167,14 +1167,69 @@ int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) { return 0; } +static int receive_token_error(cJSON** out_json, + const char* step, + int rc, + const char* error, + cJSON* mint_error_json) { + if (out_json) { + cJSON* root = cJSON_CreateObject(); + if (root) { + cJSON_AddBoolToObject(root, "success", 0); + cJSON_AddStringToObject(root, + "error", + (error && error[0]) ? error : "cashu_wallet_receive_token failed"); + if (step && step[0]) { + cJSON_AddStringToObject(root, "step", step); + } + cJSON_AddNumberToObject(root, "code", (double)rc); + if (mint_error_json) { + cJSON* dup = cJSON_Duplicate(mint_error_json, 1); + if (dup) { + cJSON_AddItemToObject(root, "mint_error", dup); + } + cJSON* detail = cJSON_GetObjectItemCaseSensitive(mint_error_json, "detail"); + if (detail && cJSON_IsString(detail) && detail->valuestring) { + cJSON_AddStringToObject(root, "mint_error_detail", detail->valuestring); + } + } + *out_json = root; + } + } + return rc; +} + int cashu_wallet_receive_token(const char* token_string, const char* mint_url, cJSON** out_json) { - if (!out_json || !token_string || token_string[0] == '\0' || !g_wallet.initialized || !g_wallet.cfg) return -1; - if (ensure_wallet_ready() != 0) return -1; + if (!out_json || !token_string || token_string[0] == '\0' || !g_wallet.initialized || !g_wallet.cfg) { + DEBUG_WARN("cashu_wallet_receive_token: invalid input or wallet not initialized"); + return receive_token_error(out_json, + "preconditions", + NOSTR_ERROR_INVALID_INPUT, + "cashu_wallet_receive_token preconditions failed", + NULL); + } + *out_json = NULL; + + int rc = ensure_wallet_ready(); + if (rc != 0) { + DEBUG_WARN("cashu_wallet_receive_token: ensure_wallet_ready failed rc=%d", rc); + return receive_token_error(out_json, + "ensure_wallet_ready", + rc, + "wallet is not ready", + NULL); + } cashu_decoded_token_t decoded; memset(&decoded, 0, sizeof(decoded)); - if (cashu_decode_token(token_string, &decoded) != 0) { - return -1; + rc = cashu_decode_token(token_string, &decoded); + if (rc != 0) { + DEBUG_WARN("cashu_wallet_receive_token: cashu_decode_token failed rc=%d", rc); + return receive_token_error(out_json, + "decode_token", + rc, + "failed to decode cashu token", + NULL); } pthread_mutex_lock(&g_wallet.mutex); @@ -1185,68 +1240,112 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c const char* resolved = cfg_resolved && cfg_resolved[0] ? cfg_resolved : decoded.mint_url; if (!resolved || resolved[0] == '\0') { + DEBUG_WARN("cashu_wallet_receive_token: no resolved mint URL"); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "resolve_mint_url", + NOSTR_ERROR_INVALID_INPUT, + "no mint URL available for token receive", + NULL); } if (decoded.mint_url && decoded.mint_url[0] && strcmp(decoded.mint_url, resolved) != 0) { + DEBUG_WARN("cashu_wallet_receive_token: token mint mismatch token_mint=%s resolved_mint=%s", + decoded.mint_url, + resolved); cashu_free_decoded_token(&decoded); - return NOSTR_ERROR_INVALID_INPUT; + return receive_token_error(out_json, + "validate_mint_match", + NOSTR_ERROR_INVALID_INPUT, + "token mint does not match selected wallet mint", + NULL); } uint64_t incoming_total = nostr_nip60_sum_proofs(decoded.proofs, decoded.proof_count); if (incoming_total == 0) { + DEBUG_WARN("cashu_wallet_receive_token: incoming token proof total is zero"); cashu_free_decoded_token(&decoded); - return NOSTR_ERROR_INVALID_INPUT; + return receive_token_error(out_json, + "sum_proofs", + NOSTR_ERROR_INVALID_INPUT, + "token proofs total to zero", + NULL); } cashu_mint_info_t info; memset(&info, 0, sizeof(info)); - if (cashu_mint_get_info(resolved, &info, timeout) != 0) { + rc = cashu_mint_get_info(resolved, &info, timeout); + if (rc != 0) { + DEBUG_WARN("cashu_wallet_receive_token: cashu_mint_get_info failed rc=%d mint=%s", rc, resolved); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "mint_get_info", + rc, + "failed to query mint info", + NULL); } cashu_keyset_t keyset; memset(&keyset, 0, sizeof(keyset)); - if (cashu_mint_get_active_keyset(&info, unit, &keyset) != 0 || !info.pubkey || info.pubkey[0] == '\0') { + rc = cashu_mint_get_active_keyset(&info, unit, &keyset); + if (rc != 0 || !info.pubkey || info.pubkey[0] == '\0') { + DEBUG_WARN("cashu_wallet_receive_token: active keyset/pubkey unavailable rc=%d unit=%s", rc, unit); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "get_active_keyset", + rc != 0 ? rc : NOSTR_ERROR_CASHU_INVALID_KEYSET, + "mint has no usable active keyset/pubkey for requested unit", + NULL); } uint64_t parts[CASHU_WALLET_MAX_SPLIT_PARTS] = {0}; int part_count = 0; - if (cashu_mint_plan_split_amounts(incoming_total, parts, CASHU_WALLET_MAX_SPLIT_PARTS, &part_count) != 0 || part_count <= 0) { + rc = cashu_mint_plan_split_amounts(incoming_total, parts, CASHU_WALLET_MAX_SPLIT_PARTS, &part_count); + if (rc != 0 || part_count <= 0) { + DEBUG_WARN("cashu_wallet_receive_token: cashu_mint_plan_split_amounts failed rc=%d incoming_total=%llu", + rc, + (unsigned long long)incoming_total); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "plan_split_amounts", + rc != 0 ? rc : -1, + "failed to split receive amount into blinded output parts", + NULL); } cashu_blinded_output_t* outputs = (cashu_blinded_output_t*)calloc((size_t)part_count, sizeof(cashu_blinded_output_t)); unsigned char* blinds = (unsigned char*)calloc((size_t)part_count, 32); char** secrets = (char**)calloc((size_t)part_count, sizeof(char*)); if (!outputs || !blinds || !secrets) { + DEBUG_WARN("cashu_wallet_receive_token: allocation failed for outputs/blinds/secrets part_count=%d", part_count); free(outputs); free(blinds); free(secrets); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "allocate_receive_buffers", + NOSTR_ERROR_MEMORY_FAILED, + "failed to allocate receive buffers", + NULL); } - int rc = 0; + rc = 0; for (int i = 0; i < part_count; i++) { unsigned char rnd_priv[32] = {0}; unsigned char rnd_pub[32] = {0}; if (nostr_generate_keypair(rnd_priv, rnd_pub) != NOSTR_SUCCESS) { + DEBUG_WARN("cashu_wallet_receive_token: nostr_generate_keypair failed at part=%d", i); rc = -1; break; } char* secret = (char*)malloc(65); if (!secret) { - rc = -1; + DEBUG_WARN("cashu_wallet_receive_token: secret allocation failed at part=%d", i); + rc = NOSTR_ERROR_MEMORY_FAILED; break; } nostr_bytes_to_hex(rnd_priv, 32, secret); @@ -1255,6 +1354,7 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c outputs[i].amount = parts[i]; if (cashu_blind_message(secret, info.pubkey, outputs[i].B_, &blinds[(size_t)i * 32]) != 0) { + DEBUG_WARN("cashu_wallet_receive_token: cashu_blind_message failed at part=%d", i); free(secret); rc = -1; break; @@ -1270,7 +1370,11 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c free(outputs); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "prepare_blinded_outputs", + rc, + "failed to prepare blinded outputs for swap", + NULL); } cashu_swap_request_t swap_req; @@ -1284,41 +1388,62 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c rc = cashu_build_swap_request(&swap_req, &request_body); free(outputs); if (rc != 0 || !request_body) { + DEBUG_WARN("cashu_wallet_receive_token: cashu_build_swap_request failed rc=%d", rc); for (int i = 0; i < part_count; i++) free(secrets[i]); free(secrets); free(blinds); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); cJSON_Delete(request_body); - return -1; + return receive_token_error(out_json, + "build_swap_request", + rc != 0 ? rc : -1, + "failed to build swap request", + NULL); } cJSON* response_body = NULL; rc = cashu_mint_swap(resolved, request_body, &response_body, timeout); cJSON_Delete(request_body); if (rc != 0 || !response_body) { + DEBUG_WARN("cashu_wallet_receive_token: cashu_mint_swap failed rc=%d mint=%s", rc, resolved); for (int i = 0; i < part_count; i++) free(secrets[i]); free(secrets); free(blinds); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); + int err_rc = receive_token_error(out_json, + "mint_swap", + rc != 0 ? rc : -1, + "mint rejected swap request", + response_body); cJSON_Delete(response_body); - return -1; + return err_rc; } cashu_swap_response_t parsed; memset(&parsed, 0, sizeof(parsed)); rc = cashu_parse_swap_response(response_body, &parsed); - cJSON_Delete(response_body); if (rc != 0 || parsed.signature_count <= 0 || parsed.signature_count != part_count) { + DEBUG_WARN("cashu_wallet_receive_token: cashu_parse_swap_response failed rc=%d signature_count=%d expected=%d", + rc, + parsed.signature_count, + part_count); cashu_mint_free_swap_response(&parsed); for (int i = 0; i < part_count; i++) free(secrets[i]); free(secrets); free(blinds); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); - return -1; + int err_rc = receive_token_error(out_json, + "parse_swap_response", + rc != 0 ? rc : -1, + "invalid swap response from mint", + response_body); + cJSON_Delete(response_body); + return err_rc; } + cJSON_Delete(response_body); nostr_nip60_token_data_t token; memset(&token, 0, sizeof(token)); @@ -1326,6 +1451,7 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c token.proof_count = parsed.signature_count; token.proofs = (nostr_cashu_proof_t*)calloc((size_t)token.proof_count, sizeof(nostr_cashu_proof_t)); if (!token.mint_url || !token.proofs) { + DEBUG_WARN("cashu_wallet_receive_token: token allocation failed proof_count=%d", token.proof_count); nostr_nip60_free_token_data(&token); cashu_mint_free_swap_response(&parsed); for (int i = 0; i < part_count; i++) free(secrets[i]); @@ -1333,7 +1459,11 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c free(blinds); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "allocate_token", + NOSTR_ERROR_MEMORY_FAILED, + "failed to allocate received token storage", + NULL); } for (int i = 0; i < parsed.signature_count; i++) { @@ -1342,6 +1472,7 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c char unblinded_C[67] = {0}; if (cashu_unblind_signature(parsed.signatures[i].C_, info.pubkey, &blinds[(size_t)i * 32], unblinded_C) != 0) { + DEBUG_WARN("cashu_wallet_receive_token: cashu_unblind_signature failed at index=%d", i); nostr_nip60_free_token_data(&token); cashu_mint_free_swap_response(&parsed); for (int j = 0; j < part_count; j++) free(secrets[j]); @@ -1349,12 +1480,17 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c free(blinds); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "unblind_signature", + -1, + "failed to unblind mint signature", + NULL); } token.proofs[i].secret = secrets[i] ? strdup(secrets[i]) : NULL; token.proofs[i].C = strdup(unblinded_C); if (!token.proofs[i].secret || !token.proofs[i].C) { + DEBUG_WARN("cashu_wallet_receive_token: proof string allocation failed at index=%d", i); nostr_nip60_free_token_data(&token); cashu_mint_free_swap_response(&parsed); for (int j = 0; j < part_count; j++) free(secrets[j]); @@ -1362,7 +1498,11 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c free(blinds); cashu_mint_free_info(&info); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "store_unblinded_proofs", + NOSTR_ERROR_MEMORY_FAILED, + "failed to store unblinded proofs", + NULL); } } @@ -1377,9 +1517,14 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c rc = publish_token_event_and_store_locked(&token, token_event_id); pthread_mutex_unlock(&g_wallet.mutex); if (rc != 0) { + DEBUG_WARN("cashu_wallet_receive_token: publish_token_event_and_store_locked failed rc=%d", rc); nostr_nip60_free_token_data(&token); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "publish_token_event", + rc, + "failed to persist received token event", + NULL); } (void)publish_history_event(NOSTR_NIP60_DIRECTION_IN, @@ -1389,8 +1534,13 @@ int cashu_wallet_receive_token(const char* token_string, const char* mint_url, c cJSON* root = cJSON_CreateObject(); if (!root) { + DEBUG_WARN("cashu_wallet_receive_token: failed to create success JSON root"); cashu_free_decoded_token(&decoded); - return -1; + return receive_token_error(out_json, + "build_success_json", + NOSTR_ERROR_MEMORY_FAILED, + "failed to build success response", + NULL); } cJSON_AddBoolToObject(root, "success", 1); diff --git a/src/main.h b/src/main.h index 985c508..5b6a082 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 6 -#define DIDACTYL_VERSION "v0.1.6" +#define DIDACTYL_VERSION_PATCH 7 +#define DIDACTYL_VERSION "v0.1.7" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/tools/tool_cashu_wallet.c b/src/tools/tool_cashu_wallet.c index 9fc6625..b6be309 100644 --- a/src/tools/tool_cashu_wallet.c +++ b/src/tools/tool_cashu_wallet.c @@ -29,13 +29,27 @@ static cJSON* parse_args_local(const char* args_json) { } static char* wrap_wallet_json_result(int rc, cJSON* obj, const char* default_error) { - if (rc != 0 || !obj) { - cJSON_Delete(obj); + if (!obj) { return json_error_local(default_error ? default_error : "cashu wallet operation failed"); } + if (rc != 0) { + cJSON* success = cJSON_GetObjectItemCaseSensitive(obj, "success"); + if (!success) { + cJSON_AddBoolToObject(obj, "success", 0); + } + if (!cJSON_GetObjectItemCaseSensitive(obj, "error")) { + cJSON_AddStringToObject(obj, + "error", + default_error ? default_error : "cashu wallet operation failed"); + } + } + char* out = cJSON_PrintUnformatted(obj); cJSON_Delete(obj); + if (!out) { + return json_error_local(default_error ? default_error : "cashu wallet operation failed"); + } return out; }