Added cashu functionality.

This commit is contained in:
Your Name
2026-03-19 14:25:53 -04:00
parent 98cfd81b01
commit ea40aec6b8
4 changed files with 537 additions and 3 deletions
+1 -1
View File
@@ -1 +1 @@
0.5.0
0.5.1
+426
View File
@@ -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));
}
+108
View File
@@ -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
+2 -2
View File
@@ -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