nip 17, and 59
This commit is contained in:
parent
54a6044083
commit
6b95ad37c5
66
build.sh
66
build.sh
|
@ -58,6 +58,7 @@ FORCE_NIPS=""
|
|||
VERBOSE=false
|
||||
HELP=false
|
||||
BUILD_TESTS=false
|
||||
BUILD_EXAMPLES=false
|
||||
NO_COLOR_FLAG=false
|
||||
|
||||
# Parse command line arguments
|
||||
|
@ -83,6 +84,10 @@ while [[ $# -gt 0 ]]; do
|
|||
BUILD_TESTS=true
|
||||
shift
|
||||
;;
|
||||
--examples|-e)
|
||||
BUILD_EXAMPLES=true
|
||||
shift
|
||||
;;
|
||||
--no-color)
|
||||
NO_COLOR_FLAG=true
|
||||
shift
|
||||
|
@ -119,6 +124,7 @@ if [ "$HELP" = true ]; then
|
|||
echo " --nips=1,5,6,19 Force specific NIPs (comma-separated)"
|
||||
echo " --nips=all Include all available NIPs"
|
||||
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 " --no-color Disable colored output"
|
||||
echo " --help, -h Show this help"
|
||||
|
@ -134,9 +140,11 @@ if [ "$HELP" = true ]; then
|
|||
echo " 006 - Key derivation from mnemonic"
|
||||
echo " 011 - Relay information document"
|
||||
echo " 013 - Proof of Work"
|
||||
echo " 017 - Private Direct Messages"
|
||||
echo " 019 - Bech32 encoding (nsec/npub)"
|
||||
echo " 042 - Authentication of clients to relays"
|
||||
echo " 044 - Encryption (modern)"
|
||||
echo " 059 - Gift Wrap"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
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=""
|
||||
if [ -n "$FORCE_NIPS" ]; 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"
|
||||
else
|
||||
# Convert comma-separated list to space-separated with 3-digit format
|
||||
|
@ -220,10 +228,10 @@ else
|
|||
fi
|
||||
fi
|
||||
|
||||
# If building tests, include all NIPs to ensure test compatibility
|
||||
if [ "$BUILD_TESTS" = true ] && [ -z "$FORCE_NIPS" ]; then
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
|
||||
print_info "Building tests - including all available NIPs for test compatibility"
|
||||
# If building tests or examples, include all NIPs to ensure compatibility
|
||||
if ([ "$BUILD_TESTS" = true ] || [ "$BUILD_EXAMPLES" = true ]) && [ -z "$FORCE_NIPS" ]; then
|
||||
NEEDED_NIPS="001 004 005 006 011 013 017 019 042 044 059"
|
||||
print_info "Building tests/examples - including all available NIPs for compatibility"
|
||||
fi
|
||||
|
||||
# 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)" ;;
|
||||
011) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-011(Relay-Info)" ;;
|
||||
013) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-013(PoW)" ;;
|
||||
017) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-017(DMs)" ;;
|
||||
019) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-019(Bech32)" ;;
|
||||
042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;;
|
||||
044) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-044(Encrypt)" ;;
|
||||
059) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-059(Gift-Wrap)" ;;
|
||||
esac
|
||||
else
|
||||
print_warning "NIP file not found: $NIP_FILE - skipping"
|
||||
|
@ -668,6 +678,52 @@ if [ $AR_RESULT -eq 0 ]; then
|
|||
echo ""
|
||||
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 " gcc your_app.c $OUTPUT -lz -ldl -lpthread -lm $SYSTEM_LIBS -o your_app"
|
||||
echo ""
|
||||
|
|
Binary file not shown.
|
@ -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)
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -481,9 +481,9 @@ int main() {
|
|||
}
|
||||
|
||||
// Create pool with custom reconnection configuration for faster testing
|
||||
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
|
||||
config->ping_interval_seconds = 5; // Ping every 5 seconds for testing
|
||||
pool = nostr_relay_pool_create(config);
|
||||
nostr_pool_reconnect_config_t config = *nostr_pool_reconnect_config_default();
|
||||
config.ping_interval_seconds = 5; // Ping every 5 seconds for testing
|
||||
pool = nostr_relay_pool_create(&config);
|
||||
if (!pool) {
|
||||
printf("❌ Failed to create pool\n");
|
||||
break;
|
Binary file not shown.
|
@ -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;
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -139,7 +139,8 @@ print_info "Creating git tag: $NEW_VERSION"
|
|||
git tag "$NEW_VERSION"
|
||||
|
||||
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"
|
||||
|
||||
print_success "Version $NEW_VERSION successfully released!"
|
||||
|
|
|
@ -85,6 +85,14 @@ typedef struct relay_connection {
|
|||
subscription_timing_t pending_subscriptions[NOSTR_POOL_MAX_PENDING_SUBSCRIPTIONS];
|
||||
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
|
||||
nostr_relay_stats_t stats;
|
||||
} relay_connection_t;
|
||||
|
@ -236,6 +244,10 @@ static int ensure_relay_connection(relay_connection_t* relay) {
|
|||
relay->status = NOSTR_POOL_RELAY_ERROR;
|
||||
relay->reconnect_attempts++;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -257,6 +269,11 @@ static int ensure_relay_connection(relay_connection_t* relay) {
|
|||
relay->reconnect_attempts++;
|
||||
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
|
||||
nostr_ws_close(relay->ws_client);
|
||||
relay->ws_client = NULL;
|
||||
|
@ -448,6 +465,11 @@ nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* confi
|
|||
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) {
|
||||
if (!pool || !relay_url || pool->relay_count >= NOSTR_POOL_MAX_RELAYS) {
|
||||
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);
|
||||
|
||||
// Debug: Print all message types received
|
||||
printf("📨 DEBUG: Received %s message from %s\n", msg_type, relay->url);
|
||||
|
||||
if (strcmp(msg_type, "EVENT") == 0) {
|
||||
// Handle EVENT message: ["EVENT", subscription_id, event]
|
||||
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++;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
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(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url) {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -2,10 +2,10 @@
|
|||
#define NOSTR_CORE_H
|
||||
|
||||
// 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_MINOR 4
|
||||
#define VERSION_PATCH 3
|
||||
#define VERSION_PATCH 4
|
||||
|
||||
/*
|
||||
* NOSTR Core Library - Complete API Reference
|
||||
|
@ -49,6 +49,21 @@
|
|||
* - nostr_nip44_encrypt_with_nonce() -> Encrypt with specific nonce (testing)
|
||||
* - 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:
|
||||
* - nostr_nip42_create_auth_event() -> Create authentication event (kind 22242)
|
||||
* - 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);
|
||||
* 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 "nip011.h" // Relay information document
|
||||
#include "nip013.h" // Proof of Work
|
||||
#include "nip017.h" // Private Direct Messages
|
||||
#include "nip019.h" // Bech32 encoding (nsec/npub)
|
||||
#include "nip042.h" // Authentication of clients to relays
|
||||
#include "nip044.h" // Encryption (modern)
|
||||
#include "nip059.h" // Gift Wrap
|
||||
|
||||
// Authentication and request validation system
|
||||
#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(
|
||||
nostr_relay_pool_t* pool,
|
||||
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(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
|
|
|
@ -784,9 +784,9 @@ static int ws_perform_handshake(nostr_ws_client_t* client, const char* key) {
|
|||
|
||||
while ((size_t)total_received < sizeof(response) - 1) {
|
||||
int received = client->transport->recv(&client->transport_ctx,
|
||||
response + total_received,
|
||||
sizeof(response) - total_received - 1,
|
||||
client->timeout_ms);
|
||||
response + total_received,
|
||||
sizeof(response) - total_received - 1,
|
||||
client->timeout_ms);
|
||||
if (received <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -795,7 +795,9 @@ static int ws_perform_handshake(nostr_ws_client_t* client, const char* key) {
|
|||
response[total_received] = '\0';
|
||||
|
||||
// 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
|
||||
|
@ -803,7 +805,8 @@ static int ws_perform_handshake(nostr_ws_client_t* client, const char* key) {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
74
pool.log
74
pool.log
|
@ -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...
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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;
|
||||
}
|
BIN
tests/pool_test
BIN
tests/pool_test
Binary file not shown.
Binary file not shown.
|
@ -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;
|
||||
}
|
Binary file not shown.
Loading…
Reference in New Issue