Compare commits

...
7 Commits
8 changed files with 1690 additions and 45 deletions
+1 -1
View File
@@ -1 +1 @@
0.5.3
0.5.7
+2 -2
View File
@@ -197,7 +197,7 @@ print_info "Auto-detecting needed NIPs from your source code..."
NEEDED_NIPS=""
if [ -n "$FORCE_NIPS" ]; then
if [ "$FORCE_NIPS" = "all" ]; then
NEEDED_NIPS="001 004 005 006 011 013 017 019 042 044 046 059 060 061"
NEEDED_NIPS="001 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
print_info "Forced: Building all available NIPs"
else
# Convert comma-separated list to space-separated with 3-digit format
@@ -216,7 +216,7 @@ else
# Check for nostr_core.h (includes everything)
if grep -q '#include[[:space:]]*["\<]nostr_core\.h["\>]' *.c 2>/dev/null; then
print_info "Found #include \"nostr_core.h\" - building all NIPs"
NEEDED_NIPS="001 004 005 006 011 013 019 042 044 046 059 060 061"
NEEDED_NIPS="001 004 005 006 011 013 019 021 042 044 046 059 060 061"
elif [ -n "$DETECTED" ]; then
NEEDED_NIPS="$DETECTED"
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
+1143 -38
View File
File diff suppressed because it is too large Load Diff
+63 -1
View File
@@ -17,11 +17,28 @@ extern "C" {
#define CASHU_API_VERSION "v1"
typedef struct {
char id[17];
char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
char unit[16];
int active;
} cashu_keyset_t;
typedef enum {
CASHU_TOKEN_FORMAT_A = 0,
CASHU_TOKEN_FORMAT_B = 1
} cashu_token_format_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;
@@ -106,12 +123,38 @@ typedef struct {
int state_count;
} cashu_checkstate_response_t;
typedef struct {
char* mint_url;
nostr_cashu_proof_t* proofs;
int proof_count;
} cashu_decoded_token_t;
void cashu_mint_set_ca_bundle(const char* ca_bundle_path);
int cashu_mint_get_info(const char* mint_url,
cashu_mint_info_t* info_out,
int timeout_seconds);
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,
@@ -203,6 +246,25 @@ int cashu_parse_checkstate_response(cJSON* response_body,
void cashu_mint_free_checkstate_response(cashu_checkstate_response_t* response);
int cashu_decode_token(const char* token_string,
cashu_decoded_token_t* token_out);
int cashu_encode_token(const cashu_decoded_token_t* token,
cashu_token_format_t format,
char** token_string_out);
void cashu_free_decoded_token(cashu_decoded_token_t* token);
int cashu_blind_message(const char* secret,
const char* mint_pubkey_hex,
char out_B_hex[67],
unsigned char out_r[32]);
int cashu_unblind_signature(const char* C_blinded_hex,
const char* mint_pubkey_hex,
const unsigned char r[32],
char out_C_hex[67]);
#ifdef __cplusplus
}
#endif
+71
View File
@@ -237,6 +237,77 @@ int nostr_secp256k1_ecdh(unsigned char *result, const nostr_secp256k1_pubkey *pu
return secp256k1_ecdh(g_ctx, result, &internal_pubkey, seckey, hashfp, data);
}
int nostr_secp256k1_ec_pubkey_tweak_mul(nostr_secp256k1_pubkey* pubkey, const unsigned char* tweak32) {
if (g_ctx == NULL || pubkey == NULL || tweak32 == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
if (!secp256k1_ec_pubkey_tweak_mul(g_ctx, &internal_pubkey, tweak32)) {
return 0;
}
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_ec_pubkey_negate(nostr_secp256k1_pubkey* pubkey) {
if (g_ctx == NULL || pubkey == NULL) {
return 0;
}
secp256k1_pubkey internal_pubkey;
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
if (!secp256k1_ec_pubkey_negate(g_ctx, &internal_pubkey)) {
return 0;
}
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_ec_pubkey_combine(nostr_secp256k1_pubkey* out,
const nostr_secp256k1_pubkey* const* ins,
size_t n) {
if (g_ctx == NULL || out == NULL || ins == NULL || n == 0) {
return 0;
}
secp256k1_pubkey* parsed = (secp256k1_pubkey*)calloc(n, sizeof(secp256k1_pubkey));
const secp256k1_pubkey** ptrs = (const secp256k1_pubkey**)calloc(n, sizeof(secp256k1_pubkey*));
if (!parsed || !ptrs) {
free(parsed);
free(ptrs);
return 0;
}
for (size_t i = 0; i < n; i++) {
if (!ins[i]) {
free(parsed);
free(ptrs);
return 0;
}
memcpy(&parsed[i], ins[i]->data, sizeof(secp256k1_pubkey));
ptrs[i] = &parsed[i];
}
secp256k1_pubkey combined;
int ok = secp256k1_ec_pubkey_combine(g_ctx, &combined, ptrs, n);
free(parsed);
free(ptrs);
if (!ok) {
return 0;
}
memcpy(out->data, &combined, sizeof(secp256k1_pubkey));
return 1;
}
int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len) {
if (buf == NULL || len == 0) {
return 0;
+1 -1
View File
@@ -22,7 +22,7 @@ extern "C" {
#define NOSTR_NIP60_HISTORY_KIND 7376
#define NOSTR_NIP60_QUOTE_KIND 7374
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 17
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 65
#define NOSTR_CASHU_PUBKEY_HEX_SIZE 67
#define NOSTR_CASHU_EVENT_ID_HEX_SIZE 65
+2 -2
View File
@@ -2,10 +2,10 @@
#define NOSTR_CORE_H
// Version information (auto-updated by increment_and_push.sh)
#define VERSION "v0.5.3"
#define VERSION "v0.5.7"
#define VERSION_MAJOR 0
#define VERSION_MINOR 5
#define VERSION_PATCH 3
#define VERSION_PATCH 7
/*
* NOSTR Core Library - Complete API Reference
+407
View File
@@ -0,0 +1,407 @@
/*
* NIP-60 Live Cashu Receive Integration Test
*
* This test performs a live mint against testnut mints, constructs a cashuA token
* string from the returned signature, and verifies our receive/decode path.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../nostr_core/nostr_core.h"
/* private secp256k1 wrappers from utils.c */
typedef struct {
unsigned char data[64];
} nostr_secp256k1_pubkey;
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey* pubkey, const unsigned char* input, size_t inputlen);
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (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) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
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);
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;
}
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;
}
}
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) {
if (!in || !out || out_size == 0) return NOSTR_ERROR_INVALID_INPUT;
size_t needed = ((in_len + 2) / 3) * 4 + 1;
char* b64 = (char*)malloc(needed);
if (!b64) return NOSTR_ERROR_MEMORY_FAILED;
size_t n = base64_encode(in, in_len, b64, needed);
if (n == 0) {
free(b64);
return NOSTR_ERROR_INVALID_INPUT;
}
size_t j = 0;
for (size_t i = 0; i < n && b64[i] != '\0'; i++) {
char c = b64[i];
if (c == '=') continue;
if (c == '+') c = '-';
else if (c == '/') c = '_';
if (j + 1 >= out_size) {
free(b64);
return NOSTR_ERROR_INVALID_INPUT;
}
out[j++] = c;
}
out[j] = '\0';
free(b64);
return NOSTR_SUCCESS;
}
static int build_cashuA_token_string(const char* mint_url,
const char* keyset_id,
uint64_t amount,
const char* secret,
const char* C_hex,
char* out_token,
size_t out_token_size) {
if (!mint_url || !keyset_id || !secret || !C_hex || !out_token || out_token_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* root = cJSON_CreateObject();
cJSON* token_arr = cJSON_CreateArray();
cJSON* entry = cJSON_CreateObject();
cJSON* proofs = cJSON_CreateArray();
cJSON* proof = cJSON_CreateObject();
if (!root || !token_arr || !entry || !proofs || !proof) {
cJSON_Delete(root);
cJSON_Delete(token_arr);
cJSON_Delete(entry);
cJSON_Delete(proofs);
cJSON_Delete(proof);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(entry, "mint", mint_url);
cJSON_AddStringToObject(proof, "id", keyset_id);
cJSON_AddNumberToObject(proof, "amount", (double)amount);
cJSON_AddStringToObject(proof, "secret", secret);
cJSON_AddStringToObject(proof, "C", C_hex);
cJSON_AddItemToArray(proofs, proof);
cJSON_AddItemToObject(entry, "proofs", proofs);
cJSON_AddItemToArray(token_arr, entry);
cJSON_AddItemToObject(root, "token", token_arr);
char* json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!json) return NOSTR_ERROR_MEMORY_FAILED;
char b64url[8192];
int rc = base64_to_base64url_no_pad((const unsigned char*)json, strlen(json), b64url, sizeof(b64url));
free(json);
if (rc != NOSTR_SUCCESS) return rc;
int written = snprintf(out_token, out_token_size, "cashuA%s", b64url);
if (written < 0 || (size_t)written >= out_token_size) {
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}
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));
int rc = cashu_mint_get_info(mint_url, &info, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "get mint info");
if (rc != NOSTR_SUCCESS) return;
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;
}
cashu_keyset_t active_keyset;
memset(&active_keyset, 0, sizeof(active_keyset));
rc = cashu_mint_get_active_keyset(&info, "sat", &active_keyset);
TEST_ASSERT(rc == NOSTR_SUCCESS, "select active sat keyset");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_keyset_keys_t keyset_keys;
memset(&keyset_keys, 0, sizeof(keyset_keys));
rc = cashu_mint_get_keys(mint_url, active_keyset.id, &keyset_keys, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "fetch per-amount keyset keys");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
const char* amount_pubkey = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, 1, &amount_pubkey);
TEST_ASSERT(rc == NOSTR_SUCCESS && amount_pubkey != NULL, "find pubkey for amount=1");
if (rc != NOSTR_SUCCESS || !amount_pubkey) {
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
cashu_mint_quote_t quote;
memset(&quote, 0, sizeof(quote));
rc = cashu_mint_request_mint_quote(mint_url, 1, "sat", &quote, 15);
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;
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.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;
}
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;
}
cashu_decoded_token_t decoded;
memset(&decoded, 0, sizeof(decoded));
rc = cashu_decode_token(token_a, &decoded);
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");
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");
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_b);
cashu_free_decoded_token(&decoded);
free(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);
}
int main(void) {
printf("NIP-60 Live Cashu Receive Integration Test\n");
printf("========================================\n");
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize NOSTR library\n");
return 1;
}
test_live_mint_and_receive("https://nofee.testnut.cashu.space");
printf("\n=== Test Summary ===\n");
printf("Passed: %d/%d\n", tests_passed, tests_run);
nostr_cleanup();
return (tests_passed == tests_run) ? 0 : 1;
}