diff --git a/VERSION b/VERSION index d1d899fa..b49b2533 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.5 +0.5.6 diff --git a/nostr_core/cashu_mint.c b/nostr_core/cashu_mint.c index 5de7178a..00a5b308 100644 --- a/nostr_core/cashu_mint.c +++ b/nostr_core/cashu_mint.c @@ -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, diff --git a/nostr_core/cashu_mint.h b/nostr_core/cashu_mint.h index 46ee8151..39d14782 100644 --- a/nostr_core/cashu_mint.h +++ b/nostr_core/cashu_mint.h @@ -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, diff --git a/nostr_core/nip060.h b/nostr_core/nip060.h index c0c4eb6f..1a00acee 100644 --- a/nostr_core/nip060.h +++ b/nostr_core/nip060.h @@ -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 diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index 8cb9665d..24004b3c 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.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 diff --git a/tests/nip60_live_receive_test b/tests/nip60_live_receive_test index 86c9e445..1795ac75 100755 Binary files a/tests/nip60_live_receive_test and b/tests/nip60_live_receive_test differ diff --git a/tests/nip60_live_receive_test.c b/tests/nip60_live_receive_test.c index b8bad91e..02d4b3b7 100644 --- a/tests/nip60_live_receive_test.c +++ b/tests/nip60_live_receive_test.c @@ -9,9 +9,15 @@ #include #include #include -#include #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("e, 0, sizeof(quote)); rc = cashu_mint_request_mint_quote(mint_url, 1, "sat", "e, 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); }