Compare commits

...
1 Commits
10 changed files with 299 additions and 6 deletions
+2 -2
View File
@@ -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 \
+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.1
## Current Status — v0.1.2
**Active build — this project is barely working. Experiment at your own risk.**
> Last release update: v0.1.1Fix cron trigger firing with validation, shorthand normalization, immediate polling, and status/log diagnostics
> Last release update: v0.1.2Implement 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
Binary file not shown.
+238
View File
@@ -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;
}
+1
View File
@@ -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
+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 1
#define DIDACTYL_VERSION "v0.1.1"
#define DIDACTYL_VERSION_PATCH 2
#define DIDACTYL_VERSION "v0.1.2"
// Agent metadata
#define DIDACTYL_NAME "Didactyl"
+21
View File
@@ -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");
}
+3
View File
@@ -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");
}
+1
View File
@@ -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);
+29
View File
@@ -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;