From ea40aec6b867168625edaaad2e1aece5d2069293 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Mar 2026 14:25:53 -0400 Subject: [PATCH] Added cashu functionality. --- VERSION | 2 +- nostr_core/cashu_mint.c | 426 ++++++++++++++++++++++++++++++++++++++++ nostr_core/cashu_mint.h | 108 ++++++++++ nostr_core/nostr_core.h | 4 +- 4 files changed, 537 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 8f0916f7..4b9fcbec 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.5.1 diff --git a/nostr_core/cashu_mint.c b/nostr_core/cashu_mint.c index 2cf90371..4910b72a 100644 --- a/nostr_core/cashu_mint.c +++ b/nostr_core/cashu_mint.c @@ -393,3 +393,429 @@ int cashu_mint_check_proofs_state(const char* mint_url, free(body); return rc; } + +int cashu_mint_get_active_keyset(const cashu_mint_info_t* info, + const char* optional_unit, + cashu_keyset_t* keyset_out) { + if (!info || !keyset_out || !info->keysets || info->keyset_count <= 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + + for (int i = 0; i < info->keyset_count; i++) { + const cashu_keyset_t* ks = &info->keysets[i]; + if (!ks->active) continue; + if (optional_unit && optional_unit[0] != '\0' && strcmp(ks->unit, optional_unit) != 0) continue; + memset(keyset_out, 0, sizeof(*keyset_out)); + memcpy(keyset_out, ks, sizeof(*keyset_out)); + return NOSTR_SUCCESS; + } + + if (optional_unit && optional_unit[0] != '\0') { + for (int i = 0; i < info->keyset_count; i++) { + const cashu_keyset_t* ks = &info->keysets[i]; + if (strcmp(ks->unit, optional_unit) != 0) continue; + memset(keyset_out, 0, sizeof(*keyset_out)); + memcpy(keyset_out, ks, sizeof(*keyset_out)); + return NOSTR_SUCCESS; + } + } + + return NOSTR_ERROR_CASHU_INVALID_KEYSET; +} + +int cashu_mint_select_proofs_for_amount(const nostr_cashu_proof_t* proofs, + int proof_count, + uint64_t target_amount, + int* selected_indices_out, + int selected_indices_cap, + uint64_t* selected_total_out) { + if (!proofs || proof_count <= 0 || !selected_indices_out || selected_indices_cap <= 0 || target_amount == 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + + int* used = (int*)calloc((size_t)proof_count, sizeof(int)); + if (!used) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + int selected_count = 0; + uint64_t total = 0; + + while (total < target_amount) { + int best_idx = -1; + uint64_t best_amt = 0; + + for (int i = 0; i < proof_count; i++) { + if (used[i]) continue; + if (proofs[i].amount > best_amt) { + best_amt = proofs[i].amount; + best_idx = i; + } + } + + if (best_idx < 0 || best_amt == 0) { + free(used); + return NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS; + } + + if (selected_count >= selected_indices_cap) { + free(used); + return NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS; + } + + used[best_idx] = 1; + selected_indices_out[selected_count++] = best_idx; + total += proofs[best_idx].amount; + } + + free(used); + + if (selected_total_out) { + *selected_total_out = total; + } + + return selected_count; +} + +int cashu_mint_plan_split_amounts(uint64_t amount, + uint64_t* amounts_out, + int max_amounts, + int* amount_count_out) { + if (!amounts_out || max_amounts <= 0 || !amount_count_out || amount == 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + + int count = 0; + + while (amount > 0) { + uint64_t part = 1; + while ((part << 1) > part && (part << 1) <= amount) { + part <<= 1; + } + + if (count >= max_amounts) { + return NOSTR_ERROR_INVALID_INPUT; + } + + amounts_out[count++] = part; + amount -= part; + } + + *amount_count_out = count; + return NOSTR_SUCCESS; +} + +static int cashu_build_outputs_json(const cashu_blinded_output_t* outputs, + int output_count, + cJSON** outputs_out) { + if (!outputs || output_count <= 0 || !outputs_out) return NOSTR_ERROR_INVALID_INPUT; + + cJSON* arr = cJSON_CreateArray(); + if (!arr) return NOSTR_ERROR_MEMORY_FAILED; + + for (int i = 0; i < output_count; i++) { + cJSON* o = cJSON_CreateObject(); + if (!o) { + cJSON_Delete(arr); + return NOSTR_ERROR_MEMORY_FAILED; + } + cJSON_AddStringToObject(o, "id", outputs[i].id); + cJSON_AddNumberToObject(o, "amount", (double)outputs[i].amount); + cJSON_AddStringToObject(o, "B_", outputs[i].B_); + cJSON_AddItemToArray(arr, o); + } + + *outputs_out = arr; + return NOSTR_SUCCESS; +} + +static int cashu_parse_signatures_array(cJSON* signatures, + cashu_blinded_signature_t** sigs_out, + int* sig_count_out) { + if (!sigs_out || !sig_count_out) return NOSTR_ERROR_INVALID_INPUT; + + *sigs_out = NULL; + *sig_count_out = 0; + + if (!signatures || !cJSON_IsArray(signatures)) { + return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED; + } + + int n = cJSON_GetArraySize(signatures); + if (n <= 0) return NOSTR_SUCCESS; + + cashu_blinded_signature_t* sigs = + (cashu_blinded_signature_t*)calloc((size_t)n, sizeof(cashu_blinded_signature_t)); + if (!sigs) return NOSTR_ERROR_MEMORY_FAILED; + + for (int i = 0; i < n; i++) { + cJSON* item = cJSON_GetArrayItem(signatures, i); + if (!item || !cJSON_IsObject(item)) { + free(sigs); + return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED; + } + + cJSON* id = cJSON_GetObjectItemCaseSensitive(item, "id"); + cJSON* amount = cJSON_GetObjectItemCaseSensitive(item, "amount"); + cJSON* C_ = cJSON_GetObjectItemCaseSensitive(item, "C_"); + + if (!id || !cJSON_IsString(id) || !id->valuestring || + !amount || !cJSON_IsNumber(amount) || + !C_ || !cJSON_IsString(C_) || !C_->valuestring) { + free(sigs); + return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED; + } + + snprintf(sigs[i].id, sizeof(sigs[i].id), "%s", id->valuestring); + sigs[i].amount = (uint64_t)cJSON_GetNumberValue(amount); + snprintf(sigs[i].C_, sizeof(sigs[i].C_), "%s", C_->valuestring); + } + + *sigs_out = sigs; + *sig_count_out = n; + return NOSTR_SUCCESS; +} + +int cashu_build_mint_tokens_request(const cashu_mint_tokens_request_t* req, + cJSON** request_body_out) { + if (!req || !request_body_out || req->quote_id[0] == '\0' || req->output_count <= 0 || !req->outputs) { + return NOSTR_ERROR_INVALID_INPUT; + } + + cJSON* root = cJSON_CreateObject(); + if (!root) return NOSTR_ERROR_MEMORY_FAILED; + + cJSON_AddStringToObject(root, "quote", req->quote_id); + if (req->unit[0] != '\0') { + cJSON_AddStringToObject(root, "unit", req->unit); + } + + cJSON* outputs = NULL; + int rc = cashu_build_outputs_json(req->outputs, req->output_count, &outputs); + if (rc != NOSTR_SUCCESS) { + cJSON_Delete(root); + return rc; + } + + cJSON_AddItemToObject(root, "outputs", outputs); + *request_body_out = root; + return NOSTR_SUCCESS; +} + +int cashu_parse_mint_tokens_response(cJSON* response_body, + cashu_mint_tokens_response_t* response_out) { + if (!response_body || !response_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + memset(response_out, 0, sizeof(*response_out)); + + cJSON* signatures = cJSON_GetObjectItemCaseSensitive(response_body, "signatures"); + return cashu_parse_signatures_array(signatures, + &response_out->signatures, + &response_out->signature_count); +} + +void cashu_mint_free_mint_tokens_response(cashu_mint_tokens_response_t* response) { + if (!response) return; + free(response->signatures); + memset(response, 0, sizeof(*response)); +} + +int cashu_build_swap_request(const cashu_swap_request_t* req, + cJSON** request_body_out) { + if (!req || !request_body_out || !req->inputs || req->input_count <= 0 || !req->outputs || req->output_count <= 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + + cJSON* root = cJSON_CreateObject(); + if (!root) return NOSTR_ERROR_MEMORY_FAILED; + + cJSON* inputs = nostr_nip60_proofs_to_json(req->inputs, req->input_count); + if (!inputs) { + cJSON_Delete(root); + return NOSTR_ERROR_MEMORY_FAILED; + } + cJSON_AddItemToObject(root, "inputs", inputs); + + cJSON* outputs = NULL; + int rc = cashu_build_outputs_json(req->outputs, req->output_count, &outputs); + if (rc != NOSTR_SUCCESS) { + cJSON_Delete(root); + return rc; + } + cJSON_AddItemToObject(root, "outputs", outputs); + + *request_body_out = root; + return NOSTR_SUCCESS; +} + +int cashu_parse_swap_response(cJSON* response_body, + cashu_swap_response_t* response_out) { + if (!response_body || !response_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + memset(response_out, 0, sizeof(*response_out)); + + cJSON* signatures = cJSON_GetObjectItemCaseSensitive(response_body, "signatures"); + return cashu_parse_signatures_array(signatures, + &response_out->signatures, + &response_out->signature_count); +} + +void cashu_mint_free_swap_response(cashu_swap_response_t* response) { + if (!response) return; + free(response->signatures); + memset(response, 0, sizeof(*response)); +} + +int cashu_build_melt_tokens_request(const cashu_melt_tokens_request_t* req, + cJSON** request_body_out) { + if (!req || !request_body_out || req->quote_id[0] == '\0' || !req->inputs || req->input_count <= 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + + cJSON* root = cJSON_CreateObject(); + if (!root) return NOSTR_ERROR_MEMORY_FAILED; + + cJSON_AddStringToObject(root, "quote", req->quote_id); + + cJSON* inputs = nostr_nip60_proofs_to_json(req->inputs, req->input_count); + if (!inputs) { + cJSON_Delete(root); + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_AddItemToObject(root, "inputs", inputs); + *request_body_out = root; + return NOSTR_SUCCESS; +} + +int cashu_parse_melt_tokens_response(cJSON* response_body, + cashu_melt_tokens_response_t* response_out) { + if (!response_body || !response_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + memset(response_out, 0, sizeof(*response_out)); + + cJSON* paid = cJSON_GetObjectItemCaseSensitive(response_body, "paid"); + cJSON* preimage = cJSON_GetObjectItemCaseSensitive(response_body, "payment_preimage"); + cJSON* change = cJSON_GetObjectItemCaseSensitive(response_body, "change"); + + response_out->paid = (paid && cJSON_IsBool(paid) && cJSON_IsTrue(paid)) ? 1 : 0; + + if (preimage && cJSON_IsString(preimage) && preimage->valuestring) { + response_out->payment_preimage = cashu_strdup(preimage->valuestring); + if (!response_out->payment_preimage) { + return NOSTR_ERROR_MEMORY_FAILED; + } + } + + if (change) { + response_out->change = cJSON_Duplicate(change, 1); + if (!response_out->change) { + free(response_out->payment_preimage); + memset(response_out, 0, sizeof(*response_out)); + return NOSTR_ERROR_MEMORY_FAILED; + } + } + + return NOSTR_SUCCESS; +} + +void cashu_mint_free_melt_tokens_response(cashu_melt_tokens_response_t* response) { + if (!response) return; + free(response->payment_preimage); + cJSON_Delete(response->change); + memset(response, 0, sizeof(*response)); +} + +int cashu_build_checkstate_request(const char** Ys, + int y_count, + cJSON** request_body_out) { + if (!Ys || y_count <= 0 || !request_body_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + cJSON* root = cJSON_CreateObject(); + cJSON* ys = cJSON_CreateArray(); + if (!root || !ys) { + cJSON_Delete(root); + cJSON_Delete(ys); + return NOSTR_ERROR_MEMORY_FAILED; + } + + for (int i = 0; i < y_count; i++) { + if (!Ys[i] || Ys[i][0] == '\0') { + cJSON_Delete(root); + return NOSTR_ERROR_INVALID_INPUT; + } + cJSON_AddItemToArray(ys, cJSON_CreateString(Ys[i])); + } + + cJSON_AddItemToObject(root, "Ys", ys); + *request_body_out = root; + return NOSTR_SUCCESS; +} + +int cashu_parse_checkstate_response(cJSON* response_body, + cashu_checkstate_response_t* response_out) { + if (!response_body || !response_out) { + return NOSTR_ERROR_INVALID_INPUT; + } + + memset(response_out, 0, sizeof(*response_out)); + + cJSON* states = cJSON_GetObjectItemCaseSensitive(response_body, "states"); + if (!states || !cJSON_IsArray(states)) { + return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED; + } + + int n = cJSON_GetArraySize(states); + if (n <= 0) { + return NOSTR_SUCCESS; + } + + response_out->states = (cashu_proof_state_t*)calloc((size_t)n, sizeof(cashu_proof_state_t)); + if (!response_out->states) { + return NOSTR_ERROR_MEMORY_FAILED; + } + + for (int i = 0; i < n; i++) { + cJSON* item = cJSON_GetArrayItem(states, i); + if (!item || !cJSON_IsObject(item)) { + cashu_mint_free_checkstate_response(response_out); + return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED; + } + + cJSON* Y = cJSON_GetObjectItemCaseSensitive(item, "Y"); + cJSON* state = cJSON_GetObjectItemCaseSensitive(item, "state"); + cJSON* witness = cJSON_GetObjectItemCaseSensitive(item, "witness"); + + if (Y && cJSON_IsString(Y) && Y->valuestring) { + snprintf(response_out->states[i].Y, sizeof(response_out->states[i].Y), "%s", Y->valuestring); + } + if (state && cJSON_IsString(state) && state->valuestring) { + snprintf(response_out->states[i].state, sizeof(response_out->states[i].state), "%s", state->valuestring); + } + if (witness && cJSON_IsString(witness) && witness->valuestring) { + snprintf(response_out->states[i].witness, sizeof(response_out->states[i].witness), "%s", witness->valuestring); + } + + if (response_out->states[i].state[0] == '\0') { + cashu_mint_free_checkstate_response(response_out); + return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED; + } + } + + response_out->state_count = n; + return NOSTR_SUCCESS; +} + +void cashu_mint_free_checkstate_response(cashu_checkstate_response_t* response) { + if (!response) return; + free(response->states); + memset(response, 0, sizeof(*response)); +} diff --git a/nostr_core/cashu_mint.h b/nostr_core/cashu_mint.h index 666f9b03..635c6c18 100644 --- a/nostr_core/cashu_mint.h +++ b/nostr_core/cashu_mint.h @@ -47,6 +47,65 @@ typedef struct { time_t expiry; } cashu_melt_quote_t; +typedef struct { + char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE]; + uint64_t amount; + char B_[256]; +} cashu_blinded_output_t; + +typedef struct { + char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE]; + uint64_t amount; + char C_[256]; +} cashu_blinded_signature_t; + +typedef struct { + char quote_id[128]; + char unit[16]; + cashu_blinded_output_t* outputs; + int output_count; +} cashu_mint_tokens_request_t; + +typedef struct { + cashu_blinded_signature_t* signatures; + int signature_count; +} cashu_mint_tokens_response_t; + +typedef struct { + nostr_cashu_proof_t* inputs; + int input_count; + cashu_blinded_output_t* outputs; + int output_count; +} cashu_swap_request_t; + +typedef struct { + cashu_blinded_signature_t* signatures; + int signature_count; +} cashu_swap_response_t; + +typedef struct { + char quote_id[128]; + nostr_cashu_proof_t* inputs; + int input_count; +} cashu_melt_tokens_request_t; + +typedef struct { + int paid; + char* payment_preimage; + cJSON* change; +} cashu_melt_tokens_response_t; + +typedef struct { + char Y[256]; + char state[32]; + char witness[512]; +} cashu_proof_state_t; + +typedef struct { + cashu_proof_state_t* states; + int state_count; +} cashu_checkstate_response_t; + int cashu_mint_get_info(const char* mint_url, cashu_mint_info_t* info_out, int timeout_seconds); @@ -95,6 +154,55 @@ int cashu_mint_check_proofs_state(const char* mint_url, cJSON** response_out, int timeout_seconds); +int cashu_mint_get_active_keyset(const cashu_mint_info_t* info, + const char* optional_unit, + cashu_keyset_t* keyset_out); + +int cashu_mint_select_proofs_for_amount(const nostr_cashu_proof_t* proofs, + int proof_count, + uint64_t target_amount, + int* selected_indices_out, + int selected_indices_cap, + uint64_t* selected_total_out); + +int cashu_mint_plan_split_amounts(uint64_t amount, + uint64_t* amounts_out, + int max_amounts, + int* amount_count_out); + +int cashu_build_mint_tokens_request(const cashu_mint_tokens_request_t* req, + cJSON** request_body_out); + +int cashu_parse_mint_tokens_response(cJSON* response_body, + cashu_mint_tokens_response_t* response_out); + +void cashu_mint_free_mint_tokens_response(cashu_mint_tokens_response_t* response); + +int cashu_build_swap_request(const cashu_swap_request_t* req, + cJSON** request_body_out); + +int cashu_parse_swap_response(cJSON* response_body, + cashu_swap_response_t* response_out); + +void cashu_mint_free_swap_response(cashu_swap_response_t* response); + +int cashu_build_melt_tokens_request(const cashu_melt_tokens_request_t* req, + cJSON** request_body_out); + +int cashu_parse_melt_tokens_response(cJSON* response_body, + cashu_melt_tokens_response_t* response_out); + +void cashu_mint_free_melt_tokens_response(cashu_melt_tokens_response_t* response); + +int cashu_build_checkstate_request(const char** Ys, + int y_count, + cJSON** request_body_out); + +int cashu_parse_checkstate_response(cJSON* response_body, + cashu_checkstate_response_t* response_out); + +void cashu_mint_free_checkstate_response(cashu_checkstate_response_t* response); + #ifdef __cplusplus } #endif diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index b7bdbb67..ad53fde2 100644 --- a/nostr_core/nostr_core.h +++ b/nostr_core/nostr_core.h @@ -2,10 +2,10 @@ #define NOSTR_CORE_H // Version information (auto-updated by increment_and_push.sh) -#define VERSION "v0.5.0" +#define VERSION "v0.5.1" #define VERSION_MAJOR 0 #define VERSION_MINOR 5 -#define VERSION_PATCH 0 +#define VERSION_PATCH 1 /* * NOSTR Core Library - Complete API Reference