#define _POSIX_C_SOURCE 200809L #include #include #include #include #include "./nostr_core_lib/cjson/cJSON.h" #include "./nostr_core_lib/nostr_core/nostr_core.h" static const char* k_default_relays[] = { "wss://relay.damus.io", "wss://relay.primal.net", "wss://relay.laantungir.net" }; typedef struct { int eose_received; int events_received; int kind3_events; int max_p_tags; } test_ctx_t; static volatile int g_ws_eose_seen = 0; static long long now_ms(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) { return 0; } return ((long long)ts.tv_sec * 1000LL) + (ts.tv_nsec / 1000000LL); } static int count_tag(cJSON* tags, const char* key) { if (!tags || !cJSON_IsArray(tags) || !key) { return 0; } int n = cJSON_GetArraySize(tags); int count = 0; for (int i = 0; i < n; i++) { cJSON* tag = cJSON_GetArrayItem(tags, i); if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 1) { continue; } cJSON* k = cJSON_GetArrayItem(tag, 0); if (k && cJSON_IsString(k) && k->valuestring && strcmp(k->valuestring, key) == 0) { count++; } } return count; } static void on_event(cJSON* event, const char* relay_url, void* user_data) { test_ctx_t* ctx = (test_ctx_t*)user_data; if (!ctx || !event || !cJSON_IsObject(event)) { return; } ctx->events_received++; cJSON* kind = cJSON_GetObjectItemCaseSensitive(event, "kind"); cJSON* id = cJSON_GetObjectItemCaseSensitive(event, "id"); cJSON* pubkey = cJSON_GetObjectItemCaseSensitive(event, "pubkey"); cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at"); cJSON* content = cJSON_GetObjectItemCaseSensitive(event, "content"); cJSON* tags = cJSON_GetObjectItemCaseSensitive(event, "tags"); int kind_val = (kind && cJSON_IsNumber(kind)) ? (int)kind->valuedouble : -1; int p_tags = count_tag(tags, "p"); int r_tags = count_tag(tags, "r"); int tag_count = (tags && cJSON_IsArray(tags)) ? cJSON_GetArraySize(tags) : 0; int content_len = (content && cJSON_IsString(content) && content->valuestring) ? (int)strlen(content->valuestring) : 0; if (p_tags > ctx->max_p_tags) { ctx->max_p_tags = p_tags; } if (kind_val == 3) { ctx->kind3_events++; } printf("[EVENT] relay=%s kind=%d created_at=%lld tags=%d p_tags=%d r_tags=%d content_len=%d id=%.16s pubkey=%.16s\n", relay_url ? relay_url : "", kind_val, (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : -1LL, tag_count, p_tags, r_tags, content_len, (id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "", (pubkey && cJSON_IsString(pubkey) && pubkey->valuestring) ? pubkey->valuestring : ""); } static void on_eose(cJSON** events, int event_count, void* user_data) { (void)events; test_ctx_t* ctx = (test_ctx_t*)user_data; if (!ctx) { return; } ctx->eose_received = 1; printf("[EOSE] received for subscription (event_count=%d)\n", event_count); } static void log_callback(int level, const char* component, const char* message, void* user_data) { (void)user_data; const char* lvl = "?"; switch (level) { case NOSTR_LOG_LEVEL_ERROR: lvl = "ERROR"; break; case NOSTR_LOG_LEVEL_WARN: lvl = "WARN"; break; case NOSTR_LOG_LEVEL_INFO: lvl = "INFO"; break; case NOSTR_LOG_LEVEL_DEBUG: lvl = "DEBUG"; break; case NOSTR_LOG_LEVEL_TRACE: lvl = "TRACE"; break; default: break; } if (!component) component = "core"; if (!message) message = ""; if (strcmp(component, "websocket") == 0) { printf("[NOSTR %s] [%s] %s\n", lvl, component, message); if (strstr(message, "[\"EOSE\"") != NULL) { g_ws_eose_seen = 1; } } } int main(int argc, char** argv) { const char* npub = "npub13lm5wf8dvsdnc2894pkhch9uf8phvw9varrv8zf4sc885hhdmc8q6lx7ks"; int timeout_ms = 15000; if (argc >= 2 && argv[1] && argv[1][0] != '\0') { npub = argv[1]; } if (argc >= 3) { timeout_ms = atoi(argv[2]); if (timeout_ms <= 0) { timeout_ms = 15000; } } unsigned char pubkey[32] = {0}; char pubkey_hex[65] = {0}; if (nostr_decode_npub(npub, pubkey) != 0) { fprintf(stderr, "Failed to decode npub: %s\n", npub); return 1; } nostr_bytes_to_hex(pubkey, 32, pubkey_hex); if (nostr_crypto_init() != 0) { fprintf(stderr, "nostr_crypto_init failed\n"); return 1; } nostr_set_log_callback(log_callback, NULL); nostr_set_log_level(NOSTR_LOG_LEVEL_TRACE); nostr_pool_reconnect_config_t* reconnect = nostr_pool_reconnect_config_default(); if (!reconnect) { fprintf(stderr, "nostr_pool_reconnect_config_default failed\n"); nostr_crypto_cleanup(); return 1; } reconnect->enable_auto_reconnect = 1; reconnect->ping_interval_seconds = 20; reconnect->pong_timeout_seconds = 10; nostr_relay_pool_t* pool = nostr_relay_pool_create(reconnect); if (!pool) { fprintf(stderr, "nostr_relay_pool_create failed\n"); nostr_crypto_cleanup(); return 1; } const int relay_count = (int)(sizeof(k_default_relays) / sizeof(k_default_relays[0])); for (int i = 0; i < relay_count; i++) { if (nostr_relay_pool_add_relay(pool, k_default_relays[i]) != 0) { fprintf(stderr, "Failed to add relay: %s\n", k_default_relays[i]); } } cJSON* filter = cJSON_CreateObject(); cJSON* kinds = cJSON_CreateArray(); cJSON* authors = cJSON_CreateArray(); if (!filter || !kinds || !authors) { fprintf(stderr, "Failed to build filter JSON\n"); cJSON_Delete(filter); cJSON_Delete(kinds); cJSON_Delete(authors); nostr_relay_pool_destroy(pool); nostr_crypto_cleanup(); return 1; } cJSON_AddItemToArray(kinds, cJSON_CreateNumber(3)); cJSON_AddItemToObject(filter, "kinds", kinds); cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex)); cJSON_AddItemToObject(filter, "authors", authors); cJSON_AddNumberToObject(filter, "limit", 16); test_ctx_t ctx; memset(&ctx, 0, sizeof(ctx)); printf("Target npub: %s\n", npub); printf("Target hex : %s\n", pubkey_hex); printf("Relays (%d):\n", relay_count); for (int i = 0; i < relay_count; i++) { printf(" - %s\n", k_default_relays[i]); } printf("Subscribing to kind=3 with timeout=%d ms\n", timeout_ms); nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe( pool, k_default_relays, relay_count, filter, on_event, on_eose, &ctx, 0, 0, NOSTR_POOL_EOSE_FIRST, 12, 20 ); if (!sub) { fprintf(stderr, "nostr_relay_pool_subscribe failed\n"); cJSON_Delete(filter); nostr_relay_pool_destroy(pool); nostr_crypto_cleanup(); return 1; } long long started = now_ms(); while (!ctx.eose_received && !g_ws_eose_seen) { long long elapsed = now_ms() - started; if (elapsed >= timeout_ms) { break; } (void)nostr_relay_pool_poll(pool, 100); } printf("\nSummary:\n"); printf(" EOSE callback : %s\n", ctx.eose_received ? "yes" : "no"); printf(" Websocket EOSE: %s\n", g_ws_eose_seen ? "yes" : "no"); printf(" Events seen : %d\n", ctx.events_received); printf(" Kind-3 events : %d\n", ctx.kind3_events); printf(" Max p-tags : %d\n", ctx.max_p_tags); if (!ctx.eose_received && g_ws_eose_seen) { fprintf(stderr, "NOTE: Relay sent EOSE on websocket but subscription callback did not fire (pool EOSE behavior mismatch).\n"); } if (!ctx.eose_received && !g_ws_eose_seen) { fprintf(stderr, "Timed out waiting for EOSE after %d ms\n", timeout_ms); } (void)nostr_pool_subscription_close(sub); cJSON_Delete(filter); nostr_relay_pool_destroy(pool); nostr_crypto_cleanup(); return (ctx.eose_received || g_ws_eose_seen) ? 0 : 2; }