Fix NUT-00 hash_to_curve domain separator and expand live NIP-60 swap roundtrip diagnostics/tests

This commit is contained in:
2026-03-20 19:18:25 -04:00
parent 303013a518
commit fb3c9dbc76
5 changed files with 664 additions and 197 deletions
+1 -1
View File
@@ -1 +1 @@
0.5.9
0.5.10
+23 -9
View File
@@ -1791,21 +1791,35 @@ void cashu_free_decoded_token(cashu_decoded_token_t* token) {
static int cashu_hash_to_curve_pubkey(const char* secret, nostr_secp256k1_pubkey* out_y) {
if (!secret || !out_y) return NOSTR_ERROR_INVALID_INPUT;
unsigned char digest[32] = {0};
if (nostr_sha256((const unsigned char*)secret, strlen(secret), digest) != 0) {
static const unsigned char domain_separator[] = "Secp256k1_HashToCurve_Cashu_";
size_t secret_len = strlen(secret);
size_t domain_len = sizeof(domain_separator) - 1;
size_t preimage_len = domain_len + secret_len;
unsigned char* preimage = (unsigned char*)malloc(preimage_len);
if (!preimage) {
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(preimage, domain_separator, domain_len);
memcpy(preimage + domain_len, (const unsigned char*)secret, secret_len);
unsigned char msg_hash[32] = {0};
if (nostr_sha256(preimage, preimage_len, msg_hash) != 0) {
free(preimage);
return NOSTR_ERROR_CRYPTO_FAILED;
}
free(preimage);
unsigned char candidate[33] = {0};
for (int counter = 0; counter < 65536; counter++) {
unsigned char h[32] = {0};
unsigned char data[37] = {0};
data[0] = 0x02;
memcpy(data + 1, digest, 32);
data[33] = (unsigned char)(counter & 0xff);
data[34] = (unsigned char)((counter >> 8) & 0xff);
data[35] = (unsigned char)((counter >> 16) & 0xff);
data[36] = (unsigned char)((counter >> 24) & 0xff);
unsigned char data[36] = {0};
memcpy(data, msg_hash, 32);
data[32] = (unsigned char)(counter & 0xff);
data[33] = (unsigned char)((counter >> 8) & 0xff);
data[34] = (unsigned char)((counter >> 16) & 0xff);
data[35] = (unsigned char)((counter >> 24) & 0xff);
if (nostr_sha256(data, sizeof(data), h) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
+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.9"
#define VERSION "v0.5.10"
#define VERSION_MAJOR 0
#define VERSION_MINOR 5
#define VERSION_PATCH 9
#define VERSION_PATCH 10
/*
* NOSTR Core Library - Complete API Reference
Binary file not shown.
+638 -185
View File
@@ -18,6 +18,20 @@ typedef struct {
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey* pubkey, const unsigned char* input, size_t inputlen);
static void debug_print_hex_bytes(const char* label, const unsigned char* data, size_t len) {
if (!label || !data) return;
char* hex = (char*)malloc(len * 2 + 1);
if (!hex) return;
nostr_bytes_to_hex(data, len, hex);
printf("[DBG] %s: %s\n", label, hex);
free(hex);
}
static int hex_to_32(const char* hex, unsigned char out[32]) {
if (!hex || !out || strlen(hex) != 64) return NOSTR_ERROR_INVALID_INPUT;
return (nostr_hex_to_bytes(hex, out, 32) == 0) ? NOSTR_SUCCESS : NOSTR_ERROR_INVALID_INPUT;
}
static int tests_run = 0;
static int tests_passed = 0;
@@ -30,21 +44,33 @@ static int tests_passed = 0;
static int secret_to_Y_hex(const char* secret, char out_Y_hex[67]) {
if (!secret || !out_Y_hex) return NOSTR_ERROR_INVALID_INPUT;
unsigned char digest[32] = {0};
if (nostr_sha256((const unsigned char*)secret, strlen(secret), digest) != 0) {
static const unsigned char domain_separator[] = "Secp256k1_HashToCurve_Cashu_";
size_t secret_len = strlen(secret);
size_t domain_len = sizeof(domain_separator) - 1;
size_t preimage_len = domain_len + secret_len;
unsigned char* preimage = (unsigned char*)malloc(preimage_len);
if (!preimage) return NOSTR_ERROR_MEMORY_FAILED;
memcpy(preimage, domain_separator, domain_len);
memcpy(preimage + domain_len, (const unsigned char*)secret, secret_len);
unsigned char msg_hash[32] = {0};
if (nostr_sha256(preimage, preimage_len, msg_hash) != 0) {
free(preimage);
return NOSTR_ERROR_CRYPTO_FAILED;
}
free(preimage);
unsigned char candidate[33] = {0};
for (int counter = 0; counter < 65536; counter++) {
unsigned char h[32] = {0};
unsigned char data[37] = {0};
data[0] = 0x02;
memcpy(data + 1, digest, 32);
data[33] = (unsigned char)(counter & 0xff);
data[34] = (unsigned char)((counter >> 8) & 0xff);
data[35] = (unsigned char)((counter >> 16) & 0xff);
data[36] = (unsigned char)((counter >> 24) & 0xff);
unsigned char data[36] = {0};
memcpy(data, msg_hash, 32);
data[32] = (unsigned char)(counter & 0xff);
data[33] = (unsigned char)((counter >> 8) & 0xff);
data[34] = (unsigned char)((counter >> 16) & 0xff);
data[35] = (unsigned char)((counter >> 24) & 0xff);
if (nostr_sha256(data, sizeof(data), h) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
@@ -63,6 +89,79 @@ static int secret_to_Y_hex(const char* secret, char out_Y_hex[67]) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
static int hash_to_curve_bytes_Y_hex(const unsigned char* msg, size_t msg_len, char out_Y_hex[67]) {
if (!msg || !out_Y_hex) return NOSTR_ERROR_INVALID_INPUT;
static const unsigned char domain_separator[] = "Secp256k1_HashToCurve_Cashu_";
size_t domain_len = sizeof(domain_separator) - 1;
size_t preimage_len = domain_len + msg_len;
unsigned char* preimage = (unsigned char*)malloc(preimage_len);
if (!preimage) return NOSTR_ERROR_MEMORY_FAILED;
memcpy(preimage, domain_separator, domain_len);
memcpy(preimage + domain_len, msg, msg_len);
unsigned char msg_hash[32] = {0};
if (nostr_sha256(preimage, preimage_len, msg_hash) != 0) {
free(preimage);
return NOSTR_ERROR_CRYPTO_FAILED;
}
free(preimage);
unsigned char candidate[33] = {0};
for (int counter = 0; counter < 65536; counter++) {
unsigned char h[32] = {0};
unsigned char data[36] = {0};
memcpy(data, msg_hash, 32);
data[32] = (unsigned char)(counter & 0xff);
data[33] = (unsigned char)((counter >> 8) & 0xff);
data[34] = (unsigned char)((counter >> 16) & 0xff);
data[35] = (unsigned char)((counter >> 24) & 0xff);
if (nostr_sha256(data, sizeof(data), h) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
candidate[0] = 0x02;
memcpy(candidate + 1, h, 32);
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;
}
}
return NOSTR_ERROR_CRYPTO_FAILED;
}
static void test_hash_to_curve_vectors(void) {
unsigned char m0[32] = {0};
unsigned char m1[32] = {0};
unsigned char m2[32] = {0};
m1[31] = 0x01;
m2[31] = 0x02;
char y0[67] = {0};
char y1[67] = {0};
char y2[67] = {0};
int rc = hash_to_curve_bytes_Y_hex(m0, sizeof(m0), y0);
TEST_ASSERT(rc == NOSTR_SUCCESS && strcmp(y0, "024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725") == 0,
"hash_to_curve vector #0");
printf("[DBG] vector#0 Y=%s\n", y0);
rc = hash_to_curve_bytes_Y_hex(m1, sizeof(m1), y1);
TEST_ASSERT(rc == NOSTR_SUCCESS && strcmp(y1, "022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf") == 0,
"hash_to_curve vector #1");
printf("[DBG] vector#1 Y=%s\n", y1);
rc = hash_to_curve_bytes_Y_hex(m2, sizeof(m2), y2);
TEST_ASSERT(rc == NOSTR_SUCCESS && strcmp(y2, "026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f") == 0,
"hash_to_curve vector #2");
printf("[DBG] vector#2 Y=%s\n", y2);
}
static int base64_to_base64url_no_pad(const unsigned char* in, size_t in_len, char* out, size_t out_size) {
if (!in || !out || out_size == 0) return NOSTR_ERROR_INVALID_INPUT;
@@ -151,234 +250,587 @@ static int build_cashuA_token_string(const char* mint_url,
return NOSTR_SUCCESS;
}
static const char* find_state_for_y(const cashu_checkstate_response_t* state, const char* y_hex) {
if (!state || !y_hex) return NULL;
for (int i = 0; i < state->state_count; i++) {
if (strcmp(state->states[i].Y, y_hex) == 0) {
return state->states[i].state;
}
}
return NULL;
}
static int run_checkstate(const char* mint_url,
const char** Ys,
int y_count,
cashu_checkstate_response_t* out_state) {
if (!mint_url || !Ys || y_count <= 0 || !out_state) return NOSTR_ERROR_INVALID_INPUT;
memset(out_state, 0, sizeof(*out_state));
cJSON* req = NULL;
int rc = cashu_build_checkstate_request(Ys, y_count, &req);
if (rc != NOSTR_SUCCESS || !req) {
cJSON_Delete(req);
return rc != NOSTR_SUCCESS ? rc : NOSTR_ERROR_MEMORY_FAILED;
}
cJSON* resp_json = NULL;
rc = cashu_mint_check_proofs_state(mint_url, req, &resp_json, 15);
cJSON_Delete(req);
if (rc != NOSTR_SUCCESS || !resp_json) {
cJSON_Delete(resp_json);
return rc != NOSTR_SUCCESS ? rc : NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
rc = cashu_parse_checkstate_response(resp_json, out_state);
cJSON_Delete(resp_json);
return rc;
}
static void test_live_mint_and_receive(const char* mint_url) {
printf("\n=== test_live_mint_and_receive: %s ===\n", mint_url);
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
cashu_keyset_keys_t keyset_keys;
memset(&keyset_keys, 0, sizeof(keyset_keys));
int rc = cashu_mint_get_info(mint_url, &info, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "get mint info");
if (rc != NOSTR_SUCCESS) return;
if (rc != NOSTR_SUCCESS) goto cleanup;
TEST_ASSERT(info.pubkey != NULL && strlen(info.pubkey) == 66, "mint pubkey available");
if (!info.pubkey || strlen(info.pubkey) != 66) {
cashu_mint_free_info(&info);
return;
}
if (!info.pubkey || strlen(info.pubkey) != 66) goto cleanup;
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;
}
if (rc != NOSTR_SUCCESS) goto cleanup;
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) goto cleanup;
const char* pk1 = NULL;
const char* pk2 = NULL;
const char* pk4 = NULL;
const char* pk8 = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, 1, &pk1);
TEST_ASSERT(rc == NOSTR_SUCCESS && pk1 != NULL, "find pubkey for amount=1");
if (rc != NOSTR_SUCCESS || !pk1) goto cleanup;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, 2, &pk2);
TEST_ASSERT(rc == NOSTR_SUCCESS && pk2 != NULL, "find pubkey for amount=2");
if (rc != NOSTR_SUCCESS || !pk2) goto cleanup;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, 4, &pk4);
TEST_ASSERT(rc == NOSTR_SUCCESS && pk4 != NULL, "find pubkey for amount=4");
if (rc != NOSTR_SUCCESS || !pk4) goto cleanup;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, 8, &pk8);
TEST_ASSERT(rc == NOSTR_SUCCESS && pk8 != NULL, "find pubkey for amount=8");
if (rc != NOSTR_SUCCESS || !pk8) goto cleanup;
/* Mint 1 sat baseline proof */
cashu_mint_quote_t q1;
memset(&q1, 0, sizeof(q1));
rc = cashu_mint_request_mint_quote(mint_url, 1, "sat", &q1, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "request mint quote (1 sat)");
if (rc != NOSTR_SUCCESS) goto cleanup;
cashu_mint_quote_t q1_check;
memset(&q1_check, 0, sizeof(q1_check));
rc = cashu_mint_check_mint_quote(mint_url, q1.quote_id, &q1_check, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "check mint quote (1 sat)");
TEST_ASSERT(q1_check.paid == 1, "mint quote paid (1 sat)");
if (rc != NOSTR_SUCCESS || q1_check.paid != 1) goto cleanup;
char secret1[160];
snprintf(secret1, sizeof(secret1), "nostr-step1-mint-%ld", (long)time(NULL));
char B1[67], C1[67], Y1[67];
unsigned char r1[32];
rc = cashu_blind_message(secret1, info.pubkey, B1, r1);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blind message for initial mint");
if (rc != NOSTR_SUCCESS) goto cleanup;
printf("[DBG] mint pubkey(amount=1): %s\n", pk1);
printf("[DBG] secret1: %s\n", secret1);
debug_print_hex_bytes("r1", r1, 32);
printf("[DBG] B1: %s\n", B1);
cashu_blinded_output_t m1_out;
memset(&m1_out, 0, sizeof(m1_out));
snprintf(m1_out.id, sizeof(m1_out.id), "%s", active_keyset.id);
m1_out.amount = 1;
snprintf(m1_out.B_, sizeof(m1_out.B_), "%s", B1);
cashu_mint_tokens_request_t m1_req;
memset(&m1_req, 0, sizeof(m1_req));
snprintf(m1_req.quote_id, sizeof(m1_req.quote_id), "%s", q1.quote_id);
snprintf(m1_req.unit, sizeof(m1_req.unit), "sat");
m1_req.outputs = &m1_out;
m1_req.output_count = 1;
cJSON* m1_body = NULL;
rc = cashu_build_mint_tokens_request(&m1_req, &m1_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && m1_body != NULL, "build mint request (1 sat)");
if (rc != NOSTR_SUCCESS || !m1_body) goto cleanup;
cJSON* m1_resp_json = NULL;
rc = cashu_mint_mint_tokens(mint_url, m1_body, &m1_resp_json, 15);
cJSON_Delete(m1_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && m1_resp_json != NULL, "mint tokens call (1 sat)");
if (rc != NOSTR_SUCCESS || !m1_resp_json) {
cJSON_Delete(m1_resp_json);
goto cleanup;
}
cashu_mint_tokens_response_t m1_resp;
memset(&m1_resp, 0, sizeof(m1_resp));
rc = cashu_parse_mint_tokens_response(m1_resp_json, &m1_resp);
cJSON_Delete(m1_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS && m1_resp.signature_count >= 1, "parse minted signatures (1 sat)");
if (rc != NOSTR_SUCCESS || m1_resp.signature_count < 1) {
cashu_mint_free_mint_tokens_response(&m1_resp);
goto cleanup;
}
char m1_sig_id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
memset(m1_sig_id, 0, sizeof(m1_sig_id));
snprintf(m1_sig_id, sizeof(m1_sig_id), "%s", m1_resp.signatures[0].id);
printf("[DBG] mint C_ (1 sat): %s\n", m1_resp.signatures[0].C_);
rc = cashu_unblind_signature(m1_resp.signatures[0].C_, pk1, r1, C1);
TEST_ASSERT(rc == NOSTR_SUCCESS, "unblind minted signature (1 sat)");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
cashu_mint_free_mint_tokens_response(&m1_resp);
goto cleanup;
}
printf("[DBG] mint C (1 sat): %s\n", C1);
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;
}
rc = secret_to_Y_hex(secret1, Y1);
TEST_ASSERT(rc == NOSTR_SUCCESS, "derive Y for minted proof (1 sat)");
printf("[DBG] Y1: %s\n", Y1);
cashu_mint_free_mint_tokens_response(&m1_resp);
if (rc != NOSTR_SUCCESS) goto cleanup;
cashu_mint_quote_t quote;
memset(&quote, 0, sizeof(quote));
rc = cashu_mint_request_mint_quote(mint_url, 1, "sat", &quote, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "request mint quote");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_mint_quote_t quote_check;
memset(&quote_check, 0, sizeof(quote_check));
rc = cashu_mint_check_mint_quote(mint_url, quote.quote_id, &quote_check, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "check mint quote");
TEST_ASSERT(quote_check.paid == 1, "mint quote paid");
if (rc != NOSTR_SUCCESS || quote_check.paid != 1) {
cashu_mint_free_info(&info);
return;
}
char secret[160];
snprintf(secret, sizeof(secret), "nostr-live-receive-test-%ld-%s", (long)time(NULL), mint_url);
char B_hex[67];
unsigned char r[32];
rc = cashu_blind_message(secret, info.pubkey, B_hex, r);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blind message");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_blinded_output_t output;
memset(&output, 0, sizeof(output));
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, active_keyset.id, keyset_len + 1);
output.amount = 1;
snprintf(output.B_, sizeof(output.B_), "%s", B_hex);
cashu_mint_tokens_request_t mint_req;
memset(&mint_req, 0, sizeof(mint_req));
snprintf(mint_req.quote_id, sizeof(mint_req.quote_id), "%s", quote.quote_id);
snprintf(mint_req.unit, sizeof(mint_req.unit), "sat");
mint_req.outputs = &output;
mint_req.output_count = 1;
cJSON* mint_body = NULL;
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;
}
cJSON* mint_resp_json = NULL;
rc = cashu_mint_mint_tokens(mint_url, mint_body, &mint_resp_json, 15);
cJSON_Delete(mint_body);
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;
}
cashu_mint_tokens_response_t mint_resp;
memset(&mint_resp, 0, sizeof(mint_resp));
rc = cashu_parse_mint_tokens_response(mint_resp_json, &mint_resp);
cJSON_Delete(mint_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse mint tokens response");
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_, 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;
}
nostr_cashu_proof_t send_proof;
memset(&send_proof, 0, sizeof(send_proof));
snprintf(send_proof.id, sizeof(send_proof.id), "%s", active_keyset.id);
send_proof.amount = 1;
send_proof.secret = secret;
send_proof.C = C_hex;
/* Existing outbound token generation coverage */
nostr_cashu_proof_t minted_proof;
memset(&minted_proof, 0, sizeof(minted_proof));
snprintf(minted_proof.id, sizeof(minted_proof.id), "%s", m1_sig_id);
minted_proof.amount = 1;
minted_proof.secret = secret1;
minted_proof.C = C1;
cashu_decoded_token_t token_for_encode;
memset(&token_for_encode, 0, sizeof(token_for_encode));
token_for_encode.mint_url = (char*)mint_url;
token_for_encode.proofs = &send_proof;
token_for_encode.proofs = &minted_proof;
token_for_encode.proof_count = 1;
char* token_a = NULL;
rc = cashu_encode_token(&token_for_encode, CASHU_TOKEN_FORMAT_A, &token_a);
TEST_ASSERT(rc == NOSTR_SUCCESS && token_a != NULL, "encode cashuA token");
if (rc != NOSTR_SUCCESS || !token_a) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
if (rc != NOSTR_SUCCESS || !token_a) goto cleanup;
char* token_b = NULL;
rc = cashu_encode_token(&token_for_encode, CASHU_TOKEN_FORMAT_B, &token_b);
TEST_ASSERT(rc == NOSTR_SUCCESS && token_b != NULL, "encode cashuB token");
if (rc != NOSTR_SUCCESS || !token_b) {
free(token_a);
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
goto cleanup;
}
cashu_decoded_token_t decoded;
memset(&decoded, 0, sizeof(decoded));
rc = cashu_decode_token(token_a, &decoded);
cashu_decoded_token_t decoded_a;
memset(&decoded_a, 0, sizeof(decoded_a));
rc = cashu_decode_token(token_a, &decoded_a);
TEST_ASSERT(rc == NOSTR_SUCCESS, "decode cashuA token");
TEST_ASSERT(decoded.mint_url != NULL && strcmp(decoded.mint_url, mint_url) == 0, "cashuA decoded mint url matches");
TEST_ASSERT(decoded.proof_count == 1, "cashuA decoded proof count");
TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].amount == 1, "cashuA decoded proof amount");
TEST_ASSERT(decoded.proofs != NULL && strcmp(decoded.proofs[0].id, active_keyset.id) == 0, "cashuA decoded keyset id");
TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].secret != NULL && strcmp(decoded.proofs[0].secret, secret) == 0,
"cashuA decoded secret matches");
TEST_ASSERT(decoded_a.proof_count == 1 && decoded_a.proofs[0].amount == 1, "cashuA decoded amount");
cashu_decoded_token_t decoded_b;
memset(&decoded_b, 0, sizeof(decoded_b));
rc = cashu_decode_token(token_b, &decoded_b);
TEST_ASSERT(rc == NOSTR_SUCCESS, "decode cashuB token");
TEST_ASSERT(decoded_b.mint_url != NULL && strcmp(decoded_b.mint_url, mint_url) == 0, "cashuB decoded mint url matches");
TEST_ASSERT(decoded_b.proof_count == 1, "cashuB decoded proof count");
TEST_ASSERT(decoded_b.proofs != NULL && decoded_b.proofs[0].amount == 1, "cashuB decoded proof amount");
TEST_ASSERT(decoded_b.proofs != NULL && strcmp(decoded_b.proofs[0].id, active_keyset.id) == 0, "cashuB decoded keyset id");
TEST_ASSERT(decoded_b.proofs != NULL && decoded_b.proofs[0].secret != NULL && strcmp(decoded_b.proofs[0].secret, secret) == 0,
"cashuB decoded secret matches");
TEST_ASSERT(decoded_b.proof_count == 1 && decoded_b.proofs[0].amount == 1, "cashuB decoded amount");
char Y_hex[67];
rc = secret_to_Y_hex(secret, Y_hex);
TEST_ASSERT(rc == NOSTR_SUCCESS, "derive Y from secret for checkstate");
cashu_free_decoded_token(&decoded_a);
cashu_free_decoded_token(&decoded_b);
free(token_a);
free(token_b);
/* Step 1: swap freshly minted 1 sat proof and verify state transition */
char secret1_swap[160], B1_swap[67], C1_swap[67], Y1_swap[67];
unsigned char r1_swap[32];
snprintf(secret1_swap, sizeof(secret1_swap), "nostr-step1-swap-%ld", (long)time(NULL));
rc = cashu_blind_message(secret1_swap, info.pubkey, B1_swap, r1_swap);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blind message for step1 swap output");
if (rc != NOSTR_SUCCESS) goto cleanup;
printf("[DBG] step1 input proof id=%s amount=%llu\n", minted_proof.id, (unsigned long long)minted_proof.amount);
printf("[DBG] step1 input secret=%s\n", minted_proof.secret);
printf("[DBG] step1 input C=%s\n", minted_proof.C);
printf("[DBG] step1 output B_=%s\n", B1_swap);
debug_print_hex_bytes("r1_swap", r1_swap, 32);
cashu_blinded_output_t s1_out;
memset(&s1_out, 0, sizeof(s1_out));
snprintf(s1_out.id, sizeof(s1_out.id), "%s", minted_proof.id);
s1_out.amount = 1;
snprintf(s1_out.B_, sizeof(s1_out.B_), "%s", B1_swap);
cashu_swap_request_t s1_req;
memset(&s1_req, 0, sizeof(s1_req));
s1_req.inputs = &minted_proof;
s1_req.input_count = 1;
s1_req.outputs = &s1_out;
s1_req.output_count = 1;
cJSON* s1_body = NULL;
rc = cashu_build_swap_request(&s1_req, &s1_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && s1_body != NULL, "build swap request (step1)");
if (rc != NOSTR_SUCCESS || !s1_body) goto cleanup;
char* s1_req_str = cJSON_PrintUnformatted(s1_body);
if (s1_req_str) {
printf("Step1 swap request: %s\n", s1_req_str);
free(s1_req_str);
}
cJSON* s1_resp_json = NULL;
rc = cashu_mint_swap(mint_url, s1_body, &s1_resp_json, 15);
cJSON_Delete(s1_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && s1_resp_json != NULL, "swap call (step1)");
if (rc != NOSTR_SUCCESS || !s1_resp_json) {
printf("Step1 swap rc=%d, response_json=%s\n", rc, s1_resp_json ? "yes" : "no");
if (s1_resp_json) {
char* s1_err = cJSON_PrintUnformatted(s1_resp_json);
if (s1_err) {
printf("Step1 swap error payload: %s\n", s1_err);
free(s1_err);
}
}
cJSON_Delete(s1_resp_json);
goto cleanup;
}
cashu_swap_response_t s1_resp;
memset(&s1_resp, 0, sizeof(s1_resp));
rc = cashu_parse_swap_response(s1_resp_json, &s1_resp);
cJSON_Delete(s1_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS && s1_resp.signature_count >= 1, "parse swap response (step1)");
if (rc != NOSTR_SUCCESS || s1_resp.signature_count < 1) {
cashu_mint_free_swap_response(&s1_resp);
goto cleanup;
}
printf("[DBG] step1 swap C_: %s\n", s1_resp.signatures[0].C_);
rc = cashu_unblind_signature(s1_resp.signatures[0].C_, pk1, r1_swap, C1_swap);
TEST_ASSERT(rc == NOSTR_SUCCESS, "unblind swap signature (step1)");
printf("[DBG] step1 swap C : %s\n", C1_swap);
cashu_mint_free_swap_response(&s1_resp);
if (rc != NOSTR_SUCCESS) goto cleanup;
rc = secret_to_Y_hex(secret1_swap, Y1_swap);
TEST_ASSERT(rc == NOSTR_SUCCESS, "derive Y for step1 swapped proof");
printf("[DBG] step1 Y(old)=%s\n", Y1);
printf("[DBG] step1 Y(new)=%s\n", Y1_swap);
if (rc != NOSTR_SUCCESS) goto cleanup;
const char* ys_step1[2] = {Y1, Y1_swap};
cashu_checkstate_response_t st1;
rc = run_checkstate(mint_url, ys_step1, 2, &st1);
TEST_ASSERT(rc == NOSTR_SUCCESS, "checkstate after step1 swap");
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");
const char* old_state = find_state_for_y(&st1, Y1);
const char* new_state = find_state_for_y(&st1, Y1_swap);
TEST_ASSERT(old_state != NULL && strcmp(old_state, "SPENT") == 0, "old proof marked SPENT after step1 swap");
TEST_ASSERT(new_state != NULL && strcmp(new_state, "UNSPENT") == 0, "new proof marked UNSPENT after step1 swap");
cashu_mint_free_checkstate_response(&st1);
}
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");
/* Step 2: mint 8 and swap into [2,4,2] */
cashu_mint_quote_t q8;
memset(&q8, 0, sizeof(q8));
rc = cashu_mint_request_mint_quote(mint_url, 8, "sat", &q8, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "request mint quote (8 sat)");
if (rc != NOSTR_SUCCESS) goto cleanup;
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_mint_quote_t q8_check;
memset(&q8_check, 0, sizeof(q8_check));
rc = cashu_mint_check_mint_quote(mint_url, q8.quote_id, &q8_check, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS && q8_check.paid == 1, "check mint quote paid (8 sat)");
if (rc != NOSTR_SUCCESS || q8_check.paid != 1) goto cleanup;
char secret8[160], B8[67], C8[67], Y8[67];
unsigned char r8[32];
snprintf(secret8, sizeof(secret8), "nostr-step2-mint8-%ld", (long)time(NULL));
rc = cashu_blind_message(secret8, info.pubkey, B8, r8);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blind message for mint 8");
if (rc != NOSTR_SUCCESS) goto cleanup;
cashu_blinded_output_t m8_out;
memset(&m8_out, 0, sizeof(m8_out));
snprintf(m8_out.id, sizeof(m8_out.id), "%s", active_keyset.id);
m8_out.amount = 8;
snprintf(m8_out.B_, sizeof(m8_out.B_), "%s", B8);
cashu_mint_tokens_request_t m8_req;
memset(&m8_req, 0, sizeof(m8_req));
snprintf(m8_req.quote_id, sizeof(m8_req.quote_id), "%s", q8.quote_id);
snprintf(m8_req.unit, sizeof(m8_req.unit), "sat");
m8_req.outputs = &m8_out;
m8_req.output_count = 1;
cJSON* m8_body = NULL;
rc = cashu_build_mint_tokens_request(&m8_req, &m8_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && m8_body != NULL, "build mint request (8 sat)");
if (rc != NOSTR_SUCCESS || !m8_body) goto cleanup;
cJSON* m8_resp_json = NULL;
rc = cashu_mint_mint_tokens(mint_url, m8_body, &m8_resp_json, 15);
cJSON_Delete(m8_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && m8_resp_json != NULL, "mint tokens call (8 sat)");
if (rc != NOSTR_SUCCESS || !m8_resp_json) {
cJSON_Delete(m8_resp_json);
goto cleanup;
}
cashu_mint_tokens_response_t m8_resp;
memset(&m8_resp, 0, sizeof(m8_resp));
rc = cashu_parse_mint_tokens_response(m8_resp_json, &m8_resp);
cJSON_Delete(m8_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS && m8_resp.signature_count >= 1, "parse minted signatures (8 sat)");
if (rc != NOSTR_SUCCESS || m8_resp.signature_count < 1) {
cashu_mint_free_mint_tokens_response(&m8_resp);
goto cleanup;
}
rc = cashu_unblind_signature(m8_resp.signatures[0].C_, pk8, r8, C8);
TEST_ASSERT(rc == NOSTR_SUCCESS, "unblind minted signature (8 sat)");
cashu_mint_free_mint_tokens_response(&m8_resp);
if (rc != NOSTR_SUCCESS) goto cleanup;
rc = secret_to_Y_hex(secret8, Y8);
TEST_ASSERT(rc == NOSTR_SUCCESS, "derive Y for minted proof (8 sat)");
if (rc != NOSTR_SUCCESS) goto cleanup;
uint64_t change_parts[8] = {0};
int change_count = 0;
rc = cashu_mint_plan_split_amounts(6, change_parts, 8, &change_count);
TEST_ASSERT(rc == NOSTR_SUCCESS && change_count >= 2, "plan split amounts for 6 sat change");
TEST_ASSERT(change_parts[0] + change_parts[1] == 6, "planned change sums to 6");
if (rc != NOSTR_SUCCESS || change_count < 2) goto cleanup;
uint64_t out_amts[3] = {2, change_parts[0], change_parts[1]};
char out_secrets[3][160];
char out_B[3][67];
unsigned char out_r[3][32];
cashu_blinded_output_t split_out[3];
memset(split_out, 0, sizeof(split_out));
for (int i = 0; i < 3; i++) {
snprintf(out_secrets[i], sizeof(out_secrets[i]), "nostr-step2-swap-%d-%ld", i, (long)time(NULL));
rc = cashu_blind_message(out_secrets[i], info.pubkey, out_B[i], out_r[i]);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blind message for step2 output");
if (rc != NOSTR_SUCCESS) goto cleanup;
snprintf(split_out[i].id, sizeof(split_out[i].id), "%s", active_keyset.id);
split_out[i].amount = out_amts[i];
snprintf(split_out[i].B_, sizeof(split_out[i].B_), "%s", out_B[i]);
}
nostr_cashu_proof_t in8;
memset(&in8, 0, sizeof(in8));
snprintf(in8.id, sizeof(in8.id), "%s", active_keyset.id);
in8.amount = 8;
in8.secret = secret8;
in8.C = C8;
cashu_swap_request_t s2_req;
memset(&s2_req, 0, sizeof(s2_req));
s2_req.inputs = &in8;
s2_req.input_count = 1;
s2_req.outputs = split_out;
s2_req.output_count = 3;
cJSON* s2_body = NULL;
rc = cashu_build_swap_request(&s2_req, &s2_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && s2_body != NULL, "build split swap request (step2)");
if (rc != NOSTR_SUCCESS || !s2_body) goto cleanup;
cJSON* s2_resp_json = NULL;
rc = cashu_mint_swap(mint_url, s2_body, &s2_resp_json, 15);
cJSON_Delete(s2_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && s2_resp_json != NULL, "split swap call (step2)");
if (rc != NOSTR_SUCCESS || !s2_resp_json) {
cJSON_Delete(s2_resp_json);
goto cleanup;
}
cashu_swap_response_t s2_resp;
memset(&s2_resp, 0, sizeof(s2_resp));
rc = cashu_parse_swap_response(s2_resp_json, &s2_resp);
cJSON_Delete(s2_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS && s2_resp.signature_count >= 3, "parse split swap signatures (step2)");
if (rc != NOSTR_SUCCESS || s2_resp.signature_count < 3) {
cashu_mint_free_swap_response(&s2_resp);
goto cleanup;
}
char split_C[3][67];
char split_Y[3][67];
for (int i = 0; i < 3; i++) {
const char* out_pk = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, out_amts[i], &out_pk);
TEST_ASSERT(rc == NOSTR_SUCCESS && out_pk != NULL, "find pubkey for split output amount");
if (rc != NOSTR_SUCCESS || !out_pk) {
cashu_mint_free_swap_response(&s2_resp);
goto cleanup;
}
rc = cashu_unblind_signature(s2_resp.signatures[i].C_, out_pk, out_r[i], split_C[i]);
TEST_ASSERT(rc == NOSTR_SUCCESS, "unblind split swap signature");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_swap_response(&s2_resp);
goto cleanup;
}
rc = secret_to_Y_hex(out_secrets[i], split_Y[i]);
TEST_ASSERT(rc == NOSTR_SUCCESS, "derive Y for split output proof");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_swap_response(&s2_resp);
goto cleanup;
}
}
cashu_mint_free_swap_response(&s2_resp);
const char* ys_step2[3] = {split_Y[0], split_Y[1], split_Y[2]};
cashu_checkstate_response_t st2;
rc = run_checkstate(mint_url, ys_step2, 3, &st2);
TEST_ASSERT(rc == NOSTR_SUCCESS, "checkstate for split outputs (step2)");
if (rc == NOSTR_SUCCESS) {
TEST_ASSERT(find_state_for_y(&st2, split_Y[0]) && strcmp(find_state_for_y(&st2, split_Y[0]), "UNSPENT") == 0,
"split output #1 is UNSPENT");
TEST_ASSERT(find_state_for_y(&st2, split_Y[1]) && strcmp(find_state_for_y(&st2, split_Y[1]), "UNSPENT") == 0,
"split output #2 is UNSPENT");
TEST_ASSERT(find_state_for_y(&st2, split_Y[2]) && strcmp(find_state_for_y(&st2, split_Y[2]), "UNSPENT") == 0,
"split output #3 is UNSPENT");
cashu_mint_free_checkstate_response(&st2);
}
/* Step 3: NIP-60 event round-trip then swap parsed proof */
const char* sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
unsigned char sk[32];
rc = (nostr_hex_to_bytes(sk_hex, sk, 32) == 0) ? NOSTR_SUCCESS : NOSTR_ERROR_INVALID_INPUT;
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse private key for NIP-60 roundtrip");
if (rc != NOSTR_SUCCESS) goto cleanup;
nostr_cashu_proof_t nip60_proof;
memset(&nip60_proof, 0, sizeof(nip60_proof));
snprintf(nip60_proof.id, sizeof(nip60_proof.id), "%s", active_keyset.id);
nip60_proof.amount = out_amts[0];
nip60_proof.secret = out_secrets[0];
nip60_proof.C = split_C[0];
nostr_nip60_token_data_t token_data;
memset(&token_data, 0, sizeof(token_data));
token_data.mint_url = (char*)mint_url;
token_data.proofs = &nip60_proof;
token_data.proof_count = 1;
cJSON* token_evt = nostr_nip60_create_token_event(&token_data, sk, 0);
TEST_ASSERT(token_evt != NULL, "create NIP-60 token event (step3)");
if (!token_evt) goto cleanup;
nostr_nip60_token_data_t parsed_token;
memset(&parsed_token, 0, sizeof(parsed_token));
rc = nostr_nip60_parse_token_event(token_evt, sk, &parsed_token);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse NIP-60 token event (step3)");
TEST_ASSERT(rc == NOSTR_SUCCESS && parsed_token.proof_count == 1, "parsed token has one proof");
if (rc != NOSTR_SUCCESS || parsed_token.proof_count != 1) {
cJSON_Delete(token_evt);
goto cleanup;
}
TEST_ASSERT(strcmp(parsed_token.proofs[0].secret, nip60_proof.secret) == 0, "parsed proof secret matches original");
TEST_ASSERT(strcmp(parsed_token.proofs[0].C, nip60_proof.C) == 0, "parsed proof C matches original");
char secret3[160], B3[67], C3[67], Y3[67];
unsigned char r3[32];
snprintf(secret3, sizeof(secret3), "nostr-step3-swap-%ld", (long)time(NULL));
rc = cashu_blind_message(secret3, info.pubkey, B3, r3);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blind message for step3 swap output");
if (rc != NOSTR_SUCCESS) {
nostr_nip60_free_token_data(&parsed_token);
cJSON_Delete(token_evt);
goto cleanup;
}
cashu_blinded_output_t s3_out;
memset(&s3_out, 0, sizeof(s3_out));
snprintf(s3_out.id, sizeof(s3_out.id), "%s", active_keyset.id);
s3_out.amount = parsed_token.proofs[0].amount;
snprintf(s3_out.B_, sizeof(s3_out.B_), "%s", B3);
cashu_swap_request_t s3_req;
memset(&s3_req, 0, sizeof(s3_req));
s3_req.inputs = parsed_token.proofs;
s3_req.input_count = 1;
s3_req.outputs = &s3_out;
s3_req.output_count = 1;
cJSON* s3_body = NULL;
rc = cashu_build_swap_request(&s3_req, &s3_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && s3_body != NULL, "build swap request from parsed NIP-60 proof");
if (rc != NOSTR_SUCCESS || !s3_body) {
nostr_nip60_free_token_data(&parsed_token);
cJSON_Delete(token_evt);
goto cleanup;
}
cJSON* s3_resp_json = NULL;
rc = cashu_mint_swap(mint_url, s3_body, &s3_resp_json, 15);
cJSON_Delete(s3_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && s3_resp_json != NULL, "swap call with parsed NIP-60 proof");
if (rc != NOSTR_SUCCESS || !s3_resp_json) {
cJSON_Delete(s3_resp_json);
nostr_nip60_free_token_data(&parsed_token);
cJSON_Delete(token_evt);
goto cleanup;
}
cashu_swap_response_t s3_resp;
memset(&s3_resp, 0, sizeof(s3_resp));
rc = cashu_parse_swap_response(s3_resp_json, &s3_resp);
cJSON_Delete(s3_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS && s3_resp.signature_count >= 1, "parse swap response (step3)");
if (rc == NOSTR_SUCCESS && s3_resp.signature_count >= 1) {
const char* pk_out = NULL;
int rc_pk = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, s3_out.amount, &pk_out);
TEST_ASSERT(rc_pk == NOSTR_SUCCESS && pk_out != NULL, "find pubkey for step3 output amount");
if (rc_pk == NOSTR_SUCCESS && pk_out) {
rc = cashu_unblind_signature(s3_resp.signatures[0].C_, pk_out, r3, C3);
TEST_ASSERT(rc == NOSTR_SUCCESS, "unblind swap signature (step3)");
if (rc == NOSTR_SUCCESS) {
rc = secret_to_Y_hex(secret3, Y3);
TEST_ASSERT(rc == NOSTR_SUCCESS, "derive Y for step3 swapped proof");
if (rc == NOSTR_SUCCESS) {
const char* ys_step3[1] = {Y3};
cashu_checkstate_response_t st3;
rc = run_checkstate(mint_url, ys_step3, 1, &st3);
TEST_ASSERT(rc == NOSTR_SUCCESS, "checkstate after step3 swap");
if (rc == NOSTR_SUCCESS) {
const char* s3_state = find_state_for_y(&st3, Y3);
TEST_ASSERT(s3_state != NULL && strcmp(s3_state, "UNSPENT") == 0,
"step3 swapped proof is UNSPENT");
cashu_mint_free_checkstate_response(&st3);
}
}
}
}
}
cashu_free_decoded_token(&decoded_b);
cashu_free_decoded_token(&decoded);
free(token_b);
free(token_a);
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_swap_response(&s3_resp);
nostr_nip60_free_token_data(&parsed_token);
cJSON_Delete(token_evt);
cleanup:
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
}
@@ -392,6 +844,7 @@ int main(void) {
return 1;
}
test_hash_to_curve_vectors();
test_live_mint_and_receive("https://nofee.testnut.cashu.space");
printf("\n=== Test Summary ===\n");