Updated synchronous relay queries to handle nip42.

This commit is contained in:
Laan Tungir 2025-09-30 12:16:23 -04:00
parent 9a63550863
commit 0d910ca181
8 changed files with 1086 additions and 98 deletions

2
.gitignore vendored
View File

@ -8,7 +8,7 @@ node_modules/
nostr-tools/
tiny-AES-c/
blossom/
ndk/
Trash/debug_tests/
node_modules/

755
POOL_API.md Normal file
View File

@ -0,0 +1,755 @@
# Relay Pool API Reference
This document describes the public API for the Nostr Relay Pool implementation in [`core_relay_pool.c`](nostr_core/core_relay_pool.c).
## Function Summary
| Function | Description |
|----------|-------------|
| [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:219) | Create and initialize a new relay pool |
| [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:304) | Destroy pool and cleanup all resources |
| [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229) | Add a relay URL to the pool |
| [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:273) | Remove a relay URL from the pool |
| [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:399) | Create async subscription with callbacks |
| [`nostr_pool_subscription_close()`](nostr_core/core_relay_pool.c:491) | Close subscription and free resources |
| [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192) | Run event loop for specified timeout |
| [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232) | Single iteration poll and dispatch |
| [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:695) | Synchronous query returning event array |
| [`nostr_relay_pool_get_event()`](nostr_core/core_relay_pool.c:825) | Get single most recent event |
| [`nostr_relay_pool_publish()`](nostr_core/core_relay_pool.c:866) | Publish event and wait for OK responses |
| [`nostr_relay_pool_get_relay_status()`](nostr_core/core_relay_pool.c:944) | Get connection status for a relay |
| [`nostr_relay_pool_list_relays()`](nostr_core/core_relay_pool.c:960) | List all relays and their statuses |
| [`nostr_relay_pool_get_relay_stats()`](nostr_core/core_relay_pool.c:992) | Get detailed statistics for a relay |
| [`nostr_relay_pool_reset_relay_stats()`](nostr_core/core_relay_pool.c:1008) | Reset statistics for a relay |
| [`nostr_relay_pool_get_relay_query_latency()`](nostr_core/core_relay_pool.c:1045) | Get average query latency for a relay |
## Pool Lifecycle
### Create Pool
**Function:** [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:219)
```c
nostr_relay_pool_t* nostr_relay_pool_create(void);
```
**Example:**
```c
#include "nostr_core.h"
int main() {
// Create a new relay pool
nostr_relay_pool_t* pool = nostr_relay_pool_create();
if (!pool) {
fprintf(stderr, "Failed to create relay pool\n");
return -1;
}
// Use the pool...
// Clean up
nostr_relay_pool_destroy(pool);
return 0;
}
```
### Destroy Pool
**Function:** [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:304)
```c
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
```
**Example:**
```c
// Properly cleanup a relay pool
void cleanup_pool(nostr_relay_pool_t* pool) {
if (pool) {
// This will close all active subscriptions and relay connections
nostr_relay_pool_destroy(pool);
pool = NULL;
}
}
```
## Relay Management
### Add Relay
**Function:** [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229)
```c
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
```
**Example:**
```c
int setup_relays(nostr_relay_pool_t* pool) {
const char* relays[] = {
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band"
};
for (int i = 0; i < 3; i++) {
int result = nostr_relay_pool_add_relay(pool, relays[i]);
if (result != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to add relay %s: %d\n", relays[i], result);
return -1;
}
printf("Added relay: %s\n", relays[i]);
}
return 0;
}
```
### Remove Relay
**Function:** [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:273)
```c
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
```
**Example:**
```c
int remove_slow_relay(nostr_relay_pool_t* pool) {
const char* slow_relay = "wss://slow-relay.example.com";
int result = nostr_relay_pool_remove_relay(pool, slow_relay);
if (result == NOSTR_SUCCESS) {
printf("Successfully removed relay: %s\n", slow_relay);
} else {
printf("Failed to remove relay %s (may not exist)\n", slow_relay);
}
return result;
}
```
## Subscriptions (Asynchronous)
### Subscribe to Events
**Function:** [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:399)
```c
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(void* user_data),
void* user_data);
```
**Example:**
```c
#include "cjson/cJSON.h"
// Event callback - called for each received event
void handle_event(cJSON* event, const char* relay_url, void* user_data) {
cJSON* content = cJSON_GetObjectItem(event, "content");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
if (content && pubkey) {
printf("Event from %s: %s (by %s)\n",
relay_url,
cJSON_GetStringValue(content),
cJSON_GetStringValue(pubkey));
}
}
// EOSE callback - called when all relays finish sending stored events
void handle_eose(void* user_data) {
printf("All relays finished sending stored events\n");
}
int subscribe_to_notes(nostr_relay_pool_t* pool) {
// Create filter for kind 1 (text notes) from last hour
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
time_t since = time(NULL) - 3600; // Last hour
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(since));
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(50));
// Subscribe to specific relays
const char* relay_urls[] = {
"wss://relay.damus.io",
"wss://nos.lol"
};
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
pool,
relay_urls,
2,
filter,
handle_event,
handle_eose,
NULL // user_data
);
cJSON_Delete(filter); // Pool makes its own copy
if (!sub) {
fprintf(stderr, "Failed to create subscription\n");
return -1;
}
// Drive the event loop to receive events
printf("Listening for events for 30 seconds...\n");
nostr_relay_pool_run(pool, 30000); // 30 seconds
// Close subscription
nostr_pool_subscription_close(sub);
return 0;
}
```
### Close Subscription
**Function:** [`nostr_pool_subscription_close()`](nostr_core/core_relay_pool.c:491)
```c
int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);
```
**Example:**
```c
// Subscription management with cleanup
typedef struct {
nostr_pool_subscription_t* subscription;
int event_count;
int should_stop;
} subscription_context_t;
void event_counter(cJSON* event, const char* relay_url, void* user_data) {
subscription_context_t* ctx = (subscription_context_t*)user_data;
ctx->event_count++;
printf("Received event #%d from %s\n", ctx->event_count, relay_url);
// Stop after 10 events
if (ctx->event_count >= 10) {
ctx->should_stop = 1;
}
}
int limited_subscription(nostr_relay_pool_t* pool) {
subscription_context_t ctx = {0};
// Create filter
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
const char* relay_urls[] = {"wss://relay.damus.io"};
ctx.subscription = nostr_relay_pool_subscribe(
pool, relay_urls, 1, filter, event_counter, NULL, &ctx);
cJSON_Delete(filter);
if (!ctx.subscription) {
return -1;
}
// Poll until we should stop
while (!ctx.should_stop) {
int events = nostr_relay_pool_poll(pool, 100);
if (events < 0) break;
}
// Clean up
int result = nostr_pool_subscription_close(ctx.subscription);
printf("Subscription closed with result: %d\n", result);
return 0;
}
```
## Event Loop
### Run Timed Loop
**Function:** [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192)
```c
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
```
**Example:**
```c
int run_event_loop(nostr_relay_pool_t* pool) {
printf("Starting event loop for 60 seconds...\n");
// Run for 60 seconds, processing all incoming events
int total_events = nostr_relay_pool_run(pool, 60000);
if (total_events < 0) {
fprintf(stderr, "Event loop error\n");
return -1;
}
printf("Processed %d events total\n", total_events);
return 0;
}
```
### Single Poll Iteration
**Function:** [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232)
```c
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
```
**Example:**
```c
// Integration with custom main loop
int custom_main_loop(nostr_relay_pool_t* pool) {
int running = 1;
int total_events = 0;
while (running) {
// Poll for Nostr events (non-blocking with 50ms timeout)
int events = nostr_relay_pool_poll(pool, 50);
if (events > 0) {
total_events += events;
printf("Processed %d events this iteration\n", events);
}
// Do other work in your application
// handle_ui_events();
// process_background_tasks();
// Check exit condition
// running = !should_exit();
// Simple exit after 100 events for demo
if (total_events >= 100) {
running = 0;
}
}
printf("Main loop finished, processed %d total events\n", total_events);
return 0;
}
```
## Synchronous Operations
### Query Multiple Events
**Function:** [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:695)
```c
cJSON** nostr_relay_pool_query_sync(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
int* event_count,
int timeout_ms);
```
**Example:**
```c
int query_recent_notes(nostr_relay_pool_t* pool) {
// Create filter for recent text notes
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(20));
const char* relay_urls[] = {
"wss://relay.damus.io",
"wss://nos.lol"
};
int event_count = 0;
cJSON** events = nostr_relay_pool_query_sync(
pool, relay_urls, 2, filter, &event_count, 10000); // 10 second timeout
cJSON_Delete(filter);
if (!events) {
printf("No events received or query failed\n");
return -1;
}
printf("Received %d events:\n", event_count);
for (int i = 0; i < event_count; i++) {
cJSON* content = cJSON_GetObjectItem(events[i], "content");
if (content) {
printf(" %d: %s\n", i + 1, cJSON_GetStringValue(content));
}
// Free each event
cJSON_Delete(events[i]);
}
// Free the events array
free(events);
return event_count;
}
```
### Get Single Most Recent Event
**Function:** [`nostr_relay_pool_get_event()`](nostr_core/core_relay_pool.c:825)
```c
cJSON* nostr_relay_pool_get_event(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
int timeout_ms);
```
**Example:**
```c
int get_latest_note_from_pubkey(nostr_relay_pool_t* pool, const char* pubkey_hex) {
// Create filter for specific author's notes
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* authors = cJSON_CreateArray();
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(1));
const char* relay_urls[] = {"wss://relay.damus.io"};
cJSON* event = nostr_relay_pool_get_event(
pool, relay_urls, 1, filter, 5000); // 5 second timeout
cJSON_Delete(filter);
if (!event) {
printf("No recent event found for pubkey %s\n", pubkey_hex);
return -1;
}
cJSON* content = cJSON_GetObjectItem(event, "content");
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
if (content && created_at) {
printf("Latest note: %s (created at %ld)\n",
cJSON_GetStringValue(content),
(long)cJSON_GetNumberValue(created_at));
}
cJSON_Delete(event);
return 0;
}
```
### Publish Event
**Function:** [`nostr_relay_pool_publish()`](nostr_core/core_relay_pool.c:866)
```c
int nostr_relay_pool_publish(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* event);
```
**Example:**
```c
int publish_text_note(nostr_relay_pool_t* pool, const char* content) {
// Create a basic text note event (this is simplified - real implementation
// would need proper signing with private key)
cJSON* event = cJSON_CreateObject();
cJSON_AddItemToObject(event, "kind", cJSON_CreateNumber(1));
cJSON_AddItemToObject(event, "content", cJSON_CreateString(content));
cJSON_AddItemToObject(event, "created_at", cJSON_CreateNumber(time(NULL)));
// In real usage, you'd add pubkey, id, sig fields here
cJSON_AddItemToObject(event, "pubkey", cJSON_CreateString("your_pubkey_hex"));
cJSON_AddItemToObject(event, "id", cJSON_CreateString("event_id_hash"));
cJSON_AddItemToObject(event, "sig", cJSON_CreateString("event_signature"));
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
const char* relay_urls[] = {
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band"
};
printf("Publishing note: %s\n", content);
int success_count = nostr_relay_pool_publish(
pool, relay_urls, 3, event);
cJSON_Delete(event);
printf("Successfully published to %d out of 3 relays\n", success_count);
if (success_count == 0) {
fprintf(stderr, "Failed to publish to any relay\n");
return -1;
}
return success_count;
}
```
## Status and Statistics
### Get Relay Status
**Function:** [`nostr_relay_pool_get_relay_status()`](nostr_core/core_relay_pool.c:944)
```c
nostr_pool_relay_status_t nostr_relay_pool_get_relay_status(
nostr_relay_pool_t* pool,
const char* relay_url);
```
**Example:**
```c
void check_relay_status(nostr_relay_pool_t* pool, const char* relay_url) {
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(pool, relay_url);
const char* status_str;
switch (status) {
case NOSTR_POOL_RELAY_DISCONNECTED:
status_str = "DISCONNECTED";
break;
case NOSTR_POOL_RELAY_CONNECTING:
status_str = "CONNECTING";
break;
case NOSTR_POOL_RELAY_CONNECTED:
status_str = "CONNECTED";
break;
case NOSTR_POOL_RELAY_ERROR:
status_str = "ERROR";
break;
default:
status_str = "UNKNOWN";
break;
}
printf("Relay %s status: %s\n", relay_url, status_str);
}
```
### List All Relays
**Function:** [`nostr_relay_pool_list_relays()`](nostr_core/core_relay_pool.c:960)
```c
int nostr_relay_pool_list_relays(
nostr_relay_pool_t* pool,
char*** relay_urls,
nostr_pool_relay_status_t** statuses);
```
**Example:**
```c
void print_all_relays(nostr_relay_pool_t* pool) {
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (count < 0) {
printf("Failed to list relays\n");
return;
}
if (count == 0) {
printf("No relays configured\n");
return;
}
printf("Configured relays (%d):\n", count);
for (int i = 0; i < count; i++) {
const char* status_str = (statuses[i] == NOSTR_POOL_RELAY_CONNECTED) ?
"CONNECTED" : "DISCONNECTED";
printf(" %s - %s\n", relay_urls[i], status_str);
// Free the duplicated URL string
free(relay_urls[i]);
}
// Free the arrays
free(relay_urls);
free(statuses);
}
```
### Get Relay Statistics
**Function:** [`nostr_relay_pool_get_relay_stats()`](nostr_core/core_relay_pool.c:992)
```c
const nostr_relay_stats_t* nostr_relay_pool_get_relay_stats(
nostr_relay_pool_t* pool,
const char* relay_url);
```
**Example:**
```c
void print_relay_stats(nostr_relay_pool_t* pool, const char* relay_url) {
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_url);
if (!stats) {
printf("No stats available for relay %s\n", relay_url);
return;
}
printf("Statistics for %s:\n", relay_url);
printf(" Connection attempts: %d\n", stats->connection_attempts);
printf(" Connection failures: %d\n", stats->connection_failures);
printf(" Events received: %d\n", stats->events_received);
printf(" Events published: %d\n", stats->events_published);
printf(" Events published OK: %d\n", stats->events_published_ok);
printf(" Events published failed: %d\n", stats->events_published_failed);
printf(" Query latency avg: %.2f ms\n", stats->query_latency_avg);
printf(" Query samples: %d\n", stats->query_samples);
printf(" Publish latency avg: %.2f ms\n", stats->publish_latency_avg);
printf(" Publish samples: %d\n", stats->publish_samples);
if (stats->last_event_time > 0) {
printf(" Last event: %ld seconds ago\n",
time(NULL) - stats->last_event_time);
}
}
```
### Reset Relay Statistics
**Function:** [`nostr_relay_pool_reset_relay_stats()`](nostr_core/core_relay_pool.c:1008)
```c
int nostr_relay_pool_reset_relay_stats(
nostr_relay_pool_t* pool,
const char* relay_url);
```
**Example:**
```c
void reset_stats_for_relay(nostr_relay_pool_t* pool, const char* relay_url) {
int result = nostr_relay_pool_reset_relay_stats(pool, relay_url);
if (result == NOSTR_SUCCESS) {
printf("Successfully reset statistics for %s\n", relay_url);
} else {
printf("Failed to reset statistics for %s\n", relay_url);
}
}
```
### Get Query Latency
**Function:** [`nostr_relay_pool_get_relay_query_latency()`](nostr_core/core_relay_pool.c:1045)
```c
double nostr_relay_pool_get_relay_query_latency(
nostr_relay_pool_t* pool,
const char* relay_url);
```
**Example:**
```c
void check_relay_performance(nostr_relay_pool_t* pool) {
const char* relays[] = {
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band"
};
printf("Relay performance comparison:\n");
for (int i = 0; i < 3; i++) {
double latency = nostr_relay_pool_get_relay_query_latency(pool, relays[i]);
if (latency >= 0) {
printf(" %s: %.2f ms average query latency\n", relays[i], latency);
} else {
printf(" %s: No latency data available\n", relays[i]);
}
}
}
```
## Complete Example Application
```c
#include "nostr_core.h"
#include "cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Global context for the example
typedef struct {
int event_count;
int max_events;
} app_context_t;
void on_text_note(cJSON* event, const char* relay_url, void* user_data) {
app_context_t* ctx = (app_context_t*)user_data;
cJSON* content = cJSON_GetObjectItem(event, "content");
if (content && cJSON_IsString(content)) {
printf("[%s] Note #%d: %s\n",
relay_url, ++ctx->event_count, cJSON_GetStringValue(content));
}
}
void on_subscription_complete(void* user_data) {
printf("All relays finished sending stored events\n");
}
int main() {
// Initialize pool
nostr_relay_pool_t* pool = nostr_relay_pool_create();
if (!pool) {
fprintf(stderr, "Failed to create relay pool\n");
return 1;
}
// Add relays
const char* relays[] = {
"wss://relay.damus.io",
"wss://nos.lol"
};
for (int i = 0; i < 2; i++) {
if (nostr_relay_pool_add_relay(pool, relays[i]) != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to add relay: %s\n", relays[i]);
}
}
// Create filter for recent text notes
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(10));
// Set up context
app_context_t ctx = {0, 10};
// Subscribe
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
pool, relays, 2, filter, on_text_note, on_subscription_complete, &ctx);
cJSON_Delete(filter);
if (!sub) {
fprintf(stderr, "Failed to create subscription\n");
nostr_relay_pool_destroy(pool);
return 1;
}
// Run event loop for 30 seconds
printf("Listening for events...\n");
nostr_relay_pool_run(pool, 30000);
// Print final stats
for (int i = 0; i < 2; i++) {
print_relay_stats(pool, relays[i]);
}
// Cleanup
nostr_pool_subscription_close(sub);
nostr_relay_pool_destroy(pool);
printf("Application finished. Received %d events total.\n", ctx.event_count);
return 0;
}
```
## Notes
- All functions are **not thread-safe**. Use from a single thread or add external synchronization.
- **Memory ownership**: The pool duplicates filters and URLs internally. Caller owns returned events and must free them.
- **Event deduplication** is applied pool-wide using a circular buffer of 1000 event IDs.
- **Ping functionality** is currently disabled in this build.
- **Reconnection** happens on-demand when sending, but active subscriptions are not automatically re-sent after reconnect.
- **Polling model**: You must drive the event loop via [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192) or [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232) to receive events.

View File

@ -51,3 +51,19 @@ Final event: {
"id": "0034dad20205876be164b768ba5efad8aaa23bdf16b273544785cae9f13a76f7",
"sig": "e1ef97bf916f9e1fd94e231e62833e6ff000be29a92e16fc3656142e51abbf4ee8795ab4100f448e6712c1a76bb128016c24306b165077188fa8e5b82c72f23f"
}
=== NOSTR WebSocket Debug Log Started ===
[11:57:56.083] SEND relay.laantungir.net:443: ["REQ", "sync_1_1759247875", {
"kinds": [1],
"limit": 1
}]
[11:57:56.742] SEND nostr.mom:443: ["REQ", "sync_2_1759247875", {
"kinds": [1],
"limit": 1
}]
[11:57:56.742] RECV relay.laantungir.net:443: ["EVENT","sync_1_1759247875",{"content":"🟠 New Bitcoin Block Mined!\n\nBlock Height: 917,088\nBlock Hash: 000000000000000000011050a3967e7d3936867a5afcae9babbdc5925c5b66d5\nTimestamp: 2025-09-30T15:46:37.000Z\nTransactions: 3,161\nBlock Size: 1.52 MB\nBlock Weight: 3,998,317 WU\nDifficulty: 1.42e+14\n\n#Bitcoin #Blockchain #Block917088","created_at":1759247235,"id":"20e0e3acf17ecde6e9ed49bd5b1c739eff1095d6e6c3c2775556b5c978359721","kind":1,"pubkey":"e568a76a4f8836be296d405eb41034260d55e2361e4b2ef88350a4003bbd5f9b","sig":"10817ffa6dafc128c8d2c16e9127d101e3a8730ae205be933d747294afd3766150e54073a6817fae3bdc5cff57436c6ab58ccb8c327e7013220b27ab2680d722","tags":[["t","bitcoin"],["t","blockchain"],["t","block"],["alt","New Bitcoin block mined - Block 917088"],["published_at","1759247197"],["client","info_bot","ws://127.0.0.1:7777"],["r","https://blockstream.info/block/000000000000000000011050a3967e7d3936867a5afcae9babbdc5925c5b66d5"],["new_block","true"],["block_height","917088"],["block_hash","000000000000000000011050a3967e7d3936867a5afcae9babbdc5925c5b66d5"],["block_time","1759247197"],["tx_count","3161"],["block_size","1590325"],["block_weight","3998317"],["difficulty","142342602928674.9"],["previous_block","00000000000000000000c50b4fcf7b8fac91e60893037a99c3dbab84f3aee313"],["mempool_tx_count","3087"],["mempool_size","1195411"],["memory_usage_pct","2.4"]]}]
[11:57:56.802] RECV relay.laantungir.net:443: ["EOSE","sync_1_1759247875"]
[11:57:56.802] SEND relay.laantungir.net:443: ["CLOSE", "sync_1_1759247875"]
[11:57:56.895] RECV nostr.mom:443: ["EVENT","sync_2_1759247875",{"content":"\"No matter how economically or politically complex our human societies appear, they nevertheless remain accountable to the regularities and vagaries of the natural world.\"\n\n-Roy Sebag\n\n#TheQuoteShelf \n\n\n\nhttps://blossom.primal.net/6968f243f29a1f125f049ea798274b13efe44144724bf507cd39246fdd5e4ff6.webp","created_at":1759247875,"id":"0f241242cc645089e4c50a3cec0cf82ec11d7867cde7e257ad7651f836088bf6","kind":1,"pubkey":"356875ffd729b06eeb4c1d7a70a1f750045d067774d21c0faffe4af2bf96a2e8","sig":"d1665367d1ae571b81a624e2ca101f3267af66923389d6ef4f8f4adb452e7632a87f104f9f328363459aaf7537cae583952f169ce5d80a73b8371fe0d1d9ea60","tags":[["t","TheQuoteShelf"],["imeta","url https://blossom.primal.net/6968f243f29a1f125f049ea798274b13efe44144724bf507cd39246fdd5e4ff6.webp","m image/webp","ox 6968f243f29a1f125f049ea798274b13efe44144724bf507cd39246fdd5e4ff6","dim 979x551"]]}]
[11:57:56.905] RECV nostr.mom:443: ["EOSE","sync_2_1759247875"]
[11:57:56.905] SEND nostr.mom:443: ["CLOSE", "sync_2_1759247875"]

View File

@ -13,7 +13,7 @@
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "nostr_common.h"
#include "nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -26,6 +26,9 @@
// cJSON for JSON handling
#include "../cjson/cJSON.h"
// NIP-42 Authentication
#include "nip042.h"
// =============================================================================
// TYPE DEFINITIONS FOR SYNCHRONOUS RELAY QUERIES
// =============================================================================
@ -51,6 +54,12 @@ typedef struct {
cJSON** events; // Array of events from this relay
int events_capacity; // Allocated capacity
char subscription_id[32]; // Unique subscription ID
// NIP-42 Authentication fields
nostr_auth_state_t auth_state; // Current authentication state
char auth_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH]; // Stored challenge
time_t auth_challenge_time; // When challenge was received
int nip42_enabled; // Whether NIP-42 is enabled for this relay
} relay_connection_t;
@ -65,7 +74,9 @@ cJSON** synchronous_query_relays_with_progress(
int* result_count,
int relay_timeout_seconds,
relay_progress_callback_t callback,
void* user_data) {
void* user_data,
int nip42_enabled,
const unsigned char* private_key) {
if (!relay_urls || relay_count <= 0 || !filter || !result_count) {
if (result_count) *result_count = 0;
@ -96,6 +107,12 @@ cJSON** synchronous_query_relays_with_progress(
relays[i].events_capacity = 10;
relays[i].events = malloc(relays[i].events_capacity * sizeof(cJSON*));
// Initialize NIP-42 authentication fields
relays[i].auth_state = NOSTR_AUTH_STATE_NONE;
memset(relays[i].auth_challenge, 0, sizeof(relays[i].auth_challenge));
relays[i].auth_challenge_time = 0;
relays[i].nip42_enabled = nip42_enabled;
// Generate unique subscription ID
snprintf(relays[i].subscription_id, sizeof(relays[i].subscription_id),
"sync_%d_%ld", i, start_time);
@ -191,7 +208,38 @@ cJSON** synchronous_query_relays_with_progress(
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
if (msg_type && strcmp(msg_type, "AUTH") == 0) {
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
if (relay->nip42_enabled && private_key && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(challenge_json)) {
const char* challenge = cJSON_GetStringValue(challenge_json);
// Store challenge and attempt authentication
strncpy(relay->auth_challenge, challenge, sizeof(relay->auth_challenge) - 1);
relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0';
relay->auth_challenge_time = time(NULL);
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
// Create and send authentication event
cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay->url, private_key, 0);
if (auth_event) {
char* auth_message = nostr_nip42_create_auth_message(auth_event);
if (auth_message) {
if (nostr_ws_send_text(relay->client, auth_message) >= 0) {
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
if (callback) {
callback(relay->url, "authenticating", NULL, 0, relay_count, completed_relays, user_data);
}
}
free(auth_message);
}
cJSON_Delete(auth_event);
}
}
}
} else if (msg_type && strcmp(msg_type, "EVENT") == 0) {
// Handle EVENT message
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
@ -400,7 +448,9 @@ publish_result_t* synchronous_publish_event_with_progress(
int* success_count,
int relay_timeout_seconds,
publish_progress_callback_t callback,
void* user_data) {
void* user_data,
int nip42_enabled,
const unsigned char* private_key) {
if (!relay_urls || relay_count <= 0 || !event || !success_count) {
if (success_count) *success_count = 0;
@ -444,6 +494,12 @@ publish_result_t* synchronous_publish_event_with_progress(
relays[i].last_activity = start_time;
results[i] = PUBLISH_ERROR; // Default to error
// Initialize NIP-42 authentication fields
relays[i].auth_state = NOSTR_AUTH_STATE_NONE;
memset(relays[i].auth_challenge, 0, sizeof(relays[i].auth_challenge));
relays[i].auth_challenge_time = 0;
relays[i].nip42_enabled = nip42_enabled;
if (callback) {
callback(relays[i].url, "connecting", NULL, 0, relay_count, 0, user_data);
}
@ -536,7 +592,38 @@ publish_result_t* synchronous_publish_event_with_progress(
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "OK") == 0) {
if (msg_type && strcmp(msg_type, "AUTH") == 0) {
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
if (relay->nip42_enabled && private_key && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(challenge_json)) {
const char* challenge = cJSON_GetStringValue(challenge_json);
// Store challenge and attempt authentication
strncpy(relay->auth_challenge, challenge, sizeof(relay->auth_challenge) - 1);
relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0';
relay->auth_challenge_time = time(NULL);
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
// Create and send authentication event
cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay->url, private_key, 0);
if (auth_event) {
char* auth_message = nostr_nip42_create_auth_message(auth_event);
if (auth_message) {
if (nostr_ws_send_text(relay->client, auth_message) >= 0) {
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
if (callback) {
callback(relay->url, "authenticating", NULL, 0, relay_count, completed_relays, user_data);
}
}
free(auth_message);
}
cJSON_Delete(auth_event);
}
}
}
} else if (msg_type && strcmp(msg_type, "OK") == 0) {
// Handle OK message: ["OK", <event_id>, <true|false>, <message>]
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* ok_event_id = cJSON_GetArrayItem(parsed, 1);

View File

@ -81,40 +81,6 @@
// Forward declaration for cJSON (to avoid requiring cJSON.h in header)
struct cJSON;
// Relay query modes
typedef enum {
RELAY_QUERY_FIRST_RESULT, // Return as soon as first event is received
RELAY_QUERY_MOST_RECENT, // Return the most recent event from all relays
RELAY_QUERY_ALL_RESULTS // Return all unique events from all relays
} relay_query_mode_t;
// Publish result types
typedef enum {
PUBLISH_SUCCESS, // Event was accepted by relay
PUBLISH_REJECTED, // Event was rejected by relay
PUBLISH_TIMEOUT, // No response within timeout
PUBLISH_ERROR // Connection or other error
} publish_result_t;
// Progress callback function types
typedef void (*relay_progress_callback_t)(
const char* relay_url,
const char* status,
const char* event_id,
int events_received,
int total_relays,
int completed_relays,
void* user_data);
typedef void (*publish_progress_callback_t)(
const char* relay_url,
const char* status,
const char* message,
int success_count,
int total_relays,
int completed_relays,
void* user_data);
// Function declarations
const char* nostr_strerror(int error_code);
@ -122,25 +88,4 @@ const char* nostr_strerror(int error_code);
int nostr_init(void);
void nostr_cleanup(void);
// Relay query functions
struct cJSON** synchronous_query_relays_with_progress(
const char** relay_urls,
int relay_count,
struct cJSON* filter,
relay_query_mode_t mode,
int* result_count,
int relay_timeout_seconds,
relay_progress_callback_t callback,
void* user_data);
// Relay publish functions
publish_result_t* synchronous_publish_event_with_progress(
const char** relay_urls,
int relay_count,
struct cJSON* event,
int* success_count,
int relay_timeout_seconds,
publish_progress_callback_t callback,
void* user_data);
#endif // NOSTR_COMMON_H

View File

@ -82,6 +82,15 @@
* - nostr_auth_rule_add() -> Add authentication rule
* - nostr_auth_rule_remove() -> Remove authentication rule
*
* RELAY OPERATIONS:
* - synchronous_query_relays_with_progress() -> One-off query from multiple relays
* - synchronous_publish_event_with_progress() -> One-off publish to multiple relays
* *
* RELAY POOL OPERATIONS:
* - nostr_relay_pool_create() -> Create relay pool for persistent connections
* - nostr_relay_pool_subscribe() -> Subscribe to events with callbacks
* - nostr_relay_pool_run() -> Run event loop for receiving events
* SYSTEM FUNCTIONS:
* - nostr_crypto_init() -> Initialize crypto subsystem
* - nostr_crypto_cleanup() -> Cleanup crypto subsystem
@ -142,6 +151,155 @@ extern "C" {
// Authentication and request validation system
#include "request_validator.h" // Request validation and authentication rules
// Relay pool types and functions
typedef enum {
NOSTR_POOL_RELAY_DISCONNECTED = 0,
NOSTR_POOL_RELAY_CONNECTING = 1,
NOSTR_POOL_RELAY_CONNECTED = 2,
NOSTR_POOL_RELAY_ERROR = -1
} nostr_pool_relay_status_t;
typedef struct {
int connection_attempts;
int connection_failures;
int events_received;
int events_published;
int events_published_ok;
int events_published_failed;
time_t last_event_time;
time_t connection_uptime_start;
double ping_latency_avg;
double ping_latency_min;
double ping_latency_max;
double ping_latency_current;
int ping_samples;
double query_latency_avg;
double query_latency_min;
double query_latency_max;
int query_samples;
double publish_latency_avg;
int publish_samples;
} nostr_relay_stats_t;
typedef struct nostr_relay_pool nostr_relay_pool_t;
typedef struct nostr_pool_subscription nostr_pool_subscription_t;
// Relay pool management functions
nostr_relay_pool_t* nostr_relay_pool_create(void);
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
// Subscription management
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(void* user_data),
void* user_data);
int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);
// Event loop functions
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
// Synchronous query/publish functions
cJSON** nostr_relay_pool_query_sync(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
int* event_count,
int timeout_ms);
cJSON* nostr_relay_pool_get_event(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
int timeout_ms);
int nostr_relay_pool_publish(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* event);
// Status and statistics functions
nostr_pool_relay_status_t nostr_relay_pool_get_relay_status(
nostr_relay_pool_t* pool,
const char* relay_url);
int nostr_relay_pool_list_relays(
nostr_relay_pool_t* pool,
char*** relay_urls,
nostr_pool_relay_status_t** statuses);
const nostr_relay_stats_t* nostr_relay_pool_get_relay_stats(
nostr_relay_pool_t* pool,
const char* relay_url);
int nostr_relay_pool_reset_relay_stats(
nostr_relay_pool_t* pool,
const char* relay_url);
double nostr_relay_pool_get_relay_query_latency(
nostr_relay_pool_t* pool,
const char* relay_url);
// Synchronous relay operations (one-off queries/publishes)
typedef enum {
RELAY_QUERY_FIRST_RESULT, // Return as soon as first event is received
RELAY_QUERY_MOST_RECENT, // Return the most recent event from all relays
RELAY_QUERY_ALL_RESULTS // Return all unique events from all relays
} relay_query_mode_t;
typedef enum {
PUBLISH_SUCCESS, // Event was accepted by relay
PUBLISH_REJECTED, // Event was rejected by relay
PUBLISH_TIMEOUT, // No response within timeout
PUBLISH_ERROR // Connection or other error
} publish_result_t;
typedef void (*relay_progress_callback_t)(
const char* relay_url,
const char* status,
const char* event_id,
int events_received,
int total_relays,
int completed_relays,
void* user_data);
typedef void (*publish_progress_callback_t)(
const char* relay_url,
const char* status,
const char* message,
int success_count,
int total_relays,
int completed_relays,
void* user_data);
// Synchronous relay query functions
struct cJSON** synchronous_query_relays_with_progress(
const char** relay_urls,
int relay_count,
struct cJSON* filter,
relay_query_mode_t mode,
int* result_count,
int relay_timeout_seconds,
relay_progress_callback_t callback,
void* user_data,
int nip42_enabled,
const unsigned char* private_key);
// Synchronous relay publish functions
publish_result_t* synchronous_publish_event_with_progress(
const char** relay_urls,
int relay_count,
struct cJSON* event,
int* success_count,
int relay_timeout_seconds,
publish_progress_callback_t callback,
void* user_data,
int nip42_enabled,
const unsigned char* private_key);
// Relay communication functions are defined in nostr_common.h
// WebSocket functions are defined in nostr_common.h

View File

@ -13,6 +13,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sqlite3.h>
#include <time.h>
@ -243,6 +244,8 @@ void nostr_request_validator_cleanup(void) {
int nostr_auth_check_upload(const char* pubkey, const char* auth_header,
const char* hash, const char* mime_type, long file_size) {
(void)pubkey; // Parameter not used in this convenience function
nostr_request_t request = {
.operation = "upload",
.auth_header = auth_header,
@ -265,6 +268,8 @@ int nostr_auth_check_upload(const char* pubkey, const char* auth_header,
}
int nostr_auth_check_delete(const char* pubkey, const char* auth_header, const char* hash) {
(void)pubkey; // Parameter not used in this convenience function
nostr_request_t request = {
.operation = "delete",
.auth_header = auth_header,
@ -287,6 +292,8 @@ int nostr_auth_check_delete(const char* pubkey, const char* auth_header, const c
}
int nostr_auth_check_publish(const char* pubkey, struct cJSON* event) {
(void)pubkey; // Parameter not used in this convenience function
if (!event) {
return NOSTR_ERROR_INVALID_INPUT;
}
@ -459,6 +466,8 @@ static int validate_nostr_event(struct cJSON* event, const char* expected_hash,
//=============================================================================
static int sqlite_auth_init(const char* db_path, const char* app_name) {
(void)app_name; // Parameter not used in this implementation
if (g_auth_db) {
return NOSTR_SUCCESS; // Already initialized
}
@ -765,6 +774,8 @@ static int sqlite_auth_rule_update(const nostr_auth_rule_t* rule) {
}
static int sqlite_auth_rule_list(const char* operation, nostr_auth_rule_t** rules, int* count) {
(void)operation; // Parameter not used in this implementation
if (!g_auth_db || !rules || !count) {
return NOSTR_ERROR_INVALID_INPUT;
}
@ -1130,19 +1141,21 @@ static int evaluate_auth_rules(const char* pubkey, const char* operation, const
static void generate_auth_cache_key(const char* pubkey, const char* operation, const char* hash,
const char* mime_type, long file_size, char* cache_key, size_t key_size) {
char temp_buffer[1024];
snprintf(temp_buffer, sizeof(temp_buffer), "%s|%s|%s|%s|%ld",
pubkey ? pubkey : "", operation ? operation : "",
hash ? hash : "", mime_type ? mime_type : "", file_size);
int written = snprintf(temp_buffer, sizeof(temp_buffer), "%s|%s|%s|%s|%ld",
pubkey ? pubkey : "", operation ? operation : "",
hash ? hash : "", mime_type ? mime_type : "", file_size);
// Generate SHA-256 hash of the key components for consistent cache keys
unsigned char hash_bytes[32];
if (nostr_sha256((unsigned char*)temp_buffer, strlen(temp_buffer), hash_bytes) == NOSTR_SUCCESS) {
size_t hash_len = (written >= 0 && (size_t)written < sizeof(temp_buffer)) ? (size_t)written : sizeof(temp_buffer) - 1;
if (nostr_sha256((unsigned char*)temp_buffer, hash_len, hash_bytes) == NOSTR_SUCCESS) {
nostr_bytes_to_hex(hash_bytes, 32, cache_key);
cache_key[64] = '\0'; // Ensure null termination
} else {
// Fallback if hashing fails
strncpy(cache_key, temp_buffer, key_size - 1);
cache_key[key_size - 1] = '\0';
// Fallback if hashing fails - safely copy up to key_size - 1 characters
size_t copy_len = (written >= 0 && (size_t)written < key_size - 1) ? (size_t)written : key_size - 1;
memcpy(cache_key, temp_buffer, copy_len);
cache_key[copy_len] = '\0';
}
}
@ -1179,6 +1192,8 @@ int nostr_auth_rule_list(const char* operation, nostr_auth_rule_t** rules, int*
}
void nostr_auth_rules_free(nostr_auth_rule_t* rules, int count) {
(void)count; // Parameter not used in this implementation
if (rules) {
free(rules);
}
@ -1200,6 +1215,8 @@ int nostr_auth_cache_stats(int* hit_count, int* miss_count, int* entries) {
}
int nostr_auth_register_db_backend(const nostr_auth_db_interface_t* backend) {
(void)backend; // Parameter not used in this implementation
// For now, only SQLite backend is supported
return NOSTR_ERROR_AUTH_RULES_BACKEND_NOT_FOUND;
}

View File

@ -0,0 +1,10 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"git.ignoreLimitWarning": true
}
}