Add Cashu keyset endpoints support and per-amount unblinding with live checkstate validation

This commit is contained in:
2026-03-20 10:32:43 -04:00
parent 39ebfcd90a
commit a595747aff
7 changed files with 332 additions and 154 deletions
+1 -1
View File
@@ -1 +1 @@
0.5.5
0.5.6
+198 -32
View File
@@ -226,6 +226,82 @@ static int cashu_call_raw(const char* mint_url,
return NOSTR_SUCCESS;
}
static int cashu_parse_keysets_array(cJSON* keysets,
cashu_keyset_t** keysets_out,
int* keyset_count_out) {
if (!keysets_out || !keyset_count_out) return NOSTR_ERROR_INVALID_INPUT;
*keysets_out = NULL;
*keyset_count_out = 0;
if (!keysets || !cJSON_IsArray(keysets)) {
return NOSTR_SUCCESS;
}
int n = cJSON_GetArraySize(keysets);
if (n <= 0) return NOSTR_SUCCESS;
cashu_keyset_t* out = (cashu_keyset_t*)calloc((size_t)n, sizeof(cashu_keyset_t));
if (!out) return NOSTR_ERROR_MEMORY_FAILED;
int out_count = 0;
for (int i = 0; i < n; i++) {
cJSON* ks = cJSON_GetArrayItem(keysets, i);
if (!ks || !cJSON_IsObject(ks)) continue;
cJSON* id = cJSON_GetObjectItem(ks, "id");
cJSON* unit = cJSON_GetObjectItem(ks, "unit");
cJSON* active = cJSON_GetObjectItem(ks, "active");
if (!id || !cJSON_IsString(id) || !id->valuestring || id->valuestring[0] == '\0') continue;
snprintf(out[out_count].id, sizeof(out[out_count].id), "%s", id->valuestring);
if (unit && cJSON_IsString(unit) && unit->valuestring) {
snprintf(out[out_count].unit, sizeof(out[out_count].unit), "%s", unit->valuestring);
}
if (active && cJSON_IsBool(active)) {
out[out_count].active = cJSON_IsTrue(active) ? 1 : 0;
}
out_count++;
}
if (out_count == 0) {
free(out);
return NOSTR_SUCCESS;
}
*keysets_out = out;
*keyset_count_out = out_count;
return NOSTR_SUCCESS;
}
int cashu_mint_get_keysets(const char* mint_url,
cashu_keyset_t** keysets_out,
int* keyset_count_out,
int timeout_seconds) {
if (!mint_url || !keysets_out || !keyset_count_out) return NOSTR_ERROR_INVALID_INPUT;
*keysets_out = NULL;
*keyset_count_out = 0;
cJSON* json = NULL;
int rc = cashu_call_raw(mint_url, "/v1/keysets", "GET", NULL, &json, timeout_seconds);
if (rc != NOSTR_SUCCESS) {
cJSON_Delete(json);
return rc;
}
cJSON* keysets = cJSON_GetObjectItem(json, "keysets");
rc = cashu_parse_keysets_array(keysets, keysets_out, keyset_count_out);
cJSON_Delete(json);
return rc;
}
void cashu_mint_free_keysets(cashu_keyset_t* keysets) {
free(keysets);
}
int cashu_mint_get_info(const char* mint_url,
cashu_mint_info_t* info_out,
int timeout_seconds) {
@@ -249,41 +325,23 @@ int cashu_mint_get_info(const char* mint_url,
if (version && cJSON_IsString(version)) info_out->version = cashu_strdup(cJSON_GetStringValue(version));
cJSON* keysets = cJSON_GetObjectItem(json, "keysets");
if (keysets && cJSON_IsArray(keysets)) {
int n = cJSON_GetArraySize(keysets);
if (n > 0) {
info_out->keysets = (cashu_keyset_t*)calloc((size_t)n, sizeof(cashu_keyset_t));
if (!info_out->keysets) {
cJSON_Delete(json);
cashu_mint_free_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
rc = cashu_parse_keysets_array(keysets, &info_out->keysets, &info_out->keyset_count);
cJSON_Delete(json);
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(info_out);
return rc;
}
for (int i = 0; i < n; i++) {
cJSON* ks = cJSON_GetArrayItem(keysets, i);
if (!ks || !cJSON_IsObject(ks)) continue;
cJSON* id = cJSON_GetObjectItem(ks, "id");
cJSON* unit = cJSON_GetObjectItem(ks, "unit");
cJSON* active = cJSON_GetObjectItem(ks, "active");
if (id && cJSON_IsString(id)) {
strncpy(info_out->keysets[info_out->keyset_count].id,
cJSON_GetStringValue(id), sizeof(info_out->keysets[info_out->keyset_count].id) - 1);
}
if (unit && cJSON_IsString(unit)) {
strncpy(info_out->keysets[info_out->keyset_count].unit,
cJSON_GetStringValue(unit), sizeof(info_out->keysets[info_out->keyset_count].unit) - 1);
}
if (active && cJSON_IsBool(active)) {
info_out->keysets[info_out->keyset_count].active = cJSON_IsTrue(active) ? 1 : 0;
}
info_out->keyset_count++;
}
if (!info_out->keysets || info_out->keyset_count == 0) {
cashu_keyset_t* fetched = NULL;
int fetched_count = 0;
rc = cashu_mint_get_keysets(mint_url, &fetched, &fetched_count, timeout_seconds);
if (rc == NOSTR_SUCCESS && fetched && fetched_count > 0) {
info_out->keysets = fetched;
info_out->keyset_count = fetched_count;
}
}
cJSON_Delete(json);
return NOSTR_SUCCESS;
}
@@ -292,10 +350,118 @@ void cashu_mint_free_info(cashu_mint_info_t* info) {
free(info->name);
free(info->pubkey);
free(info->version);
free(info->keysets);
cashu_mint_free_keysets(info->keysets);
memset(info, 0, sizeof(*info));
}
int cashu_mint_get_keys(const char* mint_url,
const char* keyset_id,
cashu_keyset_keys_t* keys_out,
int timeout_seconds) {
if (!mint_url || !keyset_id || !keys_out) return NOSTR_ERROR_INVALID_INPUT;
memset(keys_out, 0, sizeof(*keys_out));
char endpoint[640];
snprintf(endpoint, sizeof(endpoint), "/v1/keys/%s", keyset_id);
cJSON* json = NULL;
int rc = cashu_call_raw(mint_url, endpoint, "GET", NULL, &json, timeout_seconds);
if (rc != NOSTR_SUCCESS) {
cJSON_Delete(json);
return rc;
}
cJSON* keysets = cJSON_GetObjectItem(json, "keysets");
if (!keysets || !cJSON_IsArray(keysets) || cJSON_GetArraySize(keysets) <= 0) {
cJSON_Delete(json);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
cJSON* ks = cJSON_GetArrayItem(keysets, 0);
if (!ks || !cJSON_IsObject(ks)) {
cJSON_Delete(json);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
cJSON* id = cJSON_GetObjectItem(ks, "id");
cJSON* unit = cJSON_GetObjectItem(ks, "unit");
cJSON* keys = cJSON_GetObjectItem(ks, "keys");
if (!id || !cJSON_IsString(id) || !id->valuestring || !keys || !cJSON_IsObject(keys)) {
cJSON_Delete(json);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
snprintf(keys_out->keyset_id, sizeof(keys_out->keyset_id), "%s", id->valuestring);
if (unit && cJSON_IsString(unit) && unit->valuestring) {
snprintf(keys_out->unit, sizeof(keys_out->unit), "%s", unit->valuestring);
}
int count = 0;
for (cJSON* it = keys->child; it; it = it->next) {
if (!it->string || !cJSON_IsString(it) || !it->valuestring) continue;
count++;
}
if (count <= 0) {
cJSON_Delete(json);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
keys_out->keys = (cashu_amount_key_t*)calloc((size_t)count, sizeof(cashu_amount_key_t));
if (!keys_out->keys) {
cJSON_Delete(json);
return NOSTR_ERROR_MEMORY_FAILED;
}
int out_idx = 0;
for (cJSON* it = keys->child; it; it = it->next) {
if (!it->string || !cJSON_IsString(it) || !it->valuestring) continue;
unsigned long long amount = strtoull(it->string, NULL, 10);
if (amount == 0) continue;
keys_out->keys[out_idx].amount = (uint64_t)amount;
snprintf(keys_out->keys[out_idx].pubkey, sizeof(keys_out->keys[out_idx].pubkey), "%s", it->valuestring);
out_idx++;
}
keys_out->key_count = out_idx;
cJSON_Delete(json);
if (keys_out->key_count <= 0) {
cashu_mint_free_keyset_keys(keys_out);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
return NOSTR_SUCCESS;
}
void cashu_mint_free_keyset_keys(cashu_keyset_keys_t* keys) {
if (!keys) return;
free(keys->keys);
memset(keys, 0, sizeof(*keys));
}
int cashu_keyset_keys_find_pubkey_for_amount(const cashu_keyset_keys_t* keys,
uint64_t amount,
const char** pubkey_out) {
if (!keys || !keys->keys || keys->key_count <= 0 || amount == 0 || !pubkey_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*pubkey_out = NULL;
for (int i = 0; i < keys->key_count; i++) {
if (keys->keys[i].amount != amount) continue;
if (keys->keys[i].pubkey[0] == '\0') continue;
*pubkey_out = keys->keys[i].pubkey;
return NOSTR_SUCCESS;
}
return NOSTR_ERROR_CASHU_INVALID_KEYSET;
}
int cashu_mint_request_mint_quote(const char* mint_url,
uint64_t amount,
const char* unit,
+30
View File
@@ -22,6 +22,18 @@ typedef struct {
int active;
} cashu_keyset_t;
typedef struct {
uint64_t amount;
char pubkey[67];
} cashu_amount_key_t;
typedef struct {
char keyset_id[65];
char unit[16];
cashu_amount_key_t* keys;
int key_count;
} cashu_keyset_keys_t;
typedef struct {
char* name;
char* pubkey;
@@ -120,6 +132,24 @@ int cashu_mint_get_info(const char* mint_url,
void cashu_mint_free_info(cashu_mint_info_t* info);
int cashu_mint_get_keysets(const char* mint_url,
cashu_keyset_t** keysets_out,
int* keyset_count_out,
int timeout_seconds);
void cashu_mint_free_keysets(cashu_keyset_t* keysets);
int cashu_mint_get_keys(const char* mint_url,
const char* keyset_id,
cashu_keyset_keys_t* keys_out,
int timeout_seconds);
void cashu_mint_free_keyset_keys(cashu_keyset_keys_t* keys);
int cashu_keyset_keys_find_pubkey_for_amount(const cashu_keyset_keys_t* keys,
uint64_t amount,
const char** pubkey_out);
int cashu_mint_request_mint_quote(const char* mint_url,
uint64_t amount,
const char* unit,
+1 -1
View File
@@ -22,7 +22,7 @@ extern "C" {
#define NOSTR_NIP60_HISTORY_KIND 7376
#define NOSTR_NIP60_QUOTE_KIND 7374
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 17
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 65
#define NOSTR_CASHU_PUBKEY_HEX_SIZE 67
#define NOSTR_CASHU_EVENT_ID_HEX_SIZE 65
+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.5"
#define VERSION "v0.5.6"
#define VERSION_MAJOR 0
#define VERSION_MINOR 5
#define VERSION_PATCH 5
#define VERSION_PATCH 6
/*
* NOSTR Core Library - Complete API Reference
Binary file not shown.
+100 -118
View File
@@ -9,9 +9,15 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <curl/curl.h>
#include "../nostr_core/nostr_core.h"
/* private secp256k1 wrappers from utils.c */
typedef struct {
unsigned char data[64];
} nostr_secp256k1_pubkey;
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey* pubkey, const unsigned char* input, size_t inputlen);
static int tests_run = 0;
static int tests_passed = 0;
@@ -21,123 +27,45 @@ static int tests_passed = 0;
else { printf("❌ %s\n", msg); } \
} while (0)
typedef struct {
char* data;
size_t size;
size_t capacity;
} http_buf_t;
static int secret_to_Y_hex(const char* secret, char out_Y_hex[67]) {
if (!secret || !out_Y_hex) return NOSTR_ERROR_INVALID_INPUT;
static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
http_buf_t* b = (http_buf_t*)userp;
size_t n = size * nmemb;
if (b->size + n + 1 > b->capacity) {
size_t cap = b->capacity ? b->capacity * 2 : 1024;
while (cap < b->size + n + 1) cap *= 2;
char* p = (char*)realloc(b->data, cap);
if (!p) return 0;
b->data = p;
b->capacity = cap;
unsigned char digest[32] = {0};
if (nostr_sha256((const unsigned char*)secret, strlen(secret), digest) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
memcpy(b->data + b->size, contents, n);
b->size += n;
b->data[b->size] = '\0';
return n;
}
unsigned char candidate[33] = {0};
for (int counter = 0; counter < 1024; counter++) {
unsigned char h[32] = {0};
unsigned char data[36] = {0};
memcpy(data, digest, 32);
data[32] = (unsigned char)((counter >> 24) & 0xff);
data[33] = (unsigned char)((counter >> 16) & 0xff);
data[34] = (unsigned char)((counter >> 8) & 0xff);
data[35] = (unsigned char)(counter & 0xff);
static int http_get_json(const char* url, cJSON** out_json) {
if (!url || !out_json) return NOSTR_ERROR_INVALID_INPUT;
*out_json = NULL;
if (nostr_sha256(data, sizeof(data), h) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
CURL* curl = curl_easy_init();
if (!curl) return NOSTR_ERROR_NETWORK_FAILED;
candidate[0] = 0x02;
memcpy(candidate + 1, h, 32);
http_buf_t b;
memset(&b, 0, sizeof(b));
b.capacity = 2048;
b.data = (char*)malloc(b.capacity);
if (!b.data) {
curl_easy_cleanup(curl);
return NOSTR_ERROR_MEMORY_FAILED;
}
b.data[0] = '\0';
nostr_secp256k1_pubkey pub;
if (nostr_secp256k1_ec_pubkey_parse(&pub, candidate, sizeof(candidate))) {
nostr_bytes_to_hex(candidate, sizeof(candidate), out_Y_hex);
return NOSTR_SUCCESS;
}
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &b);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
CURLcode rc = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_easy_cleanup(curl);
if (rc != CURLE_OK || status < 200 || status >= 300) {
free(b.data);
return NOSTR_ERROR_CASHU_HTTP_FAILED;
}
cJSON* json = cJSON_Parse(b.data);
free(b.data);
if (!json) return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
*out_json = json;
return NOSTR_SUCCESS;
}
static int get_active_keyset_id(const char* mint_url, char out_id[65]) {
if (!mint_url || !out_id) return NOSTR_ERROR_INVALID_INPUT;
out_id[0] = '\0';
char url[1024];
snprintf(url, sizeof(url), "%s/v1/keysets", mint_url);
cJSON* root = NULL;
int rc = http_get_json(url, &root);
if (rc != NOSTR_SUCCESS) return rc;
cJSON* arr = cJSON_GetObjectItem(root, "keysets");
if (!arr || !cJSON_IsArray(arr)) {
cJSON_Delete(root);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
cJSON* selected = NULL;
int n = cJSON_GetArraySize(arr);
for (int i = 0; i < n; i++) {
cJSON* it = cJSON_GetArrayItem(arr, i);
if (!it || !cJSON_IsObject(it)) continue;
cJSON* active = cJSON_GetObjectItem(it, "active");
if (active && cJSON_IsBool(active) && cJSON_IsTrue(active)) {
selected = it;
break;
candidate[0] = 0x03;
if (nostr_secp256k1_ec_pubkey_parse(&pub, candidate, sizeof(candidate))) {
nostr_bytes_to_hex(candidate, sizeof(candidate), out_Y_hex);
return NOSTR_SUCCESS;
}
}
if (!selected && n > 0) {
selected = cJSON_GetArrayItem(arr, 0);
}
if (!selected) {
cJSON_Delete(root);
return NOSTR_ERROR_CASHU_INVALID_KEYSET;
}
cJSON* id = cJSON_GetObjectItem(selected, "id");
const char* id_s = (id && cJSON_IsString(id)) ? cJSON_GetStringValue(id) : NULL;
if (!id_s || id_s[0] == '\0') {
cJSON_Delete(root);
return NOSTR_ERROR_CASHU_INVALID_KEYSET;
}
snprintf(out_id, 65, "%s", id_s);
cJSON_Delete(root);
return NOSTR_SUCCESS;
return NOSTR_ERROR_CRYPTO_FAILED;
}
static int base64_to_base64url_no_pad(const unsigned char* in, size_t in_len, char* out, size_t out_size) {
@@ -243,15 +171,33 @@ static void test_live_mint_and_receive(const char* mint_url) {
return;
}
char keyset_id[65];
memset(keyset_id, 0, sizeof(keyset_id));
rc = get_active_keyset_id(mint_url, keyset_id);
TEST_ASSERT(rc == NOSTR_SUCCESS, "fetch active keyset id");
cashu_keyset_t active_keyset;
memset(&active_keyset, 0, sizeof(active_keyset));
rc = cashu_mint_get_active_keyset(&info, "sat", &active_keyset);
TEST_ASSERT(rc == NOSTR_SUCCESS, "select active sat keyset");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_keyset_keys_t keyset_keys;
memset(&keyset_keys, 0, sizeof(keyset_keys));
rc = cashu_mint_get_keys(mint_url, active_keyset.id, &keyset_keys, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "fetch per-amount keyset keys");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
const char* amount_pubkey = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, 1, &amount_pubkey);
TEST_ASSERT(rc == NOSTR_SUCCESS && amount_pubkey != NULL, "find pubkey for amount=1");
if (rc != NOSTR_SUCCESS || !amount_pubkey) {
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
cashu_mint_quote_t quote;
memset(&quote, 0, sizeof(quote));
rc = cashu_mint_request_mint_quote(mint_url, 1, "sat", &quote, 15);
@@ -286,14 +232,15 @@ static void test_live_mint_and_receive(const char* mint_url) {
cashu_blinded_output_t output;
memset(&output, 0, sizeof(output));
size_t keyset_len = strlen(keyset_id);
size_t keyset_len = strlen(active_keyset.id);
TEST_ASSERT(keyset_len < sizeof(output.id), "keyset id fits library struct");
if (keyset_len >= sizeof(output.id)) {
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
memcpy(output.id, keyset_id, keyset_len + 1);
memcpy(output.id, active_keyset.id, keyset_len + 1);
output.amount = 1;
snprintf(output.B_, sizeof(output.B_), "%s", B_hex);
@@ -308,6 +255,7 @@ static void test_live_mint_and_receive(const char* mint_url) {
rc = cashu_build_mint_tokens_request(&mint_req, &mint_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && mint_body != NULL, "build mint tokens request");
if (rc != NOSTR_SUCCESS || !mint_body) {
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
@@ -318,6 +266,7 @@ static void test_live_mint_and_receive(const char* mint_url) {
TEST_ASSERT(rc == NOSTR_SUCCESS && mint_resp_json != NULL, "mint tokens call");
if (rc != NOSTR_SUCCESS || !mint_resp_json) {
cJSON_Delete(mint_resp_json);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
@@ -330,24 +279,27 @@ static void test_live_mint_and_receive(const char* mint_url) {
TEST_ASSERT(mint_resp.signature_count >= 1, "mint returned signatures");
if (rc != NOSTR_SUCCESS || mint_resp.signature_count < 1) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
char C_hex[67];
rc = cashu_unblind_signature(mint_resp.signatures[0].C_, info.pubkey, r, C_hex);
TEST_ASSERT(rc == NOSTR_SUCCESS, "unblind signature");
rc = cashu_unblind_signature(mint_resp.signatures[0].C_, amount_pubkey, r, C_hex);
TEST_ASSERT(rc == NOSTR_SUCCESS, "unblind signature with per-amount key");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
char token_string[10000];
rc = build_cashuA_token_string(mint_url, keyset_id, 1, secret, C_hex, token_string, sizeof(token_string));
rc = build_cashuA_token_string(mint_url, active_keyset.id, 1, secret, C_hex, token_string, sizeof(token_string));
TEST_ASSERT(rc == NOSTR_SUCCESS, "build cashuA token string");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
@@ -359,12 +311,42 @@ static void test_live_mint_and_receive(const char* mint_url) {
TEST_ASSERT(decoded.mint_url != NULL && strcmp(decoded.mint_url, mint_url) == 0, "decoded mint url matches");
TEST_ASSERT(decoded.proof_count == 1, "decoded proof count");
TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].amount == 1, "decoded proof amount");
TEST_ASSERT(decoded.proofs != NULL && strcmp(decoded.proofs[0].id, keyset_id) == 0, "decoded keyset id");
TEST_ASSERT(decoded.proofs != NULL && strcmp(decoded.proofs[0].id, active_keyset.id) == 0, "decoded keyset id");
TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].secret != NULL && strcmp(decoded.proofs[0].secret, secret) == 0,
"decoded secret matches");
char Y_hex[67];
rc = secret_to_Y_hex(secret, Y_hex);
TEST_ASSERT(rc == NOSTR_SUCCESS, "derive Y from secret for checkstate");
if (rc == NOSTR_SUCCESS) {
const char* Ys[1] = {Y_hex};
cJSON* check_req = NULL;
rc = cashu_build_checkstate_request(Ys, 1, &check_req);
TEST_ASSERT(rc == NOSTR_SUCCESS && check_req != NULL, "build checkstate request");
if (rc == NOSTR_SUCCESS && check_req != NULL) {
cJSON* check_resp_json = NULL;
rc = cashu_mint_check_proofs_state(mint_url, check_req, &check_resp_json, 15);
cJSON_Delete(check_req);
TEST_ASSERT(rc == NOSTR_SUCCESS && check_resp_json != NULL, "checkstate call");
if (rc == NOSTR_SUCCESS && check_resp_json != NULL) {
cashu_checkstate_response_t state;
memset(&state, 0, sizeof(state));
rc = cashu_parse_checkstate_response(check_resp_json, &state);
cJSON_Delete(check_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse checkstate response");
TEST_ASSERT(state.state_count >= 1, "checkstate returned at least one state");
TEST_ASSERT(state.state_count >= 1 && strcmp(state.states[0].state, "UNSPENT") == 0,
"mint reports proof as UNSPENT");
cashu_mint_free_checkstate_response(&state);
}
}
}
cashu_free_decoded_token(&decoded);
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
}