diff --git a/Dockerfile.alpine-musl b/Dockerfile.alpine-musl index 890e0cc..fb1befd 100644 --- a/Dockerfile.alpine-musl +++ b/Dockerfile.alpine-musl @@ -121,8 +121,8 @@ RUN NOSTR_LIB=$(ls /build/nostr_core_lib/libnostr_core_*.a 2>/dev/null | head -1 src/tools/tool_nostr_query.c src/tools/tool_nostr_my_events.c src/tools/tool_nostr_identity.c src/tools/tool_nostr_social.c \ src/tools/tool_nostr_relay.c src/tools/tool_nostr_dm.c src/tools/tool_admin.c \ src/tools/tool_task.c src/tools/tool_nostr_list.c src/tools/tool_local.c \ - src/tools/tool_skill.c src/tools/tool_nostr_post.c src/tools/tool_memory.c src/tools/tool_config.c src/trigger_manager.c \ - src/prompt_template.c src/http_api.c src/setup_wizard.c src/mongoose.c src/debug.c \ + src/tools/tool_skill.c src/tools/tool_nostr_post.c src/tools/tool_memory.c src/tools/tool_config.c src/tools/tool_cashu_wallet.c src/trigger_manager.c \ + src/cashu_wallet.c src/prompt_template.c src/http_api.c src/setup_wizard.c src/mongoose.c src/debug.c \ -o /build/didactyl_static \ $NOSTR_LIB \ -lsecp256k1 \ diff --git a/README.md b/README.md index e6f70b3..942ac59 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.1 +## Current Status — v0.1.2 **Active build — this project is barely working. Experiment at your own risk.** -> Last release update: v0.1.1 — Fix cron trigger firing with validation, shorthand normalization, immediate polling, and status/log diagnostics +> Last release update: v0.1.2 — Implement cashu_wallet_receive_token tool flow and integrate Cashu receive/swap handling - 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/didactyl_static_arm64 b/didactyl_static_arm64 index c0b4b1a..97d665c 100755 Binary files a/didactyl_static_arm64 and b/didactyl_static_arm64 differ diff --git a/src/cashu_wallet.c b/src/cashu_wallet.c index bdeb25c..15be494 100644 --- a/src/cashu_wallet.c +++ b/src/cashu_wallet.c @@ -987,3 +987,241 @@ int cashu_wallet_check_proofs_json(const char* mint_url, cJSON** out_json) { cashu_mint_free_checkstate_response(&parsed); return 0; } + +int cashu_wallet_receive_token_json(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; + + cashu_decoded_token_t decoded; + memset(&decoded, 0, sizeof(decoded)); + if (cashu_decode_token(token_string, &decoded) != 0) { + return -1; + } + + pthread_mutex_lock(&g_wallet.mutex); + const char* cfg_resolved = resolve_mint_url_locked(mint_url); + int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30; + const char* unit = g_wallet.cfg->cashu_wallet.unit[0] ? g_wallet.cfg->cashu_wallet.unit : "sat"; + pthread_mutex_unlock(&g_wallet.mutex); + + const char* resolved = cfg_resolved && cfg_resolved[0] ? cfg_resolved : decoded.mint_url; + if (!resolved || resolved[0] == '\0') { + cashu_free_decoded_token(&decoded); + return -1; + } + + if (decoded.mint_url && decoded.mint_url[0] && strcmp(decoded.mint_url, resolved) != 0) { + cashu_free_decoded_token(&decoded); + return NOSTR_ERROR_INVALID_INPUT; + } + + uint64_t incoming_total = nostr_nip60_sum_proofs(decoded.proofs, decoded.proof_count); + if (incoming_total == 0) { + cashu_free_decoded_token(&decoded); + return NOSTR_ERROR_INVALID_INPUT; + } + + cashu_mint_info_t info; + memset(&info, 0, sizeof(info)); + if (cashu_mint_get_info(resolved, &info, timeout) != 0) { + cashu_free_decoded_token(&decoded); + return -1; + } + + 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') { + cashu_mint_free_info(&info); + cashu_free_decoded_token(&decoded); + return -1; + } + + 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) { + cashu_mint_free_info(&info); + cashu_free_decoded_token(&decoded); + return -1; + } + + 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) { + free(outputs); + free(blinds); + free(secrets); + cashu_mint_free_info(&info); + cashu_free_decoded_token(&decoded); + return -1; + } + + int 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) { + rc = -1; + break; + } + + char* secret = (char*)malloc(65); + if (!secret) { + rc = -1; + break; + } + nostr_bytes_to_hex(rnd_priv, 32, secret); + + snprintf(outputs[i].id, sizeof(outputs[i].id), "%s", keyset.id); + outputs[i].amount = parts[i]; + + if (cashu_blind_message(secret, info.pubkey, outputs[i].B_, &blinds[(size_t)i * 32]) != 0) { + free(secret); + rc = -1; + break; + } + + secrets[i] = secret; + } + + if (rc != 0) { + for (int i = 0; i < part_count; i++) free(secrets[i]); + free(secrets); + free(blinds); + free(outputs); + cashu_mint_free_info(&info); + cashu_free_decoded_token(&decoded); + return -1; + } + + cashu_swap_request_t swap_req; + memset(&swap_req, 0, sizeof(swap_req)); + swap_req.inputs = decoded.proofs; + swap_req.input_count = decoded.proof_count; + swap_req.outputs = outputs; + swap_req.output_count = part_count; + + cJSON* request_body = NULL; + rc = cashu_build_swap_request(&swap_req, &request_body); + free(outputs); + if (rc != 0 || !request_body) { + 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; + } + + cJSON* response_body = NULL; + rc = cashu_mint_swap(resolved, request_body, &response_body, timeout); + cJSON_Delete(request_body); + if (rc != 0 || !response_body) { + 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(response_body); + return -1; + } + + 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) { + 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; + } + + nostr_nip60_token_data_t token; + memset(&token, 0, sizeof(token)); + token.mint_url = strdup(resolved); + 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) { + nostr_nip60_free_token_data(&token); + 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; + } + + for (int i = 0; i < parsed.signature_count; i++) { + snprintf(token.proofs[i].id, sizeof(token.proofs[i].id), "%s", parsed.signatures[i].id); + token.proofs[i].amount = parsed.signatures[i].amount; + + char unblinded_C[67] = {0}; + if (cashu_unblind_signature(parsed.signatures[i].C_, info.pubkey, &blinds[(size_t)i * 32], unblinded_C) != 0) { + nostr_nip60_free_token_data(&token); + cashu_mint_free_swap_response(&parsed); + for (int j = 0; j < part_count; j++) free(secrets[j]); + free(secrets); + free(blinds); + cashu_mint_free_info(&info); + cashu_free_decoded_token(&decoded); + return -1; + } + + 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) { + nostr_nip60_free_token_data(&token); + cashu_mint_free_swap_response(&parsed); + for (int j = 0; j < part_count; j++) free(secrets[j]); + free(secrets); + free(blinds); + cashu_mint_free_info(&info); + cashu_free_decoded_token(&decoded); + return -1; + } + } + + 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); + + char token_event_id[65] = {0}; + pthread_mutex_lock(&g_wallet.mutex); + rc = publish_token_event_and_store_locked(&token, token_event_id); + pthread_mutex_unlock(&g_wallet.mutex); + if (rc != 0) { + nostr_nip60_free_token_data(&token); + cashu_free_decoded_token(&decoded); + return -1; + } + + (void)publish_history_event(NOSTR_NIP60_DIRECTION_IN, + incoming_total, + token_event_id, + NOSTR_NIP60_REF_CREATED); + + cJSON* root = cJSON_CreateObject(); + if (!root) { + cashu_free_decoded_token(&decoded); + return -1; + } + + cJSON_AddBoolToObject(root, "success", 1); + cJSON_AddStringToObject(root, "mint", resolved); + cJSON_AddNumberToObject(root, "received_amount", (double)incoming_total); + cJSON_AddStringToObject(root, "token_event_id", token_event_id); + cJSON_AddNumberToObject(root, "received_proofs", decoded.proof_count); + cJSON_AddNumberToObject(root, "new_proofs", token.proof_count); + + *out_json = root; + + cashu_free_decoded_token(&decoded); + return 0; +} diff --git a/src/cashu_wallet.h b/src/cashu_wallet.h index c990a06..cce8166 100644 --- a/src/cashu_wallet.h +++ b/src/cashu_wallet.h @@ -20,5 +20,6 @@ int cashu_wallet_mint_claim_json(const char* mint_url, const char* quote_id, uin int cashu_wallet_melt_quote_json(const char* mint_url, const char* payment_request, const char* unit, cJSON** out_json); int cashu_wallet_melt_pay_json(const char* mint_url, const char* quote_id, cJSON** out_json); int cashu_wallet_check_proofs_json(const char* mint_url, cJSON** out_json); +int cashu_wallet_receive_token_json(const char* token_string, const char* mint_url, cJSON** out_json); #endif diff --git a/src/main.h b/src/main.h index 78dca81..48204d3 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 1 -#define DIDACTYL_VERSION "v0.1.1" +#define DIDACTYL_VERSION_PATCH 2 +#define DIDACTYL_VERSION "v0.1.2" // Agent metadata #define DIDACTYL_NAME "Didactyl" diff --git a/src/tools/tool_cashu_wallet.c b/src/tools/tool_cashu_wallet.c index a93c5bc..6ee0439 100644 --- a/src/tools/tool_cashu_wallet.c +++ b/src/tools/tool_cashu_wallet.c @@ -227,3 +227,24 @@ char* execute_cashu_wallet_check_proofs(tools_context_t* ctx, const char* args_j cJSON_Delete(args); return wrap_wallet_json_result(rc, out, "cashu_wallet_check_proofs failed"); } + +char* execute_cashu_wallet_receive_token(tools_context_t* ctx, const char* args_json) { + if (!ctx || !ctx->cfg) return json_error_local("tool context unavailable"); + + cJSON* args = parse_args_local(args_json); + if (!args) return json_error_local("invalid arguments JSON"); + + cJSON* token = cJSON_GetObjectItemCaseSensitive(args, "token"); + if (!token || !cJSON_IsString(token) || !token->valuestring || token->valuestring[0] == '\0') { + cJSON_Delete(args); + return json_error_local("cashu_wallet_receive_token requires token"); + } + + cJSON* mint = cJSON_GetObjectItemCaseSensitive(args, "mint_url"); + const char* mint_url = (mint && cJSON_IsString(mint) && mint->valuestring) ? mint->valuestring : NULL; + + cJSON* out = NULL; + int rc = cashu_wallet_receive_token_json(token->valuestring, mint_url, &out); + cJSON_Delete(args); + return wrap_wallet_json_result(rc, out, "cashu_wallet_receive_token failed"); +} diff --git a/src/tools/tools_dispatch.c b/src/tools/tools_dispatch.c index 63baa2d..c4b599e 100644 --- a/src/tools/tools_dispatch.c +++ b/src/tools/tools_dispatch.c @@ -256,6 +256,9 @@ char* tools_execute_legacy(tools_context_t* ctx, const char* tool_name, const ch if (strcmp(tool_name, "cashu_wallet_check_proofs") == 0) { return execute_cashu_wallet_check_proofs(ctx, args_json); } + if (strcmp(tool_name, "cashu_wallet_receive_token") == 0) { + return execute_cashu_wallet_receive_token(ctx, args_json); + } return json_error("unknown tool"); } diff --git a/src/tools/tools_internal.h b/src/tools/tools_internal.h index 71a9b2b..827cec7 100644 --- a/src/tools/tools_internal.h +++ b/src/tools/tools_internal.h @@ -77,6 +77,7 @@ char* execute_cashu_wallet_mint_claim(tools_context_t* ctx, const char* args_jso char* execute_cashu_wallet_melt_quote(tools_context_t* ctx, const char* args_json); char* execute_cashu_wallet_melt_pay(tools_context_t* ctx, const char* args_json); char* execute_cashu_wallet_check_proofs(tools_context_t* ctx, const char* args_json); +char* execute_cashu_wallet_receive_token(tools_context_t* ctx, const char* args_json); int memory_init(tools_context_t* ctx); void memory_cleanup(void); diff --git a/src/tools/tools_schema.c b/src/tools/tools_schema.c index 8314b81..d92f617 100644 --- a/src/tools/tools_schema.c +++ b/src/tools/tools_schema.c @@ -1682,6 +1682,35 @@ char* tools_build_openai_schema_json_legacy(const tools_context_t* ctx) { cJSON_AddItemToObject(t58, "function", t58_fn); cJSON_AddItemToArray(tools, t58); + cJSON* t59 = cJSON_CreateObject(); + cJSON* t59_fn = cJSON_CreateObject(); + cJSON* t59_params = cJSON_CreateObject(); + cJSON* t59_props = cJSON_CreateObject(); + cJSON* t59_required = cJSON_CreateArray(); + + cJSON_AddStringToObject(t59, "type", "function"); + cJSON_AddStringToObject(t59_fn, "name", "cashu_wallet_receive_token"); + cJSON_AddStringToObject(t59_fn, "description", "Receive an ecash token string (cashuA/cashuB), swap to fresh proofs, and store it in wallet state"); + cJSON_AddStringToObject(t59_params, "type", "object"); + cJSON_AddItemToObject(t59_params, "properties", t59_props); + cJSON_AddItemToObject(t59_params, "required", t59_required); + + cJSON* p_wallet_rt_token = cJSON_CreateObject(); + cJSON_AddStringToObject(p_wallet_rt_token, "type", "string"); + cJSON_AddStringToObject(p_wallet_rt_token, "description", "Incoming token string beginning with cashuA or cashuB"); + cJSON_AddItemToObject(t59_props, "token", p_wallet_rt_token); + + cJSON* p_wallet_rt_mint = cJSON_CreateObject(); + cJSON_AddStringToObject(p_wallet_rt_mint, "type", "string"); + cJSON_AddStringToObject(p_wallet_rt_mint, "description", "Optional mint URL override; when provided must match token mint"); + cJSON_AddItemToObject(t59_props, "mint_url", p_wallet_rt_mint); + + cJSON_AddItemToArray(t59_required, cJSON_CreateString("token")); + + cJSON_AddItemToObject(t59_fn, "parameters", t59_params); + cJSON_AddItemToObject(t59, "function", t59_fn); + cJSON_AddItemToArray(tools, t59); + char* out = cJSON_PrintUnformatted(tools); cJSON_Delete(tools); return out;