nip 17, and 59

This commit is contained in:
Laan Tungir 2025-10-03 04:25:10 -04:00
parent 54a6044083
commit 6b95ad37c5
32 changed files with 2605 additions and 24009 deletions

View File

@ -1 +1 @@
0.4.3 0.4.4

View File

@ -58,6 +58,7 @@ FORCE_NIPS=""
VERBOSE=false VERBOSE=false
HELP=false HELP=false
BUILD_TESTS=false BUILD_TESTS=false
BUILD_EXAMPLES=false
NO_COLOR_FLAG=false NO_COLOR_FLAG=false
# Parse command line arguments # Parse command line arguments
@ -83,6 +84,10 @@ while [[ $# -gt 0 ]]; do
BUILD_TESTS=true BUILD_TESTS=true
shift shift
;; ;;
--examples|-e)
BUILD_EXAMPLES=true
shift
;;
--no-color) --no-color)
NO_COLOR_FLAG=true NO_COLOR_FLAG=true
shift shift
@ -119,6 +124,7 @@ if [ "$HELP" = true ]; then
echo " --nips=1,5,6,19 Force specific NIPs (comma-separated)" echo " --nips=1,5,6,19 Force specific NIPs (comma-separated)"
echo " --nips=all Include all available NIPs" echo " --nips=all Include all available NIPs"
echo " --tests, -t Build all test programs in tests/ directory" echo " --tests, -t Build all test programs in tests/ directory"
echo " --examples, -e Build all example programs in examples/ directory"
echo " --verbose, -v Verbose output" echo " --verbose, -v Verbose output"
echo " --no-color Disable colored output" echo " --no-color Disable colored output"
echo " --help, -h Show this help" echo " --help, -h Show this help"
@ -134,9 +140,11 @@ if [ "$HELP" = true ]; then
echo " 006 - Key derivation from mnemonic" echo " 006 - Key derivation from mnemonic"
echo " 011 - Relay information document" echo " 011 - Relay information document"
echo " 013 - Proof of Work" echo " 013 - Proof of Work"
echo " 017 - Private Direct Messages"
echo " 019 - Bech32 encoding (nsec/npub)" echo " 019 - Bech32 encoding (nsec/npub)"
echo " 042 - Authentication of clients to relays" echo " 042 - Authentication of clients to relays"
echo " 044 - Encryption (modern)" echo " 044 - Encryption (modern)"
echo " 059 - Gift Wrap"
echo "" echo ""
echo "Examples:" echo "Examples:"
echo " $0 # Auto-detect NIPs, build for current arch" echo " $0 # Auto-detect NIPs, build for current arch"
@ -185,7 +193,7 @@ print_info "Auto-detecting needed NIPs from your source code..."
NEEDED_NIPS="" NEEDED_NIPS=""
if [ -n "$FORCE_NIPS" ]; then if [ -n "$FORCE_NIPS" ]; then
if [ "$FORCE_NIPS" = "all" ]; then if [ "$FORCE_NIPS" = "all" ]; then
NEEDED_NIPS="001 004 005 006 011 013 019 042 044" NEEDED_NIPS="001 004 005 006 011 013 017 019 042 044 059"
print_info "Forced: Building all available NIPs" print_info "Forced: Building all available NIPs"
else else
# Convert comma-separated list to space-separated with 3-digit format # Convert comma-separated list to space-separated with 3-digit format
@ -220,10 +228,10 @@ else
fi fi
fi fi
# If building tests, include all NIPs to ensure test compatibility # If building tests or examples, include all NIPs to ensure compatibility
if [ "$BUILD_TESTS" = true ] && [ -z "$FORCE_NIPS" ]; then if ([ "$BUILD_TESTS" = true ] || [ "$BUILD_EXAMPLES" = true ]) && [ -z "$FORCE_NIPS" ]; then
NEEDED_NIPS="001 004 005 006 011 013 019 042 044" NEEDED_NIPS="001 004 005 006 011 013 017 019 042 044 059"
print_info "Building tests - including all available NIPs for test compatibility" print_info "Building tests/examples - including all available NIPs for compatibility"
fi fi
# Ensure NIP-001 is always included (required for core functionality) # Ensure NIP-001 is always included (required for core functionality)
@ -508,9 +516,11 @@ for nip in $NEEDED_NIPS; do
006) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-006(Keys)" ;; 006) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-006(Keys)" ;;
011) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-011(Relay-Info)" ;; 011) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-011(Relay-Info)" ;;
013) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-013(PoW)" ;; 013) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-013(PoW)" ;;
017) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-017(DMs)" ;;
019) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-019(Bech32)" ;; 019) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-019(Bech32)" ;;
042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;; 042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;;
044) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-044(Encrypt)" ;; 044) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-044(Encrypt)" ;;
059) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-059(Gift-Wrap)" ;;
esac esac
else else
print_warning "NIP file not found: $NIP_FILE - skipping" print_warning "NIP file not found: $NIP_FILE - skipping"
@ -668,6 +678,52 @@ if [ $AR_RESULT -eq 0 ]; then
echo "" echo ""
fi fi
# Build examples if requested
if [ "$BUILD_EXAMPLES" = true ]; then
print_info "Scanning examples/ directory for example programs..."
if [ ! -d "examples" ]; then
print_warning "examples/ directory not found - skipping example builds"
else
EXAMPLE_COUNT=0
SUCCESS_COUNT=0
# Find all .c files in examples/ directory (not subdirectories)
while IFS= read -r -d '' example_file; do
EXAMPLE_COUNT=$((EXAMPLE_COUNT + 1))
example_name=$(basename "$example_file" .c)
example_exe="examples/$example_name"
print_info "Building example: $example_name"
# Example compilation with system libraries
LINK_FLAGS="-lz -ldl -lpthread -lm $SYSTEM_LIBS"
if [ "$VERBOSE" = true ]; then
print_info " Command: $CC $CFLAGS $INCLUDES \"$example_file\" -o \"$example_exe\" ./$OUTPUT $LINK_FLAGS"
fi
if $CC $CFLAGS $INCLUDES "$example_file" -o "$example_exe" "./$OUTPUT" $LINK_FLAGS; then
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
print_success "Built $example_name"
if [ "$VERBOSE" = true ]; then
print_info " Executable: $example_exe"
fi
else
print_error " Failed to build: $example_name"
fi
done < <(find examples/ -maxdepth 1 -name "*.c" -type f -print0)
if [ $EXAMPLE_COUNT -eq 0 ]; then
print_warning "No .c files found in examples/ directory"
else
print_success "Built $SUCCESS_COUNT/$EXAMPLE_COUNT example programs"
fi
fi
echo ""
fi
echo "Usage in your project:" echo "Usage in your project:"
echo " gcc your_app.c $OUTPUT -lz -ldl -lpthread -lm $SYSTEM_LIBS -o your_app" echo " gcc your_app.c $OUTPUT -lz -ldl -lpthread -lm $SYSTEM_LIBS -o your_app"
echo "" echo ""

24017
debug.log

File diff suppressed because one or more lines are too long

BIN
examples/input_detection Executable file

Binary file not shown.

View File

@ -1,39 +0,0 @@
# Example CMakeLists.txt for a project using nostr_core library
cmake_minimum_required(VERSION 3.12)
project(my_nostr_app VERSION 1.0.0 LANGUAGES C)
set(CMAKE_C_STANDARD 99)
# Method 1: Find installed package
# Uncomment if nostr_core is installed system-wide
# find_package(nostr_core REQUIRED)
# Method 2: Use as subdirectory
# Uncomment if nostr_core is a subdirectory
# add_subdirectory(nostr_core)
# Method 3: Use pkg-config
# Uncomment if using pkg-config
# find_package(PkgConfig REQUIRED)
# pkg_check_modules(NOSTR_CORE REQUIRED nostr_core)
# Create executable
add_executable(my_nostr_app main.c)
# Link with nostr_core
# Choose one of the following based on your integration method:
# Method 1: Installed package
# target_link_libraries(my_nostr_app nostr_core::static)
# Method 2: Subdirectory
# target_link_libraries(my_nostr_app nostr_core_static)
# Method 3: pkg-config
# target_include_directories(my_nostr_app PRIVATE ${NOSTR_CORE_INCLUDE_DIRS})
# target_link_libraries(my_nostr_app ${NOSTR_CORE_LIBRARIES})
# For this example, we'll assume Method 2 (subdirectory)
# Add the parent nostr_core directory
add_subdirectory(../.. nostr_core)
target_link_libraries(my_nostr_app nostr_core_static)

View File

@ -1,186 +0,0 @@
# NOSTR Core Integration Example
This directory contains a complete example showing how to integrate the NOSTR Core library into your own projects.
## What This Example Demonstrates
- **Library Initialization**: Proper setup and cleanup of the NOSTR library
- **Identity Management**: Key generation, bech32 encoding, and format detection
- **Event Creation**: Creating and signing different types of NOSTR events
- **Input Handling**: Processing various input formats (mnemonic, hex, bech32)
- **Utility Functions**: Using helper functions for hex conversion and error handling
- **CMake Integration**: How to integrate the library in your CMake-based project
## Building and Running
### Method 1: Using CMake
```bash
# Create build directory
mkdir build && cd build
# Configure with CMake
cmake ..
# Build
make
# Run the example
./my_nostr_app
```
### Method 2: Manual Compilation
```bash
# Compile directly (assuming you're in the c_nostr root directory)
gcc -I. examples/integration_example/main.c nostr_core.c nostr_crypto.c cjson/cJSON.c -lm -o integration_example
# Run
./integration_example
```
## Expected Output
The example will demonstrate:
1. **Identity Management Demo**
- Generate a new keypair
- Display keys in hex and bech32 format
2. **Event Creation Demo**
- Create a text note event
- Create a profile event
- Display the JSON for both events
3. **Input Handling Demo**
- Process different input formats
- Show format detection and decoding
4. **Utility Functions Demo**
- Hex conversion round-trip
- Error message display
## Integration Patterns
### Pattern 1: CMake Find Package
If NOSTR Core is installed system-wide:
```cmake
find_package(nostr_core REQUIRED)
target_link_libraries(your_app nostr_core::static)
```
### Pattern 2: CMake Subdirectory
If NOSTR Core is a subdirectory of your project:
```cmake
add_subdirectory(nostr_core)
target_link_libraries(your_app nostr_core_static)
```
### Pattern 3: pkg-config
If using pkg-config:
```cmake
find_package(PkgConfig REQUIRED)
pkg_check_modules(NOSTR_CORE REQUIRED nostr_core)
target_include_directories(your_app PRIVATE ${NOSTR_CORE_INCLUDE_DIRS})
target_link_libraries(your_app ${NOSTR_CORE_LIBRARIES})
```
### Pattern 4: Direct Source Integration
Copy the essential files to your project:
```bash
cp nostr_core.{c,h} nostr_crypto.{c,h} your_project/src/
cp -r cjson/ your_project/src/
```
Then compile them with your project sources.
## Code Structure
### main.c Structure
The example is organized into clear demonstration functions:
- `demo_identity_management()` - Key generation and encoding
- `demo_event_creation()` - Creating different event types
- `demo_input_handling()` - Processing various input formats
- `demo_utilities()` - Using utility functions
Each function demonstrates specific aspects of the library while maintaining proper error handling and resource cleanup.
### Key Integration Points
1. **Initialization**
```c
int ret = nostr_init();
if (ret != NOSTR_SUCCESS) {
// Handle error
}
```
2. **Resource Cleanup**
```c
// Always clean up JSON objects
cJSON_Delete(event);
// Clean up library on exit
nostr_cleanup();
```
3. **Error Handling**
```c
if (ret != NOSTR_SUCCESS) {
printf("Error: %s\n", nostr_strerror(ret));
return ret;
}
```
## Customization
You can modify this example for your specific needs:
- Change the `app_config_t` structure to match your application's configuration
- Add additional event types or custom event creation logic
- Integrate with your existing error handling and logging systems
- Add networking functionality using the WebSocket layer
## Dependencies
This example requires:
- C99 compiler (gcc, clang)
- CMake 3.12+ (for CMake build)
- NOSTR Core library and its dependencies
## Testing
You can test different input formats by passing them as command line arguments:
```bash
# Test with mnemonic
./my_nostr_app "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
# Test with hex private key
./my_nostr_app "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
# Test with bech32 nsec
./my_nostr_app "nsec1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
## Next Steps
After studying this example, you can:
1. Integrate the patterns into your own application
2. Explore the WebSocket functionality for relay communication
3. Add support for additional NOSTR event types
4. Implement your own identity persistence layer
5. Add networking and relay management features
For more examples, see the other files in the `examples/` directory.

View File

@ -1,271 +0,0 @@
/*
* Example application demonstrating how to integrate nostr_core into other projects
* This shows a complete workflow from key generation to event publishing
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nostr_core.h"
// Example application configuration
typedef struct {
char* app_name;
char* version;
int debug_mode;
} app_config_t;
static app_config_t g_config = {
.app_name = "My NOSTR App",
.version = "1.0.0",
.debug_mode = 1
};
// Helper function to print hex data
static void print_hex(const char* label, const unsigned char* data, size_t len) {
if (g_config.debug_mode) {
printf("%s: ", label);
for (size_t i = 0; i < len; i++) {
printf("%02x", data[i]);
}
printf("\n");
}
}
// Helper function to print JSON nicely
static void print_event(const char* label, cJSON* event) {
if (!event) {
printf("%s: NULL\n", label);
return;
}
char* json_string = cJSON_Print(event);
if (json_string) {
printf("%s:\n%s\n", label, json_string);
free(json_string);
}
}
// Example: Generate and manage identity
static int demo_identity_management(void) {
printf("\n=== Identity Management Demo ===\n");
unsigned char private_key[32], public_key[32];
char nsec[100], npub[100];
// Generate a new keypair
printf("Generating new keypair...\n");
int ret = nostr_generate_keypair(private_key, public_key);
if (ret != NOSTR_SUCCESS) {
printf("Error generating keypair: %s\n", nostr_strerror(ret));
return ret;
}
print_hex("Private Key", private_key, 32);
print_hex("Public Key", public_key, 32);
// Convert to bech32 format
ret = nostr_key_to_bech32(private_key, "nsec", nsec);
if (ret != NOSTR_SUCCESS) {
printf("Error encoding nsec: %s\n", nostr_strerror(ret));
return ret;
}
ret = nostr_key_to_bech32(public_key, "npub", npub);
if (ret != NOSTR_SUCCESS) {
printf("Error encoding npub: %s\n", nostr_strerror(ret));
return ret;
}
printf("nsec: %s\n", nsec);
printf("npub: %s\n", npub);
return NOSTR_SUCCESS;
}
// Example: Create different types of events
static int demo_event_creation(const unsigned char* private_key) {
printf("\n=== Event Creation Demo ===\n");
// Create a text note
printf("Creating text note...\n");
cJSON* text_event = nostr_create_text_event("Hello from my NOSTR app!", private_key);
if (!text_event) {
printf("Error creating text event\n");
return NOSTR_ERROR_JSON_PARSE;
}
print_event("Text Event", text_event);
// Create a profile event
printf("\nCreating profile event...\n");
cJSON* profile_event = nostr_create_profile_event(
g_config.app_name,
"A sample application demonstrating NOSTR integration",
private_key
);
if (!profile_event) {
printf("Error creating profile event\n");
cJSON_Delete(text_event);
return NOSTR_ERROR_JSON_PARSE;
}
print_event("Profile Event", profile_event);
// Cleanup
cJSON_Delete(text_event);
cJSON_Delete(profile_event);
return NOSTR_SUCCESS;
}
// Example: Handle different input formats
static int demo_input_handling(const char* user_input) {
printf("\n=== Input Handling Demo ===\n");
printf("Processing input: %s\n", user_input);
// Detect input type
int input_type = nostr_detect_input_type(user_input);
switch (input_type) {
case NOSTR_INPUT_MNEMONIC:
printf("Detected: BIP39 Mnemonic\n");
{
unsigned char priv[32], pub[32];
int ret = nostr_derive_keys_from_mnemonic(user_input, 0, priv, pub);
if (ret == NOSTR_SUCCESS) {
print_hex("Derived Private Key", priv, 32);
print_hex("Derived Public Key", pub, 32);
}
}
break;
case NOSTR_INPUT_NSEC_HEX:
printf("Detected: Hex-encoded private key\n");
{
unsigned char decoded[32];
int ret = nostr_decode_nsec(user_input, decoded);
if (ret == NOSTR_SUCCESS) {
print_hex("Decoded Private Key", decoded, 32);
}
}
break;
case NOSTR_INPUT_NSEC_BECH32:
printf("Detected: Bech32-encoded private key (nsec)\n");
{
unsigned char decoded[32];
int ret = nostr_decode_nsec(user_input, decoded);
if (ret == NOSTR_SUCCESS) {
print_hex("Decoded Private Key", decoded, 32);
}
}
break;
default:
printf("Unknown input format\n");
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}
// Example: Demonstrate utility functions
static int demo_utilities(void) {
printf("\n=== Utility Functions Demo ===\n");
// Hex conversion
const char* test_hex = "deadbeef";
unsigned char bytes[4];
char hex_result[9];
printf("Testing hex conversion with: %s\n", test_hex);
int ret = nostr_hex_to_bytes(test_hex, bytes, 4);
if (ret != NOSTR_SUCCESS) {
printf("Error in hex_to_bytes: %s\n", nostr_strerror(ret));
return ret;
}
nostr_bytes_to_hex(bytes, 4, hex_result);
printf("Round-trip result: %s\n", hex_result);
// Error message testing
printf("\nTesting error messages:\n");
for (int i = 0; i >= -10; i--) {
const char* msg = nostr_strerror(i);
if (msg && strlen(msg) > 0) {
printf(" %d: %s\n", i, msg);
}
}
return NOSTR_SUCCESS;
}
int main(int argc, char* argv[]) {
printf("%s v%s\n", g_config.app_name, g_config.version);
printf("NOSTR Core Integration Example\n");
printf("=====================================\n");
// Initialize the NOSTR library
printf("Initializing NOSTR core library...\n");
int ret = nostr_init();
if (ret != NOSTR_SUCCESS) {
printf("Failed to initialize NOSTR library: %s\n", nostr_strerror(ret));
return 1;
}
// Run demonstrations
unsigned char demo_private_key[32];
// 1. Identity management
ret = demo_identity_management();
if (ret != NOSTR_SUCCESS) {
goto cleanup;
}
// Generate a key for other demos
nostr_generate_keypair(demo_private_key, NULL);
// 2. Event creation
ret = demo_event_creation(demo_private_key);
if (ret != NOSTR_SUCCESS) {
goto cleanup;
}
// 3. Input handling (use command line argument if provided)
const char* test_input = (argc > 1) ? argv[1] :
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
ret = demo_input_handling(test_input);
if (ret != NOSTR_SUCCESS && ret != NOSTR_ERROR_INVALID_INPUT) {
goto cleanup;
}
// 4. Utility functions
ret = demo_utilities();
if (ret != NOSTR_SUCCESS) {
goto cleanup;
}
printf("\n=====================================\n");
printf("All demonstrations completed successfully!\n");
printf("\nThis example shows how to:\n");
printf(" • Initialize the NOSTR library\n");
printf(" • Generate and manage keypairs\n");
printf(" • Create and sign different event types\n");
printf(" • Handle various input formats\n");
printf(" • Use utility functions\n");
printf(" • Clean up resources properly\n");
ret = NOSTR_SUCCESS;
cleanup:
// Clean up the NOSTR library
printf("\nCleaning up NOSTR library...\n");
nostr_cleanup();
if (ret == NOSTR_SUCCESS) {
printf("Example completed successfully.\n");
return 0;
} else {
printf("Example failed with error: %s\n", nostr_strerror(ret));
return 1;
}
}

BIN
examples/keypair_generation Executable file

Binary file not shown.

BIN
examples/mnemonic_derivation Executable file

Binary file not shown.

BIN
examples/mnemonic_generation Executable file

Binary file not shown.

BIN
examples/relay_pool Executable file

Binary file not shown.

View File

@ -481,9 +481,9 @@ int main() {
} }
// Create pool with custom reconnection configuration for faster testing // Create pool with custom reconnection configuration for faster testing
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default(); nostr_pool_reconnect_config_t config = *nostr_pool_reconnect_config_default();
config->ping_interval_seconds = 5; // Ping every 5 seconds for testing config.ping_interval_seconds = 5; // Ping every 5 seconds for testing
pool = nostr_relay_pool_create(config); pool = nostr_relay_pool_create(&config);
if (!pool) { if (!pool) {
printf("❌ Failed to create pool\n"); printf("❌ Failed to create pool\n");
break; break;

BIN
examples/send_nip17_dm Executable file

Binary file not shown.

242
examples/send_nip17_dm.c Normal file
View File

@ -0,0 +1,242 @@
/*
* NIP-17 Private Direct Messages - Command Line Application
*
* This example demonstrates how to send NIP-17 private direct messages
* using the Nostr Core Library.
*
* Usage:
* ./send_nip17_dm <recipient_pubkey> <message> [sender_nsec]
*
* Arguments:
* recipient_pubkey: The npub or hex public key of the recipient
* message: The message to send
* sender_nsec: (optional) The nsec private key to use for sending.
* If not provided, uses a default test key.
*
* Example:
* ./send_nip17_dm npub1example... "Hello from NIP-17!" nsec1test...
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "../nostr_core/nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// Default test private key (for demonstration - DO NOT USE IN PRODUCTION)
#define DEFAULT_SENDER_NSEC "nsec12kgt0dv2k2safv6s32w8f89z9uw27e68hjaa0d66c5xvk70ezpwqncd045"
// Default relay for sending DMs
#define DEFAULT_RELAY "wss://relay.laantungir.net"
// Progress callback for publishing
void publish_progress_callback(const char* relay_url, const char* status,
const char* message, int success_count,
int total_relays, int completed_relays, void* user_data) {
(void)user_data;
if (relay_url) {
printf("📡 [%s]: %s", relay_url, status);
if (message) {
printf(" - %s", message);
}
printf(" (%d/%d completed, %d successful)\n", completed_relays, total_relays, success_count);
} else {
printf("📡 PUBLISH COMPLETE: %d/%d successful\n", success_count, total_relays);
}
}
/**
* Convert npub to hex if needed
*/
int convert_pubkey_to_hex(const char* input_pubkey, char* output_hex) {
// Check if it's already hex (64 characters)
if (strlen(input_pubkey) == 64) {
// Assume it's already hex
strcpy(output_hex, input_pubkey);
return 0;
}
// Check if it's an npub (starts with "npub1")
if (strncmp(input_pubkey, "npub1", 5) == 0) {
// Convert npub to hex
unsigned char pubkey_bytes[32];
if (nostr_decode_npub(input_pubkey, pubkey_bytes) != 0) {
fprintf(stderr, "Error: Invalid npub format\n");
return -1;
}
nostr_bytes_to_hex(pubkey_bytes, 32, output_hex);
return 0;
}
fprintf(stderr, "Error: Public key must be 64-character hex or valid npub\n");
return -1;
}
/**
* Convert nsec to private key bytes if needed
*/
int convert_nsec_to_private_key(const char* input_nsec, unsigned char* private_key) {
// Check if it's already hex (64 characters)
if (strlen(input_nsec) == 64) {
// Convert hex to bytes
if (nostr_hex_to_bytes(input_nsec, private_key, 32) != 0) {
fprintf(stderr, "Error: Invalid hex private key\n");
return -1;
}
return 0;
}
// Check if it's an nsec (starts with "nsec1")
if (strncmp(input_nsec, "nsec1", 5) == 0) {
// Convert nsec directly to private key bytes
if (nostr_decode_nsec(input_nsec, private_key) != 0) {
fprintf(stderr, "Error: Invalid nsec format\n");
return -1;
}
return 0;
}
fprintf(stderr, "Error: Private key must be 64-character hex or valid nsec\n");
return -1;
}
/**
* Main function
*/
int main(int argc, char* argv[]) {
if (argc < 3 || argc > 4) {
fprintf(stderr, "Usage: %s <recipient_pubkey> <message> [sender_nsec]\n\n", argv[0]);
fprintf(stderr, "Arguments:\n");
fprintf(stderr, " recipient_pubkey: npub or hex public key of recipient\n");
fprintf(stderr, " message: The message to send\n");
fprintf(stderr, " sender_nsec: (optional) nsec private key. Uses test key if not provided.\n\n");
fprintf(stderr, "Example:\n");
fprintf(stderr, " %s npub1example... \"Hello!\" nsec1test...\n", argv[0]);
return 1;
}
const char* recipient_pubkey_input = argv[1];
const char* message = argv[2];
const char* sender_nsec_input = (argc >= 4) ? argv[3] : DEFAULT_SENDER_NSEC;
printf("🧪 NIP-17 Private Direct Message Sender\n");
printf("======================================\n\n");
// Initialize crypto
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize crypto\n");
return 1;
}
// Convert recipient pubkey
char recipient_pubkey_hex[65];
if (convert_pubkey_to_hex(recipient_pubkey_input, recipient_pubkey_hex) != 0) {
return 1;
}
// Convert sender private key
unsigned char sender_privkey[32];
if (convert_nsec_to_private_key(sender_nsec_input, sender_privkey) != 0) {
return 1;
}
// Derive sender public key for display
unsigned char sender_pubkey_bytes[32];
char sender_pubkey_hex[65];
if (nostr_ec_public_key_from_private_key(sender_privkey, sender_pubkey_bytes) != 0) {
fprintf(stderr, "Failed to derive sender public key\n");
return 1;
}
nostr_bytes_to_hex(sender_pubkey_bytes, 32, sender_pubkey_hex);
printf("📤 Sender: %s\n", sender_pubkey_hex);
printf("📥 Recipient: %s\n", recipient_pubkey_hex);
printf("💬 Message: %s\n", message);
printf("🌐 Relay: %s\n\n", DEFAULT_RELAY);
// Create DM event
printf("💬 Creating DM event...\n");
const char* recipient_pubkeys[] = {recipient_pubkey_hex};
cJSON* dm_event = nostr_nip17_create_chat_event(
message,
recipient_pubkeys,
1,
"NIP-17 CLI", // subject
NULL, // no reply
DEFAULT_RELAY, // relay hint
sender_pubkey_hex
);
if (!dm_event) {
fprintf(stderr, "Failed to create DM event\n");
return 1;
}
printf("✅ Created DM event (kind 14)\n");
// Send DM (create gift wraps)
printf("🎁 Creating gift wraps...\n");
cJSON* gift_wraps[10]; // Max 10 gift wraps
int gift_wrap_count = nostr_nip17_send_dm(
dm_event,
recipient_pubkeys,
1,
sender_privkey,
gift_wraps,
10
);
cJSON_Delete(dm_event); // Original DM event no longer needed
if (gift_wrap_count <= 0) {
fprintf(stderr, "Failed to create gift wraps\n");
return 1;
}
printf("✅ Created %d gift wrap(s)\n", gift_wrap_count);
// Publish the gift wrap to relay
printf("\n📤 Publishing gift wrap to relay...\n");
const char* relay_urls[] = {DEFAULT_RELAY};
int success_count = 0;
publish_result_t* publish_results = synchronous_publish_event_with_progress(
relay_urls,
1, // single relay
gift_wraps[0], // Send the first gift wrap
&success_count,
10, // 10 second timeout
publish_progress_callback,
NULL, // no user data
0, // NIP-42 disabled
NULL // no private key for auth
);
if (!publish_results || success_count != 1) {
fprintf(stderr, "\n❌ Failed to publish gift wrap (success_count: %d)\n", success_count);
// Clean up gift wraps
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
if (publish_results) free(publish_results);
return 1;
}
printf("\n✅ Successfully published NIP-17 DM!\n");
// Clean up
free(publish_results);
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
nostr_cleanup();
printf("\n🎉 DM sent successfully! The recipient can now decrypt it using their private key.\n");
return 0;
}

BIN
examples/simple_keygen Executable file

Binary file not shown.

BIN
examples/utility_functions Executable file

Binary file not shown.

BIN
examples/version_test Executable file

Binary file not shown.

View File

@ -139,7 +139,8 @@ print_info "Creating git tag: $NEW_VERSION"
git tag "$NEW_VERSION" git tag "$NEW_VERSION"
print_info "Pushing commits and tags..." print_info "Pushing commits and tags..."
git push origin main CURRENT_BRANCH=$(git branch --show-current)
git push origin "$CURRENT_BRANCH"
git push origin "$NEW_VERSION" git push origin "$NEW_VERSION"
print_success "Version $NEW_VERSION successfully released!" print_success "Version $NEW_VERSION successfully released!"

View File

@ -85,6 +85,14 @@ typedef struct relay_connection {
subscription_timing_t pending_subscriptions[NOSTR_POOL_MAX_PENDING_SUBSCRIPTIONS]; subscription_timing_t pending_subscriptions[NOSTR_POOL_MAX_PENDING_SUBSCRIPTIONS];
int pending_subscription_count; int pending_subscription_count;
// Error reporting for publish operations
char last_publish_error[512]; // Last error message from relay publish response
time_t last_publish_error_time; // When the error occurred
// Error reporting for connection operations
char last_connection_error[512]; // Last error message from relay connection
time_t last_connection_error_time; // When the connection error occurred
// Statistics // Statistics
nostr_relay_stats_t stats; nostr_relay_stats_t stats;
} relay_connection_t; } relay_connection_t;
@ -236,6 +244,10 @@ static int ensure_relay_connection(relay_connection_t* relay) {
relay->status = NOSTR_POOL_RELAY_ERROR; relay->status = NOSTR_POOL_RELAY_ERROR;
relay->reconnect_attempts++; relay->reconnect_attempts++;
relay->stats.connection_failures++; relay->stats.connection_failures++;
// Set connection error message
snprintf(relay->last_connection_error, sizeof(relay->last_connection_error),
"Failed to create WebSocket client for relay %s", relay->url);
relay->last_connection_error_time = time(NULL);
return -1; return -1;
} }
@ -257,6 +269,11 @@ static int ensure_relay_connection(relay_connection_t* relay) {
relay->reconnect_attempts++; relay->reconnect_attempts++;
relay->stats.connection_failures++; relay->stats.connection_failures++;
// Set connection error message
snprintf(relay->last_connection_error, sizeof(relay->last_connection_error),
"WebSocket connection failed for relay %s (state: %d)", relay->url, state);
relay->last_connection_error_time = time(NULL);
// Close the failed connection // Close the failed connection
nostr_ws_close(relay->ws_client); nostr_ws_close(relay->ws_client);
relay->ws_client = NULL; relay->ws_client = NULL;
@ -448,6 +465,11 @@ nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* confi
return pool; return pool;
} }
// Compatibility wrapper for old API
nostr_relay_pool_t* nostr_relay_pool_create_compat(void) {
return nostr_relay_pool_create(NULL);
}
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url) { int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url) {
if (!pool || !relay_url || pool->relay_count >= NOSTR_POOL_MAX_RELAYS) { if (!pool || !relay_url || pool->relay_count >= NOSTR_POOL_MAX_RELAYS) {
return NOSTR_ERROR_INVALID_INPUT; return NOSTR_ERROR_INVALID_INPUT;
@ -755,6 +777,9 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
relay->stats.last_event_time = time(NULL); relay->stats.last_event_time = time(NULL);
// Debug: Print all message types received
printf("📨 DEBUG: Received %s message from %s\n", msg_type, relay->url);
if (strcmp(msg_type, "EVENT") == 0) { if (strcmp(msg_type, "EVENT") == 0) {
// Handle EVENT message: ["EVENT", subscription_id, event] // Handle EVENT message: ["EVENT", subscription_id, event]
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) { if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
@ -887,6 +912,19 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
relay->stats.events_published_ok++; relay->stats.events_published_ok++;
} else { } else {
relay->stats.events_published_failed++; relay->stats.events_published_failed++;
// Store error message if available
if (cJSON_GetArraySize(parsed) >= 4) {
cJSON* error_msg = cJSON_GetArrayItem(parsed, 3);
if (cJSON_IsString(error_msg)) {
const char* msg = cJSON_GetStringValue(error_msg);
if (msg) {
strncpy(relay->last_publish_error, msg,
sizeof(relay->last_publish_error) - 1);
relay->last_publish_error[sizeof(relay->last_publish_error) - 1] = '\0';
relay->last_publish_error_time = time(NULL);
}
}
}
} }
} }
} }
@ -1273,6 +1311,50 @@ double nostr_relay_pool_get_relay_query_latency(
return relay->stats.query_latency_avg; return relay->stats.query_latency_avg;
} }
const char* nostr_relay_pool_get_relay_last_publish_error(
nostr_relay_pool_t* pool,
const char* relay_url) {
if (!pool || !relay_url) {
return NULL;
}
relay_connection_t* relay = find_relay_by_url(pool, relay_url);
if (!relay) {
return NULL;
}
// Return error message only if it's recent (within last 60 seconds)
if (relay->last_publish_error_time > 0 &&
time(NULL) - relay->last_publish_error_time < 60) {
return relay->last_publish_error[0] != '\0' ? relay->last_publish_error : NULL;
}
return NULL;
}
const char* nostr_relay_pool_get_relay_last_connection_error(
nostr_relay_pool_t* pool,
const char* relay_url) {
if (!pool || !relay_url) {
return NULL;
}
relay_connection_t* relay = find_relay_by_url(pool, relay_url);
if (!relay) {
return NULL;
}
// Return error message only if it's recent (within last 60 seconds)
if (relay->last_connection_error_time > 0 &&
time(NULL) - relay->last_connection_error_time < 60) {
return relay->last_connection_error[0] != '\0' ? relay->last_connection_error : NULL;
}
return NULL;
}
int nostr_relay_pool_ping_relay( int nostr_relay_pool_ping_relay(
nostr_relay_pool_t* pool, nostr_relay_pool_t* pool,
const char* relay_url) { const char* relay_url) {

406
nostr_core/nip017.c Normal file
View File

@ -0,0 +1,406 @@
/*
* NIP-17: Private Direct Messages Implementation
* https://github.com/nostr-protocol/nips/blob/master/17.md
*/
#define _GNU_SOURCE
#include "nip017.h"
#include "nip059.h"
#include "nip001.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
/**
* Create tags array for DM events
*/
static cJSON* create_dm_tags(const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url) {
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// Add "p" tags for each recipient
for (int i = 0; i < num_recipients; i++) {
cJSON* p_tag = cJSON_CreateArray();
if (!p_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkeys[i]));
// Add relay URL if provided (recommended)
if (reply_relay_url) {
cJSON_AddItemToArray(p_tag, cJSON_CreateString(reply_relay_url));
}
cJSON_AddItemToArray(tags, p_tag);
}
// Add subject tag if provided
if (subject && strlen(subject) > 0) {
cJSON* subject_tag = cJSON_CreateArray();
if (!subject_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(subject_tag, cJSON_CreateString("subject"));
cJSON_AddItemToArray(subject_tag, cJSON_CreateString(subject));
cJSON_AddItemToArray(tags, subject_tag);
}
// Add reply reference if provided
if (reply_to_event_id && strlen(reply_to_event_id) > 0) {
cJSON* e_tag = cJSON_CreateArray();
if (!e_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_to_event_id));
if (reply_relay_url) {
cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_relay_url));
}
// For replies, add "reply" marker as per NIP-17
cJSON_AddItemToArray(e_tag, cJSON_CreateString("reply"));
cJSON_AddItemToArray(tags, e_tag);
}
return tags;
}
/**
* NIP-17: Create a chat message event (kind 14)
*/
cJSON* nostr_nip17_create_chat_event(const char* message,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex) {
if (!message || !recipient_pubkeys || num_recipients <= 0 || !sender_pubkey_hex) {
return NULL;
}
// Create tags
cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject,
reply_to_event_id, reply_relay_url);
if (!tags) {
return NULL;
}
// Create the chat rumor (kind 14, unsigned)
cJSON* chat_event = nostr_nip59_create_rumor(14, message, tags, sender_pubkey_hex, 0);
cJSON_Delete(tags); // Tags are duplicated in create_rumor
return chat_event;
}
/**
* NIP-17: Create a file message event (kind 15)
*/
cJSON* nostr_nip17_create_file_event(const char* file_url,
const char* file_type,
const char* encryption_algorithm,
const char* decryption_key,
const char* decryption_nonce,
const char* file_hash,
const char* original_file_hash,
size_t file_size,
const char* dimensions,
const char* blurhash,
const char* thumbnail_url,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex) {
if (!file_url || !file_type || !encryption_algorithm || !decryption_key ||
!decryption_nonce || !file_hash || !recipient_pubkeys ||
num_recipients <= 0 || !sender_pubkey_hex) {
return NULL;
}
// Create base tags
cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject,
reply_to_event_id, reply_relay_url);
if (!tags) {
return NULL;
}
// Add file-specific tags
cJSON* file_type_tag = cJSON_CreateArray();
cJSON_AddItemToArray(file_type_tag, cJSON_CreateString("file-type"));
cJSON_AddItemToArray(file_type_tag, cJSON_CreateString(file_type));
cJSON_AddItemToArray(tags, file_type_tag);
cJSON* encryption_tag = cJSON_CreateArray();
cJSON_AddItemToArray(encryption_tag, cJSON_CreateString("encryption-algorithm"));
cJSON_AddItemToArray(encryption_tag, cJSON_CreateString(encryption_algorithm));
cJSON_AddItemToArray(tags, encryption_tag);
cJSON* key_tag = cJSON_CreateArray();
cJSON_AddItemToArray(key_tag, cJSON_CreateString("decryption-key"));
cJSON_AddItemToArray(key_tag, cJSON_CreateString(decryption_key));
cJSON_AddItemToArray(tags, key_tag);
cJSON* nonce_tag = cJSON_CreateArray();
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("decryption-nonce"));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(decryption_nonce));
cJSON_AddItemToArray(tags, nonce_tag);
cJSON* x_tag = cJSON_CreateArray();
cJSON_AddItemToArray(x_tag, cJSON_CreateString("x"));
cJSON_AddItemToArray(x_tag, cJSON_CreateString(file_hash));
cJSON_AddItemToArray(tags, x_tag);
// Optional tags
if (original_file_hash && strlen(original_file_hash) > 0) {
cJSON* ox_tag = cJSON_CreateArray();
cJSON_AddItemToArray(ox_tag, cJSON_CreateString("ox"));
cJSON_AddItemToArray(ox_tag, cJSON_CreateString(original_file_hash));
cJSON_AddItemToArray(tags, ox_tag);
}
if (file_size > 0) {
char size_str[32];
snprintf(size_str, sizeof(size_str), "%zu", file_size);
cJSON* size_tag = cJSON_CreateArray();
cJSON_AddItemToArray(size_tag, cJSON_CreateString("size"));
cJSON_AddItemToArray(size_tag, cJSON_CreateString(size_str));
cJSON_AddItemToArray(tags, size_tag);
}
if (dimensions && strlen(dimensions) > 0) {
cJSON* dim_tag = cJSON_CreateArray();
cJSON_AddItemToArray(dim_tag, cJSON_CreateString("dim"));
cJSON_AddItemToArray(dim_tag, cJSON_CreateString(dimensions));
cJSON_AddItemToArray(tags, dim_tag);
}
if (blurhash && strlen(blurhash) > 0) {
cJSON* blurhash_tag = cJSON_CreateArray();
cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString("blurhash"));
cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString(blurhash));
cJSON_AddItemToArray(tags, blurhash_tag);
}
if (thumbnail_url && strlen(thumbnail_url) > 0) {
cJSON* thumb_tag = cJSON_CreateArray();
cJSON_AddItemToArray(thumb_tag, cJSON_CreateString("thumb"));
cJSON_AddItemToArray(thumb_tag, cJSON_CreateString(thumbnail_url));
cJSON_AddItemToArray(tags, thumb_tag);
}
// Create the file rumor (kind 15, unsigned)
cJSON* file_event = nostr_nip59_create_rumor(15, file_url, tags, sender_pubkey_hex, 0);
cJSON_Delete(tags); // Tags are duplicated in create_rumor
return file_event;
}
/**
* NIP-17: Create a relay list event (kind 10050)
*/
cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
int num_relays,
const unsigned char* private_key) {
if (!relay_urls || num_relays <= 0 || !private_key) {
return NULL;
}
// Get public key
unsigned char public_key[32];
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
return NULL;
}
char pubkey_hex[65];
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
// Create tags with relay URLs
cJSON* tags = cJSON_CreateArray();
if (!tags) {
return NULL;
}
for (int i = 0; i < num_relays; i++) {
cJSON* relay_tag = cJSON_CreateArray();
if (!relay_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay"));
cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_urls[i]));
cJSON_AddItemToArray(tags, relay_tag);
}
// Create and sign the event
cJSON* relay_event = nostr_create_and_sign_event(10050, "", tags, private_key, time(NULL));
cJSON_Delete(tags); // Tags are duplicated in create_and_sign_event
return relay_event;
}
/**
* NIP-17: Send a direct message to recipients
*/
int nostr_nip17_send_dm(cJSON* dm_event,
const char** recipient_pubkeys,
int num_recipients,
const unsigned char* sender_private_key,
cJSON** gift_wraps_out,
int max_gift_wraps) {
if (!dm_event || !recipient_pubkeys || num_recipients <= 0 ||
!sender_private_key || !gift_wraps_out || max_gift_wraps <= 0) {
return -1;
}
int created_wraps = 0;
for (int i = 0; i < num_recipients && created_wraps < max_gift_wraps; i++) {
// Convert recipient pubkey hex to bytes
unsigned char recipient_public_key[32];
if (nostr_hex_to_bytes(recipient_pubkeys[i], recipient_public_key, 32) != 0) {
continue; // Skip invalid pubkeys
}
// Create seal for this recipient
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key);
if (!seal) {
continue; // Skip if sealing fails
}
// Create gift wrap for this recipient
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i]);
cJSON_Delete(seal); // Seal is now wrapped
if (!gift_wrap) {
continue; // Skip if wrapping fails
}
gift_wraps_out[created_wraps++] = gift_wrap;
}
// Also create a gift wrap for the sender (so they can see their own messages)
if (created_wraps < max_gift_wraps) {
// Get sender's public key
unsigned char sender_public_key[32];
if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) == 0) {
char sender_pubkey_hex[65];
nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex);
// Create seal for sender
cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key);
if (sender_seal) {
// Create gift wrap for sender
cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex);
cJSON_Delete(sender_seal);
if (sender_gift_wrap) {
gift_wraps_out[created_wraps++] = sender_gift_wrap;
}
}
}
}
return created_wraps;
}
/**
* NIP-17: Receive and decrypt a direct message
*/
cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap,
const unsigned char* recipient_private_key) {
if (!gift_wrap || !recipient_private_key) {
return NULL;
}
// Unwrap the gift wrap to get the seal
cJSON* seal = nostr_nip59_unwrap_gift(gift_wrap, recipient_private_key);
if (!seal) {
return NULL;
}
// Get sender's public key from the seal
cJSON* seal_pubkey_item = cJSON_GetObjectItem(seal, "pubkey");
if (!seal_pubkey_item || !cJSON_IsString(seal_pubkey_item)) {
cJSON_Delete(seal);
return NULL;
}
const char* sender_pubkey_hex = cJSON_GetStringValue(seal_pubkey_item);
// Convert sender pubkey hex to bytes
unsigned char sender_public_key[32];
if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) {
cJSON_Delete(seal);
return NULL;
}
// Unseal the rumor
cJSON* rumor = nostr_nip59_unseal_rumor(seal, sender_public_key, recipient_private_key);
cJSON_Delete(seal); // Seal is no longer needed
return rumor;
}
/**
* NIP-17: Extract DM relay URLs from a user's kind 10050 event
*/
int nostr_nip17_extract_dm_relays(cJSON* relay_list_event,
char** relay_urls_out,
int max_relays) {
if (!relay_list_event || !relay_urls_out || max_relays <= 0) {
return -1;
}
// Check if this is a kind 10050 event
cJSON* kind_item = cJSON_GetObjectItem(relay_list_event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || cJSON_GetNumberValue(kind_item) != 10050) {
return -1;
}
// Get tags array
cJSON* tags_item = cJSON_GetObjectItem(relay_list_event, "tags");
if (!tags_item || !cJSON_IsArray(tags_item)) {
return -1;
}
int extracted = 0;
cJSON* tag_item;
cJSON_ArrayForEach(tag_item, tags_item) {
if (!cJSON_IsArray(tag_item) || cJSON_GetArraySize(tag_item) < 2) {
continue;
}
// Check if this is a "relay" tag
cJSON* tag_name = cJSON_GetArrayItem(tag_item, 0);
cJSON* relay_url = cJSON_GetArrayItem(tag_item, 1);
if (cJSON_IsString(tag_name) && cJSON_IsString(relay_url) &&
strcmp(cJSON_GetStringValue(tag_name), "relay") == 0) {
if (extracted < max_relays) {
relay_urls_out[extracted] = strdup(cJSON_GetStringValue(relay_url));
if (relay_urls_out[extracted]) {
extracted++;
}
}
}
}
return extracted;
}

137
nostr_core/nip017.h Normal file
View File

@ -0,0 +1,137 @@
/*
* NIP-17: Private Direct Messages
* https://github.com/nostr-protocol/nips/blob/master/17.md
*/
#ifndef NOSTR_NIP017_H
#define NOSTR_NIP017_H
#include <stddef.h>
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* NIP-17: Create a chat message event (kind 14)
*
* @param message Plain text message content
* @param recipient_pubkeys Array of recipient public keys (hex strings)
* @param num_recipients Number of recipients
* @param subject Optional conversation subject/title (can be NULL)
* @param reply_to_event_id Optional event ID this message replies to (can be NULL)
* @param reply_relay_url Optional relay URL for reply reference (can be NULL)
* @param sender_pubkey_hex Sender's public key in hex format
* @return cJSON object representing the unsigned chat event, or NULL on error
*/
cJSON* nostr_nip17_create_chat_event(const char* message,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex);
/**
* NIP-17: Create a file message event (kind 15)
*
* @param file_url URL of the encrypted file
* @param file_type MIME type of the original file (e.g., "image/jpeg")
* @param encryption_algorithm Encryption algorithm used ("aes-gcm")
* @param decryption_key Base64-encoded decryption key
* @param decryption_nonce Base64-encoded decryption nonce
* @param file_hash SHA-256 hash of the encrypted file (hex)
* @param original_file_hash SHA-256 hash of the original file before encryption (hex, optional)
* @param file_size Size of encrypted file in bytes (optional, 0 to skip)
* @param dimensions Image dimensions in "WxH" format (optional, NULL to skip)
* @param blurhash Blurhash for preview (optional, NULL to skip)
* @param thumbnail_url URL of encrypted thumbnail (optional, NULL to skip)
* @param recipient_pubkeys Array of recipient public keys (hex strings)
* @param num_recipients Number of recipients
* @param subject Optional conversation subject/title (can be NULL)
* @param reply_to_event_id Optional event ID this message replies to (can be NULL)
* @param reply_relay_url Optional relay URL for reply reference (can be NULL)
* @param sender_pubkey_hex Sender's public key in hex format
* @return cJSON object representing the unsigned file event, or NULL on error
*/
cJSON* nostr_nip17_create_file_event(const char* file_url,
const char* file_type,
const char* encryption_algorithm,
const char* decryption_key,
const char* decryption_nonce,
const char* file_hash,
const char* original_file_hash,
size_t file_size,
const char* dimensions,
const char* blurhash,
const char* thumbnail_url,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex);
/**
* NIP-17: Create a relay list event (kind 10050)
*
* @param relay_urls Array of relay URLs for DM delivery
* @param num_relays Number of relay URLs
* @param private_key Sender's private key for signing
* @return cJSON object representing the signed relay list event, or NULL on error
*/
cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
int num_relays,
const unsigned char* private_key);
/**
* NIP-17: Send a direct message to recipients
*
* This function creates the appropriate rumor, seals it, gift wraps it,
* and returns the final gift wrap events ready for publishing.
*
* @param dm_event The unsigned DM event (kind 14 or 15)
* @param recipient_pubkeys Array of recipient public keys (hex strings)
* @param num_recipients Number of recipients
* @param sender_private_key 32-byte sender private key
* @param gift_wraps_out Array to store resulting gift wrap events (caller must free)
* @param max_gift_wraps Maximum number of gift wraps to create
* @return Number of gift wrap events created, or -1 on error
*/
int nostr_nip17_send_dm(cJSON* dm_event,
const char** recipient_pubkeys,
int num_recipients,
const unsigned char* sender_private_key,
cJSON** gift_wraps_out,
int max_gift_wraps);
/**
* NIP-17: Receive and decrypt a direct message
*
* This function unwraps a gift wrap, unseals the rumor, and returns the original DM event.
*
* @param gift_wrap The received gift wrap event (kind 1059)
* @param recipient_private_key 32-byte recipient private key
* @return cJSON object representing the decrypted DM event, or NULL on error
*/
cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap,
const unsigned char* recipient_private_key);
/**
* NIP-17: Extract DM relay URLs from a user's kind 10050 event
*
* @param relay_list_event The kind 10050 event
* @param relay_urls_out Array to store extracted relay URLs (caller must free)
* @param max_relays Maximum number of relays to extract
* @return Number of relay URLs extracted, or -1 on error
*/
int nostr_nip17_extract_dm_relays(cJSON* relay_list_event,
char** relay_urls_out,
int max_relays);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_NIP017_H

406
nostr_core/nip059.c Normal file
View File

@ -0,0 +1,406 @@
/*
* NIP-59: Gift Wrap Implementation
* https://github.com/nostr-protocol/nips/blob/master/59.md
*/
#include "nip059.h"
#include "nip044.h"
#include "nip001.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
int nostr_ec_sign(const unsigned char* private_key, const unsigned char* hash, unsigned char* signature);
// Memory clearing utility
static void memory_clear(const void *p, size_t len) {
if (p && len) {
memset((void *)p, 0, len);
}
}
/**
* Create a random timestamp within 2 days in the past (as per NIP-59 spec)
*/
static time_t random_past_timestamp(void) {
time_t now = time(NULL);
// Random time up to 2 days (172800 seconds) in the past
long random_offset = (long)(rand() % 172800);
return now - random_offset;
}
/**
* Generate a random private key for gift wrap
*/
static int generate_random_private_key(unsigned char* private_key) {
return nostr_secp256k1_get_random_bytes(private_key, 32);
}
/**
* Create event ID from event data (without signature)
*/
static int create_event_id(cJSON* event, char* event_id_hex) {
if (!event || !event_id_hex) {
return -1;
}
// Get event fields for serialization
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at_item = cJSON_GetObjectItem(event, "created_at");
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags_item = cJSON_GetObjectItem(event, "tags");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) {
return -1;
}
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
cJSON* serialize_array = cJSON_CreateArray();
if (!serialize_array) {
return -1;
}
cJSON_AddItemToArray(serialize_array, cJSON_CreateNumber(0));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(pubkey_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(created_at_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(kind_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(tags_item, 1));
cJSON_AddItemToArray(serialize_array, cJSON_Duplicate(content_item, 1));
char* serialize_string = cJSON_PrintUnformatted(serialize_array);
cJSON_Delete(serialize_array);
if (!serialize_string) {
return -1;
}
// Hash the serialized event
unsigned char event_hash[32];
if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) {
free(serialize_string);
return -1;
}
// Convert hash to hex
nostr_bytes_to_hex(event_hash, 32, event_id_hex);
free(serialize_string);
return 0;
}
/**
* NIP-59: Create a rumor (unsigned event)
*/
cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
const char* pubkey_hex, time_t created_at) {
if (!pubkey_hex || !content) {
return NULL;
}
// Use provided timestamp or random past timestamp
time_t event_time = (created_at == 0) ? random_past_timestamp() : created_at;
// Create event structure (without id and sig - that's what makes it a rumor)
cJSON* rumor = cJSON_CreateObject();
if (!rumor) {
return NULL;
}
cJSON_AddStringToObject(rumor, "pubkey", pubkey_hex);
cJSON_AddNumberToObject(rumor, "created_at", (double)event_time);
cJSON_AddNumberToObject(rumor, "kind", kind);
// Add tags (copy provided tags or create empty array)
if (tags) {
cJSON_AddItemToObject(rumor, "tags", cJSON_Duplicate(tags, 1));
} else {
cJSON_AddItemToObject(rumor, "tags", cJSON_CreateArray());
}
cJSON_AddStringToObject(rumor, "content", content);
// Calculate and add event ID
char event_id[65];
if (create_event_id(rumor, event_id) != 0) {
cJSON_Delete(rumor);
return NULL;
}
cJSON_AddStringToObject(rumor, "id", event_id);
return rumor;
}
/**
* NIP-59: Create a seal (kind 13) wrapping a rumor
*/
cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key,
const unsigned char* recipient_public_key) {
if (!rumor || !sender_private_key || !recipient_public_key) {
return NULL;
}
// Serialize the rumor to JSON
char* rumor_json = cJSON_PrintUnformatted(rumor);
if (!rumor_json) {
return NULL;
}
// Encrypt the rumor using NIP-44
char encrypted_content[4096]; // Should be large enough for most events
int encrypt_result = nostr_nip44_encrypt(sender_private_key, recipient_public_key,
rumor_json, encrypted_content, sizeof(encrypted_content));
free(rumor_json);
if (encrypt_result != NOSTR_SUCCESS) {
return NULL;
}
// Get sender's public key
unsigned char sender_public_key[32];
if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) != 0) {
return NULL;
}
char sender_pubkey_hex[65];
nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex);
// Create seal event (kind 13)
cJSON* seal = cJSON_CreateObject();
if (!seal) {
return NULL;
}
time_t seal_time = random_past_timestamp();
cJSON_AddStringToObject(seal, "pubkey", sender_pubkey_hex);
cJSON_AddNumberToObject(seal, "created_at", (double)seal_time);
cJSON_AddNumberToObject(seal, "kind", 13);
cJSON_AddItemToObject(seal, "tags", cJSON_CreateArray()); // Empty tags array
cJSON_AddStringToObject(seal, "content", encrypted_content);
// Calculate event ID
char event_id[65];
if (create_event_id(seal, event_id) != 0) {
cJSON_Delete(seal);
return NULL;
}
cJSON_AddStringToObject(seal, "id", event_id);
// Sign the seal
unsigned char event_hash[32];
if (nostr_hex_to_bytes(event_id, event_hash, 32) != 0) {
cJSON_Delete(seal);
return NULL;
}
unsigned char signature[64];
if (nostr_ec_sign(sender_private_key, event_hash, signature) != 0) {
cJSON_Delete(seal);
return NULL;
}
char sig_hex[129];
nostr_bytes_to_hex(signature, 64, sig_hex);
cJSON_AddStringToObject(seal, "sig", sig_hex);
return seal;
}
/**
* NIP-59: Create a gift wrap (kind 1059) wrapping a seal
*/
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_key_hex) {
if (!seal || !recipient_public_key_hex) {
return NULL;
}
// Serialize the seal to JSON
char* seal_json = cJSON_PrintUnformatted(seal);
if (!seal_json) {
return NULL;
}
// Generate random private key for gift wrap
unsigned char random_private_key[32];
if (generate_random_private_key(random_private_key) != 1) {
free(seal_json);
return NULL;
}
// Get random public key
unsigned char random_public_key[32];
if (nostr_ec_public_key_from_private_key(random_private_key, random_public_key) != 0) {
memory_clear(random_private_key, 32);
free(seal_json);
return NULL;
}
char random_pubkey_hex[65];
nostr_bytes_to_hex(random_public_key, 32, random_pubkey_hex);
// Convert recipient pubkey hex to bytes
unsigned char recipient_public_key[32];
if (nostr_hex_to_bytes(recipient_public_key_hex, recipient_public_key, 32) != 0) {
memory_clear(random_private_key, 32);
free(seal_json);
return NULL;
}
// Encrypt the seal using NIP-44
char encrypted_content[8192]; // Larger buffer for nested encryption
int encrypt_result = nostr_nip44_encrypt(random_private_key, recipient_public_key,
seal_json, encrypted_content, sizeof(encrypted_content));
free(seal_json);
if (encrypt_result != NOSTR_SUCCESS) {
memory_clear(random_private_key, 32);
return NULL;
}
// Create gift wrap event (kind 1059)
cJSON* gift_wrap = cJSON_CreateObject();
if (!gift_wrap) {
memory_clear(random_private_key, 32);
return NULL;
}
time_t wrap_time = random_past_timestamp();
cJSON_AddStringToObject(gift_wrap, "pubkey", random_pubkey_hex);
cJSON_AddNumberToObject(gift_wrap, "created_at", (double)wrap_time);
cJSON_AddNumberToObject(gift_wrap, "kind", 1059);
// Add p tag for recipient
cJSON* tags = cJSON_CreateArray();
cJSON* p_tag = cJSON_CreateArray();
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_public_key_hex));
cJSON_AddItemToArray(tags, p_tag);
cJSON_AddItemToObject(gift_wrap, "tags", tags);
cJSON_AddStringToObject(gift_wrap, "content", encrypted_content);
// Calculate event ID
char event_id[65];
if (create_event_id(gift_wrap, event_id) != 0) {
memory_clear(random_private_key, 32);
cJSON_Delete(gift_wrap);
return NULL;
}
cJSON_AddStringToObject(gift_wrap, "id", event_id);
// Sign the gift wrap
unsigned char event_hash[32];
if (nostr_hex_to_bytes(event_id, event_hash, 32) != 0) {
memory_clear(random_private_key, 32);
cJSON_Delete(gift_wrap);
return NULL;
}
unsigned char signature[64];
if (nostr_ec_sign(random_private_key, event_hash, signature) != 0) {
memory_clear(random_private_key, 32);
cJSON_Delete(gift_wrap);
return NULL;
}
char sig_hex[129];
nostr_bytes_to_hex(signature, 64, sig_hex);
cJSON_AddStringToObject(gift_wrap, "sig", sig_hex);
// Clear the random private key from memory
memory_clear(random_private_key, 32);
return gift_wrap;
}
/**
* NIP-59: Unwrap a gift wrap to get the seal
*/
cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_private_key) {
if (!gift_wrap || !recipient_private_key) {
return NULL;
}
// Get the encrypted content
cJSON* content_item = cJSON_GetObjectItem(gift_wrap, "content");
if (!content_item || !cJSON_IsString(content_item)) {
return NULL;
}
const char* encrypted_content = cJSON_GetStringValue(content_item);
// Get the sender's public key (gift wrap pubkey)
cJSON* pubkey_item = cJSON_GetObjectItem(gift_wrap, "pubkey");
if (!pubkey_item || !cJSON_IsString(pubkey_item)) {
return NULL;
}
const char* sender_pubkey_hex = cJSON_GetStringValue(pubkey_item);
// Convert sender pubkey hex to bytes
unsigned char sender_public_key[32];
if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) {
return NULL;
}
// Decrypt the content using NIP-44
char decrypted_json[8192];
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
encrypted_content, decrypted_json, sizeof(decrypted_json));
if (decrypt_result != NOSTR_SUCCESS) {
return NULL;
}
// Parse the decrypted JSON as the seal event
cJSON* seal = cJSON_Parse(decrypted_json);
if (!seal) {
return NULL;
}
return seal;
}
/**
* NIP-59: Unseal a seal to get the rumor
*/
cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key,
const unsigned char* recipient_private_key) {
if (!seal || !sender_public_key || !recipient_private_key) {
return NULL;
}
// Get the encrypted content
cJSON* content_item = cJSON_GetObjectItem(seal, "content");
if (!content_item || !cJSON_IsString(content_item)) {
return NULL;
}
const char* encrypted_content = cJSON_GetStringValue(content_item);
// Decrypt the content using NIP-44
char decrypted_json[4096];
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
encrypted_content, decrypted_json, sizeof(decrypted_json));
if (decrypt_result != NOSTR_SUCCESS) {
return NULL;
}
// Parse the decrypted JSON as the rumor event
cJSON* rumor = cJSON_Parse(decrypted_json);
if (!rumor) {
return NULL;
}
return rumor;
}

74
nostr_core/nip059.h Normal file
View File

@ -0,0 +1,74 @@
/*
* NIP-59: Gift Wrap
* https://github.com/nostr-protocol/nips/blob/master/59.md
*/
#ifndef NOSTR_NIP059_H
#define NOSTR_NIP059_H
#include <stddef.h>
#include <time.h>
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* NIP-59: Create a rumor (unsigned event)
*
* @param kind Event kind
* @param content Event content
* @param tags Event tags (cJSON array, can be NULL)
* @param pubkey_hex Sender's public key in hex format
* @param created_at Event timestamp (0 for current time)
* @return cJSON object representing the rumor, or NULL on error
*/
cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
const char* pubkey_hex, time_t created_at);
/**
* NIP-59: Create a seal (kind 13) wrapping a rumor
*
* @param rumor The rumor event to seal (cJSON object)
* @param sender_private_key 32-byte sender private key
* @param recipient_public_key 32-byte recipient public key (x-only)
* @return cJSON object representing the seal event, or NULL on error
*/
cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private_key,
const unsigned char* recipient_public_key);
/**
* NIP-59: Create a gift wrap (kind 1059) wrapping a seal
*
* @param seal The seal event to wrap (cJSON object)
* @param recipient_public_key_hex Recipient's public key in hex format
* @return cJSON object representing the gift wrap event, or NULL on error
*/
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_key_hex);
/**
* NIP-59: Unwrap a gift wrap to get the seal
*
* @param gift_wrap The gift wrap event (cJSON object)
* @param recipient_private_key 32-byte recipient private key
* @return cJSON object representing the seal event, or NULL on error
*/
cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_private_key);
/**
* NIP-59: Unseal a seal to get the rumor
*
* @param seal The seal event (cJSON object)
* @param sender_public_key 32-byte sender public key (x-only)
* @param recipient_private_key 32-byte recipient private key
* @return cJSON object representing the rumor event, or NULL on error
*/
cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key,
const unsigned char* recipient_private_key);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_NIP059_H

View File

@ -2,10 +2,10 @@
#define NOSTR_CORE_H #define NOSTR_CORE_H
// Version information (auto-updated by increment_and_push.sh) // Version information (auto-updated by increment_and_push.sh)
#define VERSION "v0.4.3" #define VERSION "v0.4.4"
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 4 #define VERSION_MINOR 4
#define VERSION_PATCH 3 #define VERSION_PATCH 4
/* /*
* NOSTR Core Library - Complete API Reference * NOSTR Core Library - Complete API Reference
@ -49,6 +49,21 @@
* - nostr_nip44_encrypt_with_nonce() -> Encrypt with specific nonce (testing) * - nostr_nip44_encrypt_with_nonce() -> Encrypt with specific nonce (testing)
* - nostr_nip44_decrypt() -> Decrypt ChaCha20 + HMAC messages * - nostr_nip44_decrypt() -> Decrypt ChaCha20 + HMAC messages
* *
* NIP-59 GIFT WRAP:
* - nostr_nip59_create_rumor() -> Create unsigned event (rumor)
* - nostr_nip59_create_seal() -> Seal rumor with sender's key (kind 13)
* - nostr_nip59_create_gift_wrap() -> Wrap seal with random key (kind 1059)
* - nostr_nip59_unwrap_gift() -> Unwrap gift wrap to get seal
* - nostr_nip59_unseal_rumor() -> Unseal to get original rumor
*
* NIP-17 PRIVATE DIRECT MESSAGES:
* - nostr_nip17_create_chat_event() -> Create chat message (kind 14)
* - nostr_nip17_create_file_event() -> Create file message (kind 15)
* - nostr_nip17_create_relay_list_event() -> Create DM relay list (kind 10050)
* - nostr_nip17_send_dm() -> Send DM to multiple recipients
* - nostr_nip17_receive_dm() -> Receive and decrypt DM
* - nostr_nip17_extract_dm_relays() -> Extract relay URLs from kind 10050
*
* NIP-42 AUTHENTICATION: * NIP-42 AUTHENTICATION:
* - nostr_nip42_create_auth_event() -> Create authentication event (kind 22242) * - nostr_nip42_create_auth_event() -> Create authentication event (kind 22242)
* - nostr_nip42_verify_auth_event() -> Verify authentication event (relay-side) * - nostr_nip42_verify_auth_event() -> Verify authentication event (relay-side)
@ -132,6 +147,16 @@
* cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay_url, private_key, 0); * cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay_url, private_key, 0);
* nostr_ws_authenticate(client, private_key, 600); // Auto-authenticate WebSocket * nostr_ws_authenticate(client, private_key, 600); // Auto-authenticate WebSocket
* *
* Private Direct Messages (NIP-17):
* // Create and send a DM
* cJSON* dm_event = nostr_nip17_create_chat_event("Hello!", &recipient_pubkey, 1, NULL, NULL, NULL, sender_pubkey);
* cJSON* gift_wraps[10];
* int count = nostr_nip17_send_dm(dm_event, &recipient_pubkey, 1, sender_privkey, gift_wraps, 10);
* // Publish gift_wraps[0] to recipient's relays
*
* // Receive a DM
* cJSON* decrypted_dm = nostr_nip17_receive_dm(received_gift_wrap, recipient_privkey);
*
* ============================================================================ * ============================================================================
*/ */
@ -150,9 +175,11 @@ extern "C" {
#include "nip006.h" // Key derivation from mnemonic #include "nip006.h" // Key derivation from mnemonic
#include "nip011.h" // Relay information document #include "nip011.h" // Relay information document
#include "nip013.h" // Proof of Work #include "nip013.h" // Proof of Work
#include "nip017.h" // Private Direct Messages
#include "nip019.h" // Bech32 encoding (nsec/npub) #include "nip019.h" // Bech32 encoding (nsec/npub)
#include "nip042.h" // Authentication of clients to relays #include "nip042.h" // Authentication of clients to relays
#include "nip044.h" // Encryption (modern) #include "nip044.h" // Encryption (modern)
#include "nip059.h" // Gift Wrap
// Authentication and request validation system // Authentication and request validation system
#include "request_validator.h" // Request validation and authentication rules #include "request_validator.h" // Request validation and authentication rules
@ -283,6 +310,12 @@ int nostr_relay_pool_reset_relay_stats(
double nostr_relay_pool_get_relay_query_latency( double nostr_relay_pool_get_relay_query_latency(
nostr_relay_pool_t* pool, nostr_relay_pool_t* pool,
const char* relay_url); const char* relay_url);
const char* nostr_relay_pool_get_relay_last_publish_error(
nostr_relay_pool_t* pool,
const char* relay_url);
const char* nostr_relay_pool_get_relay_last_connection_error(
nostr_relay_pool_t* pool,
const char* relay_url);
double nostr_relay_pool_get_relay_ping_latency( double nostr_relay_pool_get_relay_ping_latency(
nostr_relay_pool_t* pool, nostr_relay_pool_t* pool,
const char* relay_url); const char* relay_url);

View File

@ -795,7 +795,9 @@ static int ws_perform_handshake(nostr_ws_client_t* client, const char* key) {
response[total_received] = '\0'; response[total_received] = '\0';
// Check if we have complete headers // Check if we have complete headers
if (strstr(response, "\r\n\r\n")) break; if (strstr(response, "\r\n\r\n")) {
break;
}
} }
// Check if response starts with the correct HTTP status line // Check if response starts with the correct HTTP status line
@ -803,7 +805,8 @@ static int ws_perform_handshake(nostr_ws_client_t* client, const char* key) {
return -1; return -1;
} }
if (!strstr(response, "Upgrade: websocket") && !strstr(response, "upgrade: websocket")) { if (!strstr(response, "Upgrade: websocket") && !strstr(response, "upgrade: websocket") &&
!strstr(response, "Upgrade: WebSocket") && !strstr(response, "upgrade: WebSocket")) {
return -1; return -1;
} }

View File

@ -1,2 +1,74 @@
[Thu Oct 2 14:40:34 2025] 🚀 Pool test started [Thu Oct 2 19:30:49 2025] 🚀 Pool test started
[Thu Oct 2 19:30:52 2025] 🏊 Pool started with default relay
[Thu Oct 2 19:31:05 2025] Relay added: ws://127.0.0.1:7777
[Thu Oct 2 19:31:24 2025] 🔍 New subscription created (ID: 1)
Filter: {
"kinds": [15555],
"limit": 10
}
[Thu Oct 2 19:31:24 2025] 📨 EVENT from ws://127.0.0.1:7777
├── ID: 9af94cf163a2...
├── Pubkey: e568a76a4f88...
├── Kind: 15555
├── Created: 1759447876
└── Content: 📊 Bitcoin Mempool Status
Pending Transactions: 3,255
Total Size: 1.82 MB
Memory Usage: 3.4%
A...
[Thu Oct 2 19:31:24 2025] 📨 EVENT from wss://relay.laantungir.net
├── ID: 93472348ded1...
├── Pubkey: b3f282fe77f4...
├── Kind: 15555
├── Created: 1753542367
└── Content: 📊 Bitcoin Mempool Status
Pending Transactions: 9,882
Total Size: 2.85 MB
Memory Usage: 6.0%
A...
[Thu Oct 2 19:31:24 2025] 📋 EOSE received - 0 events collected
[Thu Oct 2 19:31:26 2025] 📨 EVENT from ws://127.0.0.1:7777
├── ID: 90336970f3cf...
├── Pubkey: e568a76a4f88...
├── Kind: 15555
├── Created: 1759447886
└── Content: 📊 Bitcoin Mempool Status
Pending Transactions: 3,295
Total Size: 1.83 MB
Memory Usage: 3.4%
A...
[Thu Oct 2 19:32:06 2025] 📨 EVENT from ws://127.0.0.1:7777
├── ID: 56e8e931f211...
├── Pubkey: e568a76a4f88...
├── Kind: 15555
├── Created: 1759447926
└── Content: 📊 Bitcoin Mempool Status
Pending Transactions: 3,357
Total Size: 1.91 MB
Memory Usage: 3.5%
A...
[Thu Oct 2 19:32:16 2025] 📨 EVENT from ws://127.0.0.1:7777
├── ID: 76682921a11b...
├── Pubkey: e568a76a4f88...
├── Kind: 15555
├── Created: 1759447936
└── Content: 📊 Bitcoin Mempool Status
Pending Transactions: 3,363
Total Size: 1.91 MB
Memory Usage: 3.5%
A...

BIN
tests/nip17_test Executable file

Binary file not shown.

341
tests/nip17_test.c Normal file
View File

@ -0,0 +1,341 @@
/*
* NIP-17 Private Direct Messages Test Program
*
* Tests the complete NIP-17 DM flow using synchronous relay operations:
* 1. Generate sender and recipient keypairs
* 2. Create a DM from sender to recipient
* 3. Publish the gift-wrapped DM to relay
* 4. Subscribe to receive the DM back
* 5. Decrypt and verify the received DM
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
// Enable debug output to see all relay messages
#define NOSTR_DEBUG_ENABLED
#include "../nostr_core/nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
// Test configuration
#define RELAY_URL "wss://relay.laantungir.net"
#define TEST_TIMEOUT_MS 5000
// Progress callback for publishing
void publish_progress_callback(const char* relay_url, const char* status,
const char* message, int success_count,
int total_relays, int completed_relays, void* user_data) {
(void)user_data;
if (relay_url) {
printf("📡 PUBLISH [%s]: %s", relay_url, status);
if (message) {
printf(" - %s", message);
}
printf(" (%d/%d completed, %d successful)\n", completed_relays, total_relays, success_count);
} else {
printf("📡 PUBLISH COMPLETE: %d/%d successful\n", success_count, total_relays);
}
}
// Progress callback for querying/subscribing
void query_progress_callback(const char* relay_url, const char* status,
const char* event_id, int event_count,
int total_relays, int completed_relays, void* user_data) {
(void)user_data;
if (relay_url) {
printf("🔍 QUERY [%s]: %s", relay_url, status);
if (event_id) {
printf(" - Event: %.12s...", event_id);
}
if (event_count > 0) {
printf(" (%d events)", event_count);
}
printf(" (%d/%d completed)\n", completed_relays, total_relays);
} else {
printf("🔍 QUERY COMPLETE: %d events found\n", event_count);
}
}
/**
* Generate a random keypair for testing
*/
void generate_test_keypair(unsigned char* private_key, char* pubkey_hex) {
// Generate random private key
if (nostr_secp256k1_get_random_bytes(private_key, 32) != 1) {
fprintf(stderr, "Failed to generate random private key\n");
exit(1);
}
// Derive public key
unsigned char public_key[32];
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
fprintf(stderr, "Failed to derive public key\n");
exit(1);
}
// Convert to hex
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
}
/**
* Main test function
*/
int main(int argc, char* argv[]) {
(void)argc; // Suppress unused parameter warning
(void)argv; // Suppress unused parameter warning
printf("🧪 NIP-17 Private Direct Messages Test (Synchronous)\n");
printf("=================================================\n\n");
// Initialize crypto
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize crypto\n");
return 1;
}
// Generate keypairs
unsigned char sender_privkey[32];
unsigned char recipient_privkey[32];
char sender_pubkey_hex[65];
char recipient_pubkey_hex[65];
printf("🔑 Generating keypairs...\n");
generate_test_keypair(sender_privkey, sender_pubkey_hex);
generate_test_keypair(recipient_privkey, recipient_pubkey_hex);
printf("📤 Sender pubkey: %s\n", sender_pubkey_hex);
printf("📥 Recipient pubkey: %s\n", recipient_pubkey_hex);
printf("\n");
// Create DM event with timestamp
printf("💬 Creating DM event...\n");
time_t now = time(NULL);
char test_message[256];
snprintf(test_message, sizeof(test_message),
"Hello from NIP-17! This is a private direct message sent at %ld", now);
const char* recipient_pubkeys[] = {recipient_pubkey_hex};
cJSON* dm_event = nostr_nip17_create_chat_event(
test_message,
recipient_pubkeys,
1,
"NIP-17 Test",
NULL, // no reply
RELAY_URL,
sender_pubkey_hex
);
if (!dm_event) {
fprintf(stderr, "Failed to create DM event\n");
return 1;
}
printf("📝 Created DM event (kind 14)\n");
// Send DM (create gift wraps)
printf("🎁 Creating gift wraps...\n");
cJSON* gift_wraps[10]; // Max 10 gift wraps
int gift_wrap_count = nostr_nip17_send_dm(
dm_event,
recipient_pubkeys,
1,
sender_privkey,
gift_wraps,
10
);
cJSON_Delete(dm_event); // Original DM event no longer needed
if (gift_wrap_count <= 0) {
fprintf(stderr, "Failed to create gift wraps\n");
return 1;
}
printf("✅ Created %d gift wrap(s)\n", gift_wrap_count);
// Print the gift wrap JSON
printf("\n📄 Gift wrap event JSON:\n");
printf("========================\n");
char* gift_wrap_json = cJSON_Print(gift_wraps[0]);
printf("%s\n", gift_wrap_json);
free(gift_wrap_json);
// PHASE 1: Publish the gift wrap to relay
printf("\n📤 PHASE 1: Publishing gift wrap to relay\n");
printf("==========================================\n");
const char* relay_urls[] = {RELAY_URL};
int success_count = 0;
publish_result_t* publish_results = synchronous_publish_event_with_progress(
relay_urls,
1, // single relay
gift_wraps[0], // Send the first gift wrap
&success_count,
10, // 10 second timeout
publish_progress_callback,
NULL, // no user data
0, // NIP-42 disabled
NULL // no private key for auth
);
if (!publish_results || success_count != 1) {
fprintf(stderr, "❌ Failed to publish gift wrap (success_count: %d)\n", success_count);
// Clean up gift wraps
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
if (publish_results) free(publish_results);
return 1;
}
printf("✅ Successfully published gift wrap!\n");
// Clean up publish results and gift wraps
free(publish_results);
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
// Small delay to let the relay process the event
printf("⏳ Waiting 2 seconds for relay to process...\n");
sleep(2);
// PHASE 2: Subscribe to receive the DM back
printf("\n📥 PHASE 2: Subscribing to receive DM back\n");
printf("===========================================\n");
// Create filter for gift wraps addressed to recipient
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1059)); // Gift wrap kind
cJSON_AddItemToObject(filter, "kinds", kinds);
// Filter for gift wraps with p tag matching recipient
cJSON* p_tags = cJSON_CreateArray();
cJSON_AddItemToArray(p_tags, cJSON_CreateString(recipient_pubkey_hex));
cJSON_AddItemToObject(filter, "#p", p_tags);
// Print the subscription filter JSON
printf("📄 Subscription filter JSON:\n");
printf("============================\n");
char* filter_json = cJSON_Print(filter);
printf("%s\n", filter_json);
free(filter_json);
int query_result_count = 0;
cJSON** query_results = synchronous_query_relays_with_progress(
relay_urls,
1, // single relay
filter,
RELAY_QUERY_ALL_RESULTS, // Get all matching events
&query_result_count,
10, // 10 second timeout per relay
query_progress_callback,
NULL, // no user data
0, // NIP-42 disabled
NULL // no private key for auth
);
cJSON_Delete(filter); // Clean up filter
if (!query_results || query_result_count == 0) {
fprintf(stderr, "❌ No DM events received back from relay\n");
if (query_results) free(query_results);
return 1;
}
printf("✅ Received %d event(s) from relay!\n", query_result_count);
// Process the received events
printf("\n🔍 PHASE 3: Processing received events\n");
printf("=====================================\n");
int dm_found = 0;
cJSON* received_dm = NULL;
for (int i = 0; i < query_result_count; i++) {
cJSON* event = query_results[i];
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* id_item = cJSON_GetObjectItem(event, "id");
if (kind_item && cJSON_IsNumber(kind_item) && cJSON_GetNumberValue(kind_item) == 1059) {
printf("🎁 Found gift wrap event: %.12s...\n",
id_item && cJSON_IsString(id_item) ? cJSON_GetStringValue(id_item) : "unknown");
// Try to decrypt this gift wrap
cJSON* decrypted_dm = nostr_nip17_receive_dm(event, recipient_privkey);
if (decrypted_dm) {
printf("✅ Successfully decrypted gift wrap!\n");
// Verify the decrypted DM
cJSON* content_item = cJSON_GetObjectItem(decrypted_dm, "content");
cJSON* dm_kind_item = cJSON_GetObjectItem(decrypted_dm, "kind");
cJSON* pubkey_item = cJSON_GetObjectItem(decrypted_dm, "pubkey");
if (content_item && dm_kind_item && pubkey_item) {
const char* decrypted_content = cJSON_GetStringValue(content_item);
int decrypted_kind = (int)cJSON_GetNumberValue(dm_kind_item);
const char* decrypted_pubkey = cJSON_GetStringValue(pubkey_item);
printf("📧 Decrypted DM:\n");
printf(" Kind: %d\n", decrypted_kind);
printf(" From: %s\n", decrypted_pubkey);
printf(" Content: %s\n", decrypted_content);
// Verify the DM content (check that it contains our message with timestamp)
int content_ok = strstr(decrypted_content, "Hello from NIP-17! This is a private direct message sent at ") != NULL;
if (decrypted_kind == 14 &&
content_ok &&
strlen(decrypted_content) >= 64 && // Should be at least as long as the prefix
strcmp(decrypted_pubkey, sender_pubkey_hex) == 0) {
printf("✅ DM verification successful!\n");
dm_found = 1;
received_dm = decrypted_dm; // Keep reference for cleanup
break; // Found our DM, no need to check more
} else {
printf("❌ DM verification failed\n");
cJSON_Delete(decrypted_dm);
}
} else {
printf("❌ Invalid decrypted DM structure\n");
cJSON_Delete(decrypted_dm);
}
} else {
printf("❌ Failed to decrypt gift wrap\n");
}
}
}
// Clean up query results
for (int i = 0; i < query_result_count; i++) {
cJSON_Delete(query_results[i]);
}
free(query_results);
if (!dm_found) {
fprintf(stderr, "❌ Could not find or decrypt the expected DM\n");
return 1;
}
printf("\n🎉 NIP-17 synchronous test completed successfully!\n");
// Cleanup
if (received_dm) {
cJSON_Delete(received_dm);
}
nostr_cleanup();
return 0;
}

Binary file not shown.

Binary file not shown.

188
tests/websocket_debug.c Normal file
View File

@ -0,0 +1,188 @@
/*
* Simple WebSocket Debug Tool for NIP-17 Testing
*
* Connects to a relay and sends a subscription request to see what responses we get.
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "../nostr_core/nostr_core.h"
#include "../nostr_websocket/nostr_websocket_tls.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <relay_url> [event_id]\n", argv[0]);
fprintf(stderr, "Example: %s wss://relay.laantungir.net 06cdf2cdd095ddb1ebe15d5b3c736b27a34de2683e847b871fe37d86ac998772\n");
return 1;
}
const char* relay_url = argv[1];
const char* event_id = (argc >= 3) ? argv[2] : NULL;
printf("🔍 WebSocket Debug Tool\n");
printf("=======================\n");
printf("Relay: %s\n", relay_url);
if (event_id) {
printf("Looking for event: %s\n", event_id);
}
printf("\n");
// Initialize crypto
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize crypto\n");
return 1;
}
// Connect to relay
printf("🔌 Connecting to relay...\n");
nostr_ws_client_t* client = nostr_ws_connect(relay_url);
if (!client) {
fprintf(stderr, "Failed to connect to relay - nostr_ws_connect returned NULL\n");
return 1;
}
// Check initial state
nostr_ws_state_t initial_state = nostr_ws_get_state(client);
printf("Initial connection state: %d\n", (int)initial_state);
// Wait for connection
time_t start_time = time(NULL);
while (time(NULL) - start_time < 10) { // 10 second timeout
nostr_ws_state_t state = nostr_ws_get_state(client);
if (state == NOSTR_WS_CONNECTED) {
printf("✅ Connected!\n");
break;
} else if (state == NOSTR_WS_ERROR) {
fprintf(stderr, "❌ Connection failed\n");
nostr_ws_close(client);
return 1;
}
usleep(100000); // 100ms
}
if (nostr_ws_get_state(client) != NOSTR_WS_CONNECTED) {
fprintf(stderr, "❌ Connection timeout\n");
nostr_ws_close(client);
return 1;
}
// Send subscription request
printf("📡 Sending subscription request...\n");
// Create filter for kind 1059 events
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1059));
cJSON_AddItemToObject(filter, "kinds", kinds);
// If we have a specific event ID, add it to the filter
if (event_id) {
cJSON* ids = cJSON_CreateArray();
cJSON_AddItemToArray(ids, cJSON_CreateString(event_id));
cJSON_AddItemToObject(filter, "ids", ids);
}
char* filter_json = cJSON_PrintUnformatted(filter);
printf("Filter: %s\n", filter_json);
// Send REQ message
char subscription_id[32];
snprintf(subscription_id, sizeof(subscription_id), "debug_%ld", time(NULL));
if (nostr_relay_send_req(client, subscription_id, filter) < 0) {
fprintf(stderr, "Failed to send subscription request\n");
cJSON_Delete(filter);
free(filter_json);
nostr_ws_close(client);
return 1;
}
cJSON_Delete(filter);
free(filter_json);
printf("✅ Subscription sent (ID: %s)\n", subscription_id);
printf("⏳ Listening for responses (30 seconds)...\n");
printf("Press Ctrl+C to stop\n\n");
// Listen for responses
start_time = time(NULL);
int message_count = 0;
while (time(NULL) - start_time < 30) { // 30 second timeout
char buffer[16384];
int len = nostr_ws_receive(client, buffer, sizeof(buffer) - 1, 1000); // 1 second timeout
if (len > 0) {
buffer[len] = '\0';
message_count++;
printf("📨 Message %d:\n", message_count);
printf("%s\n", buffer);
// Parse the message
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
printf(" → EVENT received\n");
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* event = cJSON_GetArrayItem(parsed, 2);
if (event) {
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* id_item = cJSON_GetObjectItem(event, "id");
if (kind_item && cJSON_IsNumber(kind_item)) {
printf(" → Kind: %d\n", (int)cJSON_GetNumberValue(kind_item));
}
if (id_item && cJSON_IsString(id_item)) {
printf(" → ID: %.12s...\n", cJSON_GetStringValue(id_item));
}
}
}
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
printf(" → EOSE (End of Stored Events)\n");
} else if (msg_type && strcmp(msg_type, "NOTICE") == 0) {
printf(" → NOTICE from relay\n");
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* notice_msg = cJSON_GetArrayItem(parsed, 1);
if (notice_msg && cJSON_IsString(notice_msg)) {
printf(" → Message: %s\n", cJSON_GetStringValue(notice_msg));
}
}
} else if (msg_type) {
printf(" → %s\n", msg_type);
}
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
}
printf("\n");
} else if (len < 0) {
printf("❌ Receive error\n");
break;
}
// Small delay to prevent busy waiting
usleep(10000); // 10ms
}
printf("📊 Total messages received: %d\n", message_count);
// Send CLOSE message
printf("🔌 Closing subscription...\n");
nostr_relay_send_close(client, subscription_id);
// Close connection
nostr_ws_close(client);
nostr_cleanup();
printf("✅ Done\n");
return 0;
}

BIN
websocket_debug Executable file

Binary file not shown.