diff --git a/VERSION b/VERSION index 416bfb0a..50c76ef8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.9 +0.5.10 diff --git a/nostr_core/cashu_mint.c b/nostr_core/cashu_mint.c index d37a1c4f..2aa4a169 100644 --- a/nostr_core/cashu_mint.c +++ b/nostr_core/cashu_mint.c @@ -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; } diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index 91d42793..4c693699 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.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 diff --git a/tests/nip60_live_receive_test b/tests/nip60_live_receive_test index c539e694..b3018bf3 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 418257f7..5387deae 100644 --- a/tests/nip60_live_receive_test.c +++ b/tests/nip60_live_receive_test.c @@ -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("e, 0, sizeof(quote)); - rc = cashu_mint_request_mint_quote(mint_url, 1, "sat", "e, 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("e_check, 0, sizeof(quote_check)); - rc = cashu_mint_check_mint_quote(mint_url, quote.quote_id, "e_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");