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

This commit is contained in:
Your Name
2026-03-20 09:45:39 -04:00
parent 14549cd581
commit a2b884d447
4 changed files with 196 additions and 32 deletions
+2 -2
View File
@@ -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.6Security: ignore genesis.jsonc, add safe genesis.jsonc.example, purge genesis history, and restore local ignored genesis file
> Last release update: v0.1.7Improve 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
+176 -26
View File
@@ -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);
+2 -2
View File
@@ -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"
+16 -2
View File
@@ -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;
}