From c76f10491a5268808ed6c74ee395341baf2170b6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 10 Dec 2025 13:30:32 -0400 Subject: [PATCH] v0.0.2 - Got it basically working --- main.c | 186 +++++++++++++++++++++++++++++++++++++++-- throw_superball.sh | 200 +++++++++++++++++++++++++++++++++++++++++++++ watch_relays.sh | 183 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 564 insertions(+), 5 deletions(-) create mode 100755 throw_superball.sh create mode 100755 watch_relays.sh diff --git a/main.c b/main.c index 62ffa2c..581e1ec 100644 --- a/main.c +++ b/main.c @@ -29,7 +29,7 @@ #include "cJSON.h" // Version -#define THROWER_VERSION "v0.0.1" +#define THROWER_VERSION "v0.0.2" // Configuration constants #define MAX_RELAYS 50 @@ -177,6 +177,8 @@ static void free_padding_payload(padding_payload_t* payload); // Thrower info functions static int publish_thrower_info(superball_thrower_t* thrower); +static int publish_metadata(superball_thrower_t* thrower); +static int publish_relay_list(superball_thrower_t* thrower); static void* auto_publish_thread_func(void* arg); // Main functions @@ -793,6 +795,13 @@ static void forward_to_next_thrower(superball_thrower_t* thrower, cJSON* event, return; } + // Log the event JSON being published + char* event_json = cJSON_Print(signed_event); + if (event_json) { + log_message(LOG_INFO, "Publishing routing event JSON:\n%s", event_json); + free(event_json); + } + // Publish to relays nostr_relay_pool_publish_async(thrower->pool, (const char**)routing->relays, routing->relay_count, signed_event, @@ -803,14 +812,29 @@ static void forward_to_next_thrower(superball_thrower_t* thrower, cJSON* event, } static void post_final_event(superball_thrower_t* thrower, cJSON* event, routing_payload_t* routing) { + (void)event; // The wrapped event is not used - we publish the inner event from routing + log_message(LOG_INFO, "Posting final event to %d relays", routing->relay_count); - // Publish the inner event directly + // The inner event is in routing->event (this is the kind 1 note, not the kind 22222 wrapper) + if (!routing->event) { + log_message(LOG_ERROR, "No inner event to publish"); + return; + } + + // Log the inner event JSON being published + char* event_json = cJSON_Print(routing->event); + if (event_json) { + log_message(LOG_INFO, "Publishing final event JSON:\n%s", event_json); + free(event_json); + } + + // Publish the inner event directly (this is the actual kind 1 note) nostr_relay_pool_publish_async(thrower->pool, (const char**)routing->relays, - routing->relay_count, event, + routing->relay_count, routing->event, publish_callback, thrower); - cJSON* id = cJSON_GetObjectItem(event, "id"); + cJSON* id = cJSON_GetObjectItem(routing->event, "id"); if (id) { log_message(LOG_INFO, "Final event posted: %.16s...", cJSON_GetStringValue(id)); } @@ -818,12 +842,23 @@ static void post_final_event(superball_thrower_t* thrower, cJSON* event, routing static void publish_callback(const char* relay_url, const char* event_id, int success, const char* message, void* user_data) { - (void)user_data; // Suppress unused parameter warning + superball_thrower_t* thrower = (superball_thrower_t*)user_data; + if (success) { log_message(LOG_INFO, "✅ Published to %s: %.16s...", relay_url, event_id); + + // Print relay response if available + if (message) { + log_message(LOG_DEBUG, "Relay response: %s", message); + } } else { log_message(LOG_ERROR, "❌ Failed to publish to %s: %s", relay_url, message ? message : "unknown error"); } + + // Note: We don't have access to the full event JSON here in the callback + // The event was already published. To see the full event, we'd need to + // log it before calling nostr_relay_pool_publish_async + (void)thrower; // Suppress unused warning for now } static void free_routing_payload(routing_payload_t* payload) { @@ -940,6 +975,143 @@ static int publish_thrower_info(superball_thrower_t* thrower) { return 0; } +static int publish_metadata(superball_thrower_t* thrower) { + log_message(LOG_INFO, "Publishing metadata (kind 0)..."); + + // Create metadata JSON content + cJSON* metadata = cJSON_CreateObject(); + + if (thrower->config->name) { + cJSON_AddStringToObject(metadata, "name", thrower->config->name); + } + + if (thrower->config->description) { + cJSON_AddStringToObject(metadata, "about", thrower->config->description); + } + + // Add Superball-specific fields + if (thrower->config->software) { + cJSON_AddStringToObject(metadata, "nip05", thrower->config->software); + } + + // Add version and supported SUPs + if (thrower->config->version) { + cJSON_AddStringToObject(metadata, "display_name", + thrower->config->version); + } + + if (thrower->config->supported_sups) { + cJSON_AddStringToObject(metadata, "website", + thrower->config->supported_sups); + } + + char* content = cJSON_PrintUnformatted(metadata); + cJSON_Delete(metadata); + + if (!content) { + log_message(LOG_ERROR, "Failed to create metadata JSON"); + return -1; + } + + // Create kind 0 event with empty tags + cJSON* tags = cJSON_CreateArray(); + cJSON* event = nostr_create_and_sign_event(0, content, tags, + thrower->private_key, time(NULL)); + free(content); + cJSON_Delete(tags); + + if (!event) { + log_message(LOG_ERROR, "Failed to create metadata event"); + return -1; + } + + // Get write-capable relays + const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*)); + int relay_count = 0; + for (int i = 0; i < thrower->config->relay_count; i++) { + if (thrower->config->relays[i].write && + strcmp(thrower->config->relays[i].auth_status, "no-auth") == 0) { + relay_urls[relay_count++] = thrower->config->relays[i].url; + } + } + + if (relay_count == 0) { + log_message(LOG_WARN, "No write-capable relays for metadata"); + free(relay_urls); + cJSON_Delete(event); + return -1; + } + + nostr_relay_pool_publish_async(thrower->pool, relay_urls, relay_count, + event, publish_callback, thrower); + + free(relay_urls); + cJSON_Delete(event); + + log_message(LOG_INFO, "Metadata published to %d relays", relay_count); + return 0; +} + +static int publish_relay_list(superball_thrower_t* thrower) { + log_message(LOG_INFO, "Publishing relay list (kind 10002)..."); + + // Create tags array with relay information + cJSON* tags = cJSON_CreateArray(); + + for (int i = 0; i < thrower->config->relay_count; i++) { + cJSON* relay_tag = cJSON_CreateArray(); + cJSON_AddItemToArray(relay_tag, cJSON_CreateString("r")); + cJSON_AddItemToArray(relay_tag, cJSON_CreateString(thrower->config->relays[i].url)); + + // Add read/write markers + if (thrower->config->relays[i].read && thrower->config->relays[i].write) { + // Both read and write - no marker needed (default) + } else if (thrower->config->relays[i].read) { + cJSON_AddItemToArray(relay_tag, cJSON_CreateString("read")); + } else if (thrower->config->relays[i].write) { + cJSON_AddItemToArray(relay_tag, cJSON_CreateString("write")); + } + + cJSON_AddItemToArray(tags, relay_tag); + } + + // Create kind 10002 event with empty content + cJSON* event = nostr_create_and_sign_event(10002, "", tags, + thrower->private_key, time(NULL)); + cJSON_Delete(tags); + + if (!event) { + log_message(LOG_ERROR, "Failed to create relay list event"); + return -1; + } + + // Get write-capable relays + const char** relay_urls = malloc(thrower->config->relay_count * sizeof(char*)); + int relay_count = 0; + for (int i = 0; i < thrower->config->relay_count; i++) { + if (thrower->config->relays[i].write && + strcmp(thrower->config->relays[i].auth_status, "no-auth") == 0) { + relay_urls[relay_count++] = thrower->config->relays[i].url; + } + } + + if (relay_count == 0) { + log_message(LOG_WARN, "No write-capable relays for relay list"); + free(relay_urls); + cJSON_Delete(event); + return -1; + } + + nostr_relay_pool_publish_async(thrower->pool, relay_urls, relay_count, + event, publish_callback, thrower); + + free(relay_urls); + cJSON_Delete(event); + + log_message(LOG_INFO, "Relay list published to %d relays", relay_count); + return 0; +} + static void* auto_publish_thread_func(void* arg) { superball_thrower_t* thrower = (superball_thrower_t*)arg; @@ -1078,6 +1250,10 @@ static int thrower_start(superball_thrower_t* thrower) { log_message(LOG_INFO, "Monitoring %d relays for routing events", thrower->config->relay_count); + // Publish initial metadata and relay list + publish_metadata(thrower); + publish_relay_list(thrower); + // Publish initial thrower info publish_thrower_info(thrower); diff --git a/throw_superball.sh b/throw_superball.sh new file mode 100755 index 0000000..3741961 --- /dev/null +++ b/throw_superball.sh @@ -0,0 +1,200 @@ +#!/bin/bash + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Configuration +CONFIG_FILE="config.json" +DELAY=10 # Default 10 second delay + +show_usage() { + echo "Superball Test Thrower" + echo "" + echo "Usage:" + echo " $0 [options]" + echo "" + echo "Options:" + echo " -c, --config Config file path (default: config.json)" + echo " -d, --delay Delay in seconds (default: 10)" + echo " -m, --message Message to send (default: 'Test superball')" + echo " -h, --help Show this help message" + echo "" + echo "Creates a Superball routing event and throws it to the thrower." +} + +# Default message includes timestamp +MESSAGE="Test superball - $(date '+%Y-%m-%d %H:%M:%S %Z')" + +while [[ $# -gt 0 ]]; do + case $1 in + -c|--config) + CONFIG_FILE="$2" + shift 2 + ;; + -d|--delay) + DELAY="$2" + shift 2 + ;; + -m|--message) + MESSAGE="$2" + shift 2 + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Check if nak is installed +if ! command -v nak &> /dev/null; then + echo -e "${RED}Error: 'nak' command not found${NC}" + echo "Please install nak: https://github.com/fiatjaf/nak" + exit 1 +fi + +# Check if jq is installed +if ! command -v jq &> /dev/null; then + echo -e "${RED}Error: 'jq' command not found${NC}" + exit 1 +fi + +# Load config.json +if [[ ! -f "$CONFIG_FILE" ]]; then + echo -e "${RED}Error: Config file not found: $CONFIG_FILE${NC}" + exit 1 +fi + +echo -e "${BLUE}Loading configuration from $CONFIG_FILE...${NC}" + +# Extract thrower pubkey from config +THROWER_PRIVKEY=$(jq -r '.thrower.privateKey // empty' "$CONFIG_FILE") +if [[ -z "$THROWER_PRIVKEY" ]]; then + echo -e "${RED}Error: Could not find thrower.privateKey in config${NC}" + exit 1 +fi + +THROWER_PUBKEY=$(echo "$THROWER_PRIVKEY" | nak key public) +if [[ -z "$THROWER_PUBKEY" ]]; then + echo -e "${RED}Error: Could not derive thrower pubkey${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Thrower pubkey: ${THROWER_PUBKEY:0:16}...${NC}" + +# Get relays from config +mapfile -t RELAYS < <(jq -r '.relays[].url' "$CONFIG_FILE") +if [[ ${#RELAYS[@]} -eq 0 ]]; then + echo -e "${RED}Error: No relays found in config${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Loaded ${#RELAYS[@]} relay(s)${NC}" + +# Generate ephemeral keypair for this superball +echo -e "${BLUE}Generating ephemeral keypair...${NC}" +EPHEMERAL_PRIVKEY=$(nak key generate) +EPHEMERAL_PUBKEY=$(echo "$EPHEMERAL_PRIVKEY" | nak key public) +echo -e "${GREEN}✓ Ephemeral pubkey: ${EPHEMERAL_PUBKEY:0:16}...${NC}" + +# Generate audit tag (random 32-byte hex) +AUDIT_TAG=$(openssl rand -hex 32) +echo -e "${GREEN}✓ Audit tag: ${AUDIT_TAG:0:16}...${NC}" + +# Create a simple kind 1 note as the inner event +echo -e "${BLUE}Creating inner event (kind 1 note)...${NC}" +INNER_EVENT=$(nak event -c "$MESSAGE" --kind 1 --sec "$EPHEMERAL_PRIVKEY") +echo -e "${GREEN}✓ Inner event created${NC}" + +# Build the routing payload +ROUTING_PAYLOAD=$(jq -n \ + --argjson event "$INNER_EVENT" \ + --arg relays "$(printf '%s\n' "${RELAYS[@]}" | jq -R . | jq -s .)" \ + --argjson delay "$DELAY" \ + --arg audit "$AUDIT_TAG" \ + '{ + event: $event, + routing: { + relays: ($relays | fromjson), + delay: $delay, + padding: "+0", + audit: $audit + } + }') + +echo -e "${CYAN}Routing payload:${NC}" +echo "$ROUTING_PAYLOAD" | jq '.' + +# Encrypt the payload with NIP-44 using thrower's pubkey +echo -e "${BLUE}Encrypting payload with NIP-44...${NC}" +ENCRYPTED_CONTENT=$(nak encrypt --sec "$EPHEMERAL_PRIVKEY" --recipient-pubkey "$THROWER_PUBKEY" "$ROUTING_PAYLOAD") + +if [[ -z "$ENCRYPTED_CONTENT" ]]; then + echo -e "${RED}Error: Failed to encrypt payload${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Payload encrypted (length: ${#ENCRYPTED_CONTENT})${NC}" + +# Create the routing event (kind 22222) +echo -e "${BLUE}Creating routing event (kind 22222)...${NC}" + +# Build tags array +TAGS=$(jq -n \ + --arg thrower "$THROWER_PUBKEY" \ + --arg audit "$AUDIT_TAG" \ + '[ + ["p", $thrower], + ["p", $audit] + ]') + +# Create the event +ROUTING_EVENT=$(nak event \ + --kind 22222 \ + --sec "$EPHEMERAL_PRIVKEY" \ + -c "$ENCRYPTED_CONTENT" \ + -t "p=$THROWER_PUBKEY" \ + -t "p=$AUDIT_TAG") + +if [[ -z "$ROUTING_EVENT" ]]; then + echo -e "${RED}Error: Failed to create routing event${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ Routing event created${NC}" +echo -e "${CYAN}Full routing event JSON:${NC}" +echo "$ROUTING_EVENT" | jq '.' + +# Publish to all relays +echo "" +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${YELLOW}Throwing Superball to relays...${NC}" +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + +for relay in "${RELAYS[@]}"; do + echo -e "${BLUE}Publishing to: $relay${NC}" + RESPONSE=$(echo "$ROUTING_EVENT" | nak event "$relay" 2>&1) + echo "$RESPONSE" + echo "" +done + +echo "" +echo -e "${GREEN}✓ Superball thrown!${NC}" +echo "" +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${YELLOW}Monitoring for audit tag appearance...${NC}" +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "Audit tag: ${GREEN}$AUDIT_TAG${NC}" +echo -e "Expected delay: ${GREEN}${DELAY}s${NC}" +echo -e "Watch with: ${BLUE}nak req -k 22222 --stream '#p=$AUDIT_TAG' ${RELAYS[0]}${NC}" +echo "" \ No newline at end of file diff --git a/watch_relays.sh b/watch_relays.sh new file mode 100755 index 0000000..d6435bd --- /dev/null +++ b/watch_relays.sh @@ -0,0 +1,183 @@ +#!/bin/bash + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Configuration +CONFIG_FILE="config.json" +OUR_PUBKEY="" +RELAYS=() + +show_usage() { + echo "Superball Relay Watcher - Simple JSON Display" + echo "" + echo "Usage:" + echo " $0 [options]" + echo "" + echo "Options:" + echo " -c, --config Config file path (default: config.json)" + echo " -h, --help Show this help message" + echo "" + echo "Watches all relays from config.json for:" + echo " - All events from our thrower (pubkey from config.json)" + echo " - Kind 0 (Metadata), Kind 10002 (Relay List)" + echo " - Kind 12222 (Thrower Info), Kind 22222 (Routing)" +} + +while [[ $# -gt 0 ]]; do + case $1 in + -c|--config) + CONFIG_FILE="$2" + shift 2 + ;; + -h|--help) + show_usage + exit 0 + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Check if nak is installed +if ! command -v nak &> /dev/null; then + echo "Error: 'nak' command not found" + echo "Please install nak: https://github.com/fiatjaf/nak" + exit 1 +fi + +# Check if jq is installed +if ! command -v jq &> /dev/null; then + echo "Error: 'jq' command not found" + exit 1 +fi + +# Load config.json if it exists +if [[ -f "$CONFIG_FILE" ]]; then + echo -e "${BLUE}Loading configuration from $CONFIG_FILE...${NC}" + + # Extract private key and derive public key + PRIVATE_KEY=$(jq -r '.thrower.privateKey // empty' "$CONFIG_FILE") + if [[ -n "$PRIVATE_KEY" ]]; then + OUR_PUBKEY=$(echo "$PRIVATE_KEY" | nak key public) + if [[ -n "$OUR_PUBKEY" ]]; then + echo -e "${GREEN}✓ Our thrower pubkey: ${OUR_PUBKEY:0:16}...${NC}" + fi + fi + + # Get all relays from config + mapfile -t RELAYS < <(jq -r '.relays[].url' "$CONFIG_FILE") + if [[ ${#RELAYS[@]} -gt 0 ]]; then + echo -e "${GREEN}✓ Loaded ${#RELAYS[@]} relay(s) from config${NC}" + for relay in "${RELAYS[@]}"; do + echo -e " - $relay" + done + fi + + echo "" +fi + +if [[ ${#RELAYS[@]} -eq 0 ]]; then + echo "Error: No relays found in config.json" + exit 1 +fi + +if [[ -z "$OUR_PUBKEY" ]]; then + echo "Warning: Could not derive pubkey from config.json" + echo "Will only show kinds 0, 10002, 12222, 22222" + echo "" +fi + +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +if [[ -n "$OUR_PUBKEY" ]]; then + echo -e "${BLUE}Watching:${NC} Events from ${OUR_PUBKEY:0:16}... + kinds 0, 10002, 12222, 22222" +else + echo -e "${BLUE}Watching:${NC} Kinds 0, 10002, 12222, 22222" +fi +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" +echo -e "${GREEN}Listening for events... (Press Ctrl+C to stop)${NC}" +echo "" + +# Build two separate nak commands: +# 1. Watch for kinds 12222 and 22222 from anyone +NAK_CMD1="nak req --stream -k 12222 -k 22222" +for relay in "${RELAYS[@]}"; do + NAK_CMD1="$NAK_CMD1 $relay" +done + +# 2. Watch for all events from our thrower (if we have the pubkey) +NAK_CMD2="" +if [[ -n "$OUR_PUBKEY" ]]; then + NAK_CMD2="nak req --stream -a $OUR_PUBKEY" + for relay in "${RELAYS[@]}"; do + NAK_CMD2="$NAK_CMD2 $relay" + done +fi + +# Combine both subscriptions and stream events +{ + if [[ -n "$NAK_CMD2" ]]; then + # Run both subscriptions in parallel, merge output + eval "$NAK_CMD1" 2>/dev/null & + eval "$NAK_CMD2" 2>/dev/null + else + # Only run the first subscription + eval "$NAK_CMD1" 2>/dev/null + fi +} | while IFS= read -r line; do + # Skip non-JSON lines + if echo "$line" | jq empty 2>/dev/null; then + # Check if it's an event array ["EVENT", "sub_id", {...}] + if echo "$line" | jq -e '.[0] == "EVENT"' > /dev/null 2>&1; then + event=$(echo "$line" | jq '.[2]') + + # Get event details + kind=$(echo "$event" | jq -r '.kind') + pubkey=$(echo "$event" | jq -r '.pubkey') + id=$(echo "$event" | jq -r '.id') + + # Check if it's from our thrower + is_ours="" + if [[ -n "$OUR_PUBKEY" && "$pubkey" == "$OUR_PUBKEY" ]]; then + is_ours=" ${GREEN}[OUR THROWER]${NC}" + fi + + # Display separator and event info + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE}Kind ${kind} | ID: ${id:0:16}...${is_ours}${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + # Display formatted JSON + echo "$event" | jq '.' + echo "" + + # Or if it's a direct event object + elif echo "$line" | jq -e '.kind' > /dev/null 2>&1; then + kind=$(echo "$line" | jq -r '.kind') + pubkey=$(echo "$line" | jq -r '.pubkey') + id=$(echo "$line" | jq -r '.id') + + # Check if it's from our thrower + is_ours="" + if [[ -n "$OUR_PUBKEY" && "$pubkey" == "$OUR_PUBKEY" ]]; then + is_ours=" ${GREEN}[OUR THROWER]${NC}" + fi + + # Display separator and event info + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE}Kind ${kind} | ID: ${id:0:16}...${is_ours}${NC}" + echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + + # Display formatted JSON + echo "$line" | jq '.' + echo "" + fi + fi +done \ No newline at end of file