Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a53455190 | ||
|
|
61581afcc9 | ||
|
|
36eb6afa72 | ||
|
|
160421c673 | ||
|
|
024b490ac1 | ||
|
|
9240f44ea3 | ||
|
|
933cf03ce7 | ||
|
|
27da465295 | ||
|
|
6ebab823a8 | ||
|
|
a3337e5207 | ||
|
|
932c770f6f | ||
|
|
be3a32b488 | ||
|
|
3236e02f41 | ||
|
|
b51bcb4943 | ||
|
|
af06f23939 | ||
|
|
fc337cf921 | ||
|
|
f478a780d7 | ||
|
|
2e7eacc02e | ||
|
|
ec8eb5555b | ||
|
|
a3a68f0fde | ||
|
|
2fe36acde4 | ||
|
|
f470759b96 | ||
|
|
fb3c9dbc76 | ||
|
|
303013a518 | ||
|
|
f7e179dd85 | ||
|
|
a3f7a1bbfd | ||
|
|
a595747aff | ||
|
|
39ebfcd90a | ||
|
|
63972d974d | ||
|
|
13fd364ca3 | ||
|
|
cb4ff93906 | ||
|
|
4d0d794a9a | ||
|
|
ae672b7047 | ||
|
|
0a8a813fcf | ||
|
|
ea40aec6b8 | ||
|
|
98cfd81b01 | ||
|
|
d8d3f9b28c | ||
|
|
669793dd67 | ||
|
|
e21c8eb5be | ||
|
|
20006df228 | ||
|
|
adc7aa9dcf | ||
|
|
1a549afa19 | ||
|
|
6ee62e6575 | ||
|
|
5afe802e8f | ||
|
|
e77973aea0 | ||
|
|
ea84795911 | ||
|
|
481bf3158d | ||
|
|
23e68bdc83 | ||
|
|
4dc753cdcc | ||
|
|
64b7c7ec33 | ||
|
|
f3068f82f3 | ||
|
|
a8dc2ed046 | ||
|
|
9a3965243c | ||
|
|
23c2a58c2b | ||
|
|
45fb6d061d | ||
|
|
c0784fc890 | ||
|
|
499accf440 | ||
|
|
6b95ad37c5 | ||
|
|
54a6044083 | ||
|
|
0f897ab1b3 | ||
|
|
0d910ca181 | ||
|
|
9a63550863 | ||
|
|
eb7a9e6098 | ||
|
|
8585e7649c | ||
|
|
55e2a9c68e | ||
|
|
445ab7a8f4 | ||
|
|
33129d82fd | ||
|
|
c0d095e57b | ||
|
|
e40f3037d3 | ||
|
|
77d92dbcf9 | ||
|
|
1da4f6751e | ||
|
|
3ebfdc06c0 | ||
|
|
d8b342ca3f | ||
|
|
df23fd618a | ||
|
|
19452f45c2 | ||
|
|
711a7cc15c | ||
|
|
58cabadc44 | ||
|
|
77186c88dd | ||
|
|
2036d0165b | ||
|
|
40dd3aa20b | ||
|
|
00df0cad99 | ||
|
|
05fe1df8aa | ||
|
|
98a802552b | ||
|
|
9fd4c61df7 | ||
|
|
76e883fad4 | ||
|
|
c3a9482882 | ||
|
|
8ed9262c65 | ||
|
|
6014a250dd | ||
|
|
f50384962d | ||
|
|
c569c0c346 | ||
|
|
3d2537603c | ||
|
|
d6a0bd67b2 | ||
|
|
9191d446d3 | ||
|
|
f1c22bdf53 | ||
|
|
993ca0f2a1 | ||
|
|
77fadc2683 | ||
|
|
2637378bf0 | ||
|
|
c109c93382 | ||
|
|
0ace93e303 | ||
|
|
af2117b574 | ||
|
|
6d7b709f9a | ||
|
|
5a7c796873 | ||
|
|
3aaa46bb9b | ||
|
|
d257ae49f1 | ||
|
|
ae4aa7cf80 | ||
|
|
e137560d64 | ||
|
|
ca6b4754f9 |
+22
-1
@@ -8,7 +8,7 @@ node_modules/
|
||||
nostr-tools/
|
||||
tiny-AES-c/
|
||||
blossom/
|
||||
|
||||
ndk/
|
||||
|
||||
Trash/debug_tests/
|
||||
node_modules/
|
||||
@@ -19,3 +19,24 @@ node_modules/
|
||||
*.dylib
|
||||
*.dll
|
||||
build/
|
||||
|
||||
# Build outputs/binaries (do not track)
|
||||
/websocket_debug
|
||||
/libnostr_core_arm64.a
|
||||
/tests/websocket_debug
|
||||
/tests/*_test
|
||||
/examples/input_detection
|
||||
/examples/keypair_generation
|
||||
/examples/mnemonic_derivation
|
||||
/examples/mnemonic_generation
|
||||
/examples/ots_tool
|
||||
/examples/relay_pool
|
||||
/examples/send_nip17_dm
|
||||
/examples/simple_keygen
|
||||
/examples/timestamping
|
||||
/examples/utility_functions
|
||||
/examples/version_test
|
||||
|
||||
# Local OpenTimestamps CLI output
|
||||
/examples/timestamped_*
|
||||
/examples/*.ots
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description: "Increments and pushes the repo"
|
||||
---
|
||||
|
||||
Run increment_and_push.sh followed in the command line with a good description of the changes that were made.
|
||||
|
||||
For example: ./increment_and_push.sh "Fixed that nasty bug"
|
||||
@@ -1,223 +0,0 @@
|
||||
# NOSTR Core Library - Automatic Versioning System
|
||||
|
||||
## Overview
|
||||
|
||||
The NOSTR Core Library now features an automatic version increment system that automatically increments the patch version (e.g., v0.2.0 → v0.2.1) with each build. This ensures consistent versioning and traceability across builds.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Version Format
|
||||
The library uses semantic versioning with the format: `vMAJOR.MINOR.PATCH`
|
||||
|
||||
- **MAJOR**: Incremented for breaking changes (manual)
|
||||
- **MINOR**: Incremented for new features (manual)
|
||||
- **PATCH**: Incremented automatically with each build
|
||||
|
||||
### Automatic Increment Process
|
||||
|
||||
1. **Version Detection**: The build system scans all git tags matching `v*.*.*` pattern
|
||||
2. **Highest Version**: Uses `sort -V` to find the numerically highest version (not chronologically latest)
|
||||
3. **Patch Increment**: Increments the patch number by 1
|
||||
4. **Git Tag Creation**: Creates a new git tag for the incremented version
|
||||
5. **File Generation**: Generates `nostr_core/version.h` and `nostr_core/version.c` with build metadata
|
||||
|
||||
### Generated Files
|
||||
|
||||
The system automatically generates two files during each build:
|
||||
|
||||
#### `nostr_core/version.h`
|
||||
```c
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 2
|
||||
#define VERSION_PATCH 1
|
||||
#define VERSION_STRING "0.2.1"
|
||||
#define VERSION_TAG "v0.2.1"
|
||||
|
||||
#define BUILD_DATE "2025-08-09"
|
||||
#define BUILD_TIME "10:42:45"
|
||||
#define BUILD_TIMESTAMP "2025-08-09 10:42:45"
|
||||
|
||||
#define GIT_HASH "ca6b475"
|
||||
#define GIT_BRANCH "master"
|
||||
|
||||
// API functions
|
||||
const char* nostr_core_get_version(void);
|
||||
const char* nostr_core_get_version_full(void);
|
||||
const char* nostr_core_get_build_info(void);
|
||||
```
|
||||
|
||||
#### `nostr_core/version.c`
|
||||
Contains the implementation of the version API functions.
|
||||
|
||||
## Usage
|
||||
|
||||
### Building with Auto-Versioning
|
||||
|
||||
All major build targets automatically increment the version:
|
||||
|
||||
```bash
|
||||
# Build static library (increments version)
|
||||
./build.sh lib
|
||||
|
||||
# Build shared library (increments version)
|
||||
./build.sh shared
|
||||
|
||||
# Build all libraries (increments version)
|
||||
./build.sh all
|
||||
|
||||
# Build examples (increments version)
|
||||
./build.sh examples
|
||||
|
||||
# Install to system (increments version)
|
||||
./build.sh install
|
||||
```
|
||||
|
||||
### Non-Versioned Builds
|
||||
|
||||
Some targets do not increment versions:
|
||||
|
||||
```bash
|
||||
# Clean build artifacts (no version increment)
|
||||
./build.sh clean
|
||||
|
||||
# Run tests (no version increment)
|
||||
./build.sh test
|
||||
```
|
||||
|
||||
### Using Version Information in Code
|
||||
|
||||
```c
|
||||
#include "version.h"
|
||||
|
||||
// Get version string
|
||||
printf("Version: %s\n", nostr_core_get_version());
|
||||
|
||||
// Get full version with timestamp and commit
|
||||
printf("Full: %s\n", nostr_core_get_version_full());
|
||||
|
||||
// Get detailed build information
|
||||
printf("Build: %s\n", nostr_core_get_build_info());
|
||||
|
||||
// Use version macros
|
||||
#if VERSION_MAJOR >= 1
|
||||
// Use new API
|
||||
#else
|
||||
// Use legacy API
|
||||
#endif
|
||||
```
|
||||
|
||||
### Testing Version System
|
||||
|
||||
A version test example is provided:
|
||||
|
||||
```bash
|
||||
# Build and run version test
|
||||
gcc -I. -Inostr_core examples/version_test.c -o examples/version_test ./libnostr_core.a ./secp256k1/.libs/libsecp256k1.a -lm
|
||||
./examples/version_test
|
||||
```
|
||||
|
||||
## Version History Tracking
|
||||
|
||||
### View All Versions
|
||||
```bash
|
||||
# List all version tags
|
||||
git tag --list
|
||||
|
||||
# View version history
|
||||
git log --oneline --decorate --graph
|
||||
```
|
||||
|
||||
### Current Version
|
||||
```bash
|
||||
# Check current version
|
||||
cat VERSION
|
||||
|
||||
# Or check the latest git tag
|
||||
git describe --tags --abbrev=0
|
||||
```
|
||||
|
||||
## Manual Version Management
|
||||
|
||||
### Major/Minor Version Bumps
|
||||
|
||||
For major or minor version changes, manually create the appropriate tag:
|
||||
|
||||
```bash
|
||||
# For a minor version bump (new features)
|
||||
git tag v0.3.0
|
||||
|
||||
# For a major version bump (breaking changes)
|
||||
git tag v1.0.0
|
||||
```
|
||||
|
||||
The next build will automatically increment from the new base version.
|
||||
|
||||
### Resetting Version
|
||||
|
||||
To reset or fix version numbering:
|
||||
|
||||
```bash
|
||||
# Delete incorrect tags (if needed)
|
||||
git tag -d v0.2.1
|
||||
git push origin --delete v0.2.1
|
||||
|
||||
# Create correct base version
|
||||
git tag v0.2.0
|
||||
|
||||
# Next build will create v0.2.1
|
||||
```
|
||||
|
||||
## Integration Notes
|
||||
|
||||
### Makefile Integration
|
||||
- The `version.c` file is automatically included in `LIB_SOURCES`
|
||||
- Version files are compiled and linked with the library
|
||||
- Clean targets remove generated version object files
|
||||
|
||||
### Git Integration
|
||||
- Version files (`version.h`, `version.c`) are excluded from git via `.gitignore`
|
||||
- Only version tags and the `VERSION` file are tracked
|
||||
- Build system works in any git repository with version tags
|
||||
|
||||
### Build System Integration
|
||||
- Version increment is integrated directly into `build.sh`
|
||||
- No separate scripts or external dependencies required
|
||||
- Self-contained and portable across systems
|
||||
|
||||
## Example Output
|
||||
|
||||
When building, you'll see output like:
|
||||
|
||||
```
|
||||
[INFO] Incrementing version...
|
||||
[INFO] Current version: v0.2.0
|
||||
[INFO] New version: v0.2.1
|
||||
[SUCCESS] Created new version tag: v0.2.1
|
||||
[SUCCESS] Generated version.h and version.c
|
||||
[SUCCESS] Updated VERSION file to 0.2.1
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Version Not Incrementing
|
||||
- Ensure you're in a git repository
|
||||
- Check that git tags exist with `git tag --list`
|
||||
- Verify tag format matches `v*.*.*` pattern
|
||||
|
||||
### Tag Already Exists
|
||||
If a tag already exists, the build will continue with the existing version:
|
||||
|
||||
```
|
||||
[WARNING] Tag v0.2.1 already exists - using existing version
|
||||
```
|
||||
|
||||
### Missing Git Information
|
||||
If git is not available, version files will show "unknown" for git hash and branch.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Automatic Traceability**: Every build has a unique version
|
||||
2. **Build Metadata**: Includes timestamp, git commit, and branch information
|
||||
3. **API Integration**: Version information accessible via C API
|
||||
4. **Zero Maintenance**: No manual version file editing required
|
||||
5. **Git Integration**: Automatic git tag creation for version history
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
if(ESP_PLATFORM)
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"nostr_core/nostr_common.c"
|
||||
"nostr_core/nostr_log.c"
|
||||
"nostr_core/nip001.c"
|
||||
"nostr_core/nip004.c"
|
||||
"nostr_core/nip006.c"
|
||||
"nostr_core/nostr_signer.c"
|
||||
"nostr_core/nip019.c"
|
||||
"nostr_core/utils.c"
|
||||
"nostr_core/crypto/nostr_secp256k1.c"
|
||||
"nostr_core/crypto/nostr_aes.c"
|
||||
"nostr_core/crypto/nostr_chacha20.c"
|
||||
"cjson/cJSON.c"
|
||||
"platform/esp32/nostr_platform_esp32.c"
|
||||
"platform/esp32/nostr_http_esp32.c"
|
||||
"platform/esp32/nostr_websocket_esp32.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"nostr_core"
|
||||
"nostr_core/crypto"
|
||||
"cjson"
|
||||
"nostr_websocket"
|
||||
REQUIRES
|
||||
secp256k1
|
||||
esp_hw_support
|
||||
esp_http_client
|
||||
esp-tls
|
||||
tcp_transport
|
||||
mbedtls
|
||||
)
|
||||
|
||||
target_compile_definitions(${COMPONENT_LIB} PRIVATE NOSTR_NO_FILESYSTEM=1)
|
||||
else()
|
||||
project(nostr_core_lib C)
|
||||
|
||||
add_library(nostr_core STATIC
|
||||
nostr_core/nostr_common.c
|
||||
nostr_core/nostr_log.c
|
||||
nostr_core/request_validator.c
|
||||
nostr_core/nip001.c
|
||||
nostr_core/nip003.c
|
||||
nostr_core/nip004.c
|
||||
nostr_core/nip005.c
|
||||
nostr_core/nip006.c
|
||||
nostr_core/nip011.c
|
||||
nostr_core/nip013.c
|
||||
nostr_core/nip017.c
|
||||
nostr_core/nip019.c
|
||||
nostr_core/nip021.c
|
||||
nostr_core/nip042.c
|
||||
nostr_core/nip044.c
|
||||
nostr_core/nip046.c
|
||||
nostr_core/nip059.c
|
||||
nostr_core/nip060.c
|
||||
nostr_core/nip061.c
|
||||
nostr_core/nostr_signer.c
|
||||
nostr_core/nsigner_transport.c
|
||||
nostr_core/nsigner_client.c
|
||||
nostr_core/utils.c
|
||||
nostr_core/nostr_http.c
|
||||
nostr_core/core_relays.c
|
||||
nostr_core/core_relay_pool.c
|
||||
nostr_core/cashu_mint.c
|
||||
nostr_core/blossom_client.c
|
||||
nostr_core/crypto/nostr_secp256k1.c
|
||||
nostr_core/crypto/nostr_aes.c
|
||||
nostr_core/crypto/nostr_chacha20.c
|
||||
nostr_core/crypto/nostr_poly1305.c
|
||||
nostr_core/crypto/nostr_chacha20poly1305.c
|
||||
cjson/cJSON.c
|
||||
nostr_websocket/nostr_websocket_openssl.c
|
||||
platform/linux.c
|
||||
)
|
||||
|
||||
target_include_directories(nostr_core PUBLIC
|
||||
.
|
||||
nostr_core
|
||||
nostr_core/crypto
|
||||
cjson
|
||||
nostr_websocket
|
||||
)
|
||||
target_compile_definitions(nostr_core PRIVATE NOSTR_ENABLE_NSIGNER_CLIENT=1)
|
||||
target_compile_definitions(nostr_core PRIVATE ENABLE_FILE_LOGGING ENABLE_WEBSOCKET_LOGGING ENABLE_DEBUG_LOGGING)
|
||||
|
||||
# Link system dependencies
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(SECP256K1 QUIET libsecp256k1)
|
||||
pkg_check_modules(OPENSSL QUIET openssl)
|
||||
pkg_check_modules(CURL QUIET libcurl)
|
||||
endif()
|
||||
|
||||
if(SECP256K1_FOUND)
|
||||
target_include_directories(nostr_core PRIVATE ${SECP256K1_INCLUDE_DIRS})
|
||||
target_link_libraries(nostr_core PRIVATE ${SECP256K1_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(nostr_core PRIVATE secp256k1)
|
||||
endif()
|
||||
|
||||
if(OPENSSL_FOUND)
|
||||
target_include_directories(nostr_core PRIVATE ${OPENSSL_INCLUDE_DIRS})
|
||||
target_link_libraries(nostr_core PRIVATE ${OPENSSL_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(nostr_core PRIVATE ssl crypto)
|
||||
endif()
|
||||
|
||||
if(CURL_FOUND)
|
||||
target_include_directories(nostr_core PRIVATE ${CURL_INCLUDE_DIRS})
|
||||
target_link_libraries(nostr_core PRIVATE ${CURL_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(nostr_core PRIVATE curl)
|
||||
endif()
|
||||
|
||||
target_link_libraries(nostr_core PRIVATE z dl pthread m)
|
||||
endif()
|
||||
+777
@@ -0,0 +1,777 @@
|
||||
# Relay Pool API Reference
|
||||
|
||||
This document describes the public API for the Nostr Relay Pool implementation in [`core_relay_pool.c`](nostr_core/core_relay_pool.c).
|
||||
|
||||
## Function Summary
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:594) | Create and initialize a new relay pool |
|
||||
| [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:733) | Destroy pool and cleanup all resources |
|
||||
| [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:648) | Add a relay URL to the pool |
|
||||
| [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:702) | Remove a relay URL from the pool |
|
||||
| [`nostr_relay_pool_set_auth()`](nostr_core/core_relay_pool.c:616) | Configure pool-wide NIP-42 authentication key and enable flag |
|
||||
| [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:778) | Create async subscription with callbacks |
|
||||
| [`nostr_pool_subscription_close()`](nostr_core/core_relay_pool.c:491) | Close subscription and free resources |
|
||||
| [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192) | Run event loop for specified timeout |
|
||||
| [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232) | Single iteration poll and dispatch |
|
||||
| [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:695) | Synchronous query returning event array |
|
||||
| [`nostr_relay_pool_get_event()`](nostr_core/core_relay_pool.c:825) | Get single most recent event |
|
||||
| [`nostr_relay_pool_publish_async()`](nostr_core/core_relay_pool.c:866) | Publish event with async callbacks |
|
||||
| [`nostr_relay_pool_get_relay_status()`](nostr_core/core_relay_pool.c:944) | Get connection status for a relay |
|
||||
| [`nostr_relay_pool_list_relays()`](nostr_core/core_relay_pool.c:960) | List all relays and their statuses |
|
||||
| [`nostr_relay_pool_get_relay_stats()`](nostr_core/core_relay_pool.c:992) | Get detailed statistics for a relay |
|
||||
| [`nostr_relay_pool_reset_relay_stats()`](nostr_core/core_relay_pool.c:1008) | Reset statistics for a relay |
|
||||
| [`nostr_relay_pool_get_relay_query_latency()`](nostr_core/core_relay_pool.c:1045) | Get average query latency for a relay |
|
||||
|
||||
## Pool Lifecycle
|
||||
|
||||
### Create Pool
|
||||
**Function:** [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:219)
|
||||
```c
|
||||
nostr_relay_pool_t* nostr_relay_pool_create(void);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
#include "nostr_core.h"
|
||||
|
||||
int main() {
|
||||
// Create a new relay pool
|
||||
nostr_relay_pool_t* pool = nostr_relay_pool_create();
|
||||
if (!pool) {
|
||||
fprintf(stderr, "Failed to create relay pool\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Use the pool...
|
||||
|
||||
// Clean up
|
||||
nostr_relay_pool_destroy(pool);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Destroy Pool
|
||||
**Function:** [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:304)
|
||||
```c
|
||||
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
// Properly cleanup a relay pool
|
||||
void cleanup_pool(nostr_relay_pool_t* pool) {
|
||||
if (pool) {
|
||||
// This will close all active subscriptions and relay connections
|
||||
nostr_relay_pool_destroy(pool);
|
||||
pool = NULL;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Relay Management
|
||||
|
||||
### Configure Pool Authentication (NIP-42)
|
||||
**Function:** [`nostr_relay_pool_set_auth()`](nostr_core/core_relay_pool.c:616)
|
||||
```c
|
||||
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable);
|
||||
```
|
||||
|
||||
**Description:**
|
||||
- Sets a pool-wide private key used to sign NIP-42 `AUTH` responses.
|
||||
- Enables/disables NIP-42 handling for all relays in the pool.
|
||||
- Propagates auth-enabled state to existing relay connections.
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
unsigned char privkey[32];
|
||||
nostr_hex_to_bytes("91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", privkey, 32);
|
||||
|
||||
nostr_relay_pool_t* pool = nostr_relay_pool_create(nostr_pool_reconnect_config_default());
|
||||
nostr_relay_pool_set_auth(pool, privkey, 1); // enable NIP-42 auto AUTH handling
|
||||
```
|
||||
|
||||
### Add Relay
|
||||
**Function:** [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229)
|
||||
```c
|
||||
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int setup_relays(nostr_relay_pool_t* pool) {
|
||||
const char* relays[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band"
|
||||
};
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int result = nostr_relay_pool_add_relay(pool, relays[i]);
|
||||
if (result != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to add relay %s: %d\n", relays[i], result);
|
||||
return -1;
|
||||
}
|
||||
printf("Added relay: %s\n", relays[i]);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Remove Relay
|
||||
**Function:** [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:273)
|
||||
```c
|
||||
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int remove_slow_relay(nostr_relay_pool_t* pool) {
|
||||
const char* slow_relay = "wss://slow-relay.example.com";
|
||||
|
||||
int result = nostr_relay_pool_remove_relay(pool, slow_relay);
|
||||
if (result == NOSTR_SUCCESS) {
|
||||
printf("Successfully removed relay: %s\n", slow_relay);
|
||||
} else {
|
||||
printf("Failed to remove relay %s (may not exist)\n", slow_relay);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## Subscriptions (Asynchronous)
|
||||
|
||||
### Subscribe to Events
|
||||
**Function:** [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:399)
|
||||
```c
|
||||
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(void* user_data),
|
||||
void* user_data);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
#include "cjson/cJSON.h"
|
||||
|
||||
// Event callback - called for each received event
|
||||
void handle_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
|
||||
|
||||
if (content && pubkey) {
|
||||
printf("Event from %s: %s (by %s)\n",
|
||||
relay_url,
|
||||
cJSON_GetStringValue(content),
|
||||
cJSON_GetStringValue(pubkey));
|
||||
}
|
||||
}
|
||||
|
||||
// EOSE callback - called when all relays finish sending stored events
|
||||
void handle_eose(void* user_data) {
|
||||
printf("All relays finished sending stored events\n");
|
||||
}
|
||||
|
||||
int subscribe_to_notes(nostr_relay_pool_t* pool) {
|
||||
// Create filter for kind 1 (text notes) from last hour
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
time_t since = time(NULL) - 3600; // Last hour
|
||||
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(since));
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(50));
|
||||
|
||||
// Subscribe to specific relays
|
||||
const char* relay_urls[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol"
|
||||
};
|
||||
|
||||
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
|
||||
pool,
|
||||
relay_urls,
|
||||
2,
|
||||
filter,
|
||||
handle_event,
|
||||
handle_eose,
|
||||
NULL // user_data
|
||||
);
|
||||
|
||||
cJSON_Delete(filter); // Pool makes its own copy
|
||||
|
||||
if (!sub) {
|
||||
fprintf(stderr, "Failed to create subscription\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Drive the event loop to receive events
|
||||
printf("Listening for events for 30 seconds...\n");
|
||||
nostr_relay_pool_run(pool, 30000); // 30 seconds
|
||||
|
||||
// Close subscription
|
||||
nostr_pool_subscription_close(sub);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Close Subscription
|
||||
**Function:** [`nostr_pool_subscription_close()`](nostr_core/core_relay_pool.c:491)
|
||||
```c
|
||||
int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
// Subscription management with cleanup
|
||||
typedef struct {
|
||||
nostr_pool_subscription_t* subscription;
|
||||
int event_count;
|
||||
int should_stop;
|
||||
} subscription_context_t;
|
||||
|
||||
void event_counter(cJSON* event, const char* relay_url, void* user_data) {
|
||||
subscription_context_t* ctx = (subscription_context_t*)user_data;
|
||||
ctx->event_count++;
|
||||
|
||||
printf("Received event #%d from %s\n", ctx->event_count, relay_url);
|
||||
|
||||
// Stop after 10 events
|
||||
if (ctx->event_count >= 10) {
|
||||
ctx->should_stop = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int limited_subscription(nostr_relay_pool_t* pool) {
|
||||
subscription_context_t ctx = {0};
|
||||
|
||||
// Create filter
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
const char* relay_urls[] = {"wss://relay.damus.io"};
|
||||
|
||||
ctx.subscription = nostr_relay_pool_subscribe(
|
||||
pool, relay_urls, 1, filter, event_counter, NULL, &ctx);
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (!ctx.subscription) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Poll until we should stop
|
||||
while (!ctx.should_stop) {
|
||||
int events = nostr_relay_pool_poll(pool, 100);
|
||||
if (events < 0) break;
|
||||
}
|
||||
|
||||
// Clean up
|
||||
int result = nostr_pool_subscription_close(ctx.subscription);
|
||||
printf("Subscription closed with result: %d\n", result);
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Event Loop
|
||||
|
||||
### Run Timed Loop
|
||||
**Function:** [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1192)
|
||||
```c
|
||||
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int run_event_loop(nostr_relay_pool_t* pool) {
|
||||
printf("Starting event loop for 60 seconds...\n");
|
||||
|
||||
// Run for 60 seconds, processing all incoming events
|
||||
int total_events = nostr_relay_pool_run(pool, 60000);
|
||||
|
||||
if (total_events < 0) {
|
||||
fprintf(stderr, "Event loop error\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("Processed %d events total\n", total_events);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Single Poll Iteration
|
||||
**Function:** [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232)
|
||||
```c
|
||||
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
// Integration with custom main loop
|
||||
int custom_main_loop(nostr_relay_pool_t* pool) {
|
||||
int running = 1;
|
||||
int total_events = 0;
|
||||
|
||||
while (running) {
|
||||
// Poll for Nostr events (non-blocking with 50ms timeout)
|
||||
int events = nostr_relay_pool_poll(pool, 50);
|
||||
if (events > 0) {
|
||||
total_events += events;
|
||||
printf("Processed %d events this iteration\n", events);
|
||||
}
|
||||
|
||||
// Do other work in your application
|
||||
// handle_ui_events();
|
||||
// process_background_tasks();
|
||||
|
||||
// Check exit condition
|
||||
// running = !should_exit();
|
||||
|
||||
// Simple exit after 100 events for demo
|
||||
if (total_events >= 100) {
|
||||
running = 0;
|
||||
}
|
||||
}
|
||||
|
||||
printf("Main loop finished, processed %d total events\n", total_events);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Synchronous Operations
|
||||
|
||||
### Query Multiple Events
|
||||
**Function:** [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:695)
|
||||
```c
|
||||
cJSON** nostr_relay_pool_query_sync(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int* event_count,
|
||||
int timeout_ms);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int query_recent_notes(nostr_relay_pool_t* pool) {
|
||||
// Create filter for recent text notes
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(20));
|
||||
|
||||
const char* relay_urls[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol"
|
||||
};
|
||||
|
||||
int event_count = 0;
|
||||
cJSON** events = nostr_relay_pool_query_sync(
|
||||
pool, relay_urls, 2, filter, &event_count, 10000); // 10 second timeout
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (!events) {
|
||||
printf("No events received or query failed\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("Received %d events:\n", event_count);
|
||||
for (int i = 0; i < event_count; i++) {
|
||||
cJSON* content = cJSON_GetObjectItem(events[i], "content");
|
||||
if (content) {
|
||||
printf(" %d: %s\n", i + 1, cJSON_GetStringValue(content));
|
||||
}
|
||||
|
||||
// Free each event
|
||||
cJSON_Delete(events[i]);
|
||||
}
|
||||
|
||||
// Free the events array
|
||||
free(events);
|
||||
return event_count;
|
||||
}
|
||||
```
|
||||
|
||||
### Get Single Most Recent Event
|
||||
**Function:** [`nostr_relay_pool_get_event()`](nostr_core/core_relay_pool.c:825)
|
||||
```c
|
||||
cJSON* nostr_relay_pool_get_event(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int timeout_ms);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int get_latest_note_from_pubkey(nostr_relay_pool_t* pool, const char* pubkey_hex) {
|
||||
// Create filter for specific author's notes
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(1));
|
||||
|
||||
const char* relay_urls[] = {"wss://relay.damus.io"};
|
||||
|
||||
cJSON* event = nostr_relay_pool_get_event(
|
||||
pool, relay_urls, 1, filter, 5000); // 5 second timeout
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (!event) {
|
||||
printf("No recent event found for pubkey %s\n", pubkey_hex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
|
||||
|
||||
if (content && created_at) {
|
||||
printf("Latest note: %s (created at %ld)\n",
|
||||
cJSON_GetStringValue(content),
|
||||
(long)cJSON_GetNumberValue(created_at));
|
||||
}
|
||||
|
||||
cJSON_Delete(event);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Publish Event
|
||||
**Function:** [`nostr_relay_pool_publish_async()`](nostr_core/core_relay_pool.c:866)
|
||||
```c
|
||||
int nostr_relay_pool_publish_async(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* event);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
int publish_text_note(nostr_relay_pool_t* pool, const char* content) {
|
||||
// Create a basic text note event (this is simplified - real implementation
|
||||
// would need proper signing with private key)
|
||||
cJSON* event = cJSON_CreateObject();
|
||||
cJSON_AddItemToObject(event, "kind", cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(event, "content", cJSON_CreateString(content));
|
||||
cJSON_AddItemToObject(event, "created_at", cJSON_CreateNumber(time(NULL)));
|
||||
|
||||
// In real usage, you'd add pubkey, id, sig fields here
|
||||
cJSON_AddItemToObject(event, "pubkey", cJSON_CreateString("your_pubkey_hex"));
|
||||
cJSON_AddItemToObject(event, "id", cJSON_CreateString("event_id_hash"));
|
||||
cJSON_AddItemToObject(event, "sig", cJSON_CreateString("event_signature"));
|
||||
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
|
||||
|
||||
const char* relay_urls[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band"
|
||||
};
|
||||
|
||||
printf("Publishing note: %s\n", content);
|
||||
|
||||
int success_count = nostr_relay_pool_publish_async(
|
||||
pool, relay_urls, 3, event, my_callback, user_data);
|
||||
|
||||
cJSON_Delete(event);
|
||||
|
||||
printf("Successfully published to %d out of 3 relays\n", success_count);
|
||||
|
||||
if (success_count == 0) {
|
||||
fprintf(stderr, "Failed to publish to any relay\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return success_count;
|
||||
}
|
||||
```
|
||||
|
||||
## Status and Statistics
|
||||
|
||||
### Get Relay Status
|
||||
**Function:** [`nostr_relay_pool_get_relay_status()`](nostr_core/core_relay_pool.c:944)
|
||||
```c
|
||||
nostr_pool_relay_status_t nostr_relay_pool_get_relay_status(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void check_relay_status(nostr_relay_pool_t* pool, const char* relay_url) {
|
||||
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(pool, relay_url);
|
||||
|
||||
const char* status_str;
|
||||
switch (status) {
|
||||
case NOSTR_POOL_RELAY_DISCONNECTED:
|
||||
status_str = "DISCONNECTED";
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_CONNECTING:
|
||||
status_str = "CONNECTING";
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_CONNECTED:
|
||||
status_str = "CONNECTED";
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_ERROR:
|
||||
status_str = "ERROR";
|
||||
break;
|
||||
default:
|
||||
status_str = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
|
||||
printf("Relay %s status: %s\n", relay_url, status_str);
|
||||
}
|
||||
```
|
||||
|
||||
### List All Relays
|
||||
**Function:** [`nostr_relay_pool_list_relays()`](nostr_core/core_relay_pool.c:960)
|
||||
```c
|
||||
int nostr_relay_pool_list_relays(
|
||||
nostr_relay_pool_t* pool,
|
||||
char*** relay_urls,
|
||||
nostr_pool_relay_status_t** statuses);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void print_all_relays(nostr_relay_pool_t* pool) {
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
|
||||
int count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
if (count < 0) {
|
||||
printf("Failed to list relays\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
printf("No relays configured\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("Configured relays (%d):\n", count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char* status_str = (statuses[i] == NOSTR_POOL_RELAY_CONNECTED) ?
|
||||
"CONNECTED" : "DISCONNECTED";
|
||||
printf(" %s - %s\n", relay_urls[i], status_str);
|
||||
|
||||
// Free the duplicated URL string
|
||||
free(relay_urls[i]);
|
||||
}
|
||||
|
||||
// Free the arrays
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
}
|
||||
```
|
||||
|
||||
### Get Relay Statistics
|
||||
**Function:** [`nostr_relay_pool_get_relay_stats()`](nostr_core/core_relay_pool.c:992)
|
||||
```c
|
||||
const nostr_relay_stats_t* nostr_relay_pool_get_relay_stats(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void print_relay_stats(nostr_relay_pool_t* pool, const char* relay_url) {
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_url);
|
||||
|
||||
if (!stats) {
|
||||
printf("No stats available for relay %s\n", relay_url);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("Statistics for %s:\n", relay_url);
|
||||
printf(" Connection attempts: %d\n", stats->connection_attempts);
|
||||
printf(" Connection failures: %d\n", stats->connection_failures);
|
||||
printf(" Events received: %d\n", stats->events_received);
|
||||
printf(" Events published: %d\n", stats->events_published);
|
||||
printf(" Events published OK: %d\n", stats->events_published_ok);
|
||||
printf(" Events published failed: %d\n", stats->events_published_failed);
|
||||
printf(" Query latency avg: %.2f ms\n", stats->query_latency_avg);
|
||||
printf(" Query samples: %d\n", stats->query_samples);
|
||||
printf(" Publish latency avg: %.2f ms\n", stats->publish_latency_avg);
|
||||
printf(" Publish samples: %d\n", stats->publish_samples);
|
||||
|
||||
if (stats->last_event_time > 0) {
|
||||
printf(" Last event: %ld seconds ago\n",
|
||||
time(NULL) - stats->last_event_time);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reset Relay Statistics
|
||||
**Function:** [`nostr_relay_pool_reset_relay_stats()`](nostr_core/core_relay_pool.c:1008)
|
||||
```c
|
||||
int nostr_relay_pool_reset_relay_stats(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void reset_stats_for_relay(nostr_relay_pool_t* pool, const char* relay_url) {
|
||||
int result = nostr_relay_pool_reset_relay_stats(pool, relay_url);
|
||||
|
||||
if (result == NOSTR_SUCCESS) {
|
||||
printf("Successfully reset statistics for %s\n", relay_url);
|
||||
} else {
|
||||
printf("Failed to reset statistics for %s\n", relay_url);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Query Latency
|
||||
**Function:** [`nostr_relay_pool_get_relay_query_latency()`](nostr_core/core_relay_pool.c:1045)
|
||||
```c
|
||||
double nostr_relay_pool_get_relay_query_latency(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```c
|
||||
void check_relay_performance(nostr_relay_pool_t* pool) {
|
||||
const char* relays[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band"
|
||||
};
|
||||
|
||||
printf("Relay performance comparison:\n");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double latency = nostr_relay_pool_get_relay_query_latency(pool, relays[i]);
|
||||
|
||||
if (latency >= 0) {
|
||||
printf(" %s: %.2f ms average query latency\n", relays[i], latency);
|
||||
} else {
|
||||
printf(" %s: No latency data available\n", relays[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example Application
|
||||
|
||||
```c
|
||||
#include "nostr_core.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// Global context for the example
|
||||
typedef struct {
|
||||
int event_count;
|
||||
int max_events;
|
||||
} app_context_t;
|
||||
|
||||
void on_text_note(cJSON* event, const char* relay_url, void* user_data) {
|
||||
app_context_t* ctx = (app_context_t*)user_data;
|
||||
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
if (content && cJSON_IsString(content)) {
|
||||
printf("[%s] Note #%d: %s\n",
|
||||
relay_url, ++ctx->event_count, cJSON_GetStringValue(content));
|
||||
}
|
||||
}
|
||||
|
||||
void on_subscription_complete(void* user_data) {
|
||||
printf("All relays finished sending stored events\n");
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Initialize pool
|
||||
nostr_relay_pool_t* pool = nostr_relay_pool_create();
|
||||
if (!pool) {
|
||||
fprintf(stderr, "Failed to create relay pool\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Add relays
|
||||
const char* relays[] = {
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol"
|
||||
};
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (nostr_relay_pool_add_relay(pool, relays[i]) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to add relay: %s\n", relays[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create filter for recent text notes
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(10));
|
||||
|
||||
// Set up context
|
||||
app_context_t ctx = {0, 10};
|
||||
|
||||
// Subscribe
|
||||
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
|
||||
pool, relays, 2, filter, on_text_note, on_subscription_complete, &ctx);
|
||||
|
||||
cJSON_Delete(filter);
|
||||
|
||||
if (!sub) {
|
||||
fprintf(stderr, "Failed to create subscription\n");
|
||||
nostr_relay_pool_destroy(pool);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Run event loop for 30 seconds
|
||||
printf("Listening for events...\n");
|
||||
nostr_relay_pool_run(pool, 30000);
|
||||
|
||||
// Print final stats
|
||||
for (int i = 0; i < 2; i++) {
|
||||
print_relay_stats(pool, relays[i]);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
nostr_pool_subscription_close(sub);
|
||||
nostr_relay_pool_destroy(pool);
|
||||
|
||||
printf("Application finished. Received %d events total.\n", ctx.event_count);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- All functions are **not thread-safe**. Use from a single thread or add external synchronization.
|
||||
- **Memory ownership**: The pool duplicates filters and URLs internally. Caller owns returned events and must free them.
|
||||
- **Event deduplication** is applied pool-wide using a circular buffer of 1000 event IDs.
|
||||
- **Ping functionality** is currently disabled in this build.
|
||||
- **NIP-42 behavior**: With [`nostr_relay_pool_set_auth()`](nostr_core/core_relay_pool.c:616) enabled, the pool will respond to relay `AUTH` challenges automatically using [`nostr_nip42_create_auth_event()`](nostr_core/nip042.c:26) and [`nostr_nip42_create_auth_message()`](nostr_core/nip042.c:84).
|
||||
- **Reconnection** happens on-demand when sending, but active subscriptions are not automatically re-sent after reconnect.
|
||||
- **Polling model**: You must drive the event loop via [`nostr_relay_pool_run()`](nostr_core/core_relay_pool.c:1745) or [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1785) to receive events.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A C library for NOSTR protocol implementation. Work in progress.
|
||||
|
||||
[](VERSION)
|
||||
[](VERSION)
|
||||
[](#license)
|
||||
[](#building)
|
||||
|
||||
@@ -12,11 +12,11 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
### Core Protocol NIPs
|
||||
- [x] [NIP-01](nips/01.md) - Basic protocol flow - event creation, signing, and validation
|
||||
- [ ] [NIP-02](nips/02.md) - Contact List and Petnames
|
||||
- [ ] [NIP-03](nips/03.md) - OpenTimestamps Attestations for Events
|
||||
- [x] [NIP-03](nips/03.md) - OpenTimestamps Attestations for Events
|
||||
- [x] [NIP-04](nips/04.md) - Encrypted Direct Messages (legacy)
|
||||
- [x] [NIP-05](nips/05.md) - Mapping Nostr keys to DNS-based internet identifiers
|
||||
- [x] [NIP-06](nips/06.md) - Basic key derivation from mnemonic seed phrase
|
||||
- [ ] [NIP-07](nips/07.md) - `window.nostr` capability for web browsers
|
||||
- [-] [NIP-07](nips/07.md) - `window.nostr` capability for web browsers
|
||||
- [ ] [NIP-08](nips/08.md) - Handling Mentions
|
||||
- [ ] [NIP-09](nips/09.md) - Event Deletion
|
||||
- [ ] [NIP-10](nips/10.md) - Conventions for clients' use of `e` and `p` tags in text events
|
||||
@@ -26,11 +26,11 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-14](nips/14.md) - Subject tag in text events
|
||||
- [ ] [NIP-15](nips/15.md) - Nostr Marketplace (for resilient marketplaces)
|
||||
- [ ] [NIP-16](nips/16.md) - Event Treatment
|
||||
- [ ] [NIP-17](nips/17.md) - Private Direct Messages
|
||||
- [x] [NIP-17](nips/17.md) - Private Direct Messages
|
||||
- [ ] [NIP-18](nips/18.md) - Reposts
|
||||
- [x] [NIP-19](nips/19.md) - bech32-encoded entities
|
||||
- [ ] [NIP-20](nips/20.md) - Command Results
|
||||
- [ ] [NIP-21](nips/21.md) - `nostr:` URI scheme
|
||||
- [x] [NIP-21](nips/21.md) - `nostr:` URI scheme
|
||||
- [ ] [NIP-22](nips/22.md) - Event `created_at` Limits
|
||||
- [ ] [NIP-23](nips/23.md) - Long-form Content
|
||||
- [ ] [NIP-24](nips/24.md) - Extra metadata fields and tags
|
||||
@@ -50,10 +50,10 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-38](nips/38.md) - User Statuses
|
||||
- [ ] [NIP-39](nips/39.md) - External Identities in Profiles
|
||||
- [ ] [NIP-40](nips/40.md) - Expiration Timestamp
|
||||
- [ ] [NIP-42](nips/42.md) - Authentication of clients to relays
|
||||
- [x] [NIP-42](nips/42.md) - Authentication of clients to relays
|
||||
- [x] [NIP-44](nips/44.md) - Versioned Encryption
|
||||
- [ ] [NIP-45](nips/45.md) - Counting results
|
||||
- [ ] [NIP-46](nips/46.md) - Nostr Connect
|
||||
- [x] [NIP-46](nips/46.md) - Nostr Remote Signing
|
||||
- [ ] [NIP-47](nips/47.md) - Wallet Connect
|
||||
- [ ] [NIP-48](nips/48.md) - Proxy Tags
|
||||
- [ ] [NIP-49](nips/49.md) - Private Key Encryption
|
||||
@@ -62,18 +62,18 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-52](nips/52.md) - Calendar Events
|
||||
- [ ] [NIP-53](nips/53.md) - Live Activities
|
||||
- [ ] [NIP-54](nips/54.md) - Wiki
|
||||
- [ ] [NIP-55](nips/55.md) - Android Signer Application
|
||||
- [-] [NIP-55](nips/55.md) - Android Signer Application
|
||||
- [ ] [NIP-56](nips/56.md) - Reporting
|
||||
- [ ] [NIP-57](nips/57.md) - Lightning Zaps
|
||||
- [ ] [NIP-58](nips/58.md) - Badges
|
||||
- [ ] [NIP-59](nips/59.md) - Gift Wrap
|
||||
- [x] [NIP-59](nips/59.md) - Gift Wrap
|
||||
- [ ] [NIP-60](nips/60.md) - Cashu Wallet
|
||||
- [ ] [NIP-61](nips/61.md) - Nutzaps
|
||||
- [ ] [NIP-62](nips/62.md) - Log events
|
||||
- [ ] [NIP-64](nips/64.md) - Chess (PGN)
|
||||
- [-] [NIP-64](nips/64.md) - Chess (PGN)
|
||||
- [ ] [NIP-65](nips/65.md) - Relay List Metadata
|
||||
- [ ] [NIP-66](nips/66.md) - Relay Monitor
|
||||
- [ ] [NIP-68](nips/68.md) - Web badges
|
||||
- [-] [NIP-68](nips/68.md) - Web badges
|
||||
- [ ] [NIP-69](nips/69.md) - Peer-to-peer Order events
|
||||
- [ ] [NIP-70](nips/70.md) - Protected Events
|
||||
- [ ] [NIP-71](nips/71.md) - Video Events
|
||||
@@ -85,7 +85,7 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-84](nips/84.md) - Highlights
|
||||
- [ ] [NIP-86](nips/86.md) - Relay Management API
|
||||
- [ ] [NIP-87](nips/87.md) - Relay List Recommendations
|
||||
- [ ] [NIP-88](nips/88.md) - Stella: A Stellar Relay
|
||||
- [-] [NIP-88](nips/88.md) - Stella: A Stellar Relay
|
||||
- [ ] [NIP-89](nips/89.md) - Recommended Application Handlers
|
||||
- [ ] [NIP-90](nips/90.md) - Data Vending Machines
|
||||
- [ ] [NIP-92](nips/92.md) - Media Attachments
|
||||
@@ -94,9 +94,9 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
- [ ] [NIP-98](nips/98.md) - HTTP Auth
|
||||
- [ ] [NIP-99](nips/99.md) - Classified Listings
|
||||
|
||||
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented
|
||||
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented | ➖ Not Applicable
|
||||
|
||||
**Implementation Summary:** 8 of 96+ NIPs fully implemented (8.3%)
|
||||
**Implementation Summary:** 13 of 96+ NIPs fully implemented (13.5%)
|
||||
|
||||
|
||||
## 📦 Quick Start
|
||||
@@ -111,12 +111,12 @@ A C library for NOSTR protocol implementation. Work in progress.
|
||||
|
||||
2. **Build the library:**
|
||||
```bash
|
||||
./build.sh lib
|
||||
./build.sh
|
||||
```
|
||||
|
||||
3. **Run examples:**
|
||||
3. **Build and run examples:**
|
||||
```bash
|
||||
./build.sh examples
|
||||
./build.sh -e
|
||||
./examples/simple_keygen
|
||||
```
|
||||
|
||||
@@ -161,7 +161,7 @@ int main() {
|
||||
|
||||
**Compile and run:**
|
||||
```bash
|
||||
gcc example.c -o example ./libnostr_core.a -lm
|
||||
gcc example.c -o example ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
||||
./example
|
||||
```
|
||||
|
||||
@@ -170,27 +170,13 @@ gcc example.c -o example ./libnostr_core.a -lm
|
||||
### Build Targets
|
||||
|
||||
```bash
|
||||
./build.sh lib # Build static library (default)
|
||||
./build.sh examples # Build examples
|
||||
./build.sh test # Run test suite
|
||||
./build.sh clean # Clean build artifacts
|
||||
./build.sh install # Install to system
|
||||
```
|
||||
|
||||
### Manual Building
|
||||
|
||||
```bash
|
||||
# Build static library
|
||||
make
|
||||
|
||||
# Build examples
|
||||
make examples
|
||||
|
||||
# Run tests
|
||||
make test-crypto
|
||||
|
||||
# Clean
|
||||
make clean
|
||||
./build.sh # Build static library (default)
|
||||
./build.sh --nips=all # Force build with all available NIPs
|
||||
./build.sh -t # Build all test executables in tests/
|
||||
./build.sh -e # Build all examples in examples/
|
||||
./build.sh -t -e # Build library + tests + examples
|
||||
./build.sh --nips=46 # Build specifically with NIP-046 support
|
||||
./build.sh --help # Show all options
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
@@ -302,20 +288,38 @@ publish_result_t* synchronous_publish_event_with_progress(const char** relay_url
|
||||
|
||||
### Relay Pools (Asynchronous)
|
||||
```c
|
||||
// Create and manage relay pool
|
||||
nostr_relay_pool_t* nostr_relay_pool_create(void);
|
||||
// Create and manage relay pool with reconnection
|
||||
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
|
||||
nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* config);
|
||||
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable);
|
||||
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
||||
|
||||
// Subscribe to events
|
||||
// Subscribe to events (with auto-reconnection)
|
||||
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
|
||||
nostr_relay_pool_t* pool, const char** relay_urls, int relay_count, cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(void* user_data), void* user_data);
|
||||
void (*on_eose)(void* user_data), void* user_data, int close_on_eose);
|
||||
|
||||
// Run event loop
|
||||
// Enable NIP-42 auth (auto-responds to relay AUTH challenges)
|
||||
unsigned char private_key[32];
|
||||
nostr_hex_to_bytes("91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", private_key, 32);
|
||||
nostr_relay_pool_set_auth(pool, private_key, 1);
|
||||
|
||||
// Run event loop (handles reconnection + incoming AUTH/NOTICE/OK messages)
|
||||
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
|
||||
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
|
||||
|
||||
// Reconnection configuration
|
||||
typedef struct {
|
||||
int enable_auto_reconnect; // Enable automatic reconnection
|
||||
int max_reconnect_attempts; // Maximum retry attempts
|
||||
int initial_reconnect_delay_ms; // Initial delay between attempts
|
||||
int max_reconnect_delay_ms; // Maximum delay cap
|
||||
int reconnect_backoff_multiplier; // Exponential backoff factor
|
||||
int ping_interval_seconds; // Health check ping interval
|
||||
int pong_timeout_seconds; // Pong response timeout
|
||||
} nostr_pool_reconnect_config_t;
|
||||
```
|
||||
|
||||
### NIP-05 Identifier Verification
|
||||
@@ -342,6 +346,171 @@ int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** inf
|
||||
void nostr_nip11_relay_info_free(nostr_relay_info_t* info);
|
||||
```
|
||||
|
||||
## Signer abstraction & nsigner integration
|
||||
|
||||
All signing/encryption operations can run through an opaque `nostr_signer_t` handle. This provides:
|
||||
|
||||
- **LOCAL backend** (`nostr_signer_local`) using a raw 32-byte private key (output-equivalent to legacy raw-key APIs).
|
||||
- **REMOTE backends** (`nostr_signer_nsigner_*`) that call a running `n_signer` process/device over supported transports.
|
||||
|
||||
### Core signer verbs (exact signatures)
|
||||
|
||||
```c
|
||||
/* Lifecycle */
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]);
|
||||
void nostr_signer_free(nostr_signer_t* signer);
|
||||
|
||||
/* Core verbs */
|
||||
int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]);
|
||||
int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out);
|
||||
|
||||
/* Encryption verbs */
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
```
|
||||
|
||||
### Backends / transports
|
||||
|
||||
Remote signer factories (available when `NOSTR_ENABLE_NSIGNER_CLIENT` is enabled):
|
||||
|
||||
```c
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms);
|
||||
int nostr_signer_nsigner_set_auth(nostr_signer_t* signer,
|
||||
const unsigned char auth_privkey[32],
|
||||
const char* label);
|
||||
```
|
||||
|
||||
Transport layer constructors and discovery helpers:
|
||||
|
||||
```c
|
||||
nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms);
|
||||
nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms);
|
||||
nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms);
|
||||
nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms);
|
||||
int nsigner_transport_list_unix(char names[][64], int max_names);
|
||||
int nsigner_transport_list_serial(char paths[][64], int max_paths);
|
||||
```
|
||||
|
||||
Supported transport modes:
|
||||
- UNIX abstract socket (`unix:<name>`, no leading `@` in API input)
|
||||
- USB CDC-ACM serial (`serial:/dev/ttyACM0`)
|
||||
- TCP (`tcp:<host>:<port>`)
|
||||
- Existing framed fd pair (`fds:<read_fd>:<write_fd>`, useful for stdio/qrexec-style bridges)
|
||||
|
||||
Build gating:
|
||||
- Desktop builds enable `NOSTR_ENABLE_NSIGNER_CLIENT`.
|
||||
- ESP32 builds exclude the nsigner client transport/client sources and do not define the flag.
|
||||
- n_signer’s own build keeps this client side disabled.
|
||||
|
||||
Wire contract (high level):
|
||||
- Framing is **4-byte big-endian length + JSON payload**.
|
||||
- Payload length must be in `[1, 65536]` bytes.
|
||||
- TCP uses a kind-27235 auth envelope (`nsigner_rpc`, `nsigner_method`, `nsigner_body_hash`).
|
||||
- See n_signer docs for authoritative protocol details.
|
||||
|
||||
### Using a signer in higher-level APIs
|
||||
|
||||
Pattern: existing raw-private-key functions remain unchanged; signer-aware siblings are provided as `*_with_signer` variants.
|
||||
|
||||
Covered modules and functions:
|
||||
|
||||
- NIP-01 (`nostr_core/nip001.h`)
|
||||
- `cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp);`
|
||||
|
||||
- NIP-03 OpenTimestamps (`nostr_core/nip003.h`)
|
||||
- `cJSON* nostr_nip03_create_proof_event_with_signer(const char* target_event_id, int target_event_kind, const char* ots_data_base64, const char* relay_url, nostr_signer_t* signer);`
|
||||
- `char* nostr_nip03_request_timestamp(const char* event_id_hex, const char* calendar_url, int timeout_seconds);`
|
||||
- `int nostr_nip03_is_proof_complete(const char* ots_data_base64);`
|
||||
- `int nostr_nip03_get_attestation_info(const char* ots_data_base64, int* has_bitcoin, int* has_litecoin, int* has_pending, uint64_t* bitcoin_height, uint64_t* litecoin_height, char** pending_uri_out);`
|
||||
- `char* nostr_nip03_upgrade_proof(const char* ots_data_base64, const char* calendar_url, int timeout_seconds);`
|
||||
- `int nostr_nip03_verify_proof(const char* ots_data_base64, const char* target_digest_hex, uint64_t* block_height_out, time_t* block_time_out);`
|
||||
|
||||
- NIP-17 (`nostr_core/nip017.h`)
|
||||
- `cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls, int num_relays, nostr_signer_t* signer);`
|
||||
- `int nostr_nip17_send_dm_with_signer(cJSON* dm_event, const char** recipient_pubkeys, int num_recipients, nostr_signer_t* signer, cJSON** gift_wraps_out, int max_gift_wraps, long max_delay_sec);`
|
||||
- `cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap, nostr_signer_t* signer);`
|
||||
|
||||
- NIP-42 (`nostr_core/nip042.h`)
|
||||
- `cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge, const char* relay_url, nostr_signer_t* signer, time_t timestamp);`
|
||||
|
||||
- NIP-46 client/event helpers (`nostr_core/nip046.h`)
|
||||
- `cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request, nostr_signer_t* signer, const unsigned char* recipient_public_key, time_t timestamp);`
|
||||
- `cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response, nostr_signer_t* signer, const unsigned char* recipient_public_key, time_t timestamp);`
|
||||
- `int nostr_nip46_decrypt_event_with_signer(cJSON* event, nostr_signer_t* signer, char** output_out);`
|
||||
- `int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session, nostr_signer_t* signer, const char* bunker_url);`
|
||||
|
||||
- NIP-60 (`nostr_core/nip060.h`)
|
||||
- `cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs, const char** deleted_event_ids, int deleted_count, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id, const char* mint_url, time_t expiration, nostr_signer_t* signer, time_t timestamp);`
|
||||
|
||||
- NIP-61 (`nostr_core/nip061.h`)
|
||||
- `cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data, nostr_signer_t* signer, time_t timestamp);`
|
||||
- `cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id, const char* nutzap_relay_hint, const char* sender_pubkey, const char* created_token_event_id, const char* created_token_relay_hint, uint64_t amount, nostr_signer_t* signer, time_t timestamp);`
|
||||
|
||||
- Blossom auth/upload/delete (`nostr_core/blossom_client.h`)
|
||||
- `char* blossom_create_auth_header_with_signer(nostr_signer_t* signer, const char* operation, const char* sha256_hex, int expiration_seconds, time_t timestamp);`
|
||||
- `int blossom_upload_with_signer(const char* server_url, const unsigned char* data, size_t data_len, const char* content_type, nostr_signer_t* signer, const char* sha256_hex, int timeout_seconds, blossom_blob_descriptor_t* descriptor_out);`
|
||||
- `int blossom_upload_file_with_signer(const char* server_url, const char* file_path, const char* content_type, nostr_signer_t* signer, int timeout_seconds, blossom_blob_descriptor_t* descriptor_out);`
|
||||
- `int blossom_delete_with_signer(const char* server_url, const char* sha256_hex, nostr_signer_t* signer, int timeout_seconds);`
|
||||
|
||||
- Relay-pool NIP-42 AUTH entry point (`nostr_core/nostr_core.h`)
|
||||
- `int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, nostr_signer_t* signer, int enable);`
|
||||
|
||||
### Driver app: `examples/note_poster`
|
||||
|
||||
Signer modes:
|
||||
|
||||
```bash
|
||||
./examples/note_poster
|
||||
```
|
||||
|
||||
Default is local signer mode; this prompts for `nsec1...` or 64-char hex private key.
|
||||
|
||||
**WARNING:** test-key-only flow. Do not use a production key; input key material is held in process memory in plaintext.
|
||||
|
||||
```bash
|
||||
./examples/note_poster --signer unix:<name>
|
||||
./examples/note_poster --signer serial:/dev/ttyACM0
|
||||
./examples/note_poster --signer tcp:<host>:<port>
|
||||
./examples/note_poster --signer fds:<read_fd>:<write_fd>
|
||||
```
|
||||
|
||||
### Boundaries / what stays in n_signer
|
||||
|
||||
`nostr_core_lib` provides the **client side only** (transports + nsigner client + signer backends). The following stay in the `n_signer` project:
|
||||
|
||||
- process spawning/embedding and deployment packaging
|
||||
- signer program trust-boundary features (mnemonic handling, approvals, role policy)
|
||||
- hardware firmware and platform/device-specific runtime pieces
|
||||
|
||||
Dependency direction remains one-way: **`n_signer -> nostr_core_lib`**.
|
||||
|
||||
Operations requiring raw secret material outside signer verbs are not remotable as-is. Example: mint-protocol operations in Cashu mint pieces (`cashu_mint`) that require direct secret access beyond event signing/NIP-04/NIP-44.
|
||||
|
||||
For implementation plan context see `plans/nsigner_integration_plan.md`. This integration supersedes per-project hand-rolled signer clients (future migration milestone M7).
|
||||
|
||||
## 📁 Examples
|
||||
|
||||
The library includes comprehensive examples:
|
||||
@@ -352,11 +521,14 @@ The library includes comprehensive examples:
|
||||
- **`mnemonic_derivation`** - NIP-06 key derivation
|
||||
- **`utility_functions`** - General utility demonstrations
|
||||
- **`input_detection`** - Input type detection and processing
|
||||
- **`relay_pool`** - Interactive relay pool and subscription testing
|
||||
- **`send_nip17_dm`** - NIP-17 private direct message send flow
|
||||
- **`nip46_remote_signer`** - NIP-46 remote signer flow and bunker URL generation
|
||||
- **`version_test`** - Library version information
|
||||
|
||||
Run all examples:
|
||||
Build all examples:
|
||||
```bash
|
||||
./build.sh examples
|
||||
./build.sh -e
|
||||
ls -la examples/
|
||||
```
|
||||
|
||||
@@ -365,24 +537,19 @@ ls -la examples/
|
||||
The library includes extensive tests:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
./build.sh test
|
||||
# Build all test executables
|
||||
./build.sh -t
|
||||
|
||||
# Individual test categories
|
||||
cd tests && make test
|
||||
# Run the new NIP-46 test
|
||||
./tests/nip46_test
|
||||
|
||||
# Run selected tests
|
||||
./tests/nip44_test
|
||||
./tests/nip42_test
|
||||
./tests/nip17_test
|
||||
```
|
||||
|
||||
**Test Categories:**
|
||||
- **Core Functionality**: `simple_init_test`, `header_test`
|
||||
- **Cryptography**: `chacha20_test`, `nostr_crypto_test`
|
||||
- **NIP-04 Encryption**: `nip04_test`
|
||||
- **NIP-05 Identifiers**: `nip05_test`
|
||||
- **NIP-11 Relay Info**: `nip11_test`
|
||||
- **NIP-44 Encryption**: `nip44_test`, `nip44_debug_test`
|
||||
- **Key Derivation**: `nostr_test_bip32`
|
||||
- **Relay Communication**: `relay_pool_test`, `sync_test`
|
||||
- **HTTP/WebSocket**: `http_test`, `wss_test`
|
||||
- **Proof of Work**: `test_pow_loop`
|
||||
**Current test binaries live in [`tests/`](tests/) and are generated from `*_test.c` sources.**
|
||||
|
||||
## 🏗️ Integration
|
||||
|
||||
@@ -392,7 +559,7 @@ cd tests && make test
|
||||
|
||||
2. **Copy required files to your project:**
|
||||
```bash
|
||||
cp libnostr_core.a /path/to/your/project/
|
||||
cp libnostr_core_x64.a /path/to/your/project/
|
||||
cp nostr_core/nostr_core.h /path/to/your/project/
|
||||
```
|
||||
|
||||
@@ -427,7 +594,7 @@ The `libnostr_core.a` library now uses **system dependencies** for all major cry
|
||||
|
||||
**Complete linking example:**
|
||||
```bash
|
||||
gcc your_app.c ./libnostr_core.a -lssl -lcrypto -lcurl -lsecp256k1 -lm -o your_app
|
||||
gcc your_app.c ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1 -o your_app
|
||||
```
|
||||
|
||||
**Check system dependencies:**
|
||||
@@ -455,8 +622,8 @@ ldd your_app # Shows linked system libraries
|
||||
### Build Flags
|
||||
|
||||
```bash
|
||||
# Enable all logging
|
||||
make LOGGING_FLAGS="-DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
|
||||
# Enable websocket and PoW debug emission (now callback-based)
|
||||
make LOGGING_FLAGS="-DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
|
||||
|
||||
# Debug build
|
||||
make debug
|
||||
@@ -465,6 +632,40 @@ make debug
|
||||
make arm64
|
||||
```
|
||||
|
||||
### Logging Integration (Consumer-Controlled)
|
||||
|
||||
`nostr_core_lib` now supports a callback logging API so host applications can route library logs into their own logger and destination.
|
||||
|
||||
- API is exposed via [`nostr_core/nostr_log.h`](nostr_core/nostr_log.h)
|
||||
- Included automatically from [`nostr_core/nostr_core.h`](nostr_core/nostr_core.h)
|
||||
|
||||
```c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
static void my_log_cb(int level, const char* component, const char* message, void* user_data) {
|
||||
(void)user_data;
|
||||
fprintf(stderr, "[nostr][%s][%d] %s\n", component ? component : "unknown", level, message ? message : "");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
if (nostr_init() != NOSTR_SUCCESS) return 1;
|
||||
|
||||
nostr_set_log_callback(my_log_cb, NULL);
|
||||
nostr_set_log_level(NOSTR_LOG_LEVEL_TRACE);
|
||||
|
||||
/* ... your code ... */
|
||||
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### Migration Notes
|
||||
|
||||
- Direct file logging to `debug.log` has been removed from websocket and NIP-13 internals.
|
||||
- To receive logs, register a callback with [`nostr_set_log_callback()`](nostr_core/nostr_log.h:24).
|
||||
- Use [`nostr_set_log_level()`](nostr_core/nostr_log.h:25) to reduce verbosity in production.
|
||||
|
||||
## 🌐 Supported Platforms
|
||||
|
||||
- **Linux** (x86_64, ARM64)
|
||||
@@ -472,42 +673,59 @@ make arm64
|
||||
- **Windows** (MinGW, MSYS2)
|
||||
- **Embedded Systems** (resource-constrained environments)
|
||||
|
||||
## 🔌 Embedded (ESP32) Support
|
||||
|
||||
`nostr_core_lib` now includes a platform abstraction layer and ESP32-native implementations for core networking and entropy APIs, while preserving desktop compatibility.
|
||||
|
||||
Implemented embedded capabilities include:
|
||||
- platform random source abstraction via `nostr_platform_random()`
|
||||
- ESP32 random provider using `esp_fill_random`
|
||||
- ESP32 HTTP client implementation via `esp_http_client`
|
||||
- ESP32 WebSocket/WSS transport via `esp_transport_ws` + TLS certificate bundle
|
||||
- NIP-04 encrypted DM flow validated on-device against public relays
|
||||
|
||||
Reference ESP-IDF example project (in this workspace):
|
||||
- `../esp32_send_kind4/` — Wi-Fi connect + `wss://` relay connect + send two kind-4 encrypted events + print relay responses
|
||||
|
||||
## 📄 Documentation
|
||||
|
||||
- **[LIBRARY_USAGE.md](LIBRARY_USAGE.md)** - Detailed integration guide
|
||||
- **[EXPORT_GUIDE.md](EXPORT_GUIDE.md)** - Library export instructions
|
||||
- **[AUTOMATIC_VERSIONING.md](AUTOMATIC_VERSIONING.md)** - Version management
|
||||
- **API Reference** - Complete documentation in `nostr_core/nostr_core.h`
|
||||
- **[POOL_API.md](POOL_API.md)** - Relay pool API notes
|
||||
- **[nostr_websocket/README.md](nostr_websocket/README.md)** - WebSocket module details
|
||||
- **[nostr_websocket/EXPORT_GUIDE.md](nostr_websocket/EXPORT_GUIDE.md)** - WebSocket export instructions
|
||||
- **API Reference** - Complete documentation in [`nostr_core/nostr_core.h`](nostr_core/nostr_core.h)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
||||
3. Make your changes and add tests
|
||||
4. Run the test suite: `./build.sh test`
|
||||
4. Build tests and run key suites: `./build.sh -t && ./tests/nip46_test`
|
||||
5. Commit your changes: `git commit -m 'Add amazing feature'`
|
||||
6. Push to the branch: `git push origin feature/amazing-feature`
|
||||
7. Open a Pull Request
|
||||
|
||||
## 📈 Version History
|
||||
|
||||
Current version: **0.2.1**
|
||||
Current version: **0.6.0**
|
||||
|
||||
The library uses automatic semantic versioning based on Git tags. Each build increments the patch version automatically.
|
||||
|
||||
**Recent Developments:**
|
||||
- **OpenSSL Migration**: Transitioned from mbedTLS to OpenSSL for improved compatibility
|
||||
- **ESP32 Embedded Enablement**: Added PAL-based embedded support directly in `nostr_core_lib`
|
||||
- **ESP32 HTTP + WSS**: Added ESP-IDF-backed HTTP and secure websocket transport implementations
|
||||
- **NIP-04 On-Device Validation**: Verified encrypted kind-4 DM publish flow from ESP32 to live relays
|
||||
- **NIP-05 Support**: DNS-based internet identifier verification
|
||||
- **NIP-11 Support**: Relay information document fetching and parsing
|
||||
- **NIP-19 Support**: Bech32-encoded entities (nsec/npub)
|
||||
- **Enhanced WebSocket**: OpenSSL-based TLS WebSocket communication
|
||||
- **Comprehensive Testing**: Extensive test suite and error handling
|
||||
|
||||
**Version Timeline:**
|
||||
- `v0.2.x` - Current development releases with enhanced NIP support
|
||||
- `v0.6.x` - Embedded-capable releases with ESP32 PAL, HTTP, and WSS support
|
||||
- `v0.4.x` - Development releases with relay pool and expanded NIP support
|
||||
- `v0.2.x` - Earlier OpenSSL-based development releases
|
||||
- `v0.1.x` - Initial development releases
|
||||
- Focus on core protocol implementation and OpenSSL-based crypto
|
||||
- Full NIP-01, NIP-04, NIP-05, NIP-06, NIP-11, NIP-13, NIP-19, NIP-44 support
|
||||
- Full NIP-01, NIP-04, NIP-05, NIP-06, NIP-11, NIP-13, NIP-17, NIP-19, NIP-21, NIP-42, NIP-44, NIP-46, NIP-59 support
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
@@ -515,27 +733,35 @@ The library uses automatic semantic versioning based on Git tags. Each build inc
|
||||
|
||||
**Build fails with secp256k1 errors:**
|
||||
```bash
|
||||
# Install secp256k1 with Schnorr support
|
||||
sudo apt install libsecp256k1-dev # Ubuntu/Debian
|
||||
# or
|
||||
sudo yum install libsecp256k1-devel # CentOS/RHEL
|
||||
# or
|
||||
brew install secp256k1 # macOS
|
||||
|
||||
# If still failing, build from source with Schnorr support:
|
||||
git clone https://github.com/bitcoin-core/secp256k1.git
|
||||
cd secp256k1
|
||||
./autogen.sh
|
||||
./configure --enable-module-schnorrsig --enable-module-ecdh
|
||||
make
|
||||
cd ..
|
||||
./build.sh lib
|
||||
sudo make install
|
||||
```
|
||||
|
||||
**Library too large:**
|
||||
The x64 library is intentionally large (~15MB) because it includes all secp256k1 cryptographic functions and OpenSSL for complete self-containment. The ARM64 library is smaller (~2.4MB) as it links against system OpenSSL.
|
||||
**Library size:**
|
||||
The library is small (~500KB) as it links against system libraries (secp256k1, OpenSSL, curl) rather than including them statically. This keeps the binary size manageable while maintaining full functionality.
|
||||
|
||||
**Linking errors:**
|
||||
Make sure to include the math library:
|
||||
```bash
|
||||
gcc your_code.c ./libnostr_core.a -lm # Note the -lm flag
|
||||
gcc your_code.c ./libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
- Check the `examples/` directory for working code
|
||||
- Run `./build.sh test` to verify your environment
|
||||
- Run `./build.sh -t` to verify your environment
|
||||
- Review the comprehensive API documentation in `nostr_core/nostr_core.h`
|
||||
|
||||
## 📜 License
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0.6.6
|
||||
Binary file not shown.
@@ -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"
|
||||
@@ -129,14 +135,21 @@ if [ "$HELP" = true ]; then
|
||||
echo ""
|
||||
echo "Available NIPs:"
|
||||
echo " 001 - Basic Protocol (event creation, signing)"
|
||||
echo " 003 - OpenTimestamps"
|
||||
echo " 004 - Encryption (legacy)"
|
||||
echo " 005 - DNS-based identifiers"
|
||||
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 " 021 - nostr: URI scheme"
|
||||
echo " 042 - Authentication of clients to relays"
|
||||
echo " 044 - Encryption (modern)"
|
||||
echo " 046 - Remote signing"
|
||||
echo " 059 - Gift Wrap"
|
||||
echo " 060 - Cashu Wallet"
|
||||
echo " 061 - Nutzaps"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Auto-detect NIPs, build for current arch"
|
||||
@@ -167,7 +180,7 @@ if [ "$CURRENT_DIR" != "nostr_core_lib" ]; then
|
||||
echo " cd nostr_core_lib"
|
||||
echo " ./build.sh"
|
||||
echo " cd .."
|
||||
echo " gcc your_app.c nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -o your_app"
|
||||
echo " gcc your_app.c nostr_core_lib/libnostr_core_x64.a -lz -ldl -lpthread -lm -lssl -lcrypto -lcurl -lsecp256k1 -o your_app"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
@@ -185,7 +198,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 003 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
|
||||
print_info "Forced: Building all available NIPs"
|
||||
else
|
||||
# Convert comma-separated list to space-separated with 3-digit format
|
||||
@@ -204,7 +217,7 @@ else
|
||||
# Check for nostr_core.h (includes everything)
|
||||
if grep -q '#include[[:space:]]*["\<]nostr_core\.h["\>]' *.c 2>/dev/null; then
|
||||
print_info "Found #include \"nostr_core.h\" - building all NIPs"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
|
||||
NEEDED_NIPS="001 003 004 005 006 011 013 019 021 042 044 046 059 060 061"
|
||||
elif [ -n "$DETECTED" ]; then
|
||||
NEEDED_NIPS="$DETECTED"
|
||||
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
|
||||
@@ -220,10 +233,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 003 004 005 006 011 013 017 019 021 042 044 046 059 060 061"
|
||||
print_info "Building tests/examples - including all available NIPs for compatibility"
|
||||
fi
|
||||
|
||||
# Ensure NIP-001 is always included (required for core functionality)
|
||||
@@ -487,12 +500,21 @@ detect_system_curl
|
||||
SOURCES="nostr_core/crypto/nostr_secp256k1.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_aes.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_poly1305.c"
|
||||
SOURCES="$SOURCES nostr_core/crypto/nostr_chacha20poly1305.c"
|
||||
SOURCES="$SOURCES cjson/cJSON.c"
|
||||
SOURCES="$SOURCES nostr_core/utils.c"
|
||||
SOURCES="$SOURCES nostr_core/nostr_common.c"
|
||||
SOURCES="$SOURCES nostr_core/nostr_signer.c"
|
||||
SOURCES="$SOURCES nostr_core/core_relays.c"
|
||||
SOURCES="$SOURCES nostr_core/core_relay_pool.c"
|
||||
SOURCES="$SOURCES nostr_core/nostr_log.c"
|
||||
SOURCES="$SOURCES nostr_websocket/nostr_websocket_openssl.c"
|
||||
SOURCES="$SOURCES nostr_core/request_validator.c"
|
||||
SOURCES="$SOURCES nostr_core/nostr_http.c"
|
||||
SOURCES="$SOURCES platform/linux.c"
|
||||
SOURCES="$SOURCES nostr_core/nsigner_transport.c"
|
||||
SOURCES="$SOURCES nostr_core/nsigner_client.c"
|
||||
|
||||
NIP_DESCRIPTIONS=""
|
||||
|
||||
@@ -502,23 +524,37 @@ for nip in $NEEDED_NIPS; do
|
||||
SOURCES="$SOURCES $NIP_FILE"
|
||||
case $nip in
|
||||
001) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-001(Basic)" ;;
|
||||
003) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-003(OpenTimestamps)" ;;
|
||||
004) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-004(Encrypt)" ;;
|
||||
005) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-005(DNS)" ;;
|
||||
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)" ;;
|
||||
021) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-021(URI)" ;;
|
||||
042) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-042(Auth)" ;;
|
||||
044) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-044(Encrypt)" ;;
|
||||
046) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-046(Remote-Signing)" ;;
|
||||
059) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-059(Gift-Wrap)" ;;
|
||||
060) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-060(Cashu-Wallet)" ;;
|
||||
061) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-061(Nutzaps)" ;;
|
||||
esac
|
||||
else
|
||||
print_warning "NIP file not found: $NIP_FILE - skipping"
|
||||
fi
|
||||
done
|
||||
|
||||
if echo "$NEEDED_NIPS" | grep -Eq '(^| )060( |$)|(^| )061( |$)'; then
|
||||
SOURCES="$SOURCES nostr_core/cashu_mint.c"
|
||||
fi
|
||||
|
||||
SOURCES="$SOURCES nostr_core/blossom_client.c"
|
||||
|
||||
# Build flags
|
||||
CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"
|
||||
CFLAGS="$CFLAGS -DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
|
||||
CFLAGS="$CFLAGS -DNOSTR_ENABLE_NSIGNER_CLIENT=1"
|
||||
INCLUDES="-I. -Inostr_core -Inostr_core/crypto -Icjson -Inostr_websocket"
|
||||
|
||||
# Add system library includes
|
||||
@@ -666,7 +702,53 @@ if [ $AR_RESULT -eq 0 ]; then
|
||||
fi
|
||||
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 ""
|
||||
|
||||
-394
@@ -1,394 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NOSTR Core Library Build Script
|
||||
# Provides convenient build targets for the standalone library
|
||||
# Automatically increments patch version with each build
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to automatically increment version
|
||||
increment_version() {
|
||||
print_status "Incrementing version..."
|
||||
|
||||
# Check if we're in a git repository
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_warning "Not in a git repository - skipping version increment"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Get the highest version tag (not necessarily the most recent chronologically)
|
||||
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.1.0")
|
||||
if [[ -z "$LATEST_TAG" ]]; then
|
||||
LATEST_TAG="v0.1.0"
|
||||
fi
|
||||
|
||||
# Extract version components (remove 'v' prefix if present)
|
||||
VERSION=${LATEST_TAG#v}
|
||||
|
||||
# Parse major.minor.patch
|
||||
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
MAJOR=${BASH_REMATCH[1]}
|
||||
MINOR=${BASH_REMATCH[2]}
|
||||
PATCH=${BASH_REMATCH[3]}
|
||||
else
|
||||
print_error "Invalid version format in tag: $LATEST_TAG"
|
||||
print_error "Expected format: v0.1.0"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Increment patch version
|
||||
NEW_PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||
|
||||
print_status "Current version: $LATEST_TAG"
|
||||
print_status "New version: $NEW_VERSION"
|
||||
|
||||
# Create new git tag
|
||||
if git tag "$NEW_VERSION" 2>/dev/null; then
|
||||
print_success "Created new version tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists - using existing version"
|
||||
NEW_VERSION=$LATEST_TAG
|
||||
fi
|
||||
|
||||
# Update VERSION file for compatibility
|
||||
echo "${NEW_VERSION#v}" > VERSION
|
||||
print_success "Updated VERSION file to ${NEW_VERSION#v}"
|
||||
}
|
||||
|
||||
# Function to perform git operations after successful build
|
||||
perform_git_operations() {
|
||||
local commit_message="$1"
|
||||
|
||||
if [[ -z "$commit_message" ]]; then
|
||||
return 0 # No commit message provided, skip git operations
|
||||
fi
|
||||
|
||||
print_status "Performing git operations..."
|
||||
|
||||
# Check if we're in a git repository
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_warning "Not in a git repository - skipping git operations"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --quiet && git diff --cached --quiet; then
|
||||
print_warning "No changes to commit"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Add all changes
|
||||
print_status "Adding changes to git..."
|
||||
if ! git add .; then
|
||||
print_error "Failed to add changes to git"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Commit changes
|
||||
print_status "Committing changes with message: '$commit_message'"
|
||||
if ! git commit -m "$commit_message"; then
|
||||
print_error "Failed to commit changes"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Push changes
|
||||
print_status "Pushing changes to remote repository..."
|
||||
if ! git push; then
|
||||
print_error "Failed to push changes to remote repository"
|
||||
print_warning "Changes have been committed locally but not pushed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_success "Git operations completed successfully!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
echo "NOSTR Core Library Build Script"
|
||||
echo "==============================="
|
||||
echo ""
|
||||
echo "Usage: $0 [target] [-m \"commit message\"]"
|
||||
echo ""
|
||||
echo "Available targets:"
|
||||
echo " clean - Clean all build artifacts"
|
||||
echo " lib - Build static libraries for both x64 and ARM64 (default)"
|
||||
echo " x64 - Build x64 static library only"
|
||||
echo " arm64 - Build ARM64 static library only"
|
||||
echo " all - Build both architectures and examples"
|
||||
echo " examples - Build example programs"
|
||||
echo " test - Run tests"
|
||||
echo " install - Install library to system"
|
||||
echo " uninstall - Remove library from system"
|
||||
echo " help - Show this help message"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -m \"message\" - Git commit message (triggers automatic git add, commit, push after successful build)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 lib -m \"Add new proof-of-work parameters\""
|
||||
echo " $0 x64 -m \"Fix OpenSSL minimal build configuration\""
|
||||
echo " $0 lib # Build without git operations"
|
||||
echo ""
|
||||
echo "Library outputs (both self-contained with secp256k1):"
|
||||
echo " libnostr_core.a - x86_64 static library"
|
||||
echo " libnostr_core_arm64.a - ARM64 static library"
|
||||
echo " examples/* - Example programs"
|
||||
echo ""
|
||||
echo "Both libraries include secp256k1 objects internally."
|
||||
echo "Users only need to link with the library + -lm."
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
TARGET=""
|
||||
COMMIT_MESSAGE=""
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-m)
|
||||
COMMIT_MESSAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-*)
|
||||
print_error "Unknown option: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
TARGET="$1"
|
||||
else
|
||||
print_error "Multiple targets specified: $TARGET and $1"
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Set default target if none specified
|
||||
TARGET=${TARGET:-lib}
|
||||
|
||||
case "$TARGET" in
|
||||
clean)
|
||||
print_status "Cleaning build artifacts..."
|
||||
make clean
|
||||
print_success "Clean completed"
|
||||
;;
|
||||
|
||||
lib|library)
|
||||
increment_version
|
||||
print_status "Building both x64 and ARM64 static libraries..."
|
||||
make clean
|
||||
make
|
||||
|
||||
# Check both libraries were built
|
||||
SUCCESS=0
|
||||
if [ -f "libnostr_core.a" ]; then
|
||||
SIZE_X64=$(stat -c%s "libnostr_core.a")
|
||||
print_success "x64 static library built successfully (${SIZE_X64} bytes)"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
print_error "Failed to build x64 static library"
|
||||
fi
|
||||
|
||||
if [ -f "libnostr_core_arm64.a" ]; then
|
||||
SIZE_ARM64=$(stat -c%s "libnostr_core_arm64.a")
|
||||
print_success "ARM64 static library built successfully (${SIZE_ARM64} bytes)"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
print_error "Failed to build ARM64 static library"
|
||||
fi
|
||||
|
||||
if [ $SUCCESS -eq 2 ]; then
|
||||
print_success "Both architectures built successfully!"
|
||||
ls -la libnostr_core*.a
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build all libraries"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
x64|x64-only)
|
||||
increment_version
|
||||
print_status "Building x64 static library only..."
|
||||
make clean
|
||||
make x64
|
||||
if [ -f "libnostr_core.a" ]; then
|
||||
SIZE=$(stat -c%s "libnostr_core.a")
|
||||
print_success "x64 static library built successfully (${SIZE} bytes)"
|
||||
ls -la libnostr_core.a
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build x64 static library"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
arm64|arm64-only)
|
||||
increment_version
|
||||
print_status "Building ARM64 static library only..."
|
||||
make clean
|
||||
make arm64
|
||||
if [ -f "libnostr_core_arm64.a" ]; then
|
||||
SIZE=$(stat -c%s "libnostr_core_arm64.a")
|
||||
print_success "ARM64 static library built successfully (${SIZE} bytes)"
|
||||
ls -la libnostr_core_arm64.a
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build ARM64 static library"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
shared)
|
||||
increment_version
|
||||
print_status "Building shared library..."
|
||||
make clean
|
||||
make libnostr_core.so
|
||||
if [ -f "libnostr_core.so" ]; then
|
||||
SIZE=$(stat -c%s "libnostr_core.so")
|
||||
print_success "Shared library built successfully (${SIZE} bytes)"
|
||||
ls -la libnostr_core.so
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build shared library"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
all)
|
||||
increment_version
|
||||
print_status "Building all libraries and examples..."
|
||||
make clean
|
||||
make all
|
||||
|
||||
# Check both libraries and examples were built
|
||||
SUCCESS=0
|
||||
if [ -f "libnostr_core.a" ]; then
|
||||
SIZE_X64=$(stat -c%s "libnostr_core.a")
|
||||
print_success "x64 static library built successfully (${SIZE_X64} bytes)"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
print_error "Failed to build x64 static library"
|
||||
fi
|
||||
|
||||
if [ -f "libnostr_core_arm64.a" ]; then
|
||||
SIZE_ARM64=$(stat -c%s "libnostr_core_arm64.a")
|
||||
print_success "ARM64 static library built successfully (${SIZE_ARM64} bytes)"
|
||||
SUCCESS=$((SUCCESS + 1))
|
||||
else
|
||||
print_error "Failed to build ARM64 static library"
|
||||
fi
|
||||
|
||||
if [ $SUCCESS -eq 2 ]; then
|
||||
print_success "All libraries and examples built successfully!"
|
||||
ls -la libnostr_core*.a
|
||||
ls -la examples/
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build all components"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
examples)
|
||||
increment_version
|
||||
print_status "Building both libraries and examples..."
|
||||
make clean
|
||||
make
|
||||
make examples
|
||||
|
||||
# Verify libraries were built
|
||||
if [ -f "libnostr_core.a" ] && [ -f "libnostr_core_arm64.a" ]; then
|
||||
print_success "Both libraries and examples built successfully"
|
||||
ls -la libnostr_core*.a
|
||||
ls -la examples/
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
else
|
||||
print_error "Failed to build libraries for examples"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
test)
|
||||
print_status "Running tests..."
|
||||
make clean
|
||||
make
|
||||
if make test-crypto 2>/dev/null; then
|
||||
print_success "All tests passed"
|
||||
else
|
||||
print_warning "Running simple test instead..."
|
||||
make test
|
||||
print_success "Basic test completed"
|
||||
fi
|
||||
;;
|
||||
|
||||
tests)
|
||||
print_status "Running tests..."
|
||||
make clean
|
||||
make
|
||||
if make test-crypto 2>/dev/null; then
|
||||
print_success "All tests passed"
|
||||
else
|
||||
print_warning "Running simple test instead..."
|
||||
make test
|
||||
print_success "Basic test completed"
|
||||
fi
|
||||
;;
|
||||
|
||||
install)
|
||||
increment_version
|
||||
print_status "Installing library to system..."
|
||||
make clean
|
||||
make all
|
||||
sudo make install
|
||||
print_success "Library installed to /usr/local"
|
||||
perform_git_operations "$COMMIT_MESSAGE"
|
||||
;;
|
||||
|
||||
uninstall)
|
||||
print_status "Uninstalling library from system..."
|
||||
sudo make uninstall
|
||||
print_success "Library uninstalled"
|
||||
;;
|
||||
|
||||
help|--help|-h)
|
||||
show_usage
|
||||
;;
|
||||
|
||||
*)
|
||||
print_error "Unknown target: $TARGET"
|
||||
echo ""
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Example: NIP-60/NIP-61 Cashu Wallet Flow
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
|
||||
static void print_event(const char* title, cJSON* evt) {
|
||||
if (!evt) {
|
||||
printf("%s: <null>\n", title);
|
||||
return;
|
||||
}
|
||||
char* s = cJSON_Print(evt);
|
||||
if (s) {
|
||||
printf("\n%s\n%s\n", title, s);
|
||||
free(s);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
printf("Failed to initialize library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* user_sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
|
||||
unsigned char user_sk[32];
|
||||
if (nostr_hex_to_bytes(user_sk_hex, user_sk, 32) != 0) {
|
||||
printf("Invalid private key\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 1) Create wallet event (kind:17375) */
|
||||
char* mint_urls[] = {
|
||||
"https://mint1.example.com",
|
||||
"https://mint2.example.com"
|
||||
};
|
||||
|
||||
nostr_nip60_wallet_data_t wallet_data;
|
||||
memset(&wallet_data, 0, sizeof(wallet_data));
|
||||
strcpy(wallet_data.privkey, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
wallet_data.mint_urls = mint_urls;
|
||||
wallet_data.mint_count = 2;
|
||||
|
||||
cJSON* wallet_event = nostr_nip60_create_wallet_event(&wallet_data, user_sk, 0);
|
||||
print_event("Wallet Event (kind:17375)", wallet_event);
|
||||
|
||||
/* 2) Create token event (kind:7375) */
|
||||
nostr_cashu_proof_t proofs[2];
|
||||
memset(proofs, 0, sizeof(proofs));
|
||||
|
||||
strcpy(proofs[0].id, "005c2502034d4f12");
|
||||
proofs[0].amount = 1;
|
||||
proofs[0].secret = "secret-1";
|
||||
proofs[0].C = "0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46";
|
||||
|
||||
strcpy(proofs[1].id, "005c2502034d4f12");
|
||||
proofs[1].amount = 2;
|
||||
proofs[1].secret = "secret-2";
|
||||
proofs[1].C = "02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f";
|
||||
|
||||
nostr_nip60_token_data_t token_data;
|
||||
memset(&token_data, 0, sizeof(token_data));
|
||||
token_data.mint_url = "https://mint1.example.com";
|
||||
token_data.proofs = proofs;
|
||||
token_data.proof_count = 2;
|
||||
|
||||
cJSON* token_event = nostr_nip60_create_token_event(&token_data, user_sk, 0);
|
||||
print_event("Token Event (kind:7375)", token_event);
|
||||
|
||||
/* 3) Create spend history event (kind:7376) */
|
||||
nostr_nip60_history_ref_t refs[1];
|
||||
memset(refs, 0, sizeof(refs));
|
||||
strcpy(refs[0].event_id, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
refs[0].marker = NOSTR_NIP60_REF_CREATED;
|
||||
|
||||
nostr_nip60_history_data_t hist;
|
||||
memset(&hist, 0, sizeof(hist));
|
||||
hist.direction = NOSTR_NIP60_DIRECTION_IN;
|
||||
hist.amount = 3;
|
||||
hist.refs = refs;
|
||||
hist.ref_count = 1;
|
||||
|
||||
cJSON* history_event = nostr_nip60_create_history_event(&hist, user_sk, 0);
|
||||
print_event("History Event (kind:7376)", history_event);
|
||||
|
||||
/* 4) Create nutzap info event (kind:10019) */
|
||||
char* relays[] = {"wss://relay1.example.com", "wss://relay2.example.com"};
|
||||
char* mint_units[] = {"sat"};
|
||||
|
||||
nostr_nip61_mint_entry_t mint_entry;
|
||||
memset(&mint_entry, 0, sizeof(mint_entry));
|
||||
mint_entry.url = "https://mint1.example.com";
|
||||
mint_entry.units = mint_units;
|
||||
mint_entry.unit_count = 1;
|
||||
|
||||
nostr_nip61_nutzap_info_t info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.relay_urls = relays;
|
||||
info.relay_count = 2;
|
||||
info.mints = &mint_entry;
|
||||
info.mint_count = 1;
|
||||
strcpy(info.pubkey, "02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed");
|
||||
|
||||
cJSON* info_event = nostr_nip61_create_nutzap_info_event(&info, user_sk, 0);
|
||||
print_event("Nutzap Info Event (kind:10019)", info_event);
|
||||
|
||||
/* 5) Optionally call Cashu mint HTTP endpoints with [cashu_mint_get_info()] */
|
||||
printf("\nCashu mint integration is available via cashu_mint_* APIs.\n");
|
||||
|
||||
cJSON_Delete(info_event);
|
||||
cJSON_Delete(history_event);
|
||||
cJSON_Delete(token_event);
|
||||
cJSON_Delete(wallet_event);
|
||||
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
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;
|
||||
}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* NIP-46 Remote Signer Example
|
||||
*
|
||||
* Demonstrates signer session setup, bunker URL generation,
|
||||
* request parsing, request handling, and encrypted response event creation.
|
||||
*/
|
||||
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int parse_hex_key(const char* hex, unsigned char out[32]) {
|
||||
if (!hex || strlen(hex) != 64) return -1;
|
||||
return nostr_hex_to_bytes(hex, out, 32);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "Usage: %s <signer_privkey_hex> <user_privkey_hex> [relay_url]\n", argv[0]);
|
||||
fprintf(stderr, "Example: %s <64hex> <64hex> wss://relay.example.com\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* relay = (argc >= 4) ? argv[3] : "wss://relay.example.com";
|
||||
|
||||
unsigned char signer_sk[32];
|
||||
unsigned char user_sk[32];
|
||||
if (parse_hex_key(argv[1], signer_sk) != 0 || parse_hex_key(argv[2], user_sk) != 0) {
|
||||
fprintf(stderr, "Invalid private key hex; expected 64 hex characters for each key\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize nostr library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* relays[] = { relay };
|
||||
nostr_nip46_signer_session_t signer;
|
||||
int rc = nostr_nip46_signer_session_init(&signer, signer_sk, user_sk, relays, 1);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "signer init failed: %s\n", nostr_strerror(rc));
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
char bunker_url[1024];
|
||||
rc = nostr_nip46_signer_create_bunker_url(&signer, "demo-secret", bunker_url, sizeof(bunker_url));
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to create bunker url: %s\n", nostr_strerror(rc));
|
||||
nostr_nip46_signer_session_destroy(&signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("NIP-46 signer initialized\n");
|
||||
printf("Signer pubkey: %s\n", signer.signer_pubkey_hex);
|
||||
printf("User pubkey: %s\n", signer.user_pubkey_hex);
|
||||
printf("Relay: %s\n", relay);
|
||||
printf("Bunker URL: %s\n\n", bunker_url);
|
||||
|
||||
// Simulate receiving a connect request and producing a response
|
||||
const char* connect_params[] = { signer.signer_pubkey_hex, "demo-secret", "sign_event:1,get_public_key" };
|
||||
nostr_nip46_request_t req;
|
||||
rc = nostr_nip46_create_request("demo-req-1", NOSTR_NIP46_METHOD_CONNECT, connect_params, 3, &req);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to create demo request: %s\n", nostr_strerror(rc));
|
||||
nostr_nip46_signer_session_destroy(&signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
nostr_nip46_response_t resp;
|
||||
rc = nostr_nip46_signer_handle_request(&signer, &req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "failed to handle demo request: %s\n", nostr_strerror(rc));
|
||||
nostr_nip46_free_request(&req);
|
||||
nostr_nip46_signer_session_destroy(&signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Handled method: %s\n", req.method_str);
|
||||
printf("Response result: %s\n", resp.result ? resp.result : "<none>");
|
||||
printf("Response error: %s\n", resp.error ? resp.error : "<none>");
|
||||
|
||||
// Simulate building encrypted response event back to a client
|
||||
unsigned char client_sk[32];
|
||||
unsigned char client_pk[32];
|
||||
memset(client_sk, 0, sizeof(client_sk));
|
||||
client_sk[31] = 1; // deterministic demo key
|
||||
if (nostr_ec_public_key_from_private_key(client_sk, client_pk) != 0) {
|
||||
fprintf(stderr, "failed to derive demo client pubkey\n");
|
||||
nostr_nip46_free_response(&resp);
|
||||
nostr_nip46_free_request(&req);
|
||||
nostr_nip46_signer_session_destroy(&signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
cJSON* response_event = nostr_nip46_create_response_event(&resp, signer.signer_private_key, client_pk, 0);
|
||||
if (!response_event) {
|
||||
fprintf(stderr, "failed to create encrypted response event\n");
|
||||
nostr_nip46_free_response(&resp);
|
||||
nostr_nip46_free_request(&req);
|
||||
nostr_nip46_signer_session_destroy(&signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
char* response_event_json = cJSON_Print(response_event);
|
||||
if (response_event_json) {
|
||||
printf("\nEncrypted response event (publish this to relays):\n%s\n", response_event_json);
|
||||
free(response_event_json);
|
||||
}
|
||||
|
||||
cJSON_Delete(response_event);
|
||||
nostr_nip46_free_response(&resp);
|
||||
nostr_nip46_free_request(&req);
|
||||
nostr_nip46_signer_session_destroy(&signer);
|
||||
nostr_cleanup();
|
||||
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* note_poster - minimal sign-in + kind-1 post test app
|
||||
*
|
||||
* Supports signer modes:
|
||||
* - local (default): sign in with a local key (nsec or hex private key)
|
||||
* - unix:<name>: use remote nsigner over AF_UNIX abstract socket
|
||||
* - serial:<path>: use remote nsigner over USB CDC serial (e.g. /dev/ttyACM0)
|
||||
* - tcp:<host>:<port>: use remote nsigner over TCP (auth envelope required by n_signer)
|
||||
* - fds:<read_fd>:<write_fd>: use remote nsigner over existing framed fd pair (stdio/qrexec helper)
|
||||
*
|
||||
* Creates and signs a kind-1 note through nostr_signer_t,
|
||||
* then publishes to wss://relay.laantungir.net
|
||||
*/
|
||||
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#define DEFAULT_RELAY "wss://relay.laantungir.net"
|
||||
|
||||
static 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 ? status : "(unknown)");
|
||||
if (message) {
|
||||
printf(" - %s", message);
|
||||
}
|
||||
printf(" (%d/%d complete, %d success)\n", completed_relays, total_relays, success_count);
|
||||
}
|
||||
}
|
||||
|
||||
static int read_line(const char* prompt, char* out, size_t out_sz) {
|
||||
size_t len;
|
||||
if (!prompt || !out || out_sz == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
printf("%s", prompt);
|
||||
if (!fgets(out, (int)out_sz, stdin)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = strlen(out);
|
||||
if (len > 0 && out[len - 1] == '\n') {
|
||||
out[len - 1] = '\0';
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_private_key_input(const char* input, unsigned char out_privkey[32]) {
|
||||
if (!input || !out_privkey) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strncmp(input, "nsec1", 5) == 0) {
|
||||
return nostr_decode_nsec(input, out_privkey);
|
||||
}
|
||||
|
||||
if (strlen(input) == 64) {
|
||||
return nostr_hex_to_bytes(input, out_privkey, 32);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
unsigned char private_key[32] = {0};
|
||||
char key_input[256] = {0};
|
||||
char note_content[2048] = {0};
|
||||
nostr_signer_t* signer = NULL;
|
||||
char pubkey_hex[65] = {0};
|
||||
cJSON* note_event = NULL;
|
||||
cJSON* id_item;
|
||||
const char* relay_urls[] = {DEFAULT_RELAY};
|
||||
int success_count = 0;
|
||||
publish_result_t* results = NULL;
|
||||
enum {
|
||||
SIGNER_MODE_LOCAL = 0,
|
||||
SIGNER_MODE_UNIX = 1,
|
||||
SIGNER_MODE_SERIAL = 2,
|
||||
SIGNER_MODE_TCP = 3,
|
||||
SIGNER_MODE_FDS = 4
|
||||
} signer_mode = SIGNER_MODE_LOCAL;
|
||||
const char* unix_name = NULL;
|
||||
const char* serial_path = NULL;
|
||||
char tcp_host[256] = {0};
|
||||
int tcp_port = 0;
|
||||
int fds_read_fd = -1;
|
||||
int fds_write_fd = -1;
|
||||
int argi = 1;
|
||||
|
||||
if (argc >= 3 && strcmp(argv[1], "--signer") == 0) {
|
||||
if (strncmp(argv[2], "unix:", 5) == 0 && argv[2][5] != '\0') {
|
||||
signer_mode = SIGNER_MODE_UNIX;
|
||||
unix_name = argv[2] + 5;
|
||||
} else if (strncmp(argv[2], "serial:", 7) == 0 && argv[2][7] != '\0') {
|
||||
signer_mode = SIGNER_MODE_SERIAL;
|
||||
serial_path = argv[2] + 7;
|
||||
} else if (strncmp(argv[2], "tcp:", 4) == 0 && argv[2][4] != '\0') {
|
||||
const char* spec = argv[2] + 4;
|
||||
const char* sep = strrchr(spec, ':');
|
||||
char* endptr = NULL;
|
||||
long parsed_port;
|
||||
size_t host_len;
|
||||
|
||||
if (sep == NULL || sep == spec || sep[1] == '\0') {
|
||||
fprintf(stderr, "Invalid tcp signer format. Use tcp:<host>:<port>\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
host_len = (size_t)(sep - spec);
|
||||
if (host_len >= sizeof(tcp_host)) {
|
||||
fprintf(stderr, "TCP host too long\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
memcpy(tcp_host, spec, host_len);
|
||||
tcp_host[host_len] = '\0';
|
||||
|
||||
parsed_port = strtol(sep + 1, &endptr, 10);
|
||||
if (endptr == NULL || *endptr != '\0' || parsed_port <= 0 || parsed_port > 65535) {
|
||||
fprintf(stderr, "Invalid TCP port in --signer tcp:<host>:<port>\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
signer_mode = SIGNER_MODE_TCP;
|
||||
tcp_port = (int)parsed_port;
|
||||
} else if (strncmp(argv[2], "fds:", 4) == 0 && argv[2][4] != '\0') {
|
||||
int parsed = -1;
|
||||
parsed = sscanf(argv[2] + 4, "%d:%d", &fds_read_fd, &fds_write_fd);
|
||||
if (parsed != 2 || fds_read_fd < 0 || fds_write_fd < 0) {
|
||||
fprintf(stderr, "Invalid fds signer format. Use fds:<read_fd>:<write_fd>\n");
|
||||
return 1;
|
||||
}
|
||||
signer_mode = SIGNER_MODE_FDS;
|
||||
} else if (strcmp(argv[2], "local") == 0) {
|
||||
signer_mode = SIGNER_MODE_LOCAL;
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"Invalid --signer value. Use --signer local, --signer unix:<name>, --signer serial:<path>, --signer tcp:<host>:<port>, or --signer fds:<read_fd>:<write_fd>\n");
|
||||
return 1;
|
||||
}
|
||||
argi = 3;
|
||||
}
|
||||
|
||||
if (signer_mode == SIGNER_MODE_LOCAL) {
|
||||
if (argc > argi) {
|
||||
strncpy(key_input, argv[argi], sizeof(key_input) - 1);
|
||||
argi++;
|
||||
} else if (read_line("Enter nsec or 64-char hex private key: ", key_input, sizeof(key_input)) != 0) {
|
||||
fprintf(stderr, "Failed to read key input\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "WARNING: This is a TEST-KEY-ONLY tool. Do not use a real/production nsec. Keys are read into process memory in plaintext.\n");
|
||||
if (parse_private_key_input(key_input, private_key) != 0) {
|
||||
fprintf(stderr, "Invalid private key input (must be nsec1... or 64-char hex)\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (argc > argi) {
|
||||
strncpy(note_content, argv[argi], sizeof(note_content) - 1);
|
||||
} else if (read_line("Enter note content: ", note_content, sizeof(note_content)) != 0) {
|
||||
fprintf(stderr, "Failed to read note content\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize nostr library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (signer_mode == SIGNER_MODE_UNIX) {
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_unix(unix_name, NULL, 15000);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize unix nsigner backend (%s)\n", unix_name);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "This build does not include nsigner client support\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
#endif
|
||||
} else if (signer_mode == SIGNER_MODE_SERIAL) {
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_serial(serial_path, NULL, 15000);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize serial nsigner backend (%s)\n", serial_path);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "This build does not include nsigner client support\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
#endif
|
||||
} else if (signer_mode == SIGNER_MODE_TCP) {
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_tcp(tcp_host, tcp_port, NULL, 15000);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize tcp nsigner backend (%s:%d)\n", tcp_host, tcp_port);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "This build does not include nsigner client support\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
#endif
|
||||
} else if (signer_mode == SIGNER_MODE_FDS) {
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_fds(fds_read_fd, fds_write_fd, NULL, 15000);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize fds nsigner backend (%d:%d)\n", fds_read_fd, fds_write_fd);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr, "This build does not include nsigner client support\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
#endif
|
||||
} else {
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
fprintf(stderr, "Failed to initialize local signer\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (nostr_signer_get_public_key(signer, pubkey_hex) != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to get signer pubkey\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Signed in as pubkey: %s\n", pubkey_hex);
|
||||
|
||||
note_event = nostr_create_and_sign_event_with_signer(1, note_content, NULL, signer, time(NULL));
|
||||
if (!note_event) {
|
||||
fprintf(stderr, "Failed to create/sign note event\n");
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
id_item = cJSON_GetObjectItem(note_event, "id");
|
||||
if (id_item && cJSON_IsString(id_item)) {
|
||||
printf("Created event id: %s\n", cJSON_GetStringValue(id_item));
|
||||
}
|
||||
|
||||
printf("Publishing to %s ...\n", DEFAULT_RELAY);
|
||||
results = synchronous_publish_event_with_progress(
|
||||
relay_urls,
|
||||
1,
|
||||
note_event,
|
||||
&success_count,
|
||||
12,
|
||||
publish_progress_callback,
|
||||
NULL,
|
||||
0,
|
||||
NULL
|
||||
);
|
||||
|
||||
if (!results || success_count < 1 || results[0] != PUBLISH_SUCCESS) {
|
||||
fprintf(stderr, "Publish failed (success_count=%d, result=%d)\n",
|
||||
success_count,
|
||||
results ? (int)results[0] : -999);
|
||||
if (results) {
|
||||
free(results);
|
||||
}
|
||||
cJSON_Delete(note_event);
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Published successfully to %s\n", DEFAULT_RELAY);
|
||||
|
||||
free(results);
|
||||
cJSON_Delete(note_event);
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
/*
|
||||
* OpenTimestamps Command-Line Tool
|
||||
*
|
||||
* A simple tool for timestamping files with OpenTimestamps.
|
||||
*
|
||||
* Usage:
|
||||
* ./ots_tool stamp Interactive: type text, save to file, submit to OTS
|
||||
* ./ots_tool stamp <file> Timestamp an existing file
|
||||
* ./ots_tool check <file.ots> Check if a proof is complete
|
||||
* ./ots_tool verify <file> <file.ots> Verify a proof against a file
|
||||
* ./ots_tool upgrade <file.ots> Upgrade a pending proof
|
||||
*
|
||||
* The "stamp" command:
|
||||
* 1. If no file argument, prompts for text input and saves it to a .txt file
|
||||
* 2. Computes the SHA-256 hash of the file
|
||||
* 3. Submits the hash to an OpenTimestamps calendar
|
||||
* 4. Saves the initial .ots proof to <file>.ots
|
||||
* 5. Polls every 60 seconds to check if the proof is complete
|
||||
* 6. When complete, saves the upgraded proof and verifies it
|
||||
*
|
||||
* Options:
|
||||
* --calendar <url> Use a specific OTS calendar (default: https://alice.btc.calendar.opentimestamps.org)
|
||||
* --interval <sec> Poll interval in seconds (default: 60)
|
||||
* --max-wait <min> Maximum wait time in minutes (default: 120 = 2 hours)
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "nostr_core/nostr_core.h"
|
||||
#include "nostr_core/nip003.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define DEFAULT_CALENDAR "https://alice.btc.calendar.opentimestamps.org"
|
||||
#define DEFAULT_INTERVAL_SEC 60
|
||||
#define DEFAULT_MAX_WAIT_MIN 120
|
||||
|
||||
/* Read an entire file into a malloc'd buffer. Returns NULL on error. */
|
||||
static unsigned char* read_file(const char* path, size_t* len_out) {
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) return NULL;
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
if (size < 0) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
unsigned char* buf = (unsigned char*)malloc(size + 1);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t nread = fread(buf, 1, size, f);
|
||||
fclose(f);
|
||||
|
||||
if (nread != (size_t)size) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf[size] = '\0';
|
||||
*len_out = (size_t)size;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Write data to a file. Returns 0 on success, -1 on error. */
|
||||
static int write_file(const char* path, const unsigned char* data, size_t len) {
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
|
||||
size_t nwritten = fwrite(data, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
return (nwritten == len) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Compute SHA-256 of a file and return as hex string (64 chars + null). */
|
||||
static int compute_file_sha256_hex(const char* path, char* hex_out) {
|
||||
size_t file_len = 0;
|
||||
unsigned char* file_data = read_file(path, &file_len);
|
||||
if (!file_data) return -1;
|
||||
|
||||
unsigned char digest[32];
|
||||
int rc = nostr_sha256(file_data, file_len, digest);
|
||||
free(file_data);
|
||||
|
||||
if (rc != NOSTR_SUCCESS) return -1;
|
||||
|
||||
nostr_bytes_to_hex(digest, 32, hex_out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Convert base64 string to binary file and save. */
|
||||
static int save_base64_to_file(const char* path, const char* b64) {
|
||||
if (!b64) return -1;
|
||||
|
||||
size_t b64_len = strlen(b64);
|
||||
size_t max_decoded = (b64_len / 4) * 3 + 3;
|
||||
unsigned char* data = (unsigned char*)malloc(max_decoded);
|
||||
if (!data) return -1;
|
||||
|
||||
size_t data_len = base64_decode(b64, data);
|
||||
if (data_len == 0) {
|
||||
free(data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int rc = write_file(path, data, data_len);
|
||||
free(data);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Read a binary file and convert to base64 string. Caller must free. */
|
||||
static char* read_file_as_base64(const char* path) {
|
||||
size_t file_len = 0;
|
||||
unsigned char* file_data = read_file(path, &file_len);
|
||||
if (!file_data) return NULL;
|
||||
|
||||
size_t b64_size = ((file_len + 2) / 3) * 4 + 1;
|
||||
char* b64 = (char*)malloc(b64_size);
|
||||
if (!b64) {
|
||||
free(file_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
base64_encode(file_data, file_len, b64, b64_size);
|
||||
free(file_data);
|
||||
return b64;
|
||||
}
|
||||
|
||||
static void print_usage(const char* prog) {
|
||||
printf("OpenTimestamps Command-Line Tool\n");
|
||||
printf("\n");
|
||||
printf("Usage:\n");
|
||||
printf(" %s stamp Interactive: type text, save, submit to OTS\n", prog);
|
||||
printf(" %s stamp <file> Timestamp an existing file\n", prog);
|
||||
printf(" %s check <file.ots> Check if a proof is complete\n", prog);
|
||||
printf(" %s verify <file> <file.ots> Verify a proof against a file\n", prog);
|
||||
printf(" %s upgrade <file.ots> [file] Upgrade a pending proof (optional: original file)\n", prog);
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" --calendar <url> OTS calendar URL (default: %s)\n", DEFAULT_CALENDAR);
|
||||
printf(" --interval <sec> Poll interval in seconds (default: %d)\n", DEFAULT_INTERVAL_SEC);
|
||||
printf(" --max-wait <min> Max wait time in minutes (default: %d)\n", DEFAULT_MAX_WAIT_MIN);
|
||||
printf("\n");
|
||||
printf("Examples:\n");
|
||||
printf(" %s stamp # Type text, timestamp it\n", prog);
|
||||
printf(" %s stamp mydocument.txt # Timestamp a file\n", prog);
|
||||
printf(" %s check mydocument.txt.ots # Check proof status\n", prog);
|
||||
printf(" %s verify mydocument.txt mydocument.txt.ots # Verify proof\n", prog);
|
||||
}
|
||||
|
||||
static int do_stamp(const char* file_path, const char* calendar_url,
|
||||
int interval_sec, int max_wait_min) {
|
||||
char auto_file[256] = {0};
|
||||
const char* target_file = file_path;
|
||||
|
||||
/* If no file specified, prompt for text input */
|
||||
if (!target_file) {
|
||||
printf("📝 Enter text to timestamp (press Enter twice to finish):\n\n");
|
||||
|
||||
/* Read multi-line input until empty line */
|
||||
char* content = NULL;
|
||||
size_t content_cap = 0;
|
||||
size_t content_len = 0;
|
||||
char line[1024];
|
||||
int empty_count = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), stdin)) {
|
||||
if (line[0] == '\n') {
|
||||
empty_count++;
|
||||
if (empty_count >= 1 && content_len > 0) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
empty_count = 0;
|
||||
}
|
||||
|
||||
size_t line_len = strlen(line);
|
||||
if (content_len + line_len + 1 > content_cap) {
|
||||
content_cap = (content_len + line_len + 1) * 2;
|
||||
char* new_content = (char*)realloc(content, content_cap);
|
||||
if (!new_content) {
|
||||
free(content);
|
||||
printf("❌ Out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
content = new_content;
|
||||
}
|
||||
memcpy(content + content_len, line, line_len);
|
||||
content_len += line_len;
|
||||
content[content_len] = '\0';
|
||||
}
|
||||
|
||||
if (!content || content_len == 0) {
|
||||
printf("❌ No text entered.\n");
|
||||
free(content);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Generate a filename based on timestamp */
|
||||
time_t now = time(NULL);
|
||||
struct tm* tm_info = localtime(&now);
|
||||
strftime(auto_file, sizeof(auto_file), "timestamped_%Y%m%d_%H%M%S.txt", tm_info);
|
||||
|
||||
/* Save the text to a file */
|
||||
if (write_file(auto_file, (const unsigned char*)content, content_len) != 0) {
|
||||
printf("❌ Failed to save text to file: %s\n", auto_file);
|
||||
free(content);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("\n💾 Saved text to: %s (%zu bytes)\n", auto_file, content_len);
|
||||
free(content);
|
||||
target_file = auto_file;
|
||||
}
|
||||
|
||||
/* Compute SHA-256 of the file */
|
||||
char digest_hex[65];
|
||||
if (compute_file_sha256_hex(target_file, digest_hex) != 0) {
|
||||
printf("❌ Failed to compute SHA-256 of file: %s\n", target_file);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("🔐 SHA-256: %s\n", digest_hex);
|
||||
|
||||
/* Submit to multiple OTS calendars for redundancy */
|
||||
const char* default_calendars[] = {
|
||||
"https://alice.btc.calendar.opentimestamps.org",
|
||||
"https://bob.btc.calendar.opentimestamps.org",
|
||||
"https://finney.calendar.eternitywall.com",
|
||||
"https://btc.calendar.catallaxy.com",
|
||||
};
|
||||
int num_calendars = 4;
|
||||
|
||||
/* If a specific calendar was specified, use only that one */
|
||||
const char** calendars_to_use;
|
||||
int num_to_use;
|
||||
if (strcmp(calendar_url, DEFAULT_CALENDAR) != 0) {
|
||||
calendars_to_use = &calendar_url;
|
||||
num_to_use = 1;
|
||||
} else {
|
||||
calendars_to_use = default_calendars;
|
||||
num_to_use = num_calendars;
|
||||
}
|
||||
|
||||
printf("\n📤 Submitting to %d OpenTimestamps calendar(s)...\n", num_to_use);
|
||||
|
||||
/* Submit to all calendars and create a combined DetachedTimestampFile */
|
||||
char* ots_b64 = nostr_nip03_stamp_with_multiple_calendars(
|
||||
digest_hex, calendars_to_use, num_to_use, 30);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to submit to any calendar.\n");
|
||||
printf(" Check your internet connection and try again.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Save the .ots proof */
|
||||
char ots_path[512];
|
||||
snprintf(ots_path, sizeof(ots_path), "%s.ots", target_file);
|
||||
|
||||
if (save_base64_to_file(ots_path, ots_b64) != 0) {
|
||||
printf("❌ Failed to save .ots proof to: %s\n", ots_path);
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Proof saved to: %s\n", ots_path);
|
||||
|
||||
/* Show attestation info - list all pending calendars */
|
||||
char** pending_uris = NULL;
|
||||
int pending_count = nostr_nip03_get_pending_uris(ots_b64, &pending_uris, 16);
|
||||
if (pending_count > 0) {
|
||||
printf(" Pending at %d calendar(s):\n", pending_count);
|
||||
for (int i = 0; i < pending_count; i++) {
|
||||
printf(" • %s\n", pending_uris[i]);
|
||||
free(pending_uris[i]);
|
||||
}
|
||||
free(pending_uris);
|
||||
}
|
||||
|
||||
/* Check if already complete (unlikely but possible) */
|
||||
int complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
if (complete == 1) {
|
||||
printf("\n🎉 Proof is already complete!\n");
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Poll for completion */
|
||||
int max_attempts = (max_wait_min * 60) / interval_sec;
|
||||
printf("\n⏳ Waiting for Bitcoin attestation...\n");
|
||||
printf(" Polling every %d seconds (max %d minutes)\n", interval_sec, max_wait_min);
|
||||
printf(" Press Ctrl+C to stop. The .ots file can be checked later with 'check'.\n\n");
|
||||
|
||||
int attempts = 0;
|
||||
while (!complete && attempts < max_attempts) {
|
||||
attempts++;
|
||||
|
||||
time_t now = time(NULL);
|
||||
char time_str[26];
|
||||
ctime_r(&now, time_str);
|
||||
time_str[24] = '\0';
|
||||
|
||||
printf("[%s] Attempt %d/%d: %s\n", time_str, attempts, max_attempts,
|
||||
complete ? "✅ COMPLETE" : "⏳ PENDING");
|
||||
fflush(stdout);
|
||||
|
||||
if (complete) break;
|
||||
|
||||
sleep(interval_sec);
|
||||
|
||||
/* Try to upgrade the proof by walking the tree and fetching upgraded sub-proofs */
|
||||
char* upgraded = nostr_nip03_upgrade_proof_tree(ots_b64, 30);
|
||||
if (upgraded) {
|
||||
free(ots_b64);
|
||||
ots_b64 = upgraded;
|
||||
|
||||
/* Save the upgraded proof */
|
||||
save_base64_to_file(ots_path, ots_b64);
|
||||
|
||||
/* Check if now complete */
|
||||
complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
}
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
printf("\n🎉 SUCCESS! Proof is complete with a Bitcoin attestation!\n");
|
||||
|
||||
/* Save the final proof */
|
||||
save_base64_to_file(ots_path, ots_b64);
|
||||
printf(" Final proof saved to: %s\n", ots_path);
|
||||
|
||||
/* Show attestation details */
|
||||
int has_bitcoin = 0;
|
||||
uint64_t btc_height = 0;
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, NULL, NULL,
|
||||
&btc_height, NULL, NULL) == 0) {
|
||||
if (has_bitcoin) {
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
}
|
||||
|
||||
/* Verify the proof */
|
||||
uint64_t verify_height = 0;
|
||||
int verify_rc = nostr_nip03_verify_proof(ots_b64, digest_hex, &verify_height, NULL);
|
||||
if (verify_rc == 0) {
|
||||
printf(" ✅ Verified: file digest matches, Bitcoin height=%llu\n",
|
||||
(unsigned long long)verify_height);
|
||||
} else {
|
||||
printf(" ⚠️ Verification returned: %d\n", verify_rc);
|
||||
}
|
||||
} else {
|
||||
printf("\n⏳ Proof is still pending after %d minutes.\n", max_wait_min);
|
||||
printf(" The .ots file has been saved: %s\n", ots_path);
|
||||
printf(" Check again later with: ./ots_tool check %s\n", ots_path);
|
||||
printf(" Or upgrade with: ./ots_tool upgrade %s\n", ots_path);
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
return complete ? 0 : 2;
|
||||
}
|
||||
|
||||
static int do_check(const char* ots_path) {
|
||||
printf("🔎 Checking proof: %s\n\n", ots_path);
|
||||
|
||||
char* ots_b64 = read_file_as_base64(ots_path);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to read .ots file: %s\n", ots_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
|
||||
if (complete == 1) {
|
||||
printf("✅ Proof is COMPLETE (has Bitcoin/Litecoin attestation)\n");
|
||||
} else if (complete == 0) {
|
||||
printf("⏳ Proof is PENDING (no Bitcoin attestation yet)\n");
|
||||
} else {
|
||||
printf("❌ Failed to parse proof (invalid OTS file?)\n");
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Show detailed attestation info */
|
||||
int has_bitcoin = 0, has_litecoin = 0, has_pending = 0;
|
||||
uint64_t btc_height = 0, ltc_height = 0;
|
||||
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, &has_litecoin, &has_pending,
|
||||
&btc_height, <c_height, NULL) == 0) {
|
||||
if (has_bitcoin) {
|
||||
printf(" ✅ Bitcoin attestation: block height %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
if (has_litecoin) {
|
||||
printf(" ✅ Litecoin attestation: block height %llu\n", (unsigned long long)ltc_height);
|
||||
}
|
||||
}
|
||||
|
||||
/* Show all pending calendar URIs */
|
||||
if (has_pending) {
|
||||
char** pending_uris = NULL;
|
||||
int pending_count = nostr_nip03_get_pending_uris(ots_b64, &pending_uris, 16);
|
||||
if (pending_count > 0) {
|
||||
printf(" ⏳ Pending at %d calendar(s):\n", pending_count);
|
||||
for (int i = 0; i < pending_count; i++) {
|
||||
printf(" • %s\n", pending_uris[i]);
|
||||
free(pending_uris[i]);
|
||||
}
|
||||
free(pending_uris);
|
||||
}
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int do_verify(const char* file_path, const char* ots_path) {
|
||||
printf("🔍 Verifying proof...\n");
|
||||
printf(" File: %s\n", file_path);
|
||||
printf(" Proof: %s\n\n", ots_path);
|
||||
|
||||
/* Compute SHA-256 of the file */
|
||||
char digest_hex[65];
|
||||
if (compute_file_sha256_hex(file_path, digest_hex) != 0) {
|
||||
printf("❌ Failed to compute SHA-256 of file: %s\n", file_path);
|
||||
return 1;
|
||||
}
|
||||
printf(" File SHA-256: %s\n", digest_hex);
|
||||
|
||||
/* Read the .ots file */
|
||||
char* ots_b64 = read_file_as_base64(ots_path);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to read .ots file: %s\n", ots_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Verify the proof */
|
||||
uint64_t block_height = 0;
|
||||
time_t block_time = 0;
|
||||
int rc = nostr_nip03_verify_proof(ots_b64, digest_hex, &block_height, &block_time);
|
||||
|
||||
switch (rc) {
|
||||
case 0:
|
||||
printf("\n✅ Proof VERIFIED!\n");
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)block_height);
|
||||
printf(" The file's hash is committed in the Bitcoin blockchain.\n");
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
|
||||
case 1:
|
||||
printf("\n⏳ Proof is valid but still PENDING (no Bitcoin attestation yet).\n");
|
||||
printf(" The file digest matches the proof.\n");
|
||||
printf(" Wait for the calendar to commit to the blockchain, then run:\n");
|
||||
printf(" ./ots_tool upgrade %s\n", ots_path);
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
|
||||
case -1:
|
||||
printf("\n❌ Failed to parse OTS proof. The .ots file may be corrupted.\n");
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
|
||||
case -2:
|
||||
printf("\n❌ DIGEST MISMATCH! The proof does not match this file.\n");
|
||||
printf(" The file may have been modified after timestamping.\n");
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
|
||||
default:
|
||||
printf("\n❌ Unknown error: %d\n", rc);
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int do_upgrade(const char* ots_path, const char* file_path,
|
||||
const char* calendar_url) {
|
||||
printf("⬆️ Upgrading proof: %s\n\n", ots_path);
|
||||
|
||||
char* ots_b64 = read_file_as_base64(ots_path);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to read .ots file: %s\n", ots_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Check current status */
|
||||
int was_complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
if (was_complete == 1) {
|
||||
printf("✅ Proof is already complete! No upgrade needed.\n");
|
||||
free(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
(void)calendar_url; /* upgrade now uses pending-attestation URIs from proof */
|
||||
(void)file_path;
|
||||
|
||||
printf("📤 Checking pending attestations and fetching upgrades...\n");
|
||||
|
||||
char* upgraded = NULL;
|
||||
/* Use the tree-walking upgrade which correctly queries commitment hashes */
|
||||
upgraded = nostr_nip03_upgrade_proof_tree(ots_b64, 30);
|
||||
|
||||
if (!upgraded) {
|
||||
printf("⏳ No upgrade available yet. The proof is still pending.\n");
|
||||
printf(" Bitcoin attestations typically take 1-12 hours.\n");
|
||||
if (!file_path) {
|
||||
printf(" Tip: provide the original file path to enable digest-based upgrade:\n");
|
||||
printf(" ./ots_tool upgrade %s <original-file>\n", ots_path);
|
||||
}
|
||||
free(ots_b64);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Save the upgraded proof */
|
||||
if (save_base64_to_file(ots_path, upgraded) != 0) {
|
||||
printf("❌ Failed to save upgraded proof.\n");
|
||||
free(ots_b64);
|
||||
free(upgraded);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Check if now complete */
|
||||
int complete = nostr_nip03_is_proof_complete(upgraded);
|
||||
if (complete == 1) {
|
||||
printf("✅ Upgrade successful! Proof is now COMPLETE with a Bitcoin attestation.\n");
|
||||
|
||||
int has_bitcoin = 0;
|
||||
uint64_t btc_height = 0;
|
||||
if (nostr_nip03_get_attestation_info(upgraded, &has_bitcoin, NULL, NULL,
|
||||
&btc_height, NULL, NULL) == 0) {
|
||||
if (has_bitcoin) {
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printf("⏳ Upgrade received but proof is still pending.\n");
|
||||
printf(" The upgraded proof has been saved. Try again later.\n");
|
||||
}
|
||||
|
||||
printf(" Updated proof saved to: %s\n", ots_path);
|
||||
|
||||
free(ots_b64);
|
||||
free(upgraded);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize NOSTR library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* calendar_url = DEFAULT_CALENDAR;
|
||||
int interval_sec = DEFAULT_INTERVAL_SEC;
|
||||
int max_wait_min = DEFAULT_MAX_WAIT_MIN;
|
||||
|
||||
enum { MODE_NONE, MODE_STAMP, MODE_CHECK, MODE_VERIFY, MODE_UPGRADE } mode = MODE_NONE;
|
||||
const char* arg1 = NULL;
|
||||
const char* arg2 = NULL;
|
||||
|
||||
/* Parse arguments */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||
print_usage(argv[0]);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "--calendar") == 0 && i + 1 < argc) {
|
||||
calendar_url = argv[++i];
|
||||
} else if (strcmp(argv[i], "--interval") == 0 && i + 1 < argc) {
|
||||
interval_sec = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--max-wait") == 0 && i + 1 < argc) {
|
||||
max_wait_min = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "stamp") == 0) {
|
||||
mode = MODE_STAMP;
|
||||
} else if (strcmp(argv[i], "check") == 0) {
|
||||
mode = MODE_CHECK;
|
||||
} else if (strcmp(argv[i], "verify") == 0) {
|
||||
mode = MODE_VERIFY;
|
||||
} else if (strcmp(argv[i], "upgrade") == 0) {
|
||||
mode = MODE_UPGRADE;
|
||||
} else if (argv[i][0] != '-') {
|
||||
if (!arg1) {
|
||||
arg1 = argv[i];
|
||||
} else if (!arg2) {
|
||||
arg2 = argv[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
|
||||
switch (mode) {
|
||||
case MODE_STAMP:
|
||||
rc = do_stamp(arg1, calendar_url, interval_sec, max_wait_min);
|
||||
break;
|
||||
|
||||
case MODE_CHECK:
|
||||
if (!arg1) {
|
||||
printf("❌ Missing .ots file path\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_check(arg1);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_VERIFY:
|
||||
if (!arg1 || !arg2) {
|
||||
printf("❌ Missing file or .ots path\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_verify(arg1, arg2);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_UPGRADE:
|
||||
if (!arg1) {
|
||||
printf("❌ Missing .ots file path\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_upgrade(arg1, arg2, calendar_url);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,889 @@
|
||||
/*
|
||||
* Interactive Relay Pool Test Program
|
||||
*
|
||||
* Interactive command-line interface for testing nostr_relay_pool functionality.
|
||||
* All output is logged to pool.log while the menu runs in the terminal.
|
||||
*
|
||||
* Usage: ./pool_test
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#define _DEFAULT_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include "../nostr_core/nostr_core.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// Global variables
|
||||
volatile sig_atomic_t running = 1;
|
||||
nostr_relay_pool_t* pool = NULL;
|
||||
nostr_pool_subscription_t** subscriptions = NULL;
|
||||
int subscription_count = 0;
|
||||
int subscription_capacity = 0;
|
||||
pthread_t poll_thread;
|
||||
int log_fd = -1;
|
||||
|
||||
// Signal handler for clean shutdown
|
||||
void signal_handler(int signum) {
|
||||
(void)signum;
|
||||
running = 0;
|
||||
}
|
||||
|
||||
// Event callback - called when an event is received
|
||||
void on_event(cJSON* event, const char* relay_url, void* user_data) {
|
||||
(void)user_data;
|
||||
|
||||
// Extract basic event information
|
||||
cJSON* id = cJSON_GetObjectItem(event, "id");
|
||||
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
|
||||
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
|
||||
cJSON* kind = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0'; // Remove newline
|
||||
|
||||
dprintf(log_fd, "[%s] 📨 EVENT from %s\n", timestamp, relay_url);
|
||||
dprintf(log_fd, "├── ID: %.12s...\n", id && cJSON_IsString(id) ? cJSON_GetStringValue(id) : "unknown");
|
||||
dprintf(log_fd, "├── Pubkey: %.12s...\n", pubkey && cJSON_IsString(pubkey) ? cJSON_GetStringValue(pubkey) : "unknown");
|
||||
dprintf(log_fd, "├── Kind: %d\n", kind && cJSON_IsNumber(kind) ? (int)cJSON_GetNumberValue(kind) : -1);
|
||||
dprintf(log_fd, "├── Created: %lld\n", created_at && cJSON_IsNumber(created_at) ? (long long)cJSON_GetNumberValue(created_at) : 0);
|
||||
|
||||
// Truncate content if too long
|
||||
if (content && cJSON_IsString(content)) {
|
||||
const char* content_str = cJSON_GetStringValue(content);
|
||||
size_t content_len = strlen(content_str);
|
||||
if (content_len > 100) {
|
||||
dprintf(log_fd, "└── Content: %.97s...\n", content_str);
|
||||
} else {
|
||||
dprintf(log_fd, "└── Content: %s\n", content_str);
|
||||
}
|
||||
} else {
|
||||
dprintf(log_fd, "└── Content: (empty)\n");
|
||||
}
|
||||
dprintf(log_fd, "\n");
|
||||
}
|
||||
|
||||
// EOSE callback - called when End of Stored Events is received
|
||||
void on_eose(cJSON** events, int event_count, void* user_data) {
|
||||
(void)user_data;
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 📋 EOSE received - %d events collected\n", timestamp, event_count);
|
||||
|
||||
// Log collected events if any
|
||||
for (int i = 0; i < event_count; i++) {
|
||||
cJSON* id = cJSON_GetObjectItem(events[i], "id");
|
||||
if (id && cJSON_IsString(id)) {
|
||||
dprintf(log_fd, " Event %d: %.12s...\n", i + 1, cJSON_GetStringValue(id));
|
||||
}
|
||||
}
|
||||
dprintf(log_fd, "\n");
|
||||
}
|
||||
|
||||
// Background polling thread
|
||||
void* poll_thread_func(void* arg) {
|
||||
(void)arg;
|
||||
|
||||
while (running) {
|
||||
if (pool) {
|
||||
nostr_relay_pool_poll(pool, 100);
|
||||
}
|
||||
struct timespec ts = {0, 10000000}; // 10ms
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Print menu
|
||||
void print_menu() {
|
||||
printf("\n=== NOSTR Relay Pool Test Menu ===\n");
|
||||
printf("1. Start Pool (ws://localhost:7555)\n");
|
||||
printf("2. Stop Pool\n");
|
||||
printf("3. Add relay to pool\n");
|
||||
printf("4. Remove relay from pool\n");
|
||||
printf("5. Add subscription\n");
|
||||
printf("6. Remove subscription\n");
|
||||
printf("7. Show pool status\n");
|
||||
printf("8. Test reconnection (simulate disconnect)\n");
|
||||
printf("9. Publish Event\n");
|
||||
printf("0. Exit\n");
|
||||
printf("Choice: ");
|
||||
}
|
||||
|
||||
// Get user input with default
|
||||
char* get_input(const char* prompt, const char* default_value) {
|
||||
static char buffer[1024];
|
||||
printf("%s", prompt);
|
||||
if (default_value) {
|
||||
printf(" [%s]", default_value);
|
||||
}
|
||||
printf(": ");
|
||||
|
||||
if (!fgets(buffer, sizeof(buffer), stdin)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Remove newline
|
||||
size_t len = strlen(buffer);
|
||||
if (len > 0 && buffer[len-1] == '\n') {
|
||||
buffer[len-1] = '\0';
|
||||
}
|
||||
|
||||
// Return default if empty
|
||||
if (strlen(buffer) == 0 && default_value) {
|
||||
return strdup(default_value);
|
||||
}
|
||||
|
||||
return strdup(buffer);
|
||||
}
|
||||
|
||||
// Parse comma-separated list into cJSON array
|
||||
cJSON* parse_comma_list(const char* input, int is_number) {
|
||||
if (!input || strlen(input) == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* array = cJSON_CreateArray();
|
||||
if (!array) return NULL;
|
||||
|
||||
char* input_copy = strdup(input);
|
||||
char* token = strtok(input_copy, ",");
|
||||
|
||||
while (token) {
|
||||
// Trim whitespace
|
||||
while (*token == ' ') token++;
|
||||
char* end = token + strlen(token) - 1;
|
||||
while (end > token && *end == ' ') *end-- = '\0';
|
||||
|
||||
if (is_number) {
|
||||
int num = atoi(token);
|
||||
cJSON_AddItemToArray(array, cJSON_CreateNumber(num));
|
||||
} else {
|
||||
cJSON_AddItemToArray(array, cJSON_CreateString(token));
|
||||
}
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
free(input_copy);
|
||||
return array;
|
||||
}
|
||||
|
||||
// Add subscription interactively
|
||||
void add_subscription() {
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("\n--- Add Subscription ---\n");
|
||||
printf("Enter filter values (press Enter for no value):\n");
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
|
||||
// ids
|
||||
char* ids_input = get_input("ids (comma-separated event ids)", NULL);
|
||||
if (ids_input && strlen(ids_input) > 0) {
|
||||
cJSON* ids = parse_comma_list(ids_input, 0);
|
||||
if (ids) cJSON_AddItemToObject(filter, "ids", ids);
|
||||
}
|
||||
free(ids_input);
|
||||
|
||||
// authors
|
||||
char* authors_input = get_input("authors (comma-separated pubkeys)", NULL);
|
||||
if (authors_input && strlen(authors_input) > 0) {
|
||||
cJSON* authors = parse_comma_list(authors_input, 0);
|
||||
if (authors) cJSON_AddItemToObject(filter, "authors", authors);
|
||||
}
|
||||
free(authors_input);
|
||||
|
||||
// kinds
|
||||
char* kinds_input = get_input("kinds (comma-separated numbers)", NULL);
|
||||
if (kinds_input && strlen(kinds_input) > 0) {
|
||||
cJSON* kinds = parse_comma_list(kinds_input, 1);
|
||||
if (kinds) cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
}
|
||||
free(kinds_input);
|
||||
|
||||
// #e tag
|
||||
char* e_input = get_input("#e (comma-separated event ids)", NULL);
|
||||
if (e_input && strlen(e_input) > 0) {
|
||||
cJSON* e_array = parse_comma_list(e_input, 0);
|
||||
if (e_array) cJSON_AddItemToObject(filter, "#e", e_array);
|
||||
}
|
||||
free(e_input);
|
||||
|
||||
// #p tag
|
||||
char* p_input = get_input("#p (comma-separated pubkeys)", NULL);
|
||||
if (p_input && strlen(p_input) > 0) {
|
||||
cJSON* p_array = parse_comma_list(p_input, 0);
|
||||
if (p_array) cJSON_AddItemToObject(filter, "#p", p_array);
|
||||
}
|
||||
free(p_input);
|
||||
|
||||
// since
|
||||
char* since_input = get_input("since (unix timestamp or 'n' for now)", NULL);
|
||||
if (since_input && strlen(since_input) > 0) {
|
||||
if (strcmp(since_input, "n") == 0) {
|
||||
// Use current timestamp
|
||||
time_t now = time(NULL);
|
||||
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber((int)now));
|
||||
printf("Using current timestamp: %ld\n", now);
|
||||
} else {
|
||||
int since = atoi(since_input);
|
||||
if (since > 0) cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(since));
|
||||
}
|
||||
}
|
||||
free(since_input);
|
||||
|
||||
// until
|
||||
char* until_input = get_input("until (unix timestamp)", NULL);
|
||||
if (until_input && strlen(until_input) > 0) {
|
||||
int until = atoi(until_input);
|
||||
if (until > 0) cJSON_AddItemToObject(filter, "until", cJSON_CreateNumber(until));
|
||||
}
|
||||
free(until_input);
|
||||
|
||||
// limit
|
||||
char* limit_input = get_input("limit (max events)", "10");
|
||||
if (limit_input && strlen(limit_input) > 0) {
|
||||
int limit = atoi(limit_input);
|
||||
if (limit > 0) cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(limit));
|
||||
}
|
||||
free(limit_input);
|
||||
|
||||
// Get relay URLs from pool
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
if (relay_count <= 0) {
|
||||
printf("❌ No relays in pool\n");
|
||||
cJSON_Delete(filter);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask about close_on_eose behavior
|
||||
char* close_input = get_input("Close subscription on EOSE? (y/n)", "n");
|
||||
int close_on_eose = (close_input && strcmp(close_input, "y") == 0) ? 1 : 0;
|
||||
free(close_input);
|
||||
|
||||
// Create subscription with new parameters
|
||||
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
|
||||
pool,
|
||||
(const char**)relay_urls,
|
||||
relay_count,
|
||||
filter,
|
||||
on_event,
|
||||
on_eose,
|
||||
NULL,
|
||||
close_on_eose,
|
||||
1, // enable_deduplication
|
||||
NOSTR_POOL_EOSE_FULL_SET, // result_mode
|
||||
30, // relay_timeout_seconds
|
||||
60 // eose_timeout_seconds
|
||||
);
|
||||
|
||||
// Free relay URLs
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
free(relay_urls[i]);
|
||||
}
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
|
||||
if (!sub) {
|
||||
printf("❌ Failed to create subscription\n");
|
||||
cJSON_Delete(filter);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store subscription
|
||||
if (subscription_count >= subscription_capacity) {
|
||||
subscription_capacity = subscription_capacity == 0 ? 10 : subscription_capacity * 2;
|
||||
subscriptions = realloc(subscriptions, subscription_capacity * sizeof(nostr_pool_subscription_t*));
|
||||
}
|
||||
subscriptions[subscription_count++] = sub;
|
||||
|
||||
printf("✅ Subscription created (ID: %d)\n", subscription_count);
|
||||
|
||||
// Log the filter
|
||||
char* filter_json = cJSON_Print(filter);
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🔍 New subscription created (ID: %d)\n", timestamp, subscription_count);
|
||||
dprintf(log_fd, "Filter: %s\n\n", filter_json);
|
||||
free(filter_json);
|
||||
}
|
||||
|
||||
// Remove subscription
|
||||
void remove_subscription() {
|
||||
if (subscription_count == 0) {
|
||||
printf("❌ No subscriptions to remove\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("\n--- Remove Subscription ---\n");
|
||||
printf("Available subscriptions:\n");
|
||||
for (int i = 0; i < subscription_count; i++) {
|
||||
printf("%d. Subscription %d\n", i + 1, i + 1);
|
||||
}
|
||||
|
||||
char* choice_input = get_input("Enter subscription number to remove", NULL);
|
||||
if (!choice_input || strlen(choice_input) == 0) {
|
||||
free(choice_input);
|
||||
return;
|
||||
}
|
||||
|
||||
int choice = atoi(choice_input) - 1;
|
||||
free(choice_input);
|
||||
|
||||
if (choice < 0 || choice >= subscription_count) {
|
||||
printf("❌ Invalid subscription number\n");
|
||||
return;
|
||||
}
|
||||
|
||||
nostr_pool_subscription_close(subscriptions[choice]);
|
||||
|
||||
// Shift remaining subscriptions
|
||||
for (int i = choice; i < subscription_count - 1; i++) {
|
||||
subscriptions[i] = subscriptions[i + 1];
|
||||
}
|
||||
subscription_count--;
|
||||
|
||||
printf("✅ Subscription removed\n");
|
||||
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🗑️ Subscription removed (was ID: %d)\n\n", timestamp, choice + 1);
|
||||
}
|
||||
|
||||
// Show pool status
|
||||
void show_pool_status() {
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Give polling thread time to establish connections
|
||||
printf("⏳ Waiting for connections to establish...\n");
|
||||
sleep(3);
|
||||
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
printf("\n📊 POOL STATUS\n");
|
||||
printf("Relays: %d\n", relay_count);
|
||||
printf("Subscriptions: %d\n", subscription_count);
|
||||
|
||||
if (relay_count > 0) {
|
||||
printf("\nRelay Details:\n");
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
const char* status_str;
|
||||
switch (statuses[i]) {
|
||||
case NOSTR_POOL_RELAY_CONNECTED: status_str = "🟢 CONNECTED"; break;
|
||||
case NOSTR_POOL_RELAY_CONNECTING: status_str = "🟡 CONNECTING"; break;
|
||||
case NOSTR_POOL_RELAY_DISCONNECTED: status_str = "⚪ DISCONNECTED"; break;
|
||||
case NOSTR_POOL_RELAY_ERROR: status_str = "🔴 ERROR"; break;
|
||||
default: status_str = "❓ UNKNOWN"; break;
|
||||
}
|
||||
|
||||
printf("├── %s: %s\n", relay_urls[i], status_str);
|
||||
|
||||
// Show connection and publish error details
|
||||
const char* conn_error = nostr_relay_pool_get_relay_last_connection_error(pool, relay_urls[i]);
|
||||
const char* pub_error = nostr_relay_pool_get_relay_last_publish_error(pool, relay_urls[i]);
|
||||
|
||||
if (conn_error) {
|
||||
printf("│ ├── Connection error: %s\n", conn_error);
|
||||
}
|
||||
if (pub_error) {
|
||||
printf("│ ├── Last publish error: %s\n", pub_error);
|
||||
}
|
||||
|
||||
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_urls[i]);
|
||||
if (stats) {
|
||||
printf("│ ├── Events received: %d\n", stats->events_received);
|
||||
printf("│ ├── Connection attempts: %d\n", stats->connection_attempts);
|
||||
printf("│ ├── Connection failures: %d\n", stats->connection_failures);
|
||||
printf("│ ├── Events published: %d (OK: %d, Failed: %d)\n",
|
||||
stats->events_published, stats->events_published_ok, stats->events_published_failed);
|
||||
printf("│ ├── Ping latency: %.2f ms\n", stats->ping_latency_current);
|
||||
printf("│ └── Query latency: %.2f ms\n", stats->query_latency_avg);
|
||||
}
|
||||
|
||||
free(relay_urls[i]);
|
||||
}
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
|
||||
// Async publish callback context
|
||||
typedef struct {
|
||||
int total_relays;
|
||||
int responses_received;
|
||||
int success_count;
|
||||
time_t start_time;
|
||||
} async_publish_context_t;
|
||||
|
||||
// Async publish callback - called for each relay response
|
||||
void async_publish_callback(const char* relay_url, const char* event_id,
|
||||
int success, const char* message, void* user_data) {
|
||||
async_publish_context_t* ctx = (async_publish_context_t*)user_data;
|
||||
|
||||
ctx->responses_received++;
|
||||
if (success) {
|
||||
ctx->success_count++;
|
||||
}
|
||||
|
||||
// Calculate elapsed time
|
||||
time_t now = time(NULL);
|
||||
double elapsed = difftime(now, ctx->start_time);
|
||||
|
||||
// Log to file with real-time feedback
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
|
||||
if (success) {
|
||||
printf("✅ %s: Published successfully (%.1fs)\n", relay_url, elapsed);
|
||||
dprintf(log_fd, "[%s] ✅ ASYNC: %s published successfully (%.1fs)\n",
|
||||
timestamp, relay_url, elapsed);
|
||||
} else {
|
||||
printf("❌ %s: Failed - %s (%.1fs)\n", relay_url, message ? message : "unknown error", elapsed);
|
||||
dprintf(log_fd, "[%s] ❌ ASYNC: %s failed - %s (%.1fs)\n",
|
||||
timestamp, relay_url, message ? message : "unknown error", elapsed);
|
||||
}
|
||||
|
||||
// Show progress
|
||||
printf(" Progress: %d/%d responses received\n", ctx->responses_received, ctx->total_relays);
|
||||
|
||||
if (ctx->responses_received >= ctx->total_relays) {
|
||||
printf("\n🎉 All relays responded! Final result: %d/%d successful\n",
|
||||
ctx->success_count, ctx->total_relays);
|
||||
dprintf(log_fd, "[%s] 🎉 ASYNC: All relays responded - %d/%d successful\n\n",
|
||||
timestamp, ctx->success_count, ctx->total_relays);
|
||||
}
|
||||
}
|
||||
|
||||
// Publish test event with async callbacks
|
||||
void publish_event() {
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("\n--- Publish Test Event ---\n");
|
||||
|
||||
// Generate random keypair
|
||||
unsigned char private_key[32], public_key[32];
|
||||
if (nostr_generate_keypair(private_key, public_key) != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to generate keypair\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current timestamp
|
||||
time_t now = time(NULL);
|
||||
|
||||
// Format content with date/time
|
||||
char content[256];
|
||||
struct tm* tm_info = localtime(&now);
|
||||
strftime(content, sizeof(content), "Test post at %Y-%m-%d %H:%M:%S", tm_info);
|
||||
|
||||
// Create kind 1 event
|
||||
cJSON* event = nostr_create_and_sign_event(1, content, NULL, private_key, now);
|
||||
if (!event) {
|
||||
printf("❌ Failed to create event\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get relay URLs from pool
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
if (relay_count <= 0) {
|
||||
printf("❌ No relays in pool\n");
|
||||
cJSON_Delete(event);
|
||||
return;
|
||||
}
|
||||
|
||||
printf("📤 Publishing event to %d relay(s)...\n", relay_count);
|
||||
printf("Watch for real-time responses below:\n\n");
|
||||
|
||||
// Setup callback context
|
||||
async_publish_context_t ctx = {0};
|
||||
ctx.total_relays = relay_count;
|
||||
ctx.start_time = time(NULL);
|
||||
|
||||
// Log the event
|
||||
char* event_json = cJSON_Print(event);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 📤 Publishing test event\n", timestamp);
|
||||
dprintf(log_fd, "Event: %s\n\n", event_json);
|
||||
free(event_json);
|
||||
|
||||
// Publish using async function
|
||||
int sent_count = nostr_relay_pool_publish_async(pool, (const char**)relay_urls,
|
||||
relay_count, event,
|
||||
async_publish_callback, &ctx);
|
||||
|
||||
if (sent_count > 0) {
|
||||
printf("📡 Event sent to %d/%d relays, waiting for responses...\n\n",
|
||||
sent_count, relay_count);
|
||||
|
||||
// Wait for all responses or timeout (10 seconds)
|
||||
time_t wait_start = time(NULL);
|
||||
while (ctx.responses_received < ctx.total_relays &&
|
||||
(time(NULL) - wait_start) < 10) {
|
||||
// Let the polling thread process messages
|
||||
usleep(100000); // 100ms
|
||||
}
|
||||
|
||||
if (ctx.responses_received < ctx.total_relays) {
|
||||
printf("\n⏰ Timeout reached - %d/%d relays responded\n",
|
||||
ctx.responses_received, ctx.total_relays);
|
||||
}
|
||||
} else {
|
||||
printf("❌ Failed to send event to any relays\n");
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
free(relay_urls[i]);
|
||||
}
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
cJSON_Delete(event);
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Setup logging to file
|
||||
log_fd = open("pool.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (log_fd == -1) {
|
||||
fprintf(stderr, "❌ Failed to open pool.log for writing\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Initialize NOSTR library
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "❌ Failed to initialize NOSTR library\n");
|
||||
close(log_fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Setup signal handler
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
|
||||
// Start polling thread
|
||||
if (pthread_create(&poll_thread, NULL, poll_thread_func, NULL) != 0) {
|
||||
fprintf(stderr, "❌ Failed to create polling thread\n");
|
||||
nostr_cleanup();
|
||||
close(log_fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("🔗 NOSTR Relay Pool Interactive Test\n");
|
||||
printf("=====================================\n");
|
||||
printf("All event output is logged to pool.log\n");
|
||||
printf("Press Ctrl+C to exit\n\n");
|
||||
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🚀 Pool test started\n\n", timestamp);
|
||||
|
||||
// Main menu loop
|
||||
while (running) {
|
||||
print_menu();
|
||||
|
||||
char choice;
|
||||
if (scanf("%c", &choice) != 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Consume newline
|
||||
int c;
|
||||
while ((c = getchar()) != '\n' && c != EOF);
|
||||
|
||||
switch (choice) {
|
||||
case '1': { // Start Pool
|
||||
if (pool) {
|
||||
printf("❌ Pool already started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (!pool) {
|
||||
printf("❌ Failed to create pool\n");
|
||||
break;
|
||||
}
|
||||
|
||||
if (nostr_relay_pool_add_relay(pool, "ws://localhost:7555") != NOSTR_SUCCESS) {
|
||||
printf("❌ Failed to add default relay\n");
|
||||
nostr_relay_pool_destroy(pool);
|
||||
pool = NULL;
|
||||
break;
|
||||
}
|
||||
|
||||
printf("✅ Pool started with ws://localhost:7555\n");
|
||||
|
||||
now = time(NULL);
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🏊 Pool started with default relay\n\n", timestamp);
|
||||
break;
|
||||
}
|
||||
|
||||
case '2': { // Stop Pool
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
// Close all subscriptions
|
||||
for (int i = 0; i < subscription_count; i++) {
|
||||
if (subscriptions[i]) {
|
||||
nostr_pool_subscription_close(subscriptions[i]);
|
||||
}
|
||||
}
|
||||
free(subscriptions);
|
||||
subscriptions = NULL;
|
||||
subscription_count = 0;
|
||||
subscription_capacity = 0;
|
||||
|
||||
nostr_relay_pool_destroy(pool);
|
||||
pool = NULL;
|
||||
|
||||
printf("✅ Pool stopped\n");
|
||||
|
||||
now = time(NULL);
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🛑 Pool stopped\n\n", timestamp);
|
||||
break;
|
||||
}
|
||||
|
||||
case '3': { // Add relay
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
char* url = get_input("Enter relay URL", "wss://relay.example.com");
|
||||
if (url && strlen(url) > 0) {
|
||||
if (nostr_relay_pool_add_relay(pool, url) == NOSTR_SUCCESS) {
|
||||
printf("✅ Relay added: %s\n", url);
|
||||
printf("⏳ Attempting to connect...\n");
|
||||
|
||||
// Give it a moment to attempt connection
|
||||
sleep(2);
|
||||
|
||||
// Check connection status and show any errors
|
||||
nostr_pool_relay_status_t status = nostr_relay_pool_get_relay_status(pool, url);
|
||||
const char* error_msg = nostr_relay_pool_get_relay_last_connection_error(pool, url);
|
||||
|
||||
switch (status) {
|
||||
case NOSTR_POOL_RELAY_CONNECTED:
|
||||
printf("🟢 Successfully connected to %s\n", url);
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_CONNECTING:
|
||||
printf("🟡 Still connecting to %s...\n", url);
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_DISCONNECTED:
|
||||
printf("⚪ Disconnected from %s\n", url);
|
||||
if (error_msg) {
|
||||
printf(" Last error: %s\n", error_msg);
|
||||
}
|
||||
break;
|
||||
case NOSTR_POOL_RELAY_ERROR:
|
||||
printf("🔴 Connection error for %s\n", url);
|
||||
if (error_msg) {
|
||||
printf(" Error details: %s\n", error_msg);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
printf("❓ Unknown status for %s\n", url);
|
||||
break;
|
||||
}
|
||||
|
||||
now = time(NULL);
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] ➕ Relay added: %s (status: %d)\n", timestamp, url, status);
|
||||
if (error_msg) {
|
||||
dprintf(log_fd, " Connection error: %s\n", error_msg);
|
||||
}
|
||||
dprintf(log_fd, "\n");
|
||||
} else {
|
||||
printf("❌ Failed to add relay to pool\n");
|
||||
}
|
||||
}
|
||||
free(url);
|
||||
break;
|
||||
}
|
||||
|
||||
case '4': { // Remove relay
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
char* url = get_input("Enter relay URL to remove", NULL);
|
||||
if (url && strlen(url) > 0) {
|
||||
if (nostr_relay_pool_remove_relay(pool, url) == NOSTR_SUCCESS) {
|
||||
printf("✅ Relay removed: %s\n", url);
|
||||
|
||||
now = time(NULL);
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] ➖ Relay removed: %s\n\n", timestamp, url);
|
||||
} else {
|
||||
printf("❌ Failed to remove relay\n");
|
||||
}
|
||||
}
|
||||
free(url);
|
||||
break;
|
||||
}
|
||||
|
||||
case '5': // Add subscription
|
||||
add_subscription();
|
||||
break;
|
||||
|
||||
case '6': // Remove subscription
|
||||
remove_subscription();
|
||||
break;
|
||||
|
||||
case '7': // Show status
|
||||
show_pool_status();
|
||||
break;
|
||||
|
||||
case '8': { // Test reconnection
|
||||
if (!pool) {
|
||||
printf("❌ Pool not started\n");
|
||||
break;
|
||||
}
|
||||
|
||||
char** relay_urls = NULL;
|
||||
nostr_pool_relay_status_t* statuses = NULL;
|
||||
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
|
||||
|
||||
if (relay_count <= 0) {
|
||||
printf("❌ No relays in pool\n");
|
||||
break;
|
||||
}
|
||||
|
||||
printf("\n--- Test Reconnection ---\n");
|
||||
printf("Available relays:\n");
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
printf("%d. %s (%s)\n", i + 1, relay_urls[i],
|
||||
statuses[i] == NOSTR_POOL_RELAY_CONNECTED ? "CONNECTED" : "NOT CONNECTED");
|
||||
}
|
||||
|
||||
char* choice_input = get_input("Enter relay number to test reconnection with", NULL);
|
||||
if (!choice_input || strlen(choice_input) == 0) {
|
||||
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
free(choice_input);
|
||||
break;
|
||||
}
|
||||
|
||||
int choice = atoi(choice_input) - 1;
|
||||
free(choice_input);
|
||||
|
||||
if (choice < 0 || choice >= relay_count) {
|
||||
printf("❌ Invalid relay number\n");
|
||||
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
break;
|
||||
}
|
||||
|
||||
printf("🔄 Testing reconnection with %s...\n", relay_urls[choice]);
|
||||
printf(" The pool is configured with automatic reconnection enabled.\n");
|
||||
printf(" If the connection drops, it will automatically attempt to reconnect\n");
|
||||
printf(" with exponential backoff (1s → 2s → 4s → 8s → 16s → 30s max).\n");
|
||||
printf(" Connection health is monitored with ping/pong every 30 seconds.\n");
|
||||
|
||||
time_t now = time(NULL);
|
||||
char timestamp[26];
|
||||
ctime_r(&now, timestamp);
|
||||
timestamp[24] = '\0';
|
||||
dprintf(log_fd, "[%s] 🔄 TEST: Testing reconnection behavior with %s\n", timestamp, relay_urls[choice]);
|
||||
dprintf(log_fd, " Pool configured with: auto-reconnect=ON, max_attempts=10, ping_interval=30s\n\n");
|
||||
|
||||
printf("✅ Reconnection test initiated. Monitor the status and logs for reconnection activity.\n");
|
||||
|
||||
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
|
||||
free(relay_urls);
|
||||
free(statuses);
|
||||
break;
|
||||
}
|
||||
|
||||
case '9': // Publish Event
|
||||
publish_event();
|
||||
break;
|
||||
|
||||
case '0': // Exit
|
||||
running = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
printf("❌ Invalid choice\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n🧹 Cleaning up...\n");
|
||||
|
||||
// Stop polling thread
|
||||
running = 0;
|
||||
pthread_join(poll_thread, NULL);
|
||||
|
||||
// Clean up pool and subscriptions
|
||||
if (pool) {
|
||||
for (int i = 0; i < subscription_count; i++) {
|
||||
if (subscriptions[i]) {
|
||||
nostr_pool_subscription_close(subscriptions[i]);
|
||||
}
|
||||
}
|
||||
free(subscriptions);
|
||||
nostr_relay_pool_destroy(pool);
|
||||
printf("✅ Pool destroyed\n");
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
nostr_cleanup();
|
||||
close(log_fd);
|
||||
|
||||
printf("👋 Test completed\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* 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,
|
||||
0
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* NIP-03 OpenTimestamps Example
|
||||
*
|
||||
* This example demonstrates the full NIP-03 timestamping workflow:
|
||||
* 1. Create a Nostr event
|
||||
* 2. Submit the event ID to an OpenTimestamps calendar
|
||||
* 3. Poll for proof completion (Bitcoin attestation)
|
||||
* 4. Create a NIP-03 proof event (kind 1040)
|
||||
* 5. Verify the proof
|
||||
*
|
||||
* Usage:
|
||||
* ./timestamping # Create and timestamp a test event
|
||||
* ./timestamping <event_id> # Timestamp an existing event ID
|
||||
* ./timestamping --verify <ots_b64> # Verify a proof
|
||||
*
|
||||
* Note: Bitcoin attestations typically take 1-12 hours to appear.
|
||||
* The OpenTimestamps calendars commit to the Bitcoin blockchain
|
||||
* periodically, so you may need to wait and poll for completion.
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "nostr_core/nostr_core.h"
|
||||
#include "nostr_core/nip003.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#define DEFAULT_CALENDAR_URL "https://alice.btc.calendar.opentimestamps.org"
|
||||
#define POLL_INTERVAL_SEC 300 /* 5 minutes */
|
||||
#define MAX_POLL_ATTEMPTS 144 /* 12 hours total */
|
||||
|
||||
static void print_usage(const char* prog_name) {
|
||||
printf("NIP-03 OpenTimestamps Example\n");
|
||||
printf("\n");
|
||||
printf("Usage:\n");
|
||||
printf(" %s Create and timestamp a test event\n", prog_name);
|
||||
printf(" %s <event_id> Timestamp an existing event ID (64-char hex)\n", prog_name);
|
||||
printf(" %s --verify <ots_b64> Verify a base64-encoded OTS proof\n", prog_name);
|
||||
printf(" %s --check <ots_b64> Check if a proof is complete\n", prog_name);
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" --calendar <url> Use a specific OTS calendar URL\n");
|
||||
printf(" --interval <seconds> Poll interval in seconds (default: %d)\n", POLL_INTERVAL_SEC);
|
||||
printf(" --max-attempts <n> Max poll attempts (default: %d)\n", MAX_POLL_ATTEMPTS);
|
||||
printf("\n");
|
||||
printf("Common calendar URLs:\n");
|
||||
printf(" https://alice.btc.calendar.opentimestamps.org\n");
|
||||
printf(" https://bob.btc.calendar.opentimestamps.org\n");
|
||||
printf(" https://finney.calendar.eternitywall.com\n");
|
||||
}
|
||||
|
||||
static void print_attestation_info(const char* ots_b64) {
|
||||
int has_bitcoin = 0, has_litecoin = 0, has_pending = 0;
|
||||
uint64_t btc_height = 0, ltc_height = 0;
|
||||
char* pending_uri = NULL;
|
||||
|
||||
if (nostr_nip03_get_attestation_info(ots_b64, &has_bitcoin, &has_litecoin, &has_pending,
|
||||
&btc_height, <c_height, &pending_uri) != 0) {
|
||||
printf(" ⚠️ Failed to parse attestation info\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (has_bitcoin) {
|
||||
printf(" ✅ Bitcoin attestation: block height %llu\n", (unsigned long long)btc_height);
|
||||
}
|
||||
if (has_litecoin) {
|
||||
printf(" ✅ Litecoin attestation: block height %llu\n", (unsigned long long)ltc_height);
|
||||
}
|
||||
if (has_pending) {
|
||||
if (pending_uri) {
|
||||
printf(" ⏳ Pending at: %s\n", pending_uri);
|
||||
free(pending_uri);
|
||||
}
|
||||
}
|
||||
if (!has_bitcoin && !has_litecoin && !has_pending) {
|
||||
printf(" ⚠️ No attestations found\n");
|
||||
}
|
||||
}
|
||||
|
||||
static int do_timestamp(const char* event_id_hex, const char* calendar_url,
|
||||
int poll_interval, int max_attempts) {
|
||||
printf("📅 NIP-03 Timestamping\n");
|
||||
printf(" Event ID: %s\n", event_id_hex);
|
||||
printf(" Calendar: %s\n", calendar_url);
|
||||
printf("\n");
|
||||
|
||||
/* Step 1: Submit to calendar */
|
||||
printf("📤 Submitting to OpenTimestamps calendar...\n");
|
||||
char* ots_b64 = nostr_nip03_request_timestamp(event_id_hex, calendar_url, 30);
|
||||
if (!ots_b64) {
|
||||
printf("❌ Failed to submit to calendar.\n");
|
||||
printf(" Check your internet connection and the calendar URL.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("✅ Initial proof received.\n");
|
||||
print_attestation_info(ots_b64);
|
||||
printf("\n");
|
||||
|
||||
/* Step 2: Poll for completion */
|
||||
printf("⏳ Polling for Bitcoin attestation (every %d seconds, max %d attempts)...\n",
|
||||
poll_interval, max_attempts);
|
||||
|
||||
int complete = 0;
|
||||
int attempts = 0;
|
||||
|
||||
while (!complete && attempts < max_attempts) {
|
||||
attempts++;
|
||||
complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
|
||||
time_t now = time(NULL);
|
||||
char time_str[26];
|
||||
ctime_r(&now, time_str);
|
||||
time_str[24] = '\0';
|
||||
|
||||
printf("[%s] Attempt %d/%d: %s\n", time_str, attempts, max_attempts,
|
||||
complete ? "✅ COMPLETE" : "⏳ PENDING");
|
||||
|
||||
if (!complete) {
|
||||
sleep(poll_interval);
|
||||
|
||||
/* Try to upgrade the proof */
|
||||
char* upgraded = nostr_nip03_upgrade_proof(ots_b64, calendar_url, 30);
|
||||
if (upgraded) {
|
||||
free(ots_b64);
|
||||
ots_b64 = upgraded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (complete) {
|
||||
printf("\n🎉 SUCCESS! The proof is now complete with a Bitcoin attestation.\n");
|
||||
print_attestation_info(ots_b64);
|
||||
|
||||
/* Verify the proof */
|
||||
uint64_t block_height = 0;
|
||||
int verify_rc = nostr_nip03_verify_proof(ots_b64, event_id_hex, &block_height, NULL);
|
||||
if (verify_rc == 0) {
|
||||
printf(" ✅ Proof verified: digest matches, Bitcoin height=%llu\n",
|
||||
(unsigned long long)block_height);
|
||||
} else {
|
||||
printf(" ⚠️ Proof verification returned: %d\n", verify_rc);
|
||||
}
|
||||
|
||||
/* Create the NIP-03 proof event */
|
||||
unsigned char private_key[32];
|
||||
unsigned char public_key[32];
|
||||
nostr_generate_keypair(private_key, public_key);
|
||||
|
||||
cJSON* proof_event = nostr_nip03_create_proof_event(event_id_hex, 1, ots_b64, NULL, private_key);
|
||||
if (proof_event) {
|
||||
char* json = cJSON_Print(proof_event);
|
||||
printf("\n📄 NIP-03 Proof Event (kind 1040):\n%s\n", json);
|
||||
free(json);
|
||||
cJSON_Delete(proof_event);
|
||||
}
|
||||
|
||||
/* Print the base64 OTS data for storage */
|
||||
printf("\n📦 OTS proof (base64, save this):\n%s\n", ots_b64);
|
||||
} else {
|
||||
printf("\n⏳ Proof is still pending after %d attempts.\n", attempts);
|
||||
printf(" Bitcoin attestations can take several hours.\n");
|
||||
printf(" Save the OTS proof and check again later:\n%s\n", ots_b64);
|
||||
}
|
||||
|
||||
free(ots_b64);
|
||||
return complete ? 0 : 2;
|
||||
}
|
||||
|
||||
static int do_verify(const char* ots_b64, const char* target_digest_hex) {
|
||||
printf("🔍 Verifying OTS proof...\n\n");
|
||||
|
||||
uint64_t block_height = 0;
|
||||
time_t block_time = 0;
|
||||
|
||||
int rc = nostr_nip03_verify_proof(ots_b64, target_digest_hex, &block_height, &block_time);
|
||||
|
||||
switch (rc) {
|
||||
case 0:
|
||||
printf("✅ Proof verified!\n");
|
||||
printf(" Bitcoin block height: %llu\n", (unsigned long long)block_height);
|
||||
print_attestation_info(ots_b64);
|
||||
return 0;
|
||||
case 1:
|
||||
printf("⏳ Proof is valid but has no Bitcoin attestation (still pending).\n");
|
||||
print_attestation_info(ots_b64);
|
||||
return 1;
|
||||
case -1:
|
||||
printf("❌ Failed to parse OTS proof.\n");
|
||||
return 1;
|
||||
case -2:
|
||||
printf("❌ Digest mismatch! The proof does not match the expected digest.\n");
|
||||
return 1;
|
||||
default:
|
||||
printf("❌ Unknown error: %d\n", rc);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int do_check(const char* ots_b64) {
|
||||
printf("🔎 Checking OTS proof status...\n\n");
|
||||
|
||||
int complete = nostr_nip03_is_proof_complete(ots_b64);
|
||||
|
||||
if (complete == 1) {
|
||||
printf("✅ Proof is COMPLETE (has Bitcoin/Litecoin attestation)\n");
|
||||
} else if (complete == 0) {
|
||||
printf("⏳ Proof is PENDING (no Bitcoin attestation yet)\n");
|
||||
} else {
|
||||
printf("❌ Failed to parse proof (error)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
print_attestation_info(ots_b64);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
fprintf(stderr, "Failed to initialize NOSTR library\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* calendar_url = DEFAULT_CALENDAR_URL;
|
||||
int poll_interval = POLL_INTERVAL_SEC;
|
||||
int max_attempts = MAX_POLL_ATTEMPTS;
|
||||
const char* event_id = NULL;
|
||||
const char* ots_b64 = NULL;
|
||||
const char* verify_digest = NULL;
|
||||
|
||||
enum { MODE_TIMESTAMP, MODE_VERIFY, MODE_CHECK } mode = MODE_TIMESTAMP;
|
||||
|
||||
/* Parse arguments */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
|
||||
print_usage(argv[0]);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "--verify") == 0) {
|
||||
mode = MODE_VERIFY;
|
||||
/* OTS data is provided as a positional argument */
|
||||
} else if (strcmp(argv[i], "--check") == 0) {
|
||||
mode = MODE_CHECK;
|
||||
/* OTS data is provided as a positional argument */
|
||||
} else if (strcmp(argv[i], "--calendar") == 0 && i + 1 < argc) {
|
||||
calendar_url = argv[++i];
|
||||
} else if (strcmp(argv[i], "--interval") == 0 && i + 1 < argc) {
|
||||
poll_interval = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--max-attempts") == 0 && i + 1 < argc) {
|
||||
max_attempts = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--digest") == 0 && i + 1 < argc) {
|
||||
verify_digest = argv[++i];
|
||||
} else if (argv[i][0] != '-' && !event_id && !ots_b64) {
|
||||
if (mode == MODE_VERIFY || mode == MODE_CHECK) {
|
||||
if (!ots_b64) ots_b64 = argv[i];
|
||||
} else {
|
||||
event_id = argv[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
|
||||
switch (mode) {
|
||||
case MODE_TIMESTAMP:
|
||||
if (!event_id) {
|
||||
/* Create a test event */
|
||||
printf("🧪 Creating a test event to timestamp...\n");
|
||||
unsigned char private_key[32];
|
||||
unsigned char public_key[32];
|
||||
nostr_generate_keypair(private_key, public_key);
|
||||
|
||||
char content[128];
|
||||
snprintf(content, sizeof(content), "NIP-03 timestamp test at %ld", time(NULL));
|
||||
|
||||
cJSON* event = nostr_create_and_sign_event(1, content, NULL, private_key, 0);
|
||||
if (!event) {
|
||||
printf("❌ Failed to create test event\n");
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
cJSON* id_item = cJSON_GetObjectItem(event, "id");
|
||||
if (!id_item || !cJSON_IsString(id_item)) {
|
||||
printf("❌ Failed to get event ID\n");
|
||||
cJSON_Delete(event);
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
char* event_json = cJSON_Print(event);
|
||||
printf("📄 Test event:\n%s\n\n", event_json);
|
||||
free(event_json);
|
||||
|
||||
rc = do_timestamp(id_item->valuestring, calendar_url, poll_interval, max_attempts);
|
||||
cJSON_Delete(event);
|
||||
} else {
|
||||
rc = do_timestamp(event_id, calendar_url, poll_interval, max_attempts);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_VERIFY:
|
||||
if (!ots_b64) {
|
||||
printf("❌ Missing OTS proof (base64)\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_verify(ots_b64, verify_digest);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_CHECK:
|
||||
if (!ots_b64) {
|
||||
printf("❌ Missing OTS proof (base64)\n");
|
||||
print_usage(argv[0]);
|
||||
rc = 1;
|
||||
} else {
|
||||
rc = do_check(ots_b64);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
nostr_cleanup();
|
||||
return rc;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
+183
@@ -0,0 +1,183 @@
|
||||
#!/bin/bash
|
||||
|
||||
# increment_and_push.sh - Version increment and git automation script
|
||||
# Usage:
|
||||
# ./increment_and_push.sh "meaningful git comment"
|
||||
# ./increment_and_push.sh --set-version vX.Y.Z "meaningful git comment"
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Color constants
|
||||
RED='\033[31m'
|
||||
GREEN='\033[32m'
|
||||
YELLOW='\033[33m'
|
||||
BLUE='\033[34m'
|
||||
BOLD='\033[1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
# Function to print output with colors
|
||||
print_info() {
|
||||
if [ "$USE_COLORS" = true ]; then
|
||||
echo -e "${BLUE}[INFO]${RESET} $1"
|
||||
else
|
||||
echo "[INFO] $1"
|
||||
fi
|
||||
}
|
||||
|
||||
print_success() {
|
||||
if [ "$USE_COLORS" = true ]; then
|
||||
echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1"
|
||||
else
|
||||
echo "[SUCCESS] $1"
|
||||
fi
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
if [ "$USE_COLORS" = true ]; then
|
||||
echo -e "${YELLOW}[WARNING]${RESET} $1"
|
||||
else
|
||||
echo "[WARNING] $1"
|
||||
fi
|
||||
}
|
||||
|
||||
print_error() {
|
||||
if [ "$USE_COLORS" = true ]; then
|
||||
echo -e "${RED}${BOLD}[ERROR]${RESET} $1"
|
||||
else
|
||||
echo "[ERROR] $1"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if we're in the correct directory
|
||||
CURRENT_DIR=$(basename "$(pwd)")
|
||||
if [ "$CURRENT_DIR" != "nostr_core_lib" ]; then
|
||||
print_error "Script must be run from the nostr_core_lib directory"
|
||||
echo ""
|
||||
echo "Current directory: $CURRENT_DIR"
|
||||
echo "Expected directory: nostr_core_lib"
|
||||
echo ""
|
||||
echo "Please change to the nostr_core_lib directory first."
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if git repository exists
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_error "Not a git repository. Please initialize git first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_VERSION=""
|
||||
COMMIT_MESSAGE=""
|
||||
|
||||
# Parse arguments
|
||||
if [ "$1" = "--set-version" ]; then
|
||||
if [ $# -lt 3 ]; then
|
||||
print_error "Usage: $0 --set-version vX.Y.Z \"meaningful git comment\""
|
||||
echo ""
|
||||
echo "Example: $0 --set-version v0.6.0 \"Release v0.6.0 with ESP32 embedded support\""
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
TARGET_VERSION="$2"
|
||||
COMMIT_MESSAGE="$3"
|
||||
else
|
||||
if [ $# -eq 0 ]; then
|
||||
print_error "Usage: $0 \"meaningful git comment\""
|
||||
echo ""
|
||||
echo "Example: $0 \"Add enhanced subscription functionality\""
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
COMMIT_MESSAGE="$1"
|
||||
fi
|
||||
|
||||
# Check if nostr_core.h exists
|
||||
if [ ! -f "nostr_core/nostr_core.h" ]; then
|
||||
print_error "nostr_core/nostr_core.h not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Starting version increment and push process..."
|
||||
|
||||
# Extract current version from nostr_core.h
|
||||
CURRENT_VERSION=$(grep '#define VERSION ' nostr_core/nostr_core.h | cut -d'"' -f2)
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
print_error "Could not find VERSION define in nostr_core.h"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version components
|
||||
VERSION_MAJOR=$(grep '#define VERSION_MAJOR ' nostr_core/nostr_core.h | awk '{print $3}')
|
||||
VERSION_MINOR=$(grep '#define VERSION_MINOR ' nostr_core/nostr_core.h | awk '{print $3}')
|
||||
VERSION_PATCH=$(grep '#define VERSION_PATCH ' nostr_core/nostr_core.h | awk '{print $3}')
|
||||
|
||||
if [ -z "$VERSION_MAJOR" ] || [ -z "$VERSION_MINOR" ] || [ -z "$VERSION_PATCH" ]; then
|
||||
print_error "Could not extract version components from nostr_core.h"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Current version: $CURRENT_VERSION (Major: $VERSION_MAJOR, Minor: $VERSION_MINOR, Patch: $VERSION_PATCH)"
|
||||
|
||||
if [ -n "$TARGET_VERSION" ]; then
|
||||
if [[ ! "$TARGET_VERSION" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
print_error "Invalid target version format: $TARGET_VERSION (expected vX.Y.Z)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW_VERSION="$TARGET_VERSION"
|
||||
NEW_MAJOR="${BASH_REMATCH[1]}"
|
||||
NEW_MINOR="${BASH_REMATCH[2]}"
|
||||
NEW_PATCH="${BASH_REMATCH[3]}"
|
||||
else
|
||||
# Increment patch version
|
||||
NEW_MAJOR="$VERSION_MAJOR"
|
||||
NEW_MINOR="$VERSION_MINOR"
|
||||
NEW_PATCH=$((VERSION_PATCH + 1))
|
||||
NEW_VERSION="v$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH"
|
||||
fi
|
||||
|
||||
print_info "New version will be: $NEW_VERSION"
|
||||
|
||||
# Update version in nostr_core.h
|
||||
sed -i "s/#define VERSION .*/#define VERSION \"$NEW_VERSION\"/" nostr_core/nostr_core.h
|
||||
sed -i "s/#define VERSION_MAJOR .*/#define VERSION_MAJOR $NEW_MAJOR/" nostr_core/nostr_core.h
|
||||
sed -i "s/#define VERSION_MINOR .*/#define VERSION_MINOR $NEW_MINOR/" nostr_core/nostr_core.h
|
||||
sed -i "s/#define VERSION_PATCH .*/#define VERSION_PATCH $NEW_PATCH/" nostr_core/nostr_core.h
|
||||
|
||||
print_success "Updated version in nostr_core.h"
|
||||
|
||||
# Check if VERSION file exists and update it
|
||||
if [ -f "VERSION" ]; then
|
||||
echo "$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH" > VERSION
|
||||
print_success "Updated VERSION file"
|
||||
fi
|
||||
|
||||
# Check git status
|
||||
if ! git diff --quiet; then
|
||||
print_info "Adding changes to git..."
|
||||
git add .
|
||||
|
||||
print_info "Committing changes..."
|
||||
git commit -m "$COMMIT_MESSAGE"
|
||||
|
||||
print_success "Changes committed"
|
||||
else
|
||||
print_warning "No changes to commit"
|
||||
fi
|
||||
|
||||
# Create and push git tag
|
||||
print_info "Creating git tag: $NEW_VERSION"
|
||||
git tag "$NEW_VERSION"
|
||||
|
||||
print_info "Pushing commits and tags..."
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
git push origin "$CURRENT_BRANCH"
|
||||
git push origin "$NEW_VERSION"
|
||||
|
||||
print_success "Version $NEW_VERSION successfully released!"
|
||||
print_info "Git commit: $COMMIT_MESSAGE"
|
||||
print_info "Tag: $NEW_VERSION"
|
||||
|
||||
echo ""
|
||||
echo "🎉 Release complete! Version $NEW_VERSION is now live."
|
||||
@@ -0,0 +1,481 @@
|
||||
# NSIGNER Integration in `nostr_core_lib`
|
||||
|
||||
This document is the developer-facing integration contract for signer abstraction + nsigner client support in `nostr_core_lib`.
|
||||
|
||||
For the concise overview, see the signer section in `README.md`.
|
||||
For implementation planning context, see `plans/nsigner_integration_plan.md`.
|
||||
|
||||
## 1) Signer lifecycle and core verbs (exact signatures)
|
||||
|
||||
From `nostr_core/nostr_signer.h`:
|
||||
|
||||
```c
|
||||
typedef struct nostr_signer nostr_signer_t;
|
||||
|
||||
/* Lifecycle */
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]);
|
||||
void nostr_signer_free(nostr_signer_t* signer);
|
||||
|
||||
/* Core verbs */
|
||||
int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]);
|
||||
int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out);
|
||||
|
||||
/* Encryption verbs (for parity with upcoming remote backends) */
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
```
|
||||
|
||||
## 2) Remote signer factories + transport API (exact signatures)
|
||||
|
||||
### 2.1 Signer-side remote factories (`nostr_core/nostr_signer.h`)
|
||||
|
||||
```c
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms);
|
||||
/* TCP mode requires auth envelope per n_signer; set this before making calls. */
|
||||
int nostr_signer_nsigner_set_auth(nostr_signer_t* signer,
|
||||
const unsigned char auth_privkey[32],
|
||||
const char* label);
|
||||
#endif
|
||||
```
|
||||
|
||||
### 2.2 Transport constructors + framed I/O + discovery (`nostr_core/nsigner_transport.h`)
|
||||
|
||||
```c
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
typedef struct nsigner_transport nsigner_transport_t;
|
||||
|
||||
struct nsigner_transport {
|
||||
int (*send_framed)(nsigner_transport_t* t, const char* json, size_t len);
|
||||
int (*recv_framed)(nsigner_transport_t* t, char** out_json, size_t* out_len); /* caller frees *out_json */
|
||||
void (*close)(nsigner_transport_t* t);
|
||||
void* ctx;
|
||||
};
|
||||
|
||||
/* Raw framed I/O helpers for existing connected file descriptors. */
|
||||
int nsigner_transport_send_framed_fd(int fd, const char* json, size_t len);
|
||||
int nsigner_transport_recv_framed_fd(int fd, char** out_json, size_t* out_len);
|
||||
|
||||
/* UNIX abstract socket transport (socket_name without leading '@'). */
|
||||
nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms);
|
||||
|
||||
/* TCP transport (host + port), with IPv4/IPv6 resolution via getaddrinfo. */
|
||||
nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms);
|
||||
|
||||
/* USB CDC-ACM serial transport (device path like /dev/ttyACM0). */
|
||||
nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms);
|
||||
|
||||
/*
|
||||
* Framed transport over existing file descriptors (read_fd, write_fd).
|
||||
* close() closes both owned fds. If passing STDIN_FILENO/STDOUT_FILENO and you do not
|
||||
* want them closed, pass dup()'d descriptors instead.
|
||||
*/
|
||||
nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms);
|
||||
|
||||
/* Enumerate /proc/net/unix abstract sockets beginning with @nsigner, returns count copied. */
|
||||
int nsigner_transport_list_unix(char names[][64], int max_names);
|
||||
|
||||
/* Best-effort enumerate serial devices (e.g. /dev/ttyACM*), returns count copied. */
|
||||
int nsigner_transport_list_serial(char paths[][64], int max_paths);
|
||||
#endif
|
||||
```
|
||||
|
||||
## 3) Client API + auth contract
|
||||
|
||||
From `nostr_core/nsigner_client.h`:
|
||||
|
||||
```c
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
typedef struct nsigner_client nsigner_client_t;
|
||||
|
||||
nsigner_client_t* nsigner_client_new(nsigner_transport_t* transport); /* takes ownership of transport */
|
||||
void nsigner_client_free(nsigner_client_t* client);
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t* client, const unsigned char privkey[32], const char* label);
|
||||
|
||||
int nsigner_client_call(nsigner_client_t* client,
|
||||
const char* method,
|
||||
cJSON* params,
|
||||
cJSON** out_result);
|
||||
|
||||
const char* nsigner_client_last_error(const nsigner_client_t* client);
|
||||
int nsigner_client_compute_body_hash_hex(const cJSON* params, char out_hash_hex[65]);
|
||||
#endif
|
||||
```
|
||||
|
||||
### n_signer wire methods
|
||||
|
||||
The remote signer backend uses n_signer's role-based Nostr methods:
|
||||
|
||||
- `nostr_get_public_key`
|
||||
- `nostr_sign_event`
|
||||
- `nostr_nip04_encrypt` / `nostr_nip04_decrypt`
|
||||
- `nostr_nip44_encrypt` / `nostr_nip44_decrypt`
|
||||
|
||||
These wire names are distinct from both the public `nostr_signer_*` C API and the
|
||||
unprefixed NIP-46 method names. In particular, n_signer's unprefixed `get_public_key`
|
||||
is algorithm-based; this library uses `nostr_get_public_key` because remote signer
|
||||
factories select keys by `role` or `nostr_index`.
|
||||
|
||||
### Auth and framing notes
|
||||
|
||||
- Framing is 4-byte big-endian payload length + JSON payload.
|
||||
- Valid payload lengths are `1..65536` bytes (`NSIGNER_CLIENT_MAX_FRAME` in transport implementation).
|
||||
- TCP requires auth (`nostr_signer_nsigner_set_auth`), unix auth is optional.
|
||||
- Auth envelope is a kind-27235 event containing tags:
|
||||
- `nsigner_rpc`
|
||||
- `nsigner_method`
|
||||
- `nsigner_body_hash`
|
||||
- `nsigner_body_hash` is SHA-256 of compact params JSON, or SHA-256 of literal `"null"` when params are null (`nsigner_client_compute_body_hash_hex`).
|
||||
|
||||
## 4) Signer-aware higher-level APIs (`*_with_signer`)
|
||||
|
||||
Below is the full list currently present in headers.
|
||||
|
||||
### NIP-01 (`nostr_core/nip001.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp);
|
||||
```
|
||||
|
||||
### NIP-17 (`nostr_core/nip017.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls,
|
||||
int num_relays,
|
||||
nostr_signer_t* signer);
|
||||
int nostr_nip17_send_dm_with_signer(cJSON* dm_event,
|
||||
const char** recipient_pubkeys,
|
||||
int num_recipients,
|
||||
nostr_signer_t* signer,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec);
|
||||
cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap,
|
||||
nostr_signer_t* signer);
|
||||
```
|
||||
|
||||
### NIP-42 (`nostr_core/nip042.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge,
|
||||
const char* relay_url,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
```
|
||||
|
||||
### NIP-46 (`nostr_core/nip046.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
int nostr_nip46_decrypt_event_with_signer(cJSON* event,
|
||||
nostr_signer_t* signer,
|
||||
char** output_out);
|
||||
int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session,
|
||||
nostr_signer_t* signer,
|
||||
const char* bunker_url);
|
||||
```
|
||||
|
||||
### NIP-60 (`nostr_core/nip060.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
```
|
||||
|
||||
### NIP-61 (`nostr_core/nip061.h`)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
```
|
||||
|
||||
### NIP-59 (`nostr_core/nip059.h`) (additional signer coverage present)
|
||||
|
||||
```c
|
||||
cJSON* nostr_nip59_create_seal_with_signer(cJSON* rumor, nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key, long max_delay_sec);
|
||||
cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* signer);
|
||||
cJSON* nostr_nip59_unseal_rumor_with_signer(cJSON* seal, const unsigned char* sender_public_key,
|
||||
nostr_signer_t* signer);
|
||||
```
|
||||
|
||||
### Blossom client (`nostr_core/blossom_client.h`)
|
||||
|
||||
```c
|
||||
char* blossom_create_auth_header_with_signer(nostr_signer_t* signer,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds,
|
||||
time_t timestamp);
|
||||
int blossom_upload_with_signer(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
int blossom_upload_file_with_signer(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
int blossom_delete_with_signer(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
nostr_signer_t* signer,
|
||||
int timeout_seconds);
|
||||
```
|
||||
|
||||
### Relay pool signer entry point (`nostr_core/nostr_core.h`)
|
||||
|
||||
```c
|
||||
int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, nostr_signer_t* signer, int enable);
|
||||
```
|
||||
|
||||
## 5) Feasibility boundaries
|
||||
|
||||
Remotable through `nostr_signer_t` / nsigner client contract:
|
||||
|
||||
- Public key retrieval
|
||||
- Event signing
|
||||
- NIP-04 encrypt/decrypt
|
||||
- NIP-44 encrypt/decrypt
|
||||
|
||||
Not generally remotable without protocol/API redesign:
|
||||
|
||||
- Operations that require direct access to raw secret material for non-signer-verb cryptography.
|
||||
- In this codebase, Cashu mint-protocol-oriented pieces (e.g. in `cashu_mint`) are the identified example.
|
||||
|
||||
## 6) Minimal C usage snippets
|
||||
|
||||
### (a) Local signer create + sign + publish
|
||||
|
||||
```c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
int main(void) {
|
||||
unsigned char privkey[32];
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
const char* relays[] = {"wss://relay.laantungir.net"};
|
||||
int ok_count = 0;
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) return 1;
|
||||
|
||||
/* fill privkey via your key-management path */
|
||||
signer = nostr_signer_local(privkey);
|
||||
if (!signer) return 1;
|
||||
|
||||
evt = nostr_create_and_sign_event_with_signer(1, "hello", NULL, signer, time(NULL));
|
||||
if (!evt) return 1;
|
||||
|
||||
publish_result_t* r = synchronous_publish_event_with_progress(
|
||||
relays, 1, evt, &ok_count, 10, NULL, NULL, 0, NULL);
|
||||
|
||||
free(r);
|
||||
cJSON_Delete(evt);
|
||||
nostr_signer_free(signer);
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### (b) Remote unix signer create + sign
|
||||
|
||||
```c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
int main(void) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (nostr_init() != NOSTR_SUCCESS) return 1;
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_unix("nsigner-main", NULL, 15000);
|
||||
if (!signer) return 1;
|
||||
|
||||
evt = nostr_create_and_sign_event_with_signer(1, "signed remotely", NULL, signer, time(NULL));
|
||||
if (!evt) return 1;
|
||||
|
||||
cJSON_Delete(evt);
|
||||
nostr_signer_free(signer);
|
||||
#endif
|
||||
|
||||
nostr_cleanup();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### (c) TCP signer + auth setup
|
||||
|
||||
```c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
int main(void) {
|
||||
nostr_signer_t* signer;
|
||||
unsigned char caller_auth_key[32];
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
signer = nostr_signer_nsigner_tcp("127.0.0.1", 7777, NULL, 15000);
|
||||
if (!signer) return 1;
|
||||
|
||||
/* Required for TCP mode */
|
||||
if (nostr_signer_nsigner_set_auth(signer, caller_auth_key, "my-caller") != NOSTR_SUCCESS) {
|
||||
nostr_signer_free(signer);
|
||||
return 1;
|
||||
}
|
||||
|
||||
nostr_signer_free(signer);
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## 7) Build/flag wiring summary (exact current lines)
|
||||
|
||||
### `build.sh`
|
||||
|
||||
```bash
|
||||
SOURCES="$SOURCES nostr_core/nsigner_transport.c"
|
||||
SOURCES="$SOURCES nostr_core/nsigner_client.c"
|
||||
CFLAGS="$CFLAGS -DNOSTR_ENABLE_NSIGNER_CLIENT=1"
|
||||
```
|
||||
|
||||
### `CMakeLists.txt`
|
||||
|
||||
Desktop branch (`else()`):
|
||||
|
||||
```cmake
|
||||
add_library(nostr_core STATIC
|
||||
nostr_core/nostr_common.c
|
||||
nostr_core/nostr_log.c
|
||||
nostr_core/request_validator.c
|
||||
nostr_core/nip001.c
|
||||
nostr_core/nip006.c
|
||||
nostr_core/nip019.c
|
||||
nostr_core/nostr_signer.c
|
||||
nostr_core/nsigner_transport.c
|
||||
nostr_core/nsigner_client.c
|
||||
nostr_core/utils.c
|
||||
nostr_core/crypto/nostr_secp256k1.c
|
||||
nostr_core/crypto/nostr_aes.c
|
||||
nostr_core/crypto/nostr_chacha20.c
|
||||
cjson/cJSON.c
|
||||
platform/linux.c
|
||||
)
|
||||
|
||||
target_compile_definitions(nostr_core PRIVATE NOSTR_ENABLE_NSIGNER_CLIENT=1)
|
||||
```
|
||||
|
||||
ESP32 branch (`if(ESP_PLATFORM)`):
|
||||
|
||||
- Does **not** include `nostr_core/nsigner_transport.c` or `nostr_core/nsigner_client.c` in `idf_component_register(SRCS ...)`.
|
||||
- Sets only:
|
||||
|
||||
```cmake
|
||||
target_compile_definitions(${COMPONENT_LIB} PRIVATE NOSTR_NO_FILESYSTEM=1)
|
||||
```
|
||||
|
||||
## 8) Testing pointers
|
||||
|
||||
Relevant tests:
|
||||
|
||||
- `tests/nsigner_client_test.c`
|
||||
- `tests/signer_modules_test.c`
|
||||
- `tests/nip01_test.c` (includes local-signer equivalence checks)
|
||||
|
||||
Build tests:
|
||||
|
||||
```bash
|
||||
./build.sh -t
|
||||
```
|
||||
|
||||
Run target binaries:
|
||||
|
||||
```bash
|
||||
./tests/nsigner_client_test
|
||||
./tests/signer_modules_test
|
||||
./tests/nip01_test
|
||||
```
|
||||
|
||||
## 9) Driver app signer modes (`examples/note_poster.c`)
|
||||
|
||||
Supported `--signer` values in the driver app:
|
||||
|
||||
- `local` (default)
|
||||
- `unix:<name>`
|
||||
- `serial:<path>`
|
||||
- `tcp:<host>:<port>`
|
||||
- `fds:<read_fd>:<write_fd>`
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
./examples/note_poster
|
||||
./examples/note_poster --signer unix:<name>
|
||||
./examples/note_poster --signer serial:/dev/ttyACM0
|
||||
./examples/note_poster --signer tcp:<host>:<port>
|
||||
./examples/note_poster --signer fds:<read_fd>:<write_fd>
|
||||
```
|
||||
|
||||
Local mode warning (from app behavior): this is test-key-only usage; do not use production keys.
|
||||
@@ -0,0 +1,642 @@
|
||||
/*
|
||||
* Blossom HTTP Client
|
||||
*/
|
||||
|
||||
#include "blossom_client.h"
|
||||
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nip001.h"
|
||||
#include "nostr_http.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
static int is_hex64_local(const char* s) {
|
||||
if (!s || strlen(s) != 64) return 0;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
char c = s[i];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void trim_trailing_slash_local(const char* in, char* out, size_t out_sz) {
|
||||
if (!in || !out || out_sz == 0) return;
|
||||
snprintf(out, out_sz, "%s", in);
|
||||
size_t n = strlen(out);
|
||||
while (n > 0 && out[n - 1] == '/') {
|
||||
out[n - 1] = '\0';
|
||||
n--;
|
||||
}
|
||||
}
|
||||
|
||||
static int build_blob_url_local(const char* server_url, const char* sha256_hex, char* out, size_t out_sz) {
|
||||
if (!server_url || !sha256_hex || !out || out_sz == 0) return -1;
|
||||
char base[512];
|
||||
trim_trailing_slash_local(server_url, base, sizeof(base));
|
||||
int n = snprintf(out, out_sz, "%s/%s", base, sha256_hex);
|
||||
if (n < 0 || (size_t)n >= out_sz) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_descriptor_local(const char* json_text, blossom_blob_descriptor_t* out) {
|
||||
if (!json_text || !out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
cJSON* root = cJSON_Parse(json_text);
|
||||
if (!root) return NOSTR_ERROR_IO_FAILED;
|
||||
|
||||
cJSON* sha = cJSON_GetObjectItemCaseSensitive(root, "sha256");
|
||||
if (!sha) sha = cJSON_GetObjectItemCaseSensitive(root, "x");
|
||||
cJSON* url = cJSON_GetObjectItemCaseSensitive(root, "url");
|
||||
cJSON* size = cJSON_GetObjectItemCaseSensitive(root, "size");
|
||||
cJSON* ct = cJSON_GetObjectItemCaseSensitive(root, "content_type");
|
||||
if (!ct) ct = cJSON_GetObjectItemCaseSensitive(root, "m");
|
||||
cJSON* created = cJSON_GetObjectItemCaseSensitive(root, "created");
|
||||
if (!created) created = cJSON_GetObjectItemCaseSensitive(root, "created_at");
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
if (sha && cJSON_IsString(sha) && sha->valuestring) {
|
||||
snprintf(out->sha256, sizeof(out->sha256), "%s", sha->valuestring);
|
||||
}
|
||||
if (url && cJSON_IsString(url) && url->valuestring) {
|
||||
snprintf(out->url, sizeof(out->url), "%s", url->valuestring);
|
||||
}
|
||||
if (size && cJSON_IsNumber(size)) out->size = (long)cJSON_GetNumberValue(size);
|
||||
if (ct && cJSON_IsString(ct) && ct->valuestring) {
|
||||
snprintf(out->content_type, sizeof(out->content_type), "%s", ct->valuestring);
|
||||
}
|
||||
if (created && cJSON_IsNumber(created)) out->created = (long)cJSON_GetNumberValue(created);
|
||||
|
||||
cJSON_Delete(root);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void blossom_set_ca_bundle(const char* ca_bundle_path) {
|
||||
nostr_http_set_ca_bundle(ca_bundle_path);
|
||||
}
|
||||
|
||||
char* blossom_create_auth_header_with_signer(nostr_signer_t* signer,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds,
|
||||
time_t timestamp) {
|
||||
if (!signer || !operation || operation[0] == '\0') return NULL;
|
||||
if (sha256_hex && !is_hex64_local(sha256_hex)) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
cJSON* t_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(t_tag, cJSON_CreateString("t"));
|
||||
cJSON_AddItemToArray(t_tag, cJSON_CreateString(operation));
|
||||
cJSON_AddItemToArray(tags, t_tag);
|
||||
|
||||
if (sha256_hex) {
|
||||
cJSON* x_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(x_tag, cJSON_CreateString("x"));
|
||||
cJSON_AddItemToArray(x_tag, cJSON_CreateString(sha256_hex));
|
||||
cJSON_AddItemToArray(tags, x_tag);
|
||||
}
|
||||
|
||||
time_t now = (timestamp > 0) ? timestamp : time(NULL);
|
||||
int exp = expiration_seconds > 0 ? expiration_seconds : 300;
|
||||
long until = (long)now + exp;
|
||||
char exp_buf[32];
|
||||
snprintf(exp_buf, sizeof(exp_buf), "%ld", until);
|
||||
cJSON* e_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString("expiration"));
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString(exp_buf));
|
||||
cJSON_AddItemToArray(tags, e_tag);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(24242, "", tags, signer, now);
|
||||
cJSON_Delete(tags);
|
||||
if (!evt) return NULL;
|
||||
|
||||
char* evt_json = cJSON_PrintUnformatted(evt);
|
||||
cJSON_Delete(evt);
|
||||
if (!evt_json) return NULL;
|
||||
|
||||
size_t evt_len = strlen(evt_json);
|
||||
size_t b64_cap = ((evt_len + 2U) / 3U) * 4U + 8U;
|
||||
char* b64 = (char*)malloc(b64_cap);
|
||||
if (!b64) {
|
||||
free(evt_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t n = base64_encode((const unsigned char*)evt_json, evt_len, b64, b64_cap);
|
||||
free(evt_json);
|
||||
if (n == 0) {
|
||||
free(b64);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t hdr_cap = n + 16U;
|
||||
char* header = (char*)malloc(hdr_cap);
|
||||
if (!header) {
|
||||
free(b64);
|
||||
return NULL;
|
||||
}
|
||||
snprintf(header, hdr_cap, "Nostr %s", b64);
|
||||
free(b64);
|
||||
return header;
|
||||
}
|
||||
|
||||
char* blossom_create_auth_header(const unsigned char* private_key,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds) {
|
||||
if (!private_key || !operation || operation[0] == '\0') return NULL;
|
||||
if (sha256_hex && !is_hex64_local(sha256_hex)) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
cJSON* t_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(t_tag, cJSON_CreateString("t"));
|
||||
cJSON_AddItemToArray(t_tag, cJSON_CreateString(operation));
|
||||
cJSON_AddItemToArray(tags, t_tag);
|
||||
|
||||
if (sha256_hex) {
|
||||
cJSON* x_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(x_tag, cJSON_CreateString("x"));
|
||||
cJSON_AddItemToArray(x_tag, cJSON_CreateString(sha256_hex));
|
||||
cJSON_AddItemToArray(tags, x_tag);
|
||||
}
|
||||
|
||||
int exp = expiration_seconds > 0 ? expiration_seconds : 300;
|
||||
long now = (long)time(NULL);
|
||||
long until = now + exp;
|
||||
char exp_buf[32];
|
||||
snprintf(exp_buf, sizeof(exp_buf), "%ld", until);
|
||||
cJSON* e_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString("expiration"));
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString(exp_buf));
|
||||
cJSON_AddItemToArray(tags, e_tag);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event(24242, "", tags, private_key, (time_t)now);
|
||||
cJSON_Delete(tags);
|
||||
if (!evt) return NULL;
|
||||
|
||||
char* evt_json = cJSON_PrintUnformatted(evt);
|
||||
cJSON_Delete(evt);
|
||||
if (!evt_json) return NULL;
|
||||
|
||||
size_t evt_len = strlen(evt_json);
|
||||
size_t b64_cap = ((evt_len + 2U) / 3U) * 4U + 8U;
|
||||
char* b64 = (char*)malloc(b64_cap);
|
||||
if (!b64) {
|
||||
free(evt_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t n = base64_encode((const unsigned char*)evt_json, evt_len, b64, b64_cap);
|
||||
free(evt_json);
|
||||
if (n == 0) {
|
||||
free(b64);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t hdr_cap = n + 16U;
|
||||
char* header = (char*)malloc(hdr_cap);
|
||||
if (!header) {
|
||||
free(b64);
|
||||
return NULL;
|
||||
}
|
||||
snprintf(header, hdr_cap, "Nostr %s", b64);
|
||||
free(b64);
|
||||
return header;
|
||||
}
|
||||
|
||||
int blossom_upload_with_signer(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
if (!server_url || !data || data_len == 0 || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
char hash_hex[65] = {0};
|
||||
if (sha256_hex) {
|
||||
if (!is_hex64_local(sha256_hex)) return NOSTR_ERROR_INVALID_INPUT;
|
||||
snprintf(hash_hex, sizeof(hash_hex), "%s", sha256_hex);
|
||||
} else {
|
||||
unsigned char hash[32];
|
||||
if (nostr_sha256(data, data_len, hash) != 0) return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
nostr_bytes_to_hex(hash, 32, hash_hex);
|
||||
}
|
||||
|
||||
char url[1024];
|
||||
char base[512];
|
||||
trim_trailing_slash_local(server_url, base, sizeof(base));
|
||||
snprintf(url, sizeof(url), "%s/upload", base);
|
||||
|
||||
char auth_buf[1024] = {0};
|
||||
char ctype_buf[256] = {0};
|
||||
const char* headers[4] = {"Accept: application/json", NULL, NULL, NULL};
|
||||
int h = 1;
|
||||
|
||||
if (content_type && content_type[0] != '\0') {
|
||||
snprintf(ctype_buf, sizeof(ctype_buf), "Content-Type: %s", content_type);
|
||||
headers[h++] = ctype_buf;
|
||||
}
|
||||
|
||||
char* auth = NULL;
|
||||
if (signer) {
|
||||
auth = blossom_create_auth_header_with_signer(signer, "upload", hash_hex, 300, 0);
|
||||
if (auth) {
|
||||
snprintf(auth_buf, sizeof(auth_buf), "Authorization: %s", auth);
|
||||
headers[h++] = auth_buf;
|
||||
}
|
||||
}
|
||||
headers[h] = NULL;
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "PUT";
|
||||
req.url = url;
|
||||
req.headers = headers;
|
||||
req.body = data;
|
||||
req.body_len = data_len;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
free(auth);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
int prc = parse_descriptor_local(resp.body ? resp.body : "{}", descriptor_out);
|
||||
if (prc != NOSTR_SUCCESS) {
|
||||
memset(descriptor_out, 0, sizeof(*descriptor_out));
|
||||
snprintf(descriptor_out->sha256, sizeof(descriptor_out->sha256), "%s", hash_hex);
|
||||
descriptor_out->size = (long)data_len;
|
||||
if (content_type && content_type[0] != '\0') {
|
||||
snprintf(descriptor_out->content_type, sizeof(descriptor_out->content_type), "%s", content_type);
|
||||
}
|
||||
}
|
||||
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int blossom_upload(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
nostr_signer_t* signer = NULL;
|
||||
int rc;
|
||||
|
||||
if (private_key) {
|
||||
signer = nostr_signer_local(private_key);
|
||||
}
|
||||
|
||||
rc = blossom_upload_with_signer(server_url, data, data_len, content_type,
|
||||
signer, sha256_hex, timeout_seconds, descriptor_out);
|
||||
nostr_signer_free(signer);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int blossom_upload_file_with_signer(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
if (!server_url || !file_path || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
FILE* fp = fopen(file_path, "rb");
|
||||
if (!fp) return NOSTR_ERROR_IO_FAILED;
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
long sz = ftell(fp);
|
||||
if (sz < 0) {
|
||||
fclose(fp);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
unsigned char* buf = (unsigned char*)malloc((size_t)sz);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
size_t n = fread(buf, 1, (size_t)sz, fp);
|
||||
fclose(fp);
|
||||
if (n != (size_t)sz) {
|
||||
free(buf);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
int rc = blossom_upload_with_signer(server_url, buf, n, content_type, signer, NULL, timeout_seconds, descriptor_out);
|
||||
free(buf);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int blossom_upload_file(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
nostr_signer_t* signer = NULL;
|
||||
int rc;
|
||||
|
||||
if (private_key) {
|
||||
signer = nostr_signer_local(private_key);
|
||||
}
|
||||
|
||||
rc = blossom_upload_file_with_signer(server_url, file_path, content_type,
|
||||
signer, timeout_seconds, descriptor_out);
|
||||
nostr_signer_free(signer);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int blossom_download(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
unsigned char** body_out,
|
||||
size_t* body_len_out,
|
||||
char* content_type_out,
|
||||
size_t content_type_out_size) {
|
||||
if (!server_url || !sha256_hex || !body_out || !body_len_out || !is_hex64_local(sha256_hex)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*body_out = NULL;
|
||||
*body_len_out = 0;
|
||||
if (content_type_out && content_type_out_size > 0) content_type_out[0] = '\0';
|
||||
|
||||
char url[1024];
|
||||
if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = url;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
|
||||
req.max_response_bytes = max_bytes;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
unsigned char* out = (unsigned char*)malloc(resp.body_len > 0 ? resp.body_len : 1);
|
||||
if (!out) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
if (resp.body_len > 0 && resp.body) memcpy(out, resp.body, resp.body_len);
|
||||
|
||||
*body_out = out;
|
||||
*body_len_out = resp.body_len;
|
||||
if (content_type_out && content_type_out_size > 0 && resp.content_type && resp.content_type[0] != '\0') {
|
||||
snprintf(content_type_out, content_type_out_size, "%s", resp.content_type);
|
||||
}
|
||||
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int blossom_download_to_file(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const char* output_path,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
if (!server_url || !sha256_hex || !output_path || !descriptor_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
unsigned char* body = NULL;
|
||||
size_t body_len = 0;
|
||||
char content_type[128] = {0};
|
||||
int rc = blossom_download(server_url, sha256_hex, timeout_seconds, max_bytes, &body, &body_len, content_type, sizeof(content_type));
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
FILE* fp = fopen(output_path, "wb");
|
||||
if (!fp) {
|
||||
free(body);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
size_t wn = fwrite(body, 1, body_len, fp);
|
||||
fclose(fp);
|
||||
if (wn != body_len) {
|
||||
free(body);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
unsigned char hash[32];
|
||||
if (nostr_sha256(body, body_len, hash) != 0) {
|
||||
free(body);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
free(body);
|
||||
|
||||
char hash_hex[65];
|
||||
nostr_bytes_to_hex(hash, 32, hash_hex);
|
||||
|
||||
memset(descriptor_out, 0, sizeof(*descriptor_out));
|
||||
snprintf(descriptor_out->sha256, sizeof(descriptor_out->sha256), "%s", hash_hex);
|
||||
descriptor_out->size = (long)body_len;
|
||||
if (content_type[0] != '\0') snprintf(descriptor_out->content_type, sizeof(descriptor_out->content_type), "%s", content_type);
|
||||
build_blob_url_local(server_url, sha256_hex, descriptor_out->url, sizeof(descriptor_out->url));
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int blossom_head(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out) {
|
||||
if (!server_url || !sha256_hex || !descriptor_out || !is_hex64_local(sha256_hex)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char url[1024];
|
||||
if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "HEAD";
|
||||
req.url = url;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 10;
|
||||
req.capture_headers = 1;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
memset(descriptor_out, 0, sizeof(*descriptor_out));
|
||||
snprintf(descriptor_out->sha256, sizeof(descriptor_out->sha256), "%s", sha256_hex);
|
||||
if (build_blob_url_local(server_url, sha256_hex, descriptor_out->url, sizeof(descriptor_out->url)) != 0) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
if (resp.content_type && resp.content_type[0] != '\0') {
|
||||
snprintf(descriptor_out->content_type, sizeof(descriptor_out->content_type), "%s", resp.content_type);
|
||||
}
|
||||
|
||||
if (resp.headers_raw) {
|
||||
const char* key = "Content-Length:";
|
||||
char* p = strstr(resp.headers_raw, key);
|
||||
if (p) {
|
||||
p += strlen(key);
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
descriptor_out->size = strtol(p, NULL, 10);
|
||||
}
|
||||
}
|
||||
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int blossom_delete_with_signer(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
nostr_signer_t* signer,
|
||||
int timeout_seconds) {
|
||||
if (!server_url || !sha256_hex || !signer || !is_hex64_local(sha256_hex)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char url[1024];
|
||||
if (build_blob_url_local(server_url, sha256_hex, url, sizeof(url)) != 0) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
char* auth = blossom_create_auth_header_with_signer(signer, "delete", sha256_hex, 300, 0);
|
||||
if (!auth) return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
|
||||
char auth_header[1200];
|
||||
snprintf(auth_header, sizeof(auth_header), "Authorization: %s", auth);
|
||||
free(auth);
|
||||
|
||||
const char* headers[] = {"Accept: application/json", auth_header, NULL};
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "DELETE";
|
||||
req.url = url;
|
||||
req.headers = headers;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
int ok = (resp.status_code >= 200 && resp.status_code < 300) ? NOSTR_SUCCESS : NOSTR_ERROR_NETWORK_FAILED;
|
||||
nostr_http_response_free(&resp);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int blossom_delete(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds) {
|
||||
nostr_signer_t* signer;
|
||||
int rc;
|
||||
|
||||
if (!private_key) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
rc = blossom_delete_with_signer(server_url, sha256_hex, signer, timeout_seconds);
|
||||
nostr_signer_free(signer);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int blossom_list(const char* server_url,
|
||||
const char* pubkey_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t** descriptors_out,
|
||||
int* count_out) {
|
||||
if (!server_url || !pubkey_hex || !descriptors_out || !count_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
*descriptors_out = NULL;
|
||||
*count_out = 0;
|
||||
|
||||
char base[512];
|
||||
trim_trailing_slash_local(server_url, base, sizeof(base));
|
||||
char url[1024];
|
||||
snprintf(url, sizeof(url), "%s/list/%s", base, pubkey_hex);
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = url;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : 20;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
cJSON* arr = cJSON_Parse(resp.body ? resp.body : "[]");
|
||||
if (!arr || !cJSON_IsArray(arr)) {
|
||||
cJSON_Delete(arr);
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(arr);
|
||||
blossom_blob_descriptor_t* out = (blossom_blob_descriptor_t*)calloc((size_t)n, sizeof(blossom_blob_descriptor_t));
|
||||
if (!out && n > 0) {
|
||||
cJSON_Delete(arr);
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* it = cJSON_GetArrayItem(arr, i);
|
||||
char* tmp = cJSON_PrintUnformatted(it);
|
||||
if (tmp) {
|
||||
(void)parse_descriptor_local(tmp, &out[i]);
|
||||
free(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
*descriptors_out = out;
|
||||
*count_out = n;
|
||||
|
||||
cJSON_Delete(arr);
|
||||
nostr_http_response_free(&resp);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Blossom HTTP Client
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_BLOSSOM_CLIENT_H
|
||||
#define NOSTR_BLOSSOM_CLIENT_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char sha256[65];
|
||||
char url[512];
|
||||
long size;
|
||||
char content_type[128];
|
||||
long created;
|
||||
} blossom_blob_descriptor_t;
|
||||
|
||||
void blossom_set_ca_bundle(const char* ca_bundle_path);
|
||||
|
||||
char* blossom_create_auth_header(const unsigned char* private_key,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds);
|
||||
|
||||
char* blossom_create_auth_header_with_signer(nostr_signer_t* signer,
|
||||
const char* operation,
|
||||
const char* sha256_hex,
|
||||
int expiration_seconds,
|
||||
time_t timestamp);
|
||||
|
||||
int blossom_upload(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_upload_with_signer(const char* server_url,
|
||||
const unsigned char* data,
|
||||
size_t data_len,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_upload_file(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_upload_file_with_signer(const char* server_url,
|
||||
const char* file_path,
|
||||
const char* content_type,
|
||||
nostr_signer_t* signer,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_download(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
unsigned char** body_out,
|
||||
size_t* body_len_out,
|
||||
char* content_type_out,
|
||||
size_t content_type_out_size);
|
||||
|
||||
int blossom_download_to_file(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const char* output_path,
|
||||
int timeout_seconds,
|
||||
size_t max_bytes,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_head(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t* descriptor_out);
|
||||
|
||||
int blossom_delete(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
const unsigned char* private_key,
|
||||
int timeout_seconds);
|
||||
|
||||
int blossom_delete_with_signer(const char* server_url,
|
||||
const char* sha256_hex,
|
||||
nostr_signer_t* signer,
|
||||
int timeout_seconds);
|
||||
|
||||
int blossom_list(const char* server_url,
|
||||
const char* pubkey_hex,
|
||||
int timeout_seconds,
|
||||
blossom_blob_descriptor_t** descriptors_out,
|
||||
int* count_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_BLOSSOM_CLIENT_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* Cashu Mint HTTP Client
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_CASHU_MINT_H
|
||||
#define NOSTR_CASHU_MINT_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "nostr_common.h"
|
||||
#include "nip060.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define CASHU_API_VERSION "v1"
|
||||
|
||||
typedef struct {
|
||||
char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
|
||||
char unit[16];
|
||||
int active;
|
||||
} cashu_keyset_t;
|
||||
|
||||
typedef enum {
|
||||
CASHU_TOKEN_FORMAT_A = 0,
|
||||
CASHU_TOKEN_FORMAT_B = 1
|
||||
} cashu_token_format_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t amount;
|
||||
char pubkey[67];
|
||||
} cashu_amount_key_t;
|
||||
|
||||
typedef struct {
|
||||
char keyset_id[65];
|
||||
char unit[16];
|
||||
cashu_amount_key_t* keys;
|
||||
int key_count;
|
||||
} cashu_keyset_keys_t;
|
||||
|
||||
typedef struct {
|
||||
char* name;
|
||||
char* pubkey;
|
||||
char* version;
|
||||
cashu_keyset_t* keysets;
|
||||
int keyset_count;
|
||||
} cashu_mint_info_t;
|
||||
|
||||
typedef struct {
|
||||
char quote_id[128];
|
||||
char payment_request[2048];
|
||||
int paid;
|
||||
uint64_t amount;
|
||||
time_t expiry;
|
||||
} cashu_mint_quote_t;
|
||||
|
||||
typedef struct {
|
||||
char quote_id[128];
|
||||
uint64_t amount;
|
||||
uint64_t fee_reserve;
|
||||
int paid;
|
||||
char* payment_preimage;
|
||||
time_t expiry;
|
||||
} cashu_melt_quote_t;
|
||||
|
||||
typedef struct {
|
||||
char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
|
||||
uint64_t amount;
|
||||
char B_[256];
|
||||
} cashu_blinded_output_t;
|
||||
|
||||
typedef struct {
|
||||
char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
|
||||
uint64_t amount;
|
||||
char C_[256];
|
||||
} cashu_blinded_signature_t;
|
||||
|
||||
typedef struct {
|
||||
char quote_id[128];
|
||||
char unit[16];
|
||||
cashu_blinded_output_t* outputs;
|
||||
int output_count;
|
||||
} cashu_mint_tokens_request_t;
|
||||
|
||||
typedef struct {
|
||||
cashu_blinded_signature_t* signatures;
|
||||
int signature_count;
|
||||
} cashu_mint_tokens_response_t;
|
||||
|
||||
typedef struct {
|
||||
nostr_cashu_proof_t* inputs;
|
||||
int input_count;
|
||||
cashu_blinded_output_t* outputs;
|
||||
int output_count;
|
||||
} cashu_swap_request_t;
|
||||
|
||||
typedef struct {
|
||||
cashu_blinded_signature_t* signatures;
|
||||
int signature_count;
|
||||
} cashu_swap_response_t;
|
||||
|
||||
typedef struct {
|
||||
char quote_id[128];
|
||||
nostr_cashu_proof_t* inputs;
|
||||
int input_count;
|
||||
} cashu_melt_tokens_request_t;
|
||||
|
||||
typedef struct {
|
||||
int paid;
|
||||
char* payment_preimage;
|
||||
cJSON* change;
|
||||
} cashu_melt_tokens_response_t;
|
||||
|
||||
typedef struct {
|
||||
char Y[256];
|
||||
char state[32];
|
||||
char witness[512];
|
||||
} cashu_proof_state_t;
|
||||
|
||||
typedef struct {
|
||||
cashu_proof_state_t* states;
|
||||
int state_count;
|
||||
} cashu_checkstate_response_t;
|
||||
|
||||
typedef struct {
|
||||
char* mint_url;
|
||||
nostr_cashu_proof_t* proofs;
|
||||
int proof_count;
|
||||
} cashu_decoded_token_t;
|
||||
|
||||
void cashu_mint_set_ca_bundle(const char* ca_bundle_path);
|
||||
|
||||
int cashu_mint_get_info(const char* mint_url,
|
||||
cashu_mint_info_t* info_out,
|
||||
int timeout_seconds);
|
||||
|
||||
void cashu_mint_free_info(cashu_mint_info_t* info);
|
||||
|
||||
int cashu_mint_get_keysets(const char* mint_url,
|
||||
cashu_keyset_t** keysets_out,
|
||||
int* keyset_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
void cashu_mint_free_keysets(cashu_keyset_t* keysets);
|
||||
|
||||
int cashu_mint_get_keys(const char* mint_url,
|
||||
const char* keyset_id,
|
||||
cashu_keyset_keys_t* keys_out,
|
||||
int timeout_seconds);
|
||||
|
||||
void cashu_mint_free_keyset_keys(cashu_keyset_keys_t* keys);
|
||||
|
||||
int cashu_keyset_keys_find_pubkey_for_amount(const cashu_keyset_keys_t* keys,
|
||||
uint64_t amount,
|
||||
const char** pubkey_out);
|
||||
|
||||
int cashu_mint_request_mint_quote(const char* mint_url,
|
||||
uint64_t amount,
|
||||
const char* unit,
|
||||
cashu_mint_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
int cashu_mint_check_mint_quote(const char* mint_url,
|
||||
const char* quote_id,
|
||||
cashu_mint_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
int cashu_mint_request_melt_quote(const char* mint_url,
|
||||
const char* payment_request,
|
||||
const char* unit,
|
||||
cashu_melt_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
int cashu_mint_check_melt_quote(const char* mint_url,
|
||||
const char* quote_id,
|
||||
cashu_melt_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
int cashu_mint_swap(const char* mint_url,
|
||||
cJSON* request_body,
|
||||
cJSON** response_out,
|
||||
int timeout_seconds);
|
||||
|
||||
int cashu_mint_mint_tokens(const char* mint_url,
|
||||
cJSON* request_body,
|
||||
cJSON** response_out,
|
||||
int timeout_seconds);
|
||||
|
||||
int cashu_mint_melt_tokens(const char* mint_url,
|
||||
cJSON* request_body,
|
||||
cJSON** response_out,
|
||||
int timeout_seconds);
|
||||
|
||||
int cashu_mint_check_proofs_state(const char* mint_url,
|
||||
cJSON* request_body,
|
||||
cJSON** response_out,
|
||||
int timeout_seconds);
|
||||
|
||||
int cashu_mint_get_active_keyset(const cashu_mint_info_t* info,
|
||||
const char* optional_unit,
|
||||
cashu_keyset_t* keyset_out);
|
||||
|
||||
int cashu_mint_select_proofs_for_amount(const nostr_cashu_proof_t* proofs,
|
||||
int proof_count,
|
||||
uint64_t target_amount,
|
||||
int* selected_indices_out,
|
||||
int selected_indices_cap,
|
||||
uint64_t* selected_total_out);
|
||||
|
||||
int cashu_mint_plan_split_amounts(uint64_t amount,
|
||||
uint64_t* amounts_out,
|
||||
int max_amounts,
|
||||
int* amount_count_out);
|
||||
|
||||
int cashu_build_mint_tokens_request(const cashu_mint_tokens_request_t* req,
|
||||
cJSON** request_body_out);
|
||||
|
||||
int cashu_parse_mint_tokens_response(cJSON* response_body,
|
||||
cashu_mint_tokens_response_t* response_out);
|
||||
|
||||
void cashu_mint_free_mint_tokens_response(cashu_mint_tokens_response_t* response);
|
||||
|
||||
int cashu_build_swap_request(const cashu_swap_request_t* req,
|
||||
cJSON** request_body_out);
|
||||
|
||||
int cashu_parse_swap_response(cJSON* response_body,
|
||||
cashu_swap_response_t* response_out);
|
||||
|
||||
void cashu_mint_free_swap_response(cashu_swap_response_t* response);
|
||||
|
||||
int cashu_build_melt_tokens_request(const cashu_melt_tokens_request_t* req,
|
||||
cJSON** request_body_out);
|
||||
|
||||
int cashu_parse_melt_tokens_response(cJSON* response_body,
|
||||
cashu_melt_tokens_response_t* response_out);
|
||||
|
||||
void cashu_mint_free_melt_tokens_response(cashu_melt_tokens_response_t* response);
|
||||
|
||||
int cashu_build_checkstate_request(const char** Ys,
|
||||
int y_count,
|
||||
cJSON** request_body_out);
|
||||
|
||||
int cashu_parse_checkstate_response(cJSON* response_body,
|
||||
cashu_checkstate_response_t* response_out);
|
||||
|
||||
void cashu_mint_free_checkstate_response(cashu_checkstate_response_t* response);
|
||||
|
||||
int cashu_decode_token(const char* token_string,
|
||||
cashu_decoded_token_t* token_out);
|
||||
|
||||
int cashu_encode_token(const cashu_decoded_token_t* token,
|
||||
cashu_token_format_t format,
|
||||
char** token_string_out);
|
||||
|
||||
void cashu_free_decoded_token(cashu_decoded_token_t* token);
|
||||
|
||||
int cashu_blind_message(const char* secret,
|
||||
const char* mint_pubkey_hex,
|
||||
char out_B_hex[67],
|
||||
unsigned char out_r[32]);
|
||||
|
||||
int cashu_unblind_signature(const char* C_blinded_hex,
|
||||
const char* mint_pubkey_hex,
|
||||
const unsigned char r[32],
|
||||
char out_C_hex[67]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_CASHU_MINT_H */
|
||||
+1268
-346
File diff suppressed because it is too large
Load Diff
+139
-34
@@ -13,7 +13,7 @@
|
||||
#define _GNU_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_core.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -26,6 +26,9 @@
|
||||
// cJSON for JSON handling
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// NIP-42 Authentication
|
||||
#include "nip042.h"
|
||||
|
||||
// =============================================================================
|
||||
// TYPE DEFINITIONS FOR SYNCHRONOUS RELAY QUERIES
|
||||
// =============================================================================
|
||||
@@ -51,6 +54,12 @@ typedef struct {
|
||||
cJSON** events; // Array of events from this relay
|
||||
int events_capacity; // Allocated capacity
|
||||
char subscription_id[32]; // Unique subscription ID
|
||||
|
||||
// NIP-42 Authentication fields
|
||||
nostr_auth_state_t auth_state; // Current authentication state
|
||||
char auth_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH]; // Stored challenge
|
||||
time_t auth_challenge_time; // When challenge was received
|
||||
int nip42_enabled; // Whether NIP-42 is enabled for this relay
|
||||
} relay_connection_t;
|
||||
|
||||
|
||||
@@ -65,7 +74,9 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
int* result_count,
|
||||
int relay_timeout_seconds,
|
||||
relay_progress_callback_t callback,
|
||||
void* user_data) {
|
||||
void* user_data,
|
||||
int nip42_enabled,
|
||||
const unsigned char* private_key) {
|
||||
|
||||
if (!relay_urls || relay_count <= 0 || !filter || !result_count) {
|
||||
if (result_count) *result_count = 0;
|
||||
@@ -95,11 +106,17 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
relays[i].last_activity = start_time;
|
||||
relays[i].events_capacity = 10;
|
||||
relays[i].events = malloc(relays[i].events_capacity * sizeof(cJSON*));
|
||||
|
||||
|
||||
// Initialize NIP-42 authentication fields
|
||||
relays[i].auth_state = NOSTR_AUTH_STATE_NONE;
|
||||
memset(relays[i].auth_challenge, 0, sizeof(relays[i].auth_challenge));
|
||||
relays[i].auth_challenge_time = 0;
|
||||
relays[i].nip42_enabled = nip42_enabled;
|
||||
|
||||
// Generate unique subscription ID
|
||||
snprintf(relays[i].subscription_id, sizeof(relays[i].subscription_id),
|
||||
snprintf(relays[i].subscription_id, sizeof(relays[i].subscription_id),
|
||||
"sync_%d_%ld", i, start_time);
|
||||
|
||||
|
||||
if (callback) {
|
||||
callback(relays[i].url, "connecting", NULL, 0, relay_count, 0, user_data);
|
||||
}
|
||||
@@ -178,9 +195,10 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to receive message (short timeout to keep UI responsive)
|
||||
char buffer[8192];
|
||||
int len = nostr_ws_receive(relay->client, buffer, sizeof(buffer)-1, 50);
|
||||
// Try to receive message (1000ms timeout to allow relay time
|
||||
// to respond — matches nostr_relay_pool_query_sync behavior)
|
||||
char buffer[262144];
|
||||
int len = nostr_ws_receive(relay->client, buffer, sizeof(buffer)-1, 1000);
|
||||
|
||||
if (len > 0) {
|
||||
buffer[len] = '\0';
|
||||
@@ -191,19 +209,50 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
cJSON* parsed = NULL;
|
||||
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
||||
|
||||
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
|
||||
if (msg_type && strcmp(msg_type, "AUTH") == 0) {
|
||||
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
|
||||
if (relay->nip42_enabled && private_key && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
|
||||
if (cJSON_IsString(challenge_json)) {
|
||||
const char* challenge = cJSON_GetStringValue(challenge_json);
|
||||
|
||||
// Store challenge and attempt authentication
|
||||
strncpy(relay->auth_challenge, challenge, sizeof(relay->auth_challenge) - 1);
|
||||
relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0';
|
||||
relay->auth_challenge_time = time(NULL);
|
||||
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
|
||||
|
||||
// Create and send authentication event
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay->url, private_key, 0);
|
||||
if (auth_event) {
|
||||
char* auth_message = nostr_nip42_create_auth_message(auth_event);
|
||||
if (auth_message) {
|
||||
if (nostr_ws_send_text(relay->client, auth_message) >= 0) {
|
||||
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
|
||||
if (callback) {
|
||||
callback(relay->url, "authenticating", NULL, 0, relay_count, completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
free(auth_message);
|
||||
}
|
||||
cJSON_Delete(auth_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (msg_type && strcmp(msg_type, "EVENT") == 0) {
|
||||
// Handle EVENT message
|
||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
|
||||
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
|
||||
cJSON* event = cJSON_GetArrayItem(parsed, 2);
|
||||
|
||||
|
||||
if (cJSON_IsString(sub_id_json) && event &&
|
||||
strcmp(cJSON_GetStringValue(sub_id_json), relay->subscription_id) == 0) {
|
||||
|
||||
|
||||
cJSON* event_id_json = cJSON_GetObjectItem(event, "id");
|
||||
if (event_id_json && cJSON_IsString(event_id_json)) {
|
||||
const char* event_id = cJSON_GetStringValue(event_id_json);
|
||||
|
||||
|
||||
// Check for duplicate
|
||||
int is_duplicate = 0;
|
||||
for (int j = 0; j < seen_count; j++) {
|
||||
@@ -212,31 +261,31 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!is_duplicate && seen_count < 1000) {
|
||||
// New event - add to seen list
|
||||
strncpy(seen_event_ids[seen_count], event_id, 64);
|
||||
seen_event_ids[seen_count][64] = '\0';
|
||||
seen_count++;
|
||||
total_unique_events++;
|
||||
|
||||
|
||||
// Store event in relay's array
|
||||
if (relay->events_received >= relay->events_capacity) {
|
||||
relay->events_capacity *= 2;
|
||||
relay->events = realloc(relay->events,
|
||||
relay->events = realloc(relay->events,
|
||||
relay->events_capacity * sizeof(cJSON*));
|
||||
}
|
||||
|
||||
|
||||
relay->events[relay->events_received] = cJSON_Duplicate(event, 1);
|
||||
relay->events_received++;
|
||||
relay->state = RELAY_STATE_ACTIVE;
|
||||
|
||||
|
||||
if (callback) {
|
||||
callback(relay->url, "event_found", event_id,
|
||||
relay->events_received, relay_count,
|
||||
callback(relay->url, "event_found", event_id,
|
||||
relay->events_received, relay_count,
|
||||
completed_relays, user_data);
|
||||
}
|
||||
|
||||
|
||||
// For FIRST_RESULT mode, return immediately
|
||||
if (mode == RELAY_QUERY_FIRST_RESULT) {
|
||||
result_array = malloc(sizeof(cJSON*));
|
||||
@@ -244,7 +293,7 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
result_array[0] = cJSON_Duplicate(event, 1);
|
||||
*result_count = 1;
|
||||
if (callback) {
|
||||
callback(NULL, "first_result", event_id,
|
||||
callback(NULL, "first_result", event_id,
|
||||
1, relay_count, completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
@@ -254,7 +303,7 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
|
||||
// Handle End of Stored Events
|
||||
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
|
||||
@@ -274,6 +323,23 @@ cJSON** synchronous_query_relays_with_progress(
|
||||
nostr_ws_close(relay->client);
|
||||
relay->client = NULL;
|
||||
}
|
||||
} else if (msg_type && (strcmp(msg_type, "NOTICE") == 0 ||
|
||||
strcmp(msg_type, "CLOSED") == 0)) {
|
||||
/* Capture NOTICE and CLOSED messages from the relay.
|
||||
* These often contain auth-required or restriction
|
||||
* notices. Pass the message content to the callback. */
|
||||
const char* notice_msg = "";
|
||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* msg_json = cJSON_GetArrayItem(parsed, 1);
|
||||
if (msg_json && cJSON_IsString(msg_json)) {
|
||||
notice_msg = cJSON_GetStringValue(msg_json);
|
||||
}
|
||||
}
|
||||
if (callback) {
|
||||
callback(relay->url, msg_type, notice_msg,
|
||||
relay->events_received, relay_count,
|
||||
completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg_type) free(msg_type);
|
||||
@@ -400,7 +466,9 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
int* success_count,
|
||||
int relay_timeout_seconds,
|
||||
publish_progress_callback_t callback,
|
||||
void* user_data) {
|
||||
void* user_data,
|
||||
int nip42_enabled,
|
||||
const unsigned char* private_key) {
|
||||
|
||||
if (!relay_urls || relay_count <= 0 || !event || !success_count) {
|
||||
if (success_count) *success_count = 0;
|
||||
@@ -443,7 +511,13 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
relays[i].state = RELAY_STATE_CONNECTING;
|
||||
relays[i].last_activity = start_time;
|
||||
results[i] = PUBLISH_ERROR; // Default to error
|
||||
|
||||
|
||||
// Initialize NIP-42 authentication fields
|
||||
relays[i].auth_state = NOSTR_AUTH_STATE_NONE;
|
||||
memset(relays[i].auth_challenge, 0, sizeof(relays[i].auth_challenge));
|
||||
relays[i].auth_challenge_time = 0;
|
||||
relays[i].nip42_enabled = nip42_enabled;
|
||||
|
||||
if (callback) {
|
||||
callback(relays[i].url, "connecting", NULL, 0, relay_count, 0, user_data);
|
||||
}
|
||||
@@ -524,7 +598,7 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
}
|
||||
|
||||
// Try to receive message (short timeout to keep UI responsive)
|
||||
char buffer[8192];
|
||||
char buffer[262144];
|
||||
int len = nostr_ws_receive(relay->client, buffer, sizeof(buffer)-1, 50);
|
||||
|
||||
if (len > 0) {
|
||||
@@ -535,34 +609,65 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
char* msg_type = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
|
||||
|
||||
if (msg_type && strcmp(msg_type, "OK") == 0) {
|
||||
|
||||
if (msg_type && strcmp(msg_type, "AUTH") == 0) {
|
||||
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
|
||||
if (relay->nip42_enabled && private_key && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
|
||||
if (cJSON_IsString(challenge_json)) {
|
||||
const char* challenge = cJSON_GetStringValue(challenge_json);
|
||||
|
||||
// Store challenge and attempt authentication
|
||||
strncpy(relay->auth_challenge, challenge, sizeof(relay->auth_challenge) - 1);
|
||||
relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0';
|
||||
relay->auth_challenge_time = time(NULL);
|
||||
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
|
||||
|
||||
// Create and send authentication event
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(challenge, relay->url, private_key, 0);
|
||||
if (auth_event) {
|
||||
char* auth_message = nostr_nip42_create_auth_message(auth_event);
|
||||
if (auth_message) {
|
||||
if (nostr_ws_send_text(relay->client, auth_message) >= 0) {
|
||||
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
|
||||
if (callback) {
|
||||
callback(relay->url, "authenticating", NULL, 0, relay_count, completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
free(auth_message);
|
||||
}
|
||||
cJSON_Delete(auth_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (msg_type && strcmp(msg_type, "OK") == 0) {
|
||||
// Handle OK message: ["OK", <event_id>, <true|false>, <message>]
|
||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
|
||||
cJSON* ok_event_id = cJSON_GetArrayItem(parsed, 1);
|
||||
cJSON* accepted = cJSON_GetArrayItem(parsed, 2);
|
||||
cJSON* message = cJSON_GetArrayItem(parsed, 3);
|
||||
|
||||
|
||||
// Verify this OK is for our event
|
||||
if (ok_event_id && cJSON_IsString(ok_event_id) && event_id &&
|
||||
strcmp(cJSON_GetStringValue(ok_event_id), event_id) == 0) {
|
||||
|
||||
|
||||
relay->state = RELAY_STATE_EOSE_RECEIVED; // Reuse for "completed"
|
||||
active_relays--;
|
||||
completed_relays++;
|
||||
|
||||
|
||||
const char* ok_message = "";
|
||||
if (message && cJSON_IsString(message)) {
|
||||
ok_message = cJSON_GetStringValue(message);
|
||||
}
|
||||
|
||||
|
||||
if (accepted && cJSON_IsBool(accepted) && cJSON_IsTrue(accepted)) {
|
||||
// Event accepted
|
||||
results[i] = PUBLISH_SUCCESS;
|
||||
(*success_count)++;
|
||||
|
||||
if (callback) {
|
||||
callback(relay->url, "accepted", ok_message,
|
||||
callback(relay->url, "accepted", ok_message,
|
||||
*success_count, relay_count, completed_relays, user_data);
|
||||
}
|
||||
} else {
|
||||
@@ -570,11 +675,11 @@ publish_result_t* synchronous_publish_event_with_progress(
|
||||
results[i] = PUBLISH_REJECTED;
|
||||
|
||||
if (callback) {
|
||||
callback(relay->url, "rejected", ok_message,
|
||||
callback(relay->url, "rejected", ok_message,
|
||||
*success_count, relay_count, completed_relays, user_data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Close connection
|
||||
nostr_ws_close(relay->client);
|
||||
relay->client = NULL;
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* nostr_chacha20poly1305.c - ChaCha20-Poly1305 AEAD implementation
|
||||
*
|
||||
* RFC 8439 Section 2.8
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// ChaCha20 private API (provided by nostr_chacha20.c)
|
||||
int chacha20_block(const uint8_t key[32], uint32_t counter,
|
||||
const uint8_t nonce[12], uint8_t output[64]);
|
||||
int chacha20_encrypt(const uint8_t key[32], uint32_t counter,
|
||||
const uint8_t nonce[12], const uint8_t* input,
|
||||
uint8_t* output, size_t length);
|
||||
|
||||
// Poly1305 API (provided by nostr_poly1305.c)
|
||||
int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg,
|
||||
size_t msg_len, unsigned char tag[16]);
|
||||
|
||||
static void store64_le(unsigned char out[8], uint64_t v) {
|
||||
out[0] = (unsigned char)(v & 0xff);
|
||||
out[1] = (unsigned char)((v >> 8) & 0xff);
|
||||
out[2] = (unsigned char)((v >> 16) & 0xff);
|
||||
out[3] = (unsigned char)((v >> 24) & 0xff);
|
||||
out[4] = (unsigned char)((v >> 32) & 0xff);
|
||||
out[5] = (unsigned char)((v >> 40) & 0xff);
|
||||
out[6] = (unsigned char)((v >> 48) & 0xff);
|
||||
out[7] = (unsigned char)((v >> 56) & 0xff);
|
||||
}
|
||||
|
||||
static int constant_time_tag_eq(const unsigned char a[16], const unsigned char b[16]) {
|
||||
unsigned char diff = 0;
|
||||
size_t i;
|
||||
for (i = 0; i < 16; i++) {
|
||||
diff |= (unsigned char)(a[i] ^ b[i]);
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
static int build_poly1305_input(const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *ciphertext, size_t ct_len,
|
||||
unsigned char **out, size_t *out_len) {
|
||||
size_t aad_pad = (16 - (aad_len % 16)) % 16;
|
||||
size_t ct_pad = (16 - (ct_len % 16)) % 16;
|
||||
size_t total = aad_len + aad_pad + ct_len + ct_pad + 16; // len(aad)||len(ct)
|
||||
unsigned char *buf;
|
||||
size_t pos = 0;
|
||||
unsigned char len_block[16];
|
||||
|
||||
if (!out || !out_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
buf = (unsigned char *)malloc(total);
|
||||
if (!buf) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (aad_len > 0 && aad) {
|
||||
memcpy(buf + pos, aad, aad_len);
|
||||
pos += aad_len;
|
||||
}
|
||||
if (aad_pad > 0) {
|
||||
memset(buf + pos, 0, aad_pad);
|
||||
pos += aad_pad;
|
||||
}
|
||||
|
||||
if (ct_len > 0 && ciphertext) {
|
||||
memcpy(buf + pos, ciphertext, ct_len);
|
||||
pos += ct_len;
|
||||
}
|
||||
if (ct_pad > 0) {
|
||||
memset(buf + pos, 0, ct_pad);
|
||||
pos += ct_pad;
|
||||
}
|
||||
|
||||
store64_le(len_block, (uint64_t)aad_len);
|
||||
store64_le(len_block + 8, (uint64_t)ct_len);
|
||||
memcpy(buf + pos, len_block, sizeof(len_block));
|
||||
pos += sizeof(len_block);
|
||||
|
||||
*out = buf;
|
||||
*out_len = pos;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int nostr_chacha20poly1305_encrypt(const unsigned char key[32],
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *plaintext, size_t pt_len,
|
||||
unsigned char *ciphertext,
|
||||
unsigned char tag[16]) {
|
||||
unsigned char block0[64];
|
||||
unsigned char poly_key[32];
|
||||
unsigned char *poly_in = NULL;
|
||||
size_t poly_in_len = 0;
|
||||
int rc;
|
||||
|
||||
if (!key || !nonce || !ciphertext || !tag || (!plaintext && pt_len != 0) || (!aad && aad_len != 0)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (chacha20_block(key, 0, nonce, block0) != 0) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(poly_key, block0, sizeof(poly_key));
|
||||
|
||||
if (pt_len > 0) {
|
||||
rc = chacha20_encrypt(key, 1, nonce, plaintext, ciphertext, pt_len);
|
||||
if (rc != 0) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
rc = build_poly1305_input(aad, aad_len, ciphertext, pt_len, &poly_in, &poly_in_len);
|
||||
if (rc != 0) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, tag);
|
||||
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
memset(block0, 0, sizeof(block0));
|
||||
memset(poly_in, 0, poly_in_len);
|
||||
free(poly_in);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
int nostr_chacha20poly1305_decrypt(const unsigned char key[32],
|
||||
const unsigned char nonce[12],
|
||||
const unsigned char *aad, size_t aad_len,
|
||||
const unsigned char *ciphertext, size_t ct_len,
|
||||
const unsigned char tag[16],
|
||||
unsigned char *plaintext) {
|
||||
unsigned char block0[64];
|
||||
unsigned char poly_key[32];
|
||||
unsigned char expected_tag[16];
|
||||
unsigned char *poly_in = NULL;
|
||||
size_t poly_in_len = 0;
|
||||
int rc;
|
||||
|
||||
if (!key || !nonce || !tag || !plaintext || (!ciphertext && ct_len != 0) || (!aad && aad_len != 0)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (chacha20_block(key, 0, nonce, block0) != 0) {
|
||||
return -1;
|
||||
}
|
||||
memcpy(poly_key, block0, sizeof(poly_key));
|
||||
|
||||
rc = build_poly1305_input(aad, aad_len, ciphertext, ct_len, &poly_in, &poly_in_len);
|
||||
if (rc != 0) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = nostr_poly1305_mac(poly_key, poly_in, poly_in_len, expected_tag);
|
||||
if (rc != 0 || !constant_time_tag_eq(tag, expected_tag)) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
memset(block0, 0, sizeof(block0));
|
||||
memset(expected_tag, 0, sizeof(expected_tag));
|
||||
memset(poly_in, 0, poly_in_len);
|
||||
free(poly_in);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ct_len > 0) {
|
||||
rc = chacha20_encrypt(key, 1, nonce, ciphertext, plaintext, ct_len);
|
||||
if (rc != 0) {
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
memset(block0, 0, sizeof(block0));
|
||||
memset(expected_tag, 0, sizeof(expected_tag));
|
||||
memset(poly_in, 0, poly_in_len);
|
||||
free(poly_in);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
memset(poly_key, 0, sizeof(poly_key));
|
||||
memset(block0, 0, sizeof(block0));
|
||||
memset(expected_tag, 0, sizeof(expected_tag));
|
||||
memset(poly_in, 0, poly_in_len);
|
||||
free(poly_in);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* nostr_poly1305.c - Poly1305 message authentication code
|
||||
*
|
||||
* Public-domain style 32-bit implementation based on the poly1305-donna
|
||||
* approach, adapted for nostr_core_lib naming and conventions.
|
||||
*
|
||||
* RFC 8439 Section 2.5
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t r0, r1, r2, r3, r4;
|
||||
uint32_t s1, s2, s3, s4;
|
||||
uint32_t h0, h1, h2, h3, h4;
|
||||
uint32_t pad0, pad1, pad2, pad3;
|
||||
size_t leftover;
|
||||
unsigned char buffer[16];
|
||||
unsigned char final;
|
||||
} nostr_poly1305_ctx_t;
|
||||
|
||||
static uint32_t u8to32_le(const unsigned char *p) {
|
||||
return ((uint32_t)p[0]) |
|
||||
((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) |
|
||||
((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static void u32to8_le(unsigned char *p, uint32_t v) {
|
||||
p[0] = (unsigned char)(v & 0xff);
|
||||
p[1] = (unsigned char)((v >> 8) & 0xff);
|
||||
p[2] = (unsigned char)((v >> 16) & 0xff);
|
||||
p[3] = (unsigned char)((v >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static void poly1305_blocks(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) {
|
||||
const uint32_t r0 = st->r0;
|
||||
const uint32_t r1 = st->r1;
|
||||
const uint32_t r2 = st->r2;
|
||||
const uint32_t r3 = st->r3;
|
||||
const uint32_t r4 = st->r4;
|
||||
const uint32_t s1 = st->s1;
|
||||
const uint32_t s2 = st->s2;
|
||||
const uint32_t s3 = st->s3;
|
||||
const uint32_t s4 = st->s4;
|
||||
uint32_t h0 = st->h0;
|
||||
uint32_t h1 = st->h1;
|
||||
uint32_t h2 = st->h2;
|
||||
uint32_t h3 = st->h3;
|
||||
uint32_t h4 = st->h4;
|
||||
const uint32_t hibit = st->final ? 0 : (1U << 24);
|
||||
|
||||
while (bytes >= 16) {
|
||||
uint32_t t0 = u8to32_le(m + 0);
|
||||
uint32_t t1 = u8to32_le(m + 4);
|
||||
uint32_t t2 = u8to32_le(m + 8);
|
||||
uint32_t t3 = u8to32_le(m + 12);
|
||||
|
||||
uint64_t d0, d1, d2, d3, d4;
|
||||
uint32_t c;
|
||||
|
||||
h0 += ( t0 ) & 0x3ffffff;
|
||||
h1 += (((t1 << 6) | (t0 >> 26)) ) & 0x3ffffff;
|
||||
h2 += (((t2 << 12) | (t1 >> 20))) & 0x3ffffff;
|
||||
h3 += (((t3 << 18) | (t2 >> 14))) & 0x3ffffff;
|
||||
h4 += (( t3 >> 8) ) & 0x00ffffff;
|
||||
h4 += hibit;
|
||||
|
||||
d0 = ((uint64_t)h0 * r0) + ((uint64_t)h1 * s4) + ((uint64_t)h2 * s3) + ((uint64_t)h3 * s2) + ((uint64_t)h4 * s1);
|
||||
d1 = ((uint64_t)h0 * r1) + ((uint64_t)h1 * r0) + ((uint64_t)h2 * s4) + ((uint64_t)h3 * s3) + ((uint64_t)h4 * s2);
|
||||
d2 = ((uint64_t)h0 * r2) + ((uint64_t)h1 * r1) + ((uint64_t)h2 * r0) + ((uint64_t)h3 * s4) + ((uint64_t)h4 * s3);
|
||||
d3 = ((uint64_t)h0 * r3) + ((uint64_t)h1 * r2) + ((uint64_t)h2 * r1) + ((uint64_t)h3 * r0) + ((uint64_t)h4 * s4);
|
||||
d4 = ((uint64_t)h0 * r4) + ((uint64_t)h1 * r3) + ((uint64_t)h2 * r2) + ((uint64_t)h3 * r1) + ((uint64_t)h4 * r0);
|
||||
|
||||
c = (uint32_t)(d0 >> 26); h0 = (uint32_t)d0 & 0x3ffffff;
|
||||
d1 += c;
|
||||
c = (uint32_t)(d1 >> 26); h1 = (uint32_t)d1 & 0x3ffffff;
|
||||
d2 += c;
|
||||
c = (uint32_t)(d2 >> 26); h2 = (uint32_t)d2 & 0x3ffffff;
|
||||
d3 += c;
|
||||
c = (uint32_t)(d3 >> 26); h3 = (uint32_t)d3 & 0x3ffffff;
|
||||
d4 += c;
|
||||
c = (uint32_t)(d4 >> 26); h4 = (uint32_t)d4 & 0x3ffffff;
|
||||
h0 += c * 5;
|
||||
c = h0 >> 26; h0 &= 0x3ffffff;
|
||||
h1 += c;
|
||||
|
||||
m += 16;
|
||||
bytes -= 16;
|
||||
}
|
||||
|
||||
st->h0 = h0;
|
||||
st->h1 = h1;
|
||||
st->h2 = h2;
|
||||
st->h3 = h3;
|
||||
st->h4 = h4;
|
||||
}
|
||||
|
||||
void nostr_poly1305_init(nostr_poly1305_ctx_t *st, const unsigned char key[32]) {
|
||||
uint32_t t0, t1, t2, t3;
|
||||
|
||||
t0 = u8to32_le(key + 0);
|
||||
t1 = u8to32_le(key + 4);
|
||||
t2 = u8to32_le(key + 8);
|
||||
t3 = u8to32_le(key + 12);
|
||||
|
||||
st->r0 = ( t0 ) & 0x3ffffff;
|
||||
st->r1 = (((t1 << 6) | (t0 >> 26)) ) & 0x3ffff03;
|
||||
st->r2 = (((t2 << 12) | (t1 >> 20))) & 0x3ffc0ff;
|
||||
st->r3 = (((t3 << 18) | (t2 >> 14))) & 0x3f03fff;
|
||||
st->r4 = (( t3 >> 8) ) & 0x00fffff;
|
||||
|
||||
st->s1 = st->r1 * 5;
|
||||
st->s2 = st->r2 * 5;
|
||||
st->s3 = st->r3 * 5;
|
||||
st->s4 = st->r4 * 5;
|
||||
|
||||
st->h0 = 0;
|
||||
st->h1 = 0;
|
||||
st->h2 = 0;
|
||||
st->h3 = 0;
|
||||
st->h4 = 0;
|
||||
|
||||
st->pad0 = u8to32_le(key + 16);
|
||||
st->pad1 = u8to32_le(key + 20);
|
||||
st->pad2 = u8to32_le(key + 24);
|
||||
st->pad3 = u8to32_le(key + 28);
|
||||
|
||||
st->leftover = 0;
|
||||
st->final = 0;
|
||||
}
|
||||
|
||||
void nostr_poly1305_update(nostr_poly1305_ctx_t *st, const unsigned char *m, size_t bytes) {
|
||||
size_t i;
|
||||
|
||||
if (st->leftover) {
|
||||
size_t want = (size_t)(16 - st->leftover);
|
||||
if (want > bytes) {
|
||||
want = bytes;
|
||||
}
|
||||
for (i = 0; i < want; i++) {
|
||||
st->buffer[st->leftover + i] = m[i];
|
||||
}
|
||||
bytes -= want;
|
||||
m += want;
|
||||
st->leftover += want;
|
||||
if (st->leftover < 16) {
|
||||
return;
|
||||
}
|
||||
poly1305_blocks(st, st->buffer, 16);
|
||||
st->leftover = 0;
|
||||
}
|
||||
|
||||
if (bytes >= 16) {
|
||||
size_t want = bytes & ~(size_t)0xf;
|
||||
poly1305_blocks(st, m, want);
|
||||
m += want;
|
||||
bytes -= want;
|
||||
}
|
||||
|
||||
if (bytes) {
|
||||
for (i = 0; i < bytes; i++) {
|
||||
st->buffer[st->leftover + i] = m[i];
|
||||
}
|
||||
st->leftover += bytes;
|
||||
}
|
||||
}
|
||||
|
||||
void nostr_poly1305_final(nostr_poly1305_ctx_t *st, unsigned char tag[16]) {
|
||||
uint32_t h0, h1, h2, h3, h4, c;
|
||||
uint32_t g0, g1, g2, g3, g4;
|
||||
uint64_t f;
|
||||
uint32_t mask;
|
||||
|
||||
if (st->leftover) {
|
||||
size_t i = st->leftover;
|
||||
st->buffer[i++] = 1;
|
||||
for (; i < 16; i++) {
|
||||
st->buffer[i] = 0;
|
||||
}
|
||||
st->final = 1;
|
||||
poly1305_blocks(st, st->buffer, 16);
|
||||
}
|
||||
|
||||
h0 = st->h0;
|
||||
h1 = st->h1;
|
||||
h2 = st->h2;
|
||||
h3 = st->h3;
|
||||
h4 = st->h4;
|
||||
|
||||
c = h1 >> 26; h1 &= 0x3ffffff;
|
||||
h2 += c;
|
||||
c = h2 >> 26; h2 &= 0x3ffffff;
|
||||
h3 += c;
|
||||
c = h3 >> 26; h3 &= 0x3ffffff;
|
||||
h4 += c;
|
||||
c = h4 >> 26; h4 &= 0x3ffffff;
|
||||
h0 += c * 5;
|
||||
c = h0 >> 26; h0 &= 0x3ffffff;
|
||||
h1 += c;
|
||||
|
||||
g0 = h0 + 5;
|
||||
c = g0 >> 26; g0 &= 0x3ffffff;
|
||||
g1 = h1 + c;
|
||||
c = g1 >> 26; g1 &= 0x3ffffff;
|
||||
g2 = h2 + c;
|
||||
c = g2 >> 26; g2 &= 0x3ffffff;
|
||||
g3 = h3 + c;
|
||||
c = g3 >> 26; g3 &= 0x3ffffff;
|
||||
g4 = h4 + c - (1U << 26);
|
||||
|
||||
mask = (g4 >> 31) - 1U;
|
||||
g0 &= mask;
|
||||
g1 &= mask;
|
||||
g2 &= mask;
|
||||
g3 &= mask;
|
||||
g4 &= mask;
|
||||
mask = ~mask;
|
||||
h0 = (h0 & mask) | g0;
|
||||
h1 = (h1 & mask) | g1;
|
||||
h2 = (h2 & mask) | g2;
|
||||
h3 = (h3 & mask) | g3;
|
||||
h4 = (h4 & mask) | g4;
|
||||
|
||||
h0 = ((h0 ) | (h1 << 26)) & 0xffffffff;
|
||||
h1 = ((h1 >> 6 ) | (h2 << 20)) & 0xffffffff;
|
||||
h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff;
|
||||
h3 = ((h3 >> 18) | (h4 << 8 )) & 0xffffffff;
|
||||
|
||||
f = (uint64_t)h0 + st->pad0;
|
||||
h0 = (uint32_t)f;
|
||||
f = (uint64_t)h1 + st->pad1 + (f >> 32);
|
||||
h1 = (uint32_t)f;
|
||||
f = (uint64_t)h2 + st->pad2 + (f >> 32);
|
||||
h2 = (uint32_t)f;
|
||||
f = (uint64_t)h3 + st->pad3 + (f >> 32);
|
||||
h3 = (uint32_t)f;
|
||||
|
||||
u32to8_le(tag + 0, h0);
|
||||
u32to8_le(tag + 4, h1);
|
||||
u32to8_le(tag + 8, h2);
|
||||
u32to8_le(tag + 12, h3);
|
||||
|
||||
memset(st, 0, sizeof(*st));
|
||||
}
|
||||
|
||||
int nostr_poly1305_mac(const unsigned char key[32], const unsigned char *msg,
|
||||
size_t msg_len, unsigned char tag[16]) {
|
||||
nostr_poly1305_ctx_t ctx;
|
||||
|
||||
if (!key || (!msg && msg_len != 0) || !tag) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
nostr_poly1305_init(&ctx, key);
|
||||
nostr_poly1305_update(&ctx, msg, msg_len);
|
||||
nostr_poly1305_final(&ctx, tag);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -3,9 +3,8 @@
|
||||
#include <secp256k1_ecdh.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <stddef.h>
|
||||
#include "../nostr_platform.h"
|
||||
|
||||
/*
|
||||
* PRIVATE INTERNAL FUNCTIONS - NOT EXPORTED
|
||||
@@ -237,28 +236,85 @@ int nostr_secp256k1_ecdh(unsigned char *result, const nostr_secp256k1_pubkey *pu
|
||||
return secp256k1_ecdh(g_ctx, result, &internal_pubkey, seckey, hashfp, data);
|
||||
}
|
||||
|
||||
int nostr_secp256k1_ec_pubkey_tweak_mul(nostr_secp256k1_pubkey* pubkey, const unsigned char* tweak32) {
|
||||
if (g_ctx == NULL || pubkey == NULL || tweak32 == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
secp256k1_pubkey internal_pubkey;
|
||||
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
|
||||
|
||||
if (!secp256k1_ec_pubkey_tweak_mul(g_ctx, &internal_pubkey, tweak32)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int nostr_secp256k1_ec_pubkey_negate(nostr_secp256k1_pubkey* pubkey) {
|
||||
if (g_ctx == NULL || pubkey == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
secp256k1_pubkey internal_pubkey;
|
||||
memcpy(&internal_pubkey, pubkey->data, sizeof(secp256k1_pubkey));
|
||||
|
||||
if (!secp256k1_ec_pubkey_negate(g_ctx, &internal_pubkey)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(pubkey->data, &internal_pubkey, sizeof(secp256k1_pubkey));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int nostr_secp256k1_ec_pubkey_combine(nostr_secp256k1_pubkey* out,
|
||||
const nostr_secp256k1_pubkey* const* ins,
|
||||
size_t n) {
|
||||
if (g_ctx == NULL || out == NULL || ins == NULL || n == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
secp256k1_pubkey* parsed = (secp256k1_pubkey*)calloc(n, sizeof(secp256k1_pubkey));
|
||||
const secp256k1_pubkey** ptrs = (const secp256k1_pubkey**)calloc(n, sizeof(secp256k1_pubkey*));
|
||||
if (!parsed || !ptrs) {
|
||||
free(parsed);
|
||||
free(ptrs);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!ins[i]) {
|
||||
free(parsed);
|
||||
free(ptrs);
|
||||
return 0;
|
||||
}
|
||||
memcpy(&parsed[i], ins[i]->data, sizeof(secp256k1_pubkey));
|
||||
ptrs[i] = &parsed[i];
|
||||
}
|
||||
|
||||
secp256k1_pubkey combined;
|
||||
int ok = secp256k1_ec_pubkey_combine(g_ctx, &combined, ptrs, n);
|
||||
|
||||
free(parsed);
|
||||
free(ptrs);
|
||||
|
||||
if (!ok) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(out->data, &combined, sizeof(secp256k1_pubkey));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int nostr_secp256k1_get_random_bytes(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL || len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Try to use /dev/urandom for good randomness
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
ssize_t result = read(fd, buf, len);
|
||||
close(fd);
|
||||
if (result == (ssize_t)len) {
|
||||
return 1;
|
||||
}
|
||||
if (nostr_platform_random(buf, len) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fallback to a simple PRNG (not cryptographically secure, but better than nothing)
|
||||
// In a real implementation, you'd want to use a proper CSPRNG
|
||||
static unsigned long seed = 1;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
seed = seed * 1103515245 + 12345;
|
||||
buf[i] = (unsigned char)(seed >> 16);
|
||||
}
|
||||
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
+39
-81
@@ -7,6 +7,7 @@
|
||||
#include "nip001.h"
|
||||
#include "utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nostr_signer.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -32,116 +33,73 @@ int nostr_ec_sign(const unsigned char* private_key, const unsigned char* hash, u
|
||||
/**
|
||||
* Create and sign a NOSTR event
|
||||
*/
|
||||
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) {
|
||||
if (!private_key) {
|
||||
cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp) {
|
||||
cJSON* event = NULL;
|
||||
cJSON* signed_event = NULL;
|
||||
char pubkey_hex[65];
|
||||
int rc;
|
||||
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
if (!content) {
|
||||
content = ""; // Default to empty content
|
||||
}
|
||||
|
||||
// Convert private key to public key
|
||||
unsigned char public_key[32];
|
||||
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
|
||||
|
||||
rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert public key to hex
|
||||
char pubkey_hex[65];
|
||||
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
|
||||
|
||||
|
||||
// Create event structure
|
||||
cJSON* event = cJSON_CreateObject();
|
||||
event = cJSON_CreateObject();
|
||||
if (!event) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
// Use provided timestamp or current time if timestamp is 0
|
||||
time_t event_time = (timestamp == 0) ? time(NULL) : timestamp;
|
||||
|
||||
|
||||
cJSON_AddStringToObject(event, "pubkey", pubkey_hex);
|
||||
cJSON_AddNumberToObject(event, "created_at", (double)event_time);
|
||||
cJSON_AddNumberToObject(event, "kind", kind);
|
||||
|
||||
|
||||
// Add tags (copy provided tags or create empty array)
|
||||
if (tags) {
|
||||
cJSON_AddItemToObject(event, "tags", cJSON_Duplicate(tags, 1));
|
||||
} else {
|
||||
cJSON_AddItemToObject(event, "tags", cJSON_CreateArray());
|
||||
}
|
||||
|
||||
|
||||
cJSON_AddStringToObject(event, "content", content);
|
||||
|
||||
// ============================================================================
|
||||
// INLINE SERIALIZATION AND SIGNING LOGIC
|
||||
// ============================================================================
|
||||
|
||||
// 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) {
|
||||
cJSON_Delete(event);
|
||||
|
||||
rc = nostr_signer_sign_event(signer, event, &signed_event);
|
||||
cJSON_Delete(event);
|
||||
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create serialization array: [0, pubkey, created_at, kind, tags, content]
|
||||
cJSON* serialize_array = cJSON_CreateArray();
|
||||
if (!serialize_array) {
|
||||
cJSON_Delete(event);
|
||||
|
||||
return signed_event;
|
||||
}
|
||||
|
||||
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp) {
|
||||
cJSON* out = NULL;
|
||||
nostr_signer_t* signer = NULL;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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) {
|
||||
cJSON_Delete(event);
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 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);
|
||||
cJSON_Delete(event);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert hash to hex for event ID
|
||||
char event_id[65];
|
||||
nostr_bytes_to_hex(event_hash, 32, event_id);
|
||||
|
||||
// Sign the hash using ECDSA
|
||||
unsigned char signature[64];
|
||||
if (nostr_ec_sign(private_key, event_hash, signature) != 0) {
|
||||
free(serialize_string);
|
||||
cJSON_Delete(event);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert signature to hex
|
||||
char sig_hex[129];
|
||||
nostr_bytes_to_hex(signature, 64, sig_hex);
|
||||
|
||||
// Add ID and signature to the event
|
||||
cJSON_AddStringToObject(event, "id", event_id);
|
||||
cJSON_AddStringToObject(event, "sig", sig_hex);
|
||||
|
||||
free(serialize_string);
|
||||
|
||||
return event;
|
||||
|
||||
out = nostr_create_and_sign_event_with_signer(kind, content, tags, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nostr_signer.h"
|
||||
|
||||
|
||||
// Function declarations
|
||||
cJSON* nostr_create_and_sign_event(int kind, const char* content, cJSON* tags, const unsigned char* private_key, time_t timestamp);
|
||||
cJSON* nostr_create_and_sign_event_with_signer(int kind, const char* content, cJSON* tags, nostr_signer_t* signer, time_t timestamp);
|
||||
|
||||
// Event validation functions
|
||||
int nostr_validate_event_structure(cJSON* event);
|
||||
|
||||
+1513
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-03: OpenTimestamps Attestations for Events
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_NIP03_H
|
||||
#define NOSTR_NIP03_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "cjson/cJSON.h"
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define NOSTR_KIND_OT_PROOF 1040
|
||||
|
||||
/* Default OTS calendars for multi-calendar stamping */
|
||||
#define NOSTR_OTS_DEFAULT_CALENDAR_COUNT 4
|
||||
extern const char* NOSTR_OTS_DEFAULT_CALENDARS[NOSTR_OTS_DEFAULT_CALENDAR_COUNT];
|
||||
|
||||
/**
|
||||
* Create a NIP-03 OpenTimestamps proof event (kind 1040)
|
||||
*
|
||||
* @param target_event_id The ID of the event being timestamped (hex string)
|
||||
* @param target_event_kind The kind of the event being timestamped
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param relay_url Optional relay URL where the target event can be found
|
||||
* @param private_key 32-byte private key for signing the proof event
|
||||
* @return cJSON* The signed proof event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip03_create_proof_event(const char* target_event_id,
|
||||
int target_event_kind,
|
||||
const char* ots_data_base64,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key);
|
||||
|
||||
/**
|
||||
* Create a NIP-03 OpenTimestamps proof event using a signer (kind 1040)
|
||||
*
|
||||
* @param target_event_id The ID of the event being timestamped (hex string)
|
||||
* @param target_event_kind The kind of the event being timestamped
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param relay_url Optional relay URL where the target event can be found
|
||||
* @param signer Signer handle for signing the proof event
|
||||
* @return cJSON* The signed proof event, or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip03_create_proof_event_with_signer(const char* target_event_id,
|
||||
int target_event_kind,
|
||||
const char* ots_data_base64,
|
||||
const char* relay_url,
|
||||
nostr_signer_t* signer);
|
||||
|
||||
/**
|
||||
* Request a timestamp for an event ID from an OpenTimestamps calendar
|
||||
*
|
||||
* This function sends the event ID (as a SHA256 digest) to an OTS calendar
|
||||
* and returns the initial .ots file data (base64 encoded).
|
||||
*
|
||||
* @param event_id_hex The event ID to timestamp (64-char hex string)
|
||||
* @param calendar_url The OTS calendar URL (e.g., "https://alice.btc.calendar.opentimestamps.org")
|
||||
* @param timeout_seconds Request timeout
|
||||
* @return char* Base64 encoded .ots data, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_request_timestamp(const char* event_id_hex,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Check if an OTS proof is complete (has a Bitcoin or Litecoin attestation)
|
||||
*
|
||||
* Parses the OTS binary file format and searches for a Bitcoin block header
|
||||
* attestation (tag 0588960d73d71901) or Litecoin attestation (tag 06856e0d73d71901).
|
||||
* A proof with only pending attestations is not yet complete.
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @return int 1 if complete (has Bitcoin/Litecoin attestation), 0 if pending, -1 on error
|
||||
*/
|
||||
int nostr_nip03_is_proof_complete(const char* ots_data_base64);
|
||||
|
||||
/**
|
||||
* Get detailed attestation information from an OTS proof
|
||||
*
|
||||
* Parses the OTS file and extracts information about all attestations found.
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param has_bitcoin Output: 1 if a Bitcoin attestation is present (optional, NULL to skip)
|
||||
* @param has_litecoin Output: 1 if a Litecoin attestation is present (optional, NULL to skip)
|
||||
* @param has_pending Output: 1 if any pending attestations are present (optional, NULL to skip)
|
||||
* @param bitcoin_height Output: Bitcoin block height (optional, NULL to skip)
|
||||
* @param litecoin_height Output: Litecoin block height (optional, NULL to skip)
|
||||
* @param pending_uri_out Output: First pending calendar URI (optional, NULL to skip, caller must free)
|
||||
* @return int 0 on success, -1 on parse error
|
||||
*/
|
||||
int nostr_nip03_get_attestation_info(const char* ots_data_base64,
|
||||
int* has_bitcoin,
|
||||
int* has_litecoin,
|
||||
int* has_pending,
|
||||
uint64_t* bitcoin_height,
|
||||
uint64_t* litecoin_height,
|
||||
char** pending_uri_out);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by fetching missing attestations from a calendar
|
||||
*
|
||||
* Sends the current proof to the calendar's /upgrade endpoint and returns
|
||||
* the updated proof. The calendar may return the same proof if no new
|
||||
* attestations are available, or an upgraded proof with Bitcoin/Litecoin
|
||||
* attestations.
|
||||
*
|
||||
* @param ots_data_base64 Current base64 encoded .ots file content
|
||||
* @param calendar_url The OTS calendar URL
|
||||
* @param timeout_seconds Request timeout
|
||||
* @return char* New base64 encoded .ots data, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof(const char* ots_data_base64,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by re-submitting the digest to a calendar
|
||||
*
|
||||
* This is the preferred upgrade method for raw timestamp proofs (returned by
|
||||
* the calendar /digest endpoint) which don't contain the file digest.
|
||||
* Re-submits the digest to the calendar, which returns the current proof
|
||||
* that may include new Bitcoin attestations.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest of the timestamped data
|
||||
* @param calendar_url The OTS calendar URL
|
||||
* @param timeout_seconds Request timeout
|
||||
* @return char* New base64 encoded .ots data, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof_with_digest(const char* digest_hex,
|
||||
const char* calendar_url,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by walking the proof tree and fetching upgraded sub-proofs
|
||||
*
|
||||
* This is the correct upgrade mechanism, matching the reference ots client:
|
||||
* 1. Parses the proof tree
|
||||
* 2. Walks the tree computing intermediate commitment hashes
|
||||
* 3. For each PendingAttestation, calls GET /timestamp/<commitment-hash>
|
||||
* 4. If the calendar returns a proof with a Bitcoin attestation, splices it in
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded DetachedTimestampFile
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* New base64 encoded upgraded proof, or NULL if no upgrade available (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof_tree(const char* ots_data_base64,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Get all pending calendar URIs from an OTS proof
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param uris_out Array of char* pointers (caller must free each string and the array)
|
||||
* @param max_uris Maximum number of URIs to return
|
||||
* @return int Number of pending URIs found, or -1 on error
|
||||
*/
|
||||
int nostr_nip03_get_pending_uris(const char* ots_data_base64,
|
||||
char*** uris_out,
|
||||
int max_uris);
|
||||
|
||||
/**
|
||||
* Create a full DetachedTimestampFile (.ots) from a digest
|
||||
*
|
||||
* Wraps a SHA-256 digest in the OTS DetachedTimestampFile format with the
|
||||
* proper header magic, version, hash op, and digest. This is the standard
|
||||
* .ots file format that can be verified by the `ots` command-line tool.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest
|
||||
* @return char* Base64 encoded DetachedTimestampFile, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_create_ots_file(const char* digest_hex);
|
||||
|
||||
/**
|
||||
* Submit a digest to multiple calendars and create a combined proof
|
||||
*
|
||||
* Submits the digest to each calendar, collects all the returned timestamps,
|
||||
* and combines them into a single DetachedTimestampFile with a fork operation.
|
||||
* This is the recommended way to timestamp data — using multiple calendars
|
||||
* provides redundancy.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest
|
||||
* @param calendar_urls Array of calendar URLs
|
||||
* @param calendar_count Number of calendars
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* Base64 encoded DetachedTimestampFile with all calendar proofs, or NULL on error
|
||||
*/
|
||||
char* nostr_nip03_stamp_with_multiple_calendars(const char* digest_hex,
|
||||
const char** calendar_urls,
|
||||
int calendar_count,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Request a timestamp for an event ID from multiple OTS calendars
|
||||
*
|
||||
* Submits the event ID to multiple calendars and combines the results into a
|
||||
* single DetachedTimestampFile. Uses the default calendars if none specified.
|
||||
*
|
||||
* @param event_id_hex The event ID to timestamp (64-char hex string)
|
||||
* @param calendar_urls Array of calendar URLs (NULL to use defaults)
|
||||
* @param calendar_count Number of calendars (0 to use defaults)
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* Base64 encoded DetachedTimestampFile, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_request_timestamp_multi(const char* event_id_hex,
|
||||
const char** calendar_urls,
|
||||
int calendar_count,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Upgrade an OTS proof by re-submitting to multiple calendars
|
||||
*
|
||||
* Re-submits the digest to all specified calendars (or defaults) and returns
|
||||
* a new combined proof. The proof may now contain Bitcoin attestations if
|
||||
* any calendar has committed since the original submission.
|
||||
*
|
||||
* @param digest_hex The 64-char hex SHA-256 digest
|
||||
* @param calendar_urls Array of calendar URLs (NULL to use defaults)
|
||||
* @param calendar_count Number of calendars (0 to use defaults)
|
||||
* @param timeout_seconds Per-request timeout
|
||||
* @return char* New base64 encoded DetachedTimestampFile, or NULL on error (caller must free)
|
||||
*/
|
||||
char* nostr_nip03_upgrade_proof_multi(const char* digest_hex,
|
||||
const char** calendar_urls,
|
||||
int calendar_count,
|
||||
int timeout_seconds);
|
||||
|
||||
/**
|
||||
* Verify an OTS proof structurally
|
||||
*
|
||||
* Parses the OTS file, optionally verifies the file digest matches the
|
||||
* expected target, and checks for a Bitcoin block header attestation.
|
||||
*
|
||||
* Note: Full on-chain verification (checking the Bitcoin block header's
|
||||
* merkle root) requires fetching the block header from a blockchain API.
|
||||
* This function performs structural verification only.
|
||||
*
|
||||
* @param ots_data_base64 Base64 encoded .ots file content
|
||||
* @param target_digest_hex Expected SHA256 digest of the timestamped data (64-char hex, or NULL to skip)
|
||||
* @param block_height_out Output: Bitcoin block height (optional, NULL to skip)
|
||||
* @param block_time_out Output: Block time (optional, NULL to skip, always 0 for structural verification)
|
||||
* @return int 0 on success (verified), 1 if no Bitcoin attestation found, -1 on parse error, -2 on digest mismatch
|
||||
*/
|
||||
int nostr_nip03_verify_proof(const char* ots_data_base64,
|
||||
const char* target_digest_hex,
|
||||
uint64_t* block_height_out,
|
||||
time_t* block_time_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_NIP03_H */
|
||||
+1
-1
@@ -13,7 +13,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
// NIP-04 constants
|
||||
// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 65535
|
||||
// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 1048576 // 1MB
|
||||
// NIP-04 Constants
|
||||
// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 16777216 // 16MB
|
||||
// #define NOSTR_NIP04_MAX_ENCRYPTED_SIZE 22369621 // ~21.3MB (accounts for base64 overhead + IV)
|
||||
|
||||
+13
-79
@@ -12,46 +12,7 @@
|
||||
#include <ctype.h>
|
||||
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
|
||||
// Structure for HTTP response handling
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t size;
|
||||
size_t capacity;
|
||||
} nip05_http_response_t;
|
||||
|
||||
/**
|
||||
* Callback function for curl to write HTTP response data
|
||||
*/
|
||||
static size_t nip05_write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
nip05_http_response_t* response = (nip05_http_response_t*)userp;
|
||||
size_t total_size = size * nmemb;
|
||||
|
||||
// Check if we need to expand the buffer
|
||||
if (response->size + total_size >= response->capacity) {
|
||||
size_t new_capacity = response->capacity * 2;
|
||||
if (new_capacity < response->size + total_size + 1) {
|
||||
new_capacity = response->size + total_size + 1;
|
||||
}
|
||||
|
||||
char* new_data = realloc(response->data, new_capacity);
|
||||
if (!new_data) {
|
||||
return 0; // Out of memory
|
||||
}
|
||||
|
||||
response->data = new_data;
|
||||
response->capacity = new_capacity;
|
||||
}
|
||||
|
||||
// Append the new data
|
||||
memcpy(response->data + response->size, contents, total_size);
|
||||
response->size += total_size;
|
||||
response->data[response->size] = '\0';
|
||||
|
||||
return total_size;
|
||||
}
|
||||
#include "nostr_http.h"
|
||||
|
||||
/**
|
||||
* Parse and validate a NIP-05 identifier into local part and domain
|
||||
@@ -104,49 +65,22 @@ static int nip05_http_get(const char* url, int timeout_seconds, char** response_
|
||||
if (!url || !response_data) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
nip05_http_response_t response = {0};
|
||||
response.capacity = 1024;
|
||||
response.data = malloc(response.capacity);
|
||||
if (!response.data) {
|
||||
curl_easy_cleanup(curl);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
|
||||
// Set curl options with proper type casting to fix warnings
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)nip05_write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : NIP05_DEFAULT_TIMEOUT));
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); // NIP-05 forbids redirects
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0");
|
||||
|
||||
// Perform the request
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long response_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
|
||||
long status = 0;
|
||||
int rc = nostr_http_get(url,
|
||||
timeout_seconds > 0 ? timeout_seconds : NIP05_DEFAULT_TIMEOUT,
|
||||
response_data,
|
||||
&status);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
if (response_code != 200) {
|
||||
free(response.data);
|
||||
|
||||
if (status != 200) {
|
||||
free(*response_data);
|
||||
*response_data = NULL;
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
*response_data = response.data;
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
+3
-17
@@ -10,24 +10,17 @@
|
||||
#include <ctype.h>
|
||||
#include <time.h>
|
||||
#include "../nostr_core/nostr_common.h"
|
||||
#include "nostr_platform.h"
|
||||
|
||||
int nostr_generate_keypair(unsigned char* private_key, unsigned char* public_key) {
|
||||
if (!private_key || !public_key) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Generate random entropy using /dev/urandom
|
||||
FILE* urandom = fopen("/dev/urandom", "rb");
|
||||
if (!urandom) {
|
||||
if (nostr_platform_random(private_key, 32) != 0) {
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
if (fread(private_key, 1, 32, urandom) != 32) {
|
||||
fclose(urandom);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
fclose(urandom);
|
||||
|
||||
// Validate private key
|
||||
if (nostr_ec_private_key_verify(private_key) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
@@ -50,17 +43,10 @@ int nostr_generate_mnemonic_and_keys(char* mnemonic, size_t mnemonic_size,
|
||||
|
||||
// Generate entropy for 12-word mnemonic
|
||||
unsigned char entropy[16];
|
||||
FILE* urandom = fopen("/dev/urandom", "rb");
|
||||
if (!urandom) {
|
||||
if (nostr_platform_random(entropy, sizeof(entropy)) != 0) {
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
if (fread(entropy, 1, sizeof(entropy), urandom) != sizeof(entropy)) {
|
||||
fclose(urandom);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
fclose(urandom);
|
||||
|
||||
// Generate mnemonic from entropy
|
||||
if (nostr_bip39_mnemonic_from_bytes(entropy, sizeof(entropy), mnemonic) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
|
||||
+25
-87
@@ -11,50 +11,13 @@
|
||||
|
||||
#ifndef DISABLE_NIP05 // NIP-11 uses the same HTTP infrastructure as NIP-05
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include "nostr_http.h"
|
||||
|
||||
// Maximum sizes for NIP-11 operations
|
||||
#define NIP11_MAX_URL_SIZE 512
|
||||
#define NIP11_MAX_RESPONSE_SIZE 16384
|
||||
#define NIP11_DEFAULT_TIMEOUT 10
|
||||
|
||||
// Structure for HTTP response handling (same as NIP-05)
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t size;
|
||||
size_t capacity;
|
||||
} nip11_http_response_t;
|
||||
|
||||
/**
|
||||
* Callback function for curl to write HTTP response data
|
||||
*/
|
||||
static size_t nip11_write_callback(void* contents, size_t size, size_t nmemb, nip11_http_response_t* response) {
|
||||
size_t total_size = size * nmemb;
|
||||
|
||||
// Check if we need to expand the buffer
|
||||
if (response->size + total_size >= response->capacity) {
|
||||
size_t new_capacity = response->capacity * 2;
|
||||
if (new_capacity < response->size + total_size + 1) {
|
||||
new_capacity = response->size + total_size + 1;
|
||||
}
|
||||
|
||||
char* new_data = realloc(response->data, new_capacity);
|
||||
if (!new_data) {
|
||||
return 0; // Out of memory
|
||||
}
|
||||
|
||||
response->data = new_data;
|
||||
response->capacity = new_capacity;
|
||||
}
|
||||
|
||||
// Append the new data
|
||||
memcpy(response->data + response->size, contents, total_size);
|
||||
response->size += total_size;
|
||||
response->data[response->size] = '\0';
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert WebSocket URL to HTTP URL for NIP-11 document retrieval
|
||||
*/
|
||||
@@ -341,60 +304,35 @@ int nostr_nip11_fetch_relay_info(const char* relay_url, nostr_relay_info_t** inf
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
// Make HTTP request with NIP-11 required header
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
free(http_url);
|
||||
free(info);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
// Use the HTTP response structure
|
||||
nip11_http_response_t response = {0};
|
||||
response.capacity = 1024;
|
||||
response.data = malloc(response.capacity);
|
||||
if (!response.data) {
|
||||
curl_easy_cleanup(curl);
|
||||
free(http_url);
|
||||
free(info);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
|
||||
// Set up headers for NIP-11
|
||||
struct curl_slist* headers = NULL;
|
||||
headers = curl_slist_append(headers, "Accept: application/nostr+json");
|
||||
|
||||
// Set curl options - use proper function pointer cast
|
||||
curl_easy_setopt(curl, CURLOPT_URL, http_url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)nip11_write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : NIP11_DEFAULT_TIMEOUT));
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // NIP-11 allows redirects
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0");
|
||||
|
||||
// Perform the request
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long response_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
const char* headers[] = {
|
||||
"Accept: application/nostr+json",
|
||||
NULL
|
||||
};
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = http_url;
|
||||
req.headers = headers;
|
||||
req.timeout_seconds = timeout_seconds > 0 ? timeout_seconds : NIP11_DEFAULT_TIMEOUT;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
nostr_http_response_t response;
|
||||
int http_rc = nostr_http_request(&req, &response);
|
||||
free(http_url);
|
||||
|
||||
if (res != CURLE_OK || response_code != 200) {
|
||||
free(response.data);
|
||||
|
||||
if (http_rc != NOSTR_SUCCESS || response.status_code != 200) {
|
||||
if (http_rc == NOSTR_SUCCESS) {
|
||||
nostr_http_response_free(&response);
|
||||
}
|
||||
free(info);
|
||||
return NOSTR_ERROR_NIP05_HTTP_FAILED;
|
||||
}
|
||||
|
||||
|
||||
// Parse the relay information
|
||||
int parse_result = nip11_parse_relay_info(response.data, info);
|
||||
free(response.data);
|
||||
int parse_result = nip11_parse_relay_info(response.body, info);
|
||||
nostr_http_response_free(&response);
|
||||
|
||||
if (parse_result != NOSTR_SUCCESS) {
|
||||
nostr_nip11_relay_info_free(info);
|
||||
|
||||
+28
-23
@@ -6,6 +6,7 @@
|
||||
#include "nip001.h"
|
||||
#include "utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nostr_log.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -175,12 +176,13 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
||||
if (attempts % timestamp_update_interval == 0) {
|
||||
current_timestamp = time(NULL);
|
||||
#ifdef ENABLE_DEBUG_LOGGING
|
||||
FILE* f = fopen("debug.log", "a");
|
||||
if (f) {
|
||||
fprintf(f, "PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d\n",
|
||||
attempts, best_difficulty_this_round, best_difficulty_overall, target_difficulty);
|
||||
fclose(f);
|
||||
}
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_DEBUG,
|
||||
"nip013",
|
||||
"PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d",
|
||||
attempts,
|
||||
best_difficulty_this_round,
|
||||
best_difficulty_overall,
|
||||
target_difficulty);
|
||||
#endif
|
||||
// Reset best difficulty for the new round
|
||||
best_difficulty_this_round = 0;
|
||||
@@ -239,18 +241,22 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
||||
// Check if we've reached the target
|
||||
if (current_difficulty >= target_difficulty) {
|
||||
#ifdef ENABLE_DEBUG_LOGGING
|
||||
FILE* f = fopen("debug.log", "a");
|
||||
if (f) {
|
||||
fprintf(f, "PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts\n",
|
||||
current_difficulty, target_difficulty, (unsigned long long)nonce, attempts + 1);
|
||||
|
||||
// Print the final event JSON
|
||||
char* event_json = cJSON_Print(test_event);
|
||||
if (event_json) {
|
||||
fprintf(f, "Final event: %s\n", event_json);
|
||||
free(event_json);
|
||||
}
|
||||
fclose(f);
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_INFO,
|
||||
"nip013",
|
||||
"PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts",
|
||||
current_difficulty,
|
||||
target_difficulty,
|
||||
(unsigned long long)nonce,
|
||||
attempts + 1);
|
||||
|
||||
// Print the final event JSON
|
||||
char* event_json = cJSON_Print(test_event);
|
||||
if (event_json) {
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_DEBUG,
|
||||
"nip013",
|
||||
"Final event: %s",
|
||||
event_json);
|
||||
free(event_json);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -267,11 +273,10 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
||||
|
||||
#ifdef ENABLE_DEBUG_LOGGING
|
||||
// Debug logging - failure
|
||||
FILE* f = fopen("debug.log", "a");
|
||||
if (f) {
|
||||
fprintf(f, "PoW FAILED: Mining failed after %d attempts\n", max_attempts);
|
||||
fclose(f);
|
||||
}
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_WARN,
|
||||
"nip013",
|
||||
"PoW FAILED: Mining failed after %d attempts",
|
||||
max_attempts);
|
||||
#endif
|
||||
|
||||
// If we reach here, we've exceeded max attempts
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* 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) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip17_create_relay_list_event_with_signer(relay_urls, num_relays, signer);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls,
|
||||
int num_relays,
|
||||
nostr_signer_t* signer) {
|
||||
cJSON* tags;
|
||||
cJSON* relay_event;
|
||||
|
||||
if (!relay_urls || num_relays <= 0 || !signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
relay_event = nostr_create_and_sign_event_with_signer(10050, "", tags, signer, time(NULL));
|
||||
cJSON_Delete(tags);
|
||||
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,
|
||||
long max_delay_sec) {
|
||||
nostr_signer_t* signer;
|
||||
int out;
|
||||
|
||||
if (!sender_private_key) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(sender_private_key);
|
||||
if (!signer) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
out = nostr_nip17_send_dm_with_signer(dm_event, recipient_pubkeys, num_recipients, signer,
|
||||
gift_wraps_out, max_gift_wraps, max_delay_sec);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
int nostr_nip17_send_dm_with_signer(cJSON* dm_event,
|
||||
const char** recipient_pubkeys,
|
||||
int num_recipients,
|
||||
nostr_signer_t* signer,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec) {
|
||||
int created_wraps = 0;
|
||||
char sender_pubkey_hex[65];
|
||||
|
||||
if (!dm_event || !recipient_pubkeys || num_recipients <= 0 ||
|
||||
!signer || !gift_wraps_out || max_gift_wraps <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_recipients && created_wraps < max_gift_wraps; i++) {
|
||||
unsigned char recipient_public_key[32];
|
||||
if (nostr_hex_to_bytes(recipient_pubkeys[i], recipient_public_key, 32) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* seal = nostr_nip59_create_seal_with_signer(dm_event, signer, recipient_public_key, max_delay_sec);
|
||||
if (!seal) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i], max_delay_sec);
|
||||
cJSON_Delete(seal);
|
||||
|
||||
if (!gift_wrap) {
|
||||
continue;
|
||||
}
|
||||
|
||||
gift_wraps_out[created_wraps++] = gift_wrap;
|
||||
}
|
||||
|
||||
if (created_wraps < max_gift_wraps && nostr_signer_get_public_key(signer, sender_pubkey_hex) == NOSTR_SUCCESS) {
|
||||
unsigned char sender_public_key[32];
|
||||
if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) == 0) {
|
||||
cJSON* sender_seal = nostr_nip59_create_seal_with_signer(dm_event, signer, sender_public_key, max_delay_sec);
|
||||
if (sender_seal) {
|
||||
cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex, max_delay_sec);
|
||||
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) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!recipient_private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(recipient_private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip17_receive_dm_with_signer(gift_wrap, signer);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap,
|
||||
nostr_signer_t* signer) {
|
||||
cJSON* seal;
|
||||
cJSON* seal_pubkey_item;
|
||||
const char* sender_pubkey_hex;
|
||||
unsigned char sender_public_key[32];
|
||||
char sender_pubkey_hex_copy[65];
|
||||
cJSON* rumor;
|
||||
cJSON* rumor_pubkey_item;
|
||||
const char* rumor_pubkey_hex;
|
||||
|
||||
if (!gift_wrap || !signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal = nostr_nip59_unwrap_gift_with_signer(gift_wrap, signer);
|
||||
if (!seal) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal_pubkey_item = cJSON_GetObjectItem(seal, "pubkey");
|
||||
if (!seal_pubkey_item || !cJSON_IsString(seal_pubkey_item)) {
|
||||
cJSON_Delete(seal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sender_pubkey_hex = cJSON_GetStringValue(seal_pubkey_item);
|
||||
if (!sender_pubkey_hex || strlen(sender_pubkey_hex) != 64) {
|
||||
cJSON_Delete(seal);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(sender_pubkey_hex_copy, sender_pubkey_hex, 65);
|
||||
if (nostr_hex_to_bytes(sender_pubkey_hex_copy, sender_public_key, 32) != 0) {
|
||||
cJSON_Delete(seal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor = nostr_nip59_unseal_rumor_with_signer(seal, sender_public_key, signer);
|
||||
cJSON_Delete(seal);
|
||||
if (!rumor) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor_pubkey_item = cJSON_GetObjectItem(rumor, "pubkey");
|
||||
if (!rumor_pubkey_item || !cJSON_IsString(rumor_pubkey_item)) {
|
||||
cJSON_Delete(rumor);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor_pubkey_hex = cJSON_GetStringValue(rumor_pubkey_item);
|
||||
if (!rumor_pubkey_hex || strcmp(sender_pubkey_hex_copy, rumor_pubkey_hex) != 0) {
|
||||
cJSON_Delete(rumor);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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,152 @@
|
||||
/*
|
||||
* 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 "nostr_signer.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);
|
||||
cJSON* nostr_nip17_create_relay_list_event_with_signer(const char** relay_urls,
|
||||
int num_relays,
|
||||
nostr_signer_t* signer);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization)
|
||||
* @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,
|
||||
long max_delay_sec);
|
||||
int nostr_nip17_send_dm_with_signer(cJSON* dm_event,
|
||||
const char** recipient_pubkeys,
|
||||
int num_recipients,
|
||||
nostr_signer_t* signer,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
cJSON* nostr_nip17_receive_dm_with_signer(cJSON* gift_wrap,
|
||||
nostr_signer_t* signer);
|
||||
|
||||
/**
|
||||
* 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
|
||||
+1
-1
@@ -180,7 +180,7 @@ nostr_input_type_t nostr_detect_input_type(const char* input) {
|
||||
if (len == 64) {
|
||||
int is_hex = 1;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (!isxdigit(input[i])) {
|
||||
if (!isxdigit((unsigned char)input[i])) {
|
||||
is_hex = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,855 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-021: nostr: URI scheme
|
||||
*/
|
||||
|
||||
#include "nip021.h"
|
||||
#include "nip019.h" // For existing bech32 functions
|
||||
#include "utils.h"
|
||||
#include "nostr_common.h" // For error codes
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// Forward declarations for internal parsing functions
|
||||
static int parse_nprofile_data(const uint8_t* data, size_t data_len, nostr_nprofile_t* nprofile);
|
||||
static int parse_nevent_data(const uint8_t* data, size_t data_len, nostr_nevent_t* nevent);
|
||||
static int parse_naddr_data(const uint8_t* data, size_t data_len, nostr_naddr_t* naddr);
|
||||
|
||||
// Bech32 constants and functions (copied from nip019.c for internal use)
|
||||
static const char bech32_charset[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
static const int8_t bech32_charset_rev[128] = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
|
||||
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
|
||||
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
|
||||
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
|
||||
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
|
||||
};
|
||||
|
||||
static uint32_t bech32_polymod_step(uint32_t pre) {
|
||||
uint8_t b = pre >> 25;
|
||||
return ((pre & 0x1FFFFFF) << 5) ^
|
||||
(-((b >> 0) & 1) & 0x3b6a57b2UL) ^
|
||||
(-((b >> 1) & 1) & 0x26508e6dUL) ^
|
||||
(-((b >> 2) & 1) & 0x1ea119faUL) ^
|
||||
(-((b >> 3) & 1) & 0x3d4233ddUL) ^
|
||||
(-((b >> 4) & 1) & 0x2a1462b3UL);
|
||||
}
|
||||
|
||||
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad) {
|
||||
uint32_t val = 0;
|
||||
int bits = 0;
|
||||
uint32_t maxv = (((uint32_t)1) << outbits) - 1;
|
||||
*outlen = 0;
|
||||
while (inlen--) {
|
||||
val = (val << inbits) | *(in++);
|
||||
bits += inbits;
|
||||
while (bits >= outbits) {
|
||||
bits -= outbits;
|
||||
out[(*outlen)++] = (val >> bits) & maxv;
|
||||
}
|
||||
}
|
||||
if (pad) {
|
||||
if (bits) {
|
||||
out[(*outlen)++] = (val << (outbits - bits)) & maxv;
|
||||
}
|
||||
} else if (((val << (outbits - bits)) & maxv) || bits >= inbits) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len) {
|
||||
uint32_t chk = 1;
|
||||
size_t i, hrp_len = strlen(hrp);
|
||||
|
||||
for (i = 0; i < hrp_len; ++i) {
|
||||
int ch = hrp[i];
|
||||
if (ch < 33 || ch > 126) return 0;
|
||||
if (ch >= 'A' && ch <= 'Z') return 0;
|
||||
chk = bech32_polymod_step(chk) ^ (ch >> 5);
|
||||
}
|
||||
|
||||
chk = bech32_polymod_step(chk);
|
||||
for (i = 0; i < hrp_len; ++i) {
|
||||
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
|
||||
*(output++) = hrp[i];
|
||||
}
|
||||
|
||||
*(output++) = '1';
|
||||
for (i = 0; i < data_len; ++i) {
|
||||
if (*data >> 5) return 0;
|
||||
chk = bech32_polymod_step(chk) ^ (*data);
|
||||
*(output++) = bech32_charset[*(data++)];
|
||||
}
|
||||
|
||||
for (i = 0; i < 6; ++i) {
|
||||
chk = bech32_polymod_step(chk);
|
||||
}
|
||||
|
||||
chk ^= 1;
|
||||
for (i = 0; i < 6; ++i) {
|
||||
*(output++) = bech32_charset[(chk >> ((5 - i) * 5)) & 0x1f];
|
||||
}
|
||||
|
||||
*output = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int bech32_decode(const char* input, const char* hrp, unsigned char* data, size_t* data_len) {
|
||||
if (!input || !hrp || !data || !data_len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t input_len = strlen(input);
|
||||
size_t hrp_len = strlen(hrp);
|
||||
|
||||
if (input_len < hrp_len + 7) return 0;
|
||||
if (strncmp(input, hrp, hrp_len) != 0) return 0;
|
||||
if (input[hrp_len] != '1') return 0;
|
||||
|
||||
const char* data_part = input + hrp_len + 1;
|
||||
size_t data_part_len = input_len - hrp_len - 1;
|
||||
|
||||
uint8_t values[256];
|
||||
for (size_t i = 0; i < data_part_len; i++) {
|
||||
unsigned char c = (unsigned char)data_part[i];
|
||||
if (c >= 128) return 0;
|
||||
int8_t val = bech32_charset_rev[c];
|
||||
if (val == -1) return 0;
|
||||
values[i] = (uint8_t)val;
|
||||
}
|
||||
|
||||
if (data_part_len < 6) return 0;
|
||||
|
||||
uint32_t chk = 1;
|
||||
for (size_t i = 0; i < hrp_len; i++) {
|
||||
chk = bech32_polymod_step(chk) ^ (hrp[i] >> 5);
|
||||
}
|
||||
chk = bech32_polymod_step(chk);
|
||||
for (size_t i = 0; i < hrp_len; i++) {
|
||||
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
|
||||
}
|
||||
for (size_t i = 0; i < data_part_len; i++) {
|
||||
chk = bech32_polymod_step(chk) ^ values[i];
|
||||
}
|
||||
|
||||
if (chk != 1) return 0;
|
||||
|
||||
size_t payload_len = data_part_len - 6;
|
||||
size_t decoded_len;
|
||||
if (!convert_bits(data, &decoded_len, 8, values, payload_len, 5, 0)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*data_len = decoded_len;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TLV (Type-Length-Value) constants for structured data
|
||||
#define TLV_SPECIAL 0
|
||||
#define TLV_RELAY 1
|
||||
#define TLV_AUTHOR 2
|
||||
#define TLV_KIND 3
|
||||
#define TLV_CREATED_AT 4
|
||||
#define TLV_IDENTIFIER 5
|
||||
|
||||
// Forward declarations for internal functions
|
||||
static int tlv_encode(const uint8_t* data, size_t data_len, uint8_t type, uint8_t** output, size_t* output_len);
|
||||
static int encode_structured_bech32(const char* hrp, const uint8_t* data, size_t data_len, char* output, size_t output_size);
|
||||
static int decode_structured_bech32(const char* input, const char* expected_hrp, uint8_t** data, size_t* data_len);
|
||||
|
||||
// Utility function to duplicate string array (removed - not used)
|
||||
|
||||
// Free string array
|
||||
static void free_string_array(char** array, int count) {
|
||||
if (!array) return;
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(array[i]);
|
||||
}
|
||||
free(array);
|
||||
}
|
||||
|
||||
// TLV encoding: Type (1 byte) + Length (1 byte) + Value
|
||||
static int tlv_encode(const uint8_t* data, size_t data_len, uint8_t type, uint8_t** output, size_t* output_len) {
|
||||
if (data_len > 255) return 0; // Length must fit in 1 byte
|
||||
|
||||
*output_len = 2 + data_len;
|
||||
*output = malloc(*output_len);
|
||||
if (!*output) return 0;
|
||||
|
||||
(*output)[0] = type;
|
||||
(*output)[1] = (uint8_t)data_len;
|
||||
memcpy(*output + 2, data, data_len);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TLV decoding (removed - not used)
|
||||
|
||||
// Encode structured data to bech32
|
||||
static int encode_structured_bech32(const char* hrp, const uint8_t* data, size_t data_len, char* output, size_t output_size) {
|
||||
// For simple cases like note (32 bytes), use the existing key encoding
|
||||
if (strcmp(hrp, "note") == 0 && data_len == 32) {
|
||||
return nostr_key_to_bech32(data, "note", output);
|
||||
}
|
||||
|
||||
uint8_t conv[256];
|
||||
size_t conv_len;
|
||||
|
||||
if (!convert_bits(conv, &conv_len, 5, data, data_len, 8, 1)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (!bech32_encode(output, hrp, conv, conv_len)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (strlen(output) >= output_size) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Decode structured bech32 data
|
||||
static int decode_structured_bech32(const char* input, const char* expected_hrp, uint8_t** data, size_t* data_len) {
|
||||
// bech32_decode already converts from 5-bit to 8-bit internally
|
||||
*data = malloc(256); // Max size
|
||||
if (!*data) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
if (!bech32_decode(input, expected_hrp, *data, data_len)) {
|
||||
free(*data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Detect URI type from string
|
||||
nostr_uri_type_t nostr_detect_uri_type(const char* uri) {
|
||||
if (!uri) return NOSTR_URI_INVALID;
|
||||
|
||||
// Check for nostr: prefix
|
||||
if (strncmp(uri, "nostr:", 6) != 0) {
|
||||
return NOSTR_URI_INVALID;
|
||||
}
|
||||
|
||||
const char* bech32_part = uri + 6;
|
||||
|
||||
// Check prefixes
|
||||
if (strncmp(bech32_part, "npub1", 5) == 0) return NOSTR_URI_NPUB;
|
||||
if (strncmp(bech32_part, "nsec1", 5) == 0) return NOSTR_URI_NSEC;
|
||||
if (strncmp(bech32_part, "note1", 5) == 0) return NOSTR_URI_NOTE;
|
||||
if (strncmp(bech32_part, "nprofile1", 9) == 0) return NOSTR_URI_NPROFILE;
|
||||
if (strncmp(bech32_part, "nevent1", 7) == 0) return NOSTR_URI_NEVENT;
|
||||
if (strncmp(bech32_part, "naddr1", 6) == 0) return NOSTR_URI_NADDR;
|
||||
|
||||
return NOSTR_URI_INVALID;
|
||||
}
|
||||
|
||||
// Free URI result resources
|
||||
void nostr_uri_result_free(nostr_uri_result_t* result) {
|
||||
if (!result) return;
|
||||
|
||||
switch (result->type) {
|
||||
case NOSTR_URI_NPROFILE:
|
||||
free_string_array(result->data.nprofile.relays, result->data.nprofile.relay_count);
|
||||
break;
|
||||
case NOSTR_URI_NEVENT:
|
||||
free_string_array(result->data.nevent.relays, result->data.nevent.relay_count);
|
||||
free(result->data.nevent.author);
|
||||
free(result->data.nevent.kind);
|
||||
free(result->data.nevent.created_at);
|
||||
break;
|
||||
case NOSTR_URI_NADDR:
|
||||
free(result->data.naddr.identifier);
|
||||
free_string_array(result->data.naddr.relays, result->data.naddr.relay_count);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Main URI parsing function
|
||||
int nostr_parse_uri(const char* uri, nostr_uri_result_t* result) {
|
||||
if (!uri || !result) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(result, 0, sizeof(nostr_uri_result_t));
|
||||
|
||||
nostr_uri_type_t type = nostr_detect_uri_type(uri);
|
||||
if (type == NOSTR_URI_INVALID) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
const char* bech32_part = uri + 6; // Skip "nostr:"
|
||||
result->type = type;
|
||||
|
||||
int ret;
|
||||
switch (type) {
|
||||
case NOSTR_URI_NPUB: {
|
||||
ret = nostr_decode_npub(bech32_part, result->data.pubkey);
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NSEC: {
|
||||
ret = nostr_decode_nsec(bech32_part, result->data.privkey);
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NOTE: {
|
||||
// Note is similar to npub but with "note" prefix
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
ret = decode_structured_bech32(bech32_part, "note", &decoded, &decoded_len);
|
||||
if (ret == NOSTR_SUCCESS) {
|
||||
if (decoded_len == 32) {
|
||||
memcpy(result->data.event_id, decoded, 32);
|
||||
} else {
|
||||
ret = NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
free(decoded);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NPROFILE: {
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
ret = decode_structured_bech32(bech32_part, "nprofile", &decoded, &decoded_len);
|
||||
if (ret == NOSTR_SUCCESS) {
|
||||
ret = parse_nprofile_data(decoded, decoded_len, &result->data.nprofile);
|
||||
free(decoded);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NEVENT: {
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
ret = decode_structured_bech32(bech32_part, "nevent", &decoded, &decoded_len);
|
||||
if (ret == NOSTR_SUCCESS) {
|
||||
ret = parse_nevent_data(decoded, decoded_len, &result->data.nevent);
|
||||
free(decoded);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NADDR: {
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
ret = decode_structured_bech32(bech32_part, "naddr", &decoded, &decoded_len);
|
||||
if (ret == NOSTR_SUCCESS) {
|
||||
ret = parse_naddr_data(decoded, decoded_len, &result->data.naddr);
|
||||
free(decoded);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ret = NOSTR_ERROR_INVALID_INPUT;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ret != NOSTR_SUCCESS) {
|
||||
nostr_uri_result_free(result);
|
||||
memset(result, 0, sizeof(nostr_uri_result_t));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Parse nprofile structured data
|
||||
static int parse_nprofile_data(const uint8_t* data, size_t data_len, nostr_nprofile_t* nprofile) {
|
||||
size_t offset = 0;
|
||||
|
||||
while (offset < data_len) {
|
||||
if (offset + 2 > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
uint8_t type = data[offset];
|
||||
uint8_t length = data[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
switch (type) {
|
||||
case TLV_SPECIAL: // pubkey
|
||||
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
|
||||
memcpy(nprofile->pubkey, data + offset, 32);
|
||||
break;
|
||||
case TLV_RELAY: // relay URL
|
||||
{
|
||||
char* relay = malloc(length + 1);
|
||||
if (!relay) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(relay, data + offset, length);
|
||||
relay[length] = '\0';
|
||||
|
||||
char** new_relays = realloc(nprofile->relays, (nprofile->relay_count + 1) * sizeof(char*));
|
||||
if (!new_relays) {
|
||||
free(relay);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
nprofile->relays = new_relays;
|
||||
nprofile->relays[nprofile->relay_count++] = relay;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Ignore unknown types
|
||||
break;
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Parse nevent structured data
|
||||
static int parse_nevent_data(const uint8_t* data, size_t data_len, nostr_nevent_t* nevent) {
|
||||
size_t offset = 0;
|
||||
|
||||
while (offset < data_len) {
|
||||
if (offset + 2 > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
uint8_t type = data[offset];
|
||||
uint8_t length = data[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
switch (type) {
|
||||
case TLV_SPECIAL: // event ID
|
||||
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
|
||||
memcpy(nevent->event_id, data + offset, 32);
|
||||
break;
|
||||
case TLV_RELAY: // relay URL
|
||||
{
|
||||
char* relay = malloc(length + 1);
|
||||
if (!relay) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(relay, data + offset, length);
|
||||
relay[length] = '\0';
|
||||
|
||||
char** new_relays = realloc(nevent->relays, (nevent->relay_count + 1) * sizeof(char*));
|
||||
if (!new_relays) {
|
||||
free(relay);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
nevent->relays = new_relays;
|
||||
nevent->relays[nevent->relay_count++] = relay;
|
||||
}
|
||||
break;
|
||||
case TLV_AUTHOR: // author pubkey
|
||||
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
|
||||
nevent->author = malloc(32);
|
||||
if (!nevent->author) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(nevent->author, data + offset, 32);
|
||||
break;
|
||||
case TLV_KIND: // kind
|
||||
if (length != 4) return NOSTR_ERROR_INVALID_INPUT;
|
||||
nevent->kind = malloc(sizeof(int));
|
||||
if (!nevent->kind) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
*nevent->kind = (data[offset] << 24) | (data[offset+1] << 16) | (data[offset+2] << 8) | data[offset+3];
|
||||
break;
|
||||
case TLV_CREATED_AT: // created_at
|
||||
if (length != 8) return NOSTR_ERROR_INVALID_INPUT;
|
||||
nevent->created_at = malloc(sizeof(time_t));
|
||||
if (!nevent->created_at) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
*nevent->created_at = ((time_t)data[offset] << 56) | ((time_t)data[offset+1] << 48) |
|
||||
((time_t)data[offset+2] << 40) | ((time_t)data[offset+3] << 32) |
|
||||
((time_t)data[offset+4] << 24) | ((time_t)data[offset+5] << 16) |
|
||||
((time_t)data[offset+6] << 8) | (time_t)data[offset+7];
|
||||
break;
|
||||
default:
|
||||
// Ignore unknown types
|
||||
break;
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Parse naddr structured data
|
||||
static int parse_naddr_data(const uint8_t* data, size_t data_len, nostr_naddr_t* naddr) {
|
||||
size_t offset = 0;
|
||||
|
||||
while (offset < data_len) {
|
||||
if (offset + 2 > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
uint8_t type = data[offset];
|
||||
uint8_t length = data[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
switch (type) {
|
||||
case TLV_IDENTIFIER: // identifier
|
||||
naddr->identifier = malloc(length + 1);
|
||||
if (!naddr->identifier) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(naddr->identifier, data + offset, length);
|
||||
naddr->identifier[length] = '\0';
|
||||
break;
|
||||
case TLV_SPECIAL: // pubkey
|
||||
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
|
||||
memcpy(naddr->pubkey, data + offset, 32);
|
||||
break;
|
||||
case TLV_KIND: // kind
|
||||
if (length != 4) return NOSTR_ERROR_INVALID_INPUT;
|
||||
naddr->kind = (data[offset] << 24) | (data[offset+1] << 16) | (data[offset+2] << 8) | data[offset+3];
|
||||
break;
|
||||
case TLV_RELAY: // relay URL
|
||||
{
|
||||
char* relay = malloc(length + 1);
|
||||
if (!relay) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(relay, data + offset, length);
|
||||
relay[length] = '\0';
|
||||
|
||||
char** new_relays = realloc(naddr->relays, (naddr->relay_count + 1) * sizeof(char*));
|
||||
if (!new_relays) {
|
||||
free(relay);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
naddr->relays = new_relays;
|
||||
naddr->relays[naddr->relay_count++] = relay;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Ignore unknown types
|
||||
break;
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
// URI construction functions
|
||||
|
||||
int nostr_build_uri_npub(const unsigned char* pubkey, char* output, size_t output_size) {
|
||||
if (!pubkey || !output || output_size < 70) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char bech32[100];
|
||||
int ret = nostr_key_to_bech32(pubkey, "npub", bech32);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
size_t len = strlen(bech32);
|
||||
if (len + 7 >= output_size) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
strcpy(output, "nostr:");
|
||||
strcpy(output + 6, bech32);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_build_uri_nsec(const unsigned char* privkey, char* output, size_t output_size) {
|
||||
if (!privkey || !output || output_size < 70) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char bech32[100];
|
||||
int ret = nostr_key_to_bech32(privkey, "nsec", bech32);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
size_t len = strlen(bech32);
|
||||
if (len + 7 >= output_size) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
strcpy(output, "nostr:");
|
||||
strcpy(output + 6, bech32);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Helper to build URI with prefix
|
||||
static int build_uri_with_prefix(const char* bech32, char* output, size_t output_size) {
|
||||
size_t len = strlen(bech32);
|
||||
if (len + 7 >= output_size) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
strcpy(output, "nostr:");
|
||||
strcpy(output + 6, bech32);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_build_uri_note(const unsigned char* event_id, char* output, size_t output_size) {
|
||||
if (!event_id || !output || output_size < 70) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char bech32[100];
|
||||
int ret = encode_structured_bech32("note", event_id, 32, bech32, sizeof(bech32));
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
return build_uri_with_prefix(bech32, output, output_size);
|
||||
}
|
||||
|
||||
int nostr_build_uri_nprofile(const unsigned char* pubkey, const char** relays, int relay_count,
|
||||
char* output, size_t output_size) {
|
||||
if (!pubkey || !output) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
// Build TLV data
|
||||
uint8_t* data = NULL;
|
||||
size_t data_len = 0;
|
||||
|
||||
// Add pubkey (special)
|
||||
uint8_t* pubkey_tlv;
|
||||
size_t pubkey_tlv_len;
|
||||
if (!tlv_encode(pubkey, 32, TLV_SPECIAL, &pubkey_tlv, &pubkey_tlv_len)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + pubkey_tlv_len);
|
||||
if (!data) {
|
||||
free(pubkey_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, pubkey_tlv, pubkey_tlv_len);
|
||||
data_len += pubkey_tlv_len;
|
||||
free(pubkey_tlv);
|
||||
|
||||
// Add relays
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
size_t relay_len = strlen(relays[i]);
|
||||
uint8_t* relay_tlv;
|
||||
size_t relay_tlv_len;
|
||||
if (!tlv_encode((uint8_t*)relays[i], relay_len, TLV_RELAY, &relay_tlv, &relay_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + relay_tlv_len);
|
||||
if (!data) {
|
||||
free(relay_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, relay_tlv, relay_tlv_len);
|
||||
data_len += relay_tlv_len;
|
||||
free(relay_tlv);
|
||||
}
|
||||
|
||||
// Encode to bech32
|
||||
char bech32[500];
|
||||
int ret = encode_structured_bech32("nprofile", data, data_len, bech32, sizeof(bech32));
|
||||
free(data);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
return build_uri_with_prefix(bech32, output, output_size);
|
||||
}
|
||||
|
||||
int nostr_build_uri_nevent(const unsigned char* event_id, const char** relays, int relay_count,
|
||||
const unsigned char* author, int kind, time_t created_at,
|
||||
char* output, size_t output_size) {
|
||||
if (!event_id || !output) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
// Build TLV data
|
||||
uint8_t* data = NULL;
|
||||
size_t data_len = 0;
|
||||
|
||||
// Add event_id (special)
|
||||
uint8_t* event_tlv;
|
||||
size_t event_tlv_len;
|
||||
if (!tlv_encode(event_id, 32, TLV_SPECIAL, &event_tlv, &event_tlv_len)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + event_tlv_len);
|
||||
if (!data) {
|
||||
free(event_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, event_tlv, event_tlv_len);
|
||||
data_len += event_tlv_len;
|
||||
free(event_tlv);
|
||||
|
||||
// Add relays
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
size_t relay_len = strlen(relays[i]);
|
||||
uint8_t* relay_tlv;
|
||||
size_t relay_tlv_len;
|
||||
if (!tlv_encode((uint8_t*)relays[i], relay_len, TLV_RELAY, &relay_tlv, &relay_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + relay_tlv_len);
|
||||
if (!data) {
|
||||
free(relay_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, relay_tlv, relay_tlv_len);
|
||||
data_len += relay_tlv_len;
|
||||
free(relay_tlv);
|
||||
}
|
||||
|
||||
// Add author if provided
|
||||
if (author) {
|
||||
uint8_t* author_tlv;
|
||||
size_t author_tlv_len;
|
||||
if (!tlv_encode(author, 32, TLV_AUTHOR, &author_tlv, &author_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + author_tlv_len);
|
||||
if (!data) {
|
||||
free(author_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, author_tlv, author_tlv_len);
|
||||
data_len += author_tlv_len;
|
||||
free(author_tlv);
|
||||
}
|
||||
|
||||
// Add kind if provided
|
||||
if (kind >= 0) {
|
||||
uint8_t kind_bytes[4];
|
||||
kind_bytes[0] = (kind >> 24) & 0xFF;
|
||||
kind_bytes[1] = (kind >> 16) & 0xFF;
|
||||
kind_bytes[2] = (kind >> 8) & 0xFF;
|
||||
kind_bytes[3] = kind & 0xFF;
|
||||
|
||||
uint8_t* kind_tlv;
|
||||
size_t kind_tlv_len;
|
||||
if (!tlv_encode(kind_bytes, 4, TLV_KIND, &kind_tlv, &kind_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + kind_tlv_len);
|
||||
if (!data) {
|
||||
free(kind_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, kind_tlv, kind_tlv_len);
|
||||
data_len += kind_tlv_len;
|
||||
free(kind_tlv);
|
||||
}
|
||||
|
||||
// Add created_at if provided
|
||||
if (created_at > 0) {
|
||||
uint8_t time_bytes[8];
|
||||
time_bytes[0] = (created_at >> 56) & 0xFF;
|
||||
time_bytes[1] = (created_at >> 48) & 0xFF;
|
||||
time_bytes[2] = (created_at >> 40) & 0xFF;
|
||||
time_bytes[3] = (created_at >> 32) & 0xFF;
|
||||
time_bytes[4] = (created_at >> 24) & 0xFF;
|
||||
time_bytes[5] = (created_at >> 16) & 0xFF;
|
||||
time_bytes[6] = (created_at >> 8) & 0xFF;
|
||||
time_bytes[7] = created_at & 0xFF;
|
||||
|
||||
uint8_t* time_tlv;
|
||||
size_t time_tlv_len;
|
||||
if (!tlv_encode(time_bytes, 8, TLV_CREATED_AT, &time_tlv, &time_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + time_tlv_len);
|
||||
if (!data) {
|
||||
free(time_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, time_tlv, time_tlv_len);
|
||||
data_len += time_tlv_len;
|
||||
free(time_tlv);
|
||||
}
|
||||
|
||||
// Encode to bech32
|
||||
char bech32[1000];
|
||||
int ret = encode_structured_bech32("nevent", data, data_len, bech32, sizeof(bech32));
|
||||
free(data);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
return build_uri_with_prefix(bech32, output, output_size);
|
||||
}
|
||||
|
||||
int nostr_build_uri_naddr(const char* identifier, const unsigned char* pubkey, int kind,
|
||||
const char** relays, int relay_count, char* output, size_t output_size) {
|
||||
if (!identifier || !pubkey || !output) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
// Build TLV data
|
||||
uint8_t* data = NULL;
|
||||
size_t data_len = 0;
|
||||
|
||||
// Add identifier
|
||||
size_t id_len = strlen(identifier);
|
||||
uint8_t* id_tlv;
|
||||
size_t id_tlv_len;
|
||||
if (!tlv_encode((uint8_t*)identifier, id_len, TLV_IDENTIFIER, &id_tlv, &id_tlv_len)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + id_tlv_len);
|
||||
if (!data) {
|
||||
free(id_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, id_tlv, id_tlv_len);
|
||||
data_len += id_tlv_len;
|
||||
free(id_tlv);
|
||||
|
||||
// Add pubkey (special)
|
||||
uint8_t* pubkey_tlv;
|
||||
size_t pubkey_tlv_len;
|
||||
if (!tlv_encode(pubkey, 32, TLV_SPECIAL, &pubkey_tlv, &pubkey_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + pubkey_tlv_len);
|
||||
if (!data) {
|
||||
free(pubkey_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, pubkey_tlv, pubkey_tlv_len);
|
||||
data_len += pubkey_tlv_len;
|
||||
free(pubkey_tlv);
|
||||
|
||||
// Add kind
|
||||
uint8_t kind_bytes[4];
|
||||
kind_bytes[0] = (kind >> 24) & 0xFF;
|
||||
kind_bytes[1] = (kind >> 16) & 0xFF;
|
||||
kind_bytes[2] = (kind >> 8) & 0xFF;
|
||||
kind_bytes[3] = kind & 0xFF;
|
||||
|
||||
uint8_t* kind_tlv;
|
||||
size_t kind_tlv_len;
|
||||
if (!tlv_encode(kind_bytes, 4, TLV_KIND, &kind_tlv, &kind_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + kind_tlv_len);
|
||||
if (!data) {
|
||||
free(kind_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, kind_tlv, kind_tlv_len);
|
||||
data_len += kind_tlv_len;
|
||||
free(kind_tlv);
|
||||
|
||||
// Add relays
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
size_t relay_len = strlen(relays[i]);
|
||||
uint8_t* relay_tlv;
|
||||
size_t relay_tlv_len;
|
||||
if (!tlv_encode((uint8_t*)relays[i], relay_len, TLV_RELAY, &relay_tlv, &relay_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + relay_tlv_len);
|
||||
if (!data) {
|
||||
free(relay_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, relay_tlv, relay_tlv_len);
|
||||
data_len += relay_tlv_len;
|
||||
free(relay_tlv);
|
||||
}
|
||||
|
||||
// Encode to bech32
|
||||
char bech32[1000];
|
||||
int ret = encode_structured_bech32("naddr", data, data_len, bech32, sizeof(bech32));
|
||||
free(data);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
return build_uri_with_prefix(bech32, output, output_size);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-021: nostr: URI scheme
|
||||
*/
|
||||
|
||||
#ifndef NIP021_H
|
||||
#define NIP021_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "nip001.h"
|
||||
|
||||
// URI type enumeration
|
||||
typedef enum {
|
||||
NOSTR_URI_NPUB, // Simple 32-byte pubkey
|
||||
NOSTR_URI_NSEC, // Simple 32-byte privkey
|
||||
NOSTR_URI_NOTE, // Simple 32-byte event ID
|
||||
NOSTR_URI_NPROFILE, // Structured: pubkey + relays
|
||||
NOSTR_URI_NEVENT, // Structured: event ID + relays + metadata
|
||||
NOSTR_URI_NADDR, // Structured: address + relays + metadata
|
||||
NOSTR_URI_INVALID
|
||||
} nostr_uri_type_t;
|
||||
|
||||
// Structured data types for complex URIs
|
||||
typedef struct {
|
||||
unsigned char pubkey[32];
|
||||
char** relays;
|
||||
int relay_count;
|
||||
} nostr_nprofile_t;
|
||||
|
||||
typedef struct {
|
||||
unsigned char event_id[32];
|
||||
char** relays;
|
||||
int relay_count;
|
||||
unsigned char* author; // Optional, 32 bytes if present
|
||||
int* kind; // Optional
|
||||
time_t* created_at; // Optional
|
||||
} nostr_nevent_t;
|
||||
|
||||
typedef struct {
|
||||
char* identifier;
|
||||
unsigned char pubkey[32];
|
||||
int kind;
|
||||
char** relays;
|
||||
int relay_count;
|
||||
} nostr_naddr_t;
|
||||
|
||||
// Unified URI result structure
|
||||
typedef struct {
|
||||
nostr_uri_type_t type;
|
||||
union {
|
||||
unsigned char pubkey[32]; // For NPUB
|
||||
unsigned char privkey[32]; // For NSEC
|
||||
unsigned char event_id[32]; // For NOTE
|
||||
nostr_nprofile_t nprofile; // For NPROFILE
|
||||
nostr_nevent_t nevent; // For NEVENT
|
||||
nostr_naddr_t naddr; // For NADDR
|
||||
} data;
|
||||
} nostr_uri_result_t;
|
||||
|
||||
// Function declarations
|
||||
|
||||
// Main parsing function - unified entry point
|
||||
int nostr_parse_uri(const char* uri, nostr_uri_result_t* result);
|
||||
|
||||
// URI construction functions
|
||||
int nostr_build_uri_npub(const unsigned char* pubkey, char* output, size_t output_size);
|
||||
int nostr_build_uri_nsec(const unsigned char* privkey, char* output, size_t output_size);
|
||||
int nostr_build_uri_note(const unsigned char* event_id, char* output, size_t output_size);
|
||||
int nostr_build_uri_nprofile(const unsigned char* pubkey, const char** relays, int relay_count,
|
||||
char* output, size_t output_size);
|
||||
int nostr_build_uri_nevent(const unsigned char* event_id, const char** relays, int relay_count,
|
||||
const unsigned char* author, int kind, time_t created_at,
|
||||
char* output, size_t output_size);
|
||||
int nostr_build_uri_naddr(const char* identifier, const unsigned char* pubkey, int kind,
|
||||
const char** relays, int relay_count, char* output, size_t output_size);
|
||||
|
||||
// Utility functions
|
||||
void nostr_uri_result_free(nostr_uri_result_t* result);
|
||||
nostr_uri_type_t nostr_detect_uri_type(const char* uri);
|
||||
|
||||
#endif // NIP021_H
|
||||
+30
-11
@@ -23,17 +23,17 @@ int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
|
||||
/**
|
||||
* Create NIP-42 authentication event (kind 22242)
|
||||
*/
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
if (!challenge || !relay_url || !private_key) {
|
||||
cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge,
|
||||
const char* relay_url,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!challenge || !relay_url || !signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Validate challenge format
|
||||
size_t challenge_len = strlen(challenge);
|
||||
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
|
||||
if (challenge_len < NOSTR_NIP42_MIN_CHALLENGE_LENGTH ||
|
||||
challenge_len >= NOSTR_NIP42_MAX_CHALLENGE_LENGTH) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -64,13 +64,11 @@ cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
cJSON_AddItemToArray(challenge_tag, cJSON_CreateString(challenge));
|
||||
cJSON_AddItemToArray(tags, challenge_tag);
|
||||
|
||||
// Create authentication event using existing function
|
||||
// Note: Empty content as per NIP-42 specification
|
||||
cJSON* auth_event = nostr_create_and_sign_event(
|
||||
cJSON* auth_event = nostr_create_and_sign_event_with_signer(
|
||||
NOSTR_NIP42_AUTH_EVENT_KIND,
|
||||
"", // Empty content
|
||||
"",
|
||||
tags,
|
||||
private_key,
|
||||
signer,
|
||||
timestamp
|
||||
);
|
||||
|
||||
@@ -78,6 +76,27 @@ cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
return auth_event;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evt = nostr_nip42_create_auth_event_with_signer(challenge, relay_url, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create AUTH message JSON for relay communication
|
||||
*/
|
||||
|
||||
+7
-1
@@ -12,6 +12,7 @@
|
||||
#include <time.h>
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -64,11 +65,16 @@ typedef struct {
|
||||
* @param timestamp Event timestamp (0 for current time)
|
||||
* @return cJSON event object or NULL on error
|
||||
*/
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
cJSON* nostr_nip42_create_auth_event(const char* challenge,
|
||||
const char* relay_url,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
cJSON* nostr_nip42_create_auth_event_with_signer(const char* challenge,
|
||||
const char* relay_url,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
/**
|
||||
* Create AUTH message JSON for relay communication
|
||||
* @param auth_event Authentication event (kind 22242)
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
// NIP-44 constants
|
||||
// #define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65535
|
||||
// #define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 1048576
|
||||
|
||||
/**
|
||||
* NIP-44: Encrypt a message using ECDH + ChaCha20 + HMAC
|
||||
|
||||
+1092
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* NIP-46: Nostr Remote Signing
|
||||
* https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_NIP046_H
|
||||
#define NOSTR_NIP046_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <time.h>
|
||||
#include "nostr_common.h"
|
||||
#include "nip001.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define NOSTR_NIP46_EVENT_KIND 24133
|
||||
#define NOSTR_NIP46_MAX_RELAYS 8
|
||||
#define NOSTR_NIP46_MAX_RELAY_URL_LEN 256
|
||||
#define NOSTR_NIP46_MAX_SECRET_LEN 128
|
||||
#define NOSTR_NIP46_MAX_REQUEST_ID_LEN 65
|
||||
#define NOSTR_NIP46_MAX_METHOD_LEN 32
|
||||
#define NOSTR_NIP46_MAX_URL_LEN 2048
|
||||
#define NOSTR_NIP46_MAX_PAYLOAD_LEN 65536
|
||||
|
||||
typedef enum {
|
||||
NOSTR_NIP46_CONN_BUNKER = 0,
|
||||
NOSTR_NIP46_CONN_NOSTRCONNECT = 1
|
||||
} nostr_nip46_connection_type_t;
|
||||
|
||||
typedef enum {
|
||||
NOSTR_NIP46_METHOD_CONNECT = 0,
|
||||
NOSTR_NIP46_METHOD_SIGN_EVENT,
|
||||
NOSTR_NIP46_METHOD_PING,
|
||||
NOSTR_NIP46_METHOD_GET_PUBLIC_KEY,
|
||||
NOSTR_NIP46_METHOD_NIP04_ENCRYPT,
|
||||
NOSTR_NIP46_METHOD_NIP04_DECRYPT,
|
||||
NOSTR_NIP46_METHOD_NIP44_ENCRYPT,
|
||||
NOSTR_NIP46_METHOD_NIP44_DECRYPT,
|
||||
NOSTR_NIP46_METHOD_UNKNOWN
|
||||
} nostr_nip46_method_t;
|
||||
|
||||
typedef struct {
|
||||
char remote_signer_pubkey[65];
|
||||
char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN];
|
||||
int relay_count;
|
||||
char secret[NOSTR_NIP46_MAX_SECRET_LEN];
|
||||
} nostr_nip46_bunker_url_t;
|
||||
|
||||
typedef struct {
|
||||
char client_pubkey[65];
|
||||
char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN];
|
||||
int relay_count;
|
||||
char secret[NOSTR_NIP46_MAX_SECRET_LEN];
|
||||
char perms[512];
|
||||
char name[128];
|
||||
char url[256];
|
||||
char image[256];
|
||||
} nostr_nip46_nostrconnect_url_t;
|
||||
|
||||
typedef struct {
|
||||
char id[NOSTR_NIP46_MAX_REQUEST_ID_LEN];
|
||||
nostr_nip46_method_t method;
|
||||
char method_str[NOSTR_NIP46_MAX_METHOD_LEN];
|
||||
char** params;
|
||||
int param_count;
|
||||
} nostr_nip46_request_t;
|
||||
|
||||
typedef struct {
|
||||
char id[NOSTR_NIP46_MAX_REQUEST_ID_LEN];
|
||||
char* result;
|
||||
char* error;
|
||||
} nostr_nip46_response_t;
|
||||
|
||||
typedef struct {
|
||||
unsigned char client_private_key[32];
|
||||
char client_pubkey_hex[65];
|
||||
char remote_signer_pubkey_hex[65];
|
||||
unsigned char remote_signer_pubkey[32];
|
||||
char user_pubkey_hex[65];
|
||||
char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN];
|
||||
int relay_count;
|
||||
int connected;
|
||||
nostr_signer_t* client_signer;
|
||||
int owns_client_signer;
|
||||
} nostr_nip46_client_session_t;
|
||||
|
||||
typedef struct {
|
||||
unsigned char signer_private_key[32];
|
||||
char signer_pubkey_hex[65];
|
||||
unsigned char user_private_key[32];
|
||||
char user_pubkey_hex[65];
|
||||
char client_pubkey_hex[65];
|
||||
unsigned char client_pubkey[32];
|
||||
char relays[NOSTR_NIP46_MAX_RELAYS][NOSTR_NIP46_MAX_RELAY_URL_LEN];
|
||||
int relay_count;
|
||||
int connected;
|
||||
} nostr_nip46_signer_session_t;
|
||||
|
||||
/* URL parsing/creation */
|
||||
int nostr_nip46_parse_bunker_url(const char* url, nostr_nip46_bunker_url_t* out);
|
||||
int nostr_nip46_parse_nostrconnect_url(const char* url, nostr_nip46_nostrconnect_url_t* out);
|
||||
int nostr_nip46_create_bunker_url(const nostr_nip46_bunker_url_t* in, char* output, size_t output_size);
|
||||
int nostr_nip46_create_nostrconnect_url(const nostr_nip46_nostrconnect_url_t* in, char* output, size_t output_size);
|
||||
|
||||
/* Method conversion */
|
||||
const char* nostr_nip46_method_to_string(nostr_nip46_method_t method);
|
||||
nostr_nip46_method_t nostr_nip46_string_to_method(const char* method);
|
||||
|
||||
/* Request/response object utilities */
|
||||
int nostr_nip46_generate_request_id(char* output, size_t output_size);
|
||||
int nostr_nip46_create_request(const char* id,
|
||||
nostr_nip46_method_t method,
|
||||
const char** params,
|
||||
int param_count,
|
||||
nostr_nip46_request_t* out);
|
||||
int nostr_nip46_create_response(const char* id,
|
||||
const char* result,
|
||||
const char* error,
|
||||
nostr_nip46_response_t* out);
|
||||
int nostr_nip46_parse_request(const char* json_payload, nostr_nip46_request_t* out);
|
||||
int nostr_nip46_parse_response(const char* json_payload, nostr_nip46_response_t* out);
|
||||
void nostr_nip46_free_request(nostr_nip46_request_t* request);
|
||||
void nostr_nip46_free_response(nostr_nip46_response_t* response);
|
||||
|
||||
/* JSON payload serialization */
|
||||
int nostr_nip46_request_to_json(const nostr_nip46_request_t* request, char** output_json);
|
||||
int nostr_nip46_response_to_json(const nostr_nip46_response_t* response, char** output_json);
|
||||
|
||||
/* Event creation/decryption */
|
||||
cJSON* nostr_nip46_create_request_event(const nostr_nip46_request_t* request,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip46_create_request_event_with_signer(const nostr_nip46_request_t* request,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip46_create_response_event(const nostr_nip46_response_t* response,
|
||||
const unsigned char* sender_private_key,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip46_create_response_event_with_signer(const nostr_nip46_response_t* response,
|
||||
nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key,
|
||||
time_t timestamp);
|
||||
int nostr_nip46_decrypt_event(cJSON* event,
|
||||
const unsigned char* recipient_private_key,
|
||||
char* output,
|
||||
size_t output_size);
|
||||
int nostr_nip46_decrypt_event_with_signer(cJSON* event,
|
||||
nostr_signer_t* signer,
|
||||
char** output_out);
|
||||
|
||||
/* Client session */
|
||||
int nostr_nip46_client_session_init(nostr_nip46_client_session_t* session,
|
||||
const unsigned char* client_private_key,
|
||||
const char* bunker_url);
|
||||
int nostr_nip46_client_session_init_with_signer(nostr_nip46_client_session_t* session,
|
||||
nostr_signer_t* signer,
|
||||
const char* bunker_url);
|
||||
void nostr_nip46_client_session_destroy(nostr_nip46_client_session_t* session);
|
||||
|
||||
int nostr_nip46_client_connect(nostr_nip46_client_session_t* session,
|
||||
const char* optional_secret,
|
||||
const char* optional_permissions,
|
||||
cJSON** request_event_out);
|
||||
int nostr_nip46_client_get_public_key(nostr_nip46_client_session_t* session,
|
||||
cJSON** request_event_out);
|
||||
int nostr_nip46_client_ping(nostr_nip46_client_session_t* session,
|
||||
cJSON** request_event_out);
|
||||
int nostr_nip46_client_sign_event(nostr_nip46_client_session_t* session,
|
||||
cJSON* unsigned_event,
|
||||
cJSON** request_event_out);
|
||||
|
||||
/* Signer session */
|
||||
int nostr_nip46_signer_session_init(nostr_nip46_signer_session_t* session,
|
||||
const unsigned char* signer_private_key,
|
||||
const unsigned char* user_private_key,
|
||||
const char** relays,
|
||||
int relay_count);
|
||||
void nostr_nip46_signer_session_destroy(nostr_nip46_signer_session_t* session);
|
||||
int nostr_nip46_signer_handle_request(nostr_nip46_signer_session_t* session,
|
||||
const nostr_nip46_request_t* request,
|
||||
nostr_nip46_response_t* response_out);
|
||||
int nostr_nip46_signer_create_bunker_url(const nostr_nip46_signer_session_t* session,
|
||||
const char* optional_secret,
|
||||
char* output,
|
||||
size_t output_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_NIP046_H */
|
||||
@@ -0,0 +1,490 @@
|
||||
/*
|
||||
* 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 max_delay_sec in the past (configurable)
|
||||
*/
|
||||
static time_t random_past_timestamp(long max_delay_sec) {
|
||||
time_t now = time(NULL);
|
||||
|
||||
// If max_delay_sec is 0, return current timestamp (no randomization)
|
||||
if (max_delay_sec == 0) {
|
||||
return now;
|
||||
}
|
||||
|
||||
// Random time up to max_delay_sec in the past
|
||||
long random_offset = (long)(rand() % max_delay_sec);
|
||||
return now - random_offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-44 padding calculation (mirrors nip044.c for output sizing)
|
||||
*/
|
||||
static size_t calc_nip44_padded_len(size_t unpadded_len) {
|
||||
if (unpadded_len <= 32) {
|
||||
return 32;
|
||||
}
|
||||
|
||||
size_t next_power = 1;
|
||||
while (next_power < unpadded_len) {
|
||||
next_power <<= 1;
|
||||
}
|
||||
|
||||
size_t chunk = (next_power <= 256) ? 32 : (next_power / 8);
|
||||
return chunk * ((unpadded_len - 1) / chunk + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate safe output buffer size for NIP-44 encrypted base64 payload.
|
||||
* Returns 0 if plaintext length exceeds NIP-44 max.
|
||||
*/
|
||||
static size_t calc_nip44_encrypted_b64_size(size_t plaintext_len) {
|
||||
if (plaintext_len > NOSTR_NIP44_MAX_PLAINTEXT_SIZE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t padded_len = calc_nip44_padded_len(plaintext_len) + 2; // +2 for length prefix
|
||||
size_t payload_len = 1 + 32 + padded_len + 32; // version + nonce + ciphertext + mac
|
||||
size_t b64_len = ((payload_len + 2) / 3) * 4 + 1; // +1 for NUL terminator
|
||||
|
||||
return b64_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (default to 0 for compatibility)
|
||||
time_t event_time = (created_at == 0) ? random_past_timestamp(0) : 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, long max_delay_sec) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!sender_private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(sender_private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip59_create_seal_with_signer(rumor, signer, recipient_public_key, max_delay_sec);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip59_create_seal_with_signer(cJSON* rumor, nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key, long max_delay_sec) {
|
||||
cJSON* seal;
|
||||
cJSON* signed_seal = NULL;
|
||||
char* rumor_json;
|
||||
char recipient_pubkey_hex[65];
|
||||
char sender_pubkey_hex[65];
|
||||
char* encrypted_content = NULL;
|
||||
int rc;
|
||||
time_t seal_time;
|
||||
|
||||
if (!rumor || !signer || !recipient_public_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor_json = cJSON_PrintUnformatted(rumor);
|
||||
if (!rumor_json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(recipient_public_key, 32, recipient_pubkey_hex);
|
||||
rc = nostr_signer_nip44_encrypt(signer, recipient_pubkey_hex, rumor_json, &encrypted_content);
|
||||
free(rumor_json);
|
||||
if (rc != NOSTR_SUCCESS || !encrypted_content) {
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rc = nostr_signer_get_public_key(signer, sender_pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal = cJSON_CreateObject();
|
||||
if (!seal) {
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal_time = random_past_timestamp(max_delay_sec);
|
||||
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());
|
||||
cJSON_AddStringToObject(seal, "content", encrypted_content);
|
||||
free(encrypted_content);
|
||||
|
||||
rc = nostr_signer_sign_event(signer, seal, &signed_seal);
|
||||
cJSON_Delete(seal);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(signed_seal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return signed_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, long max_delay_sec) {
|
||||
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
|
||||
size_t encrypted_size = calc_nip44_encrypted_b64_size(strlen(seal_json));
|
||||
if (encrypted_size == 0) {
|
||||
memory_clear(random_private_key, 32);
|
||||
free(seal_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* encrypted_content = malloc(encrypted_size);
|
||||
if (!encrypted_content) {
|
||||
memory_clear(random_private_key, 32);
|
||||
free(seal_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int encrypt_result = nostr_nip44_encrypt(random_private_key, recipient_public_key,
|
||||
seal_json, encrypted_content, encrypted_size);
|
||||
free(seal_json);
|
||||
|
||||
if (encrypt_result != NOSTR_SUCCESS) {
|
||||
memory_clear(random_private_key, 32);
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create gift wrap event (kind 1059)
|
||||
cJSON* gift_wrap = cJSON_CreateObject();
|
||||
if (!gift_wrap) {
|
||||
memory_clear(random_private_key, 32);
|
||||
free(encrypted_content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
time_t wrap_time = random_past_timestamp(max_delay_sec);
|
||||
|
||||
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);
|
||||
free(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) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!recipient_private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(recipient_private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip59_unwrap_gift_with_signer(gift_wrap, signer);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* signer) {
|
||||
cJSON* content_item;
|
||||
cJSON* pubkey_item;
|
||||
const char* encrypted_content;
|
||||
const char* sender_pubkey_hex;
|
||||
char* decrypted_json = NULL;
|
||||
int rc;
|
||||
cJSON* seal;
|
||||
|
||||
if (!gift_wrap || !signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
content_item = cJSON_GetObjectItem(gift_wrap, "content");
|
||||
pubkey_item = cJSON_GetObjectItem(gift_wrap, "pubkey");
|
||||
if (!content_item || !cJSON_IsString(content_item) || !pubkey_item || !cJSON_IsString(pubkey_item)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
encrypted_content = cJSON_GetStringValue(content_item);
|
||||
sender_pubkey_hex = cJSON_GetStringValue(pubkey_item);
|
||||
|
||||
rc = nostr_signer_nip44_decrypt(signer, sender_pubkey_hex, encrypted_content, &decrypted_json);
|
||||
if (rc != NOSTR_SUCCESS || !decrypted_json) {
|
||||
free(decrypted_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
seal = cJSON_Parse(decrypted_json);
|
||||
free(decrypted_json);
|
||||
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) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* out;
|
||||
|
||||
if (!recipient_private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(recipient_private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
out = nostr_nip59_unseal_rumor_with_signer(seal, sender_public_key, signer);
|
||||
nostr_signer_free(signer);
|
||||
return out;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip59_unseal_rumor_with_signer(cJSON* seal, const unsigned char* sender_public_key,
|
||||
nostr_signer_t* signer) {
|
||||
cJSON* content_item;
|
||||
const char* encrypted_content;
|
||||
char sender_pubkey_hex[65];
|
||||
char* decrypted_json = NULL;
|
||||
int rc;
|
||||
cJSON* rumor;
|
||||
|
||||
if (!seal || !sender_public_key || !signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
content_item = cJSON_GetObjectItem(seal, "content");
|
||||
if (!content_item || !cJSON_IsString(content_item)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
encrypted_content = cJSON_GetStringValue(content_item);
|
||||
nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex);
|
||||
|
||||
rc = nostr_signer_nip44_decrypt(signer, sender_pubkey_hex, encrypted_content, &decrypted_json);
|
||||
if (rc != NOSTR_SUCCESS || !decrypted_json) {
|
||||
free(decrypted_json);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rumor = cJSON_Parse(decrypted_json);
|
||||
free(decrypted_json);
|
||||
return rumor;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 "nostr_signer.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)
|
||||
* @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization)
|
||||
* @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, long max_delay_sec);
|
||||
cJSON* nostr_nip59_create_seal_with_signer(cJSON* rumor, nostr_signer_t* signer,
|
||||
const unsigned char* recipient_public_key, long max_delay_sec);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization)
|
||||
* @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, long max_delay_sec);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
cJSON* nostr_nip59_unwrap_gift_with_signer(cJSON* gift_wrap, nostr_signer_t* signer);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
cJSON* nostr_nip59_unseal_rumor_with_signer(cJSON* seal, const unsigned char* sender_public_key,
|
||||
nostr_signer_t* signer);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // NOSTR_NIP059_H
|
||||
@@ -0,0 +1,936 @@
|
||||
/*
|
||||
* NIP-60: Cashu Wallet Implementation
|
||||
* https://github.com/nostr-protocol/nips/blob/master/60.md
|
||||
*/
|
||||
|
||||
#include "nip060.h"
|
||||
#include "nip044.h"
|
||||
#include "utils.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
|
||||
|
||||
static char* nip60_strdup(const char* s) {
|
||||
if (!s) return NULL;
|
||||
size_t len = strlen(s);
|
||||
char* out = (char*)malloc(len + 1);
|
||||
if (!out) return NULL;
|
||||
memcpy(out, s, len + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
static const char* nip60_marker_to_string(nostr_nip60_ref_marker_t marker) {
|
||||
switch (marker) {
|
||||
case NOSTR_NIP60_REF_CREATED: return "created";
|
||||
case NOSTR_NIP60_REF_DESTROYED: return "destroyed";
|
||||
case NOSTR_NIP60_REF_REDEEMED: return "redeemed";
|
||||
default: return "created";
|
||||
}
|
||||
}
|
||||
|
||||
static nostr_nip60_ref_marker_t nip60_string_to_marker(const char* s) {
|
||||
if (!s) return NOSTR_NIP60_REF_CREATED;
|
||||
if (strcmp(s, "created") == 0) return NOSTR_NIP60_REF_CREATED;
|
||||
if (strcmp(s, "destroyed") == 0) return NOSTR_NIP60_REF_DESTROYED;
|
||||
if (strcmp(s, "redeemed") == 0) return NOSTR_NIP60_REF_REDEEMED;
|
||||
return NOSTR_NIP60_REF_CREATED;
|
||||
}
|
||||
|
||||
static const char* nip60_direction_to_string(nostr_nip60_direction_t direction) {
|
||||
return (direction == NOSTR_NIP60_DIRECTION_OUT) ? "out" : "in";
|
||||
}
|
||||
|
||||
static nostr_nip60_direction_t nip60_string_to_direction(const char* s) {
|
||||
if (s && strcmp(s, "out") == 0) return NOSTR_NIP60_DIRECTION_OUT;
|
||||
return NOSTR_NIP60_DIRECTION_IN;
|
||||
}
|
||||
|
||||
static int nip60_encrypt_self_with_signer(nostr_signer_t* signer,
|
||||
const char* plaintext,
|
||||
char* output,
|
||||
size_t output_size) {
|
||||
if (!signer || !plaintext || !output || output_size == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char pubkey_hex[65];
|
||||
int rc = nostr_signer_get_public_key(signer, pubkey_hex);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
char* encrypted = NULL;
|
||||
rc = nostr_signer_nip44_encrypt(signer, pubkey_hex, plaintext, &encrypted);
|
||||
if (rc != NOSTR_SUCCESS || !encrypted) {
|
||||
free(encrypted);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
if (strlen(encrypted) + 1 > output_size) {
|
||||
free(encrypted);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
strcpy(output, encrypted);
|
||||
free(encrypted);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int nip60_encrypt_self(const unsigned char* private_key,
|
||||
const char* plaintext,
|
||||
char* output,
|
||||
size_t output_size) {
|
||||
nostr_signer_t* signer;
|
||||
int rc;
|
||||
|
||||
if (!private_key) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
rc = nip60_encrypt_self_with_signer(signer, plaintext, output, output_size);
|
||||
nostr_signer_free(signer);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int nip60_decrypt_event_content(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
char* output,
|
||||
size_t output_size) {
|
||||
if (!event || !private_key || !output || output_size == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
|
||||
cJSON* content_item = cJSON_GetObjectItem(event, "content");
|
||||
if (!pubkey_item || !content_item || !cJSON_IsString(pubkey_item) || !cJSON_IsString(content_item)) {
|
||||
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
|
||||
}
|
||||
|
||||
const char* pubkey_hex = cJSON_GetStringValue(pubkey_item);
|
||||
const char* content = cJSON_GetStringValue(content_item);
|
||||
if (!pubkey_hex || !content) {
|
||||
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
|
||||
}
|
||||
|
||||
unsigned char sender_pubkey[32];
|
||||
if (nostr_hex_to_bytes(pubkey_hex, sender_pubkey, 32) != 0) {
|
||||
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
|
||||
}
|
||||
|
||||
int rc = nostr_nip44_decrypt(private_key, sender_pubkey, content, output, output_size);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
uint64_t nostr_nip60_sum_proofs(const nostr_cashu_proof_t* proofs, int proof_count) {
|
||||
if (!proofs || proof_count <= 0) return 0;
|
||||
|
||||
uint64_t total = 0;
|
||||
for (int i = 0; i < proof_count; i++) {
|
||||
total += proofs[i].amount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void nostr_nip60_free_proofs(nostr_cashu_proof_t* proofs, int proof_count) {
|
||||
if (!proofs) return;
|
||||
|
||||
for (int i = 0; i < proof_count; i++) {
|
||||
free(proofs[i].secret);
|
||||
free(proofs[i].C);
|
||||
proofs[i].secret = NULL;
|
||||
proofs[i].C = NULL;
|
||||
}
|
||||
free(proofs);
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_proofs_to_json(const nostr_cashu_proof_t* proofs, int proof_count) {
|
||||
if (!proofs || proof_count < 0) return NULL;
|
||||
|
||||
cJSON* arr = cJSON_CreateArray();
|
||||
if (!arr) return NULL;
|
||||
|
||||
for (int i = 0; i < proof_count; i++) {
|
||||
cJSON* obj = cJSON_CreateObject();
|
||||
if (!obj) {
|
||||
cJSON_Delete(arr);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(obj, "id", proofs[i].id);
|
||||
cJSON_AddNumberToObject(obj, "amount", (double)proofs[i].amount);
|
||||
cJSON_AddStringToObject(obj, "secret", proofs[i].secret ? proofs[i].secret : "");
|
||||
cJSON_AddStringToObject(obj, "C", proofs[i].C ? proofs[i].C : "");
|
||||
cJSON_AddItemToArray(arr, obj);
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
int nostr_nip60_proofs_from_json(cJSON* json_array,
|
||||
nostr_cashu_proof_t** proofs_out,
|
||||
int* proof_count_out) {
|
||||
if (!json_array || !cJSON_IsArray(json_array) || !proofs_out || !proof_count_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*proofs_out = NULL;
|
||||
*proof_count_out = 0;
|
||||
|
||||
int count = cJSON_GetArraySize(json_array);
|
||||
if (count <= 0) {
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
nostr_cashu_proof_t* proofs = (nostr_cashu_proof_t*)calloc((size_t)count, sizeof(nostr_cashu_proof_t));
|
||||
if (!proofs) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
cJSON* p = cJSON_GetArrayItem(json_array, i);
|
||||
if (!p || !cJSON_IsObject(p)) {
|
||||
nostr_nip60_free_proofs(proofs, count);
|
||||
return NOSTR_ERROR_NIP60_INVALID_PROOFS;
|
||||
}
|
||||
|
||||
cJSON* id_item = cJSON_GetObjectItem(p, "id");
|
||||
cJSON* amount_item = cJSON_GetObjectItem(p, "amount");
|
||||
cJSON* secret_item = cJSON_GetObjectItem(p, "secret");
|
||||
cJSON* c_item = cJSON_GetObjectItem(p, "C");
|
||||
|
||||
if (!id_item || !amount_item || !secret_item || !c_item ||
|
||||
!cJSON_IsString(id_item) || !cJSON_IsNumber(amount_item) ||
|
||||
!cJSON_IsString(secret_item) || !cJSON_IsString(c_item)) {
|
||||
nostr_nip60_free_proofs(proofs, count);
|
||||
return NOSTR_ERROR_NIP60_INVALID_PROOFS;
|
||||
}
|
||||
|
||||
const char* id = cJSON_GetStringValue(id_item);
|
||||
const char* secret = cJSON_GetStringValue(secret_item);
|
||||
const char* C = cJSON_GetStringValue(c_item);
|
||||
if (!id || !secret || !C) {
|
||||
nostr_nip60_free_proofs(proofs, count);
|
||||
return NOSTR_ERROR_NIP60_INVALID_PROOFS;
|
||||
}
|
||||
|
||||
strncpy(proofs[i].id, id, sizeof(proofs[i].id) - 1);
|
||||
proofs[i].id[sizeof(proofs[i].id) - 1] = '\0';
|
||||
proofs[i].amount = (uint64_t)cJSON_GetNumberValue(amount_item);
|
||||
proofs[i].secret = nip60_strdup(secret);
|
||||
proofs[i].C = nip60_strdup(C);
|
||||
|
||||
if (!proofs[i].secret || !proofs[i].C) {
|
||||
nostr_nip60_free_proofs(proofs, count);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
*proofs_out = proofs;
|
||||
*proof_count_out = count;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!wallet_data || !signer || wallet_data->mint_count <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* payload = cJSON_CreateArray();
|
||||
if (!payload) return NULL;
|
||||
|
||||
cJSON* priv_row = cJSON_CreateArray();
|
||||
if (!priv_row) {
|
||||
cJSON_Delete(payload);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToArray(priv_row, cJSON_CreateString("privkey"));
|
||||
cJSON_AddItemToArray(priv_row, cJSON_CreateString(wallet_data->privkey));
|
||||
cJSON_AddItemToArray(payload, priv_row);
|
||||
|
||||
for (int i = 0; i < wallet_data->mint_count; i++) {
|
||||
if (!wallet_data->mint_urls || !wallet_data->mint_urls[i]) continue;
|
||||
cJSON* mint_row = cJSON_CreateArray();
|
||||
if (!mint_row) {
|
||||
cJSON_Delete(payload);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToArray(mint_row, cJSON_CreateString("mint"));
|
||||
cJSON_AddItemToArray(mint_row, cJSON_CreateString(wallet_data->mint_urls[i]));
|
||||
cJSON_AddItemToArray(payload, mint_row);
|
||||
}
|
||||
|
||||
char* plain = cJSON_PrintUnformatted(payload);
|
||||
cJSON_Delete(payload);
|
||||
if (!plain) return NULL;
|
||||
|
||||
char encrypted[65536];
|
||||
int rc = nip60_encrypt_self_with_signer(signer, plain, encrypted, sizeof(encrypted));
|
||||
free(plain);
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
return nostr_create_and_sign_event_with_signer(NOSTR_NIP60_WALLET_KIND, encrypted, NULL, signer, timestamp);
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evt = nostr_nip60_create_wallet_event_with_signer(wallet_data, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip60_parse_wallet_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_wallet_data_t* wallet_data_out) {
|
||||
if (!event || !private_key || !wallet_data_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(wallet_data_out, 0, sizeof(*wallet_data_out));
|
||||
|
||||
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_WALLET_KIND) {
|
||||
return NOSTR_ERROR_NIP60_INVALID_WALLET;
|
||||
}
|
||||
|
||||
char decrypted[65536];
|
||||
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
cJSON* payload = cJSON_Parse(decrypted);
|
||||
if (!payload || !cJSON_IsArray(payload)) {
|
||||
cJSON_Delete(payload);
|
||||
return NOSTR_ERROR_NIP60_INVALID_WALLET;
|
||||
}
|
||||
|
||||
int mint_count = 0;
|
||||
cJSON* row = NULL;
|
||||
cJSON_ArrayForEach(row, payload) {
|
||||
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
|
||||
cJSON* k = cJSON_GetArrayItem(row, 0);
|
||||
cJSON* v = cJSON_GetArrayItem(row, 1);
|
||||
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
|
||||
const char* key = cJSON_GetStringValue(k);
|
||||
if (key && strcmp(key, "mint") == 0) mint_count++;
|
||||
}
|
||||
|
||||
if (mint_count > 0) {
|
||||
wallet_data_out->mint_urls = (char**)calloc((size_t)mint_count, sizeof(char*));
|
||||
if (!wallet_data_out->mint_urls) {
|
||||
cJSON_Delete(payload);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int mint_idx = 0;
|
||||
cJSON_ArrayForEach(row, payload) {
|
||||
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
|
||||
cJSON* k = cJSON_GetArrayItem(row, 0);
|
||||
cJSON* v = cJSON_GetArrayItem(row, 1);
|
||||
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
|
||||
|
||||
const char* key = cJSON_GetStringValue(k);
|
||||
const char* val = cJSON_GetStringValue(v);
|
||||
if (!key || !val) continue;
|
||||
|
||||
if (strcmp(key, "privkey") == 0) {
|
||||
strncpy(wallet_data_out->privkey, val, sizeof(wallet_data_out->privkey) - 1);
|
||||
wallet_data_out->privkey[sizeof(wallet_data_out->privkey) - 1] = '\0';
|
||||
} else if (strcmp(key, "mint") == 0 && mint_idx < mint_count) {
|
||||
wallet_data_out->mint_urls[mint_idx] = nip60_strdup(val);
|
||||
if (!wallet_data_out->mint_urls[mint_idx]) {
|
||||
cJSON_Delete(payload);
|
||||
nostr_nip60_free_wallet_data(wallet_data_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
mint_idx++;
|
||||
}
|
||||
}
|
||||
|
||||
wallet_data_out->mint_count = mint_idx;
|
||||
cJSON_Delete(payload);
|
||||
|
||||
if (wallet_data_out->privkey[0] == '\0' || wallet_data_out->mint_count == 0) {
|
||||
nostr_nip60_free_wallet_data(wallet_data_out);
|
||||
return NOSTR_ERROR_NIP60_INVALID_WALLET;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data) {
|
||||
if (!data) return;
|
||||
if (data->mint_urls) {
|
||||
for (int i = 0; i < data->mint_count; i++) {
|
||||
free(data->mint_urls[i]);
|
||||
}
|
||||
free(data->mint_urls);
|
||||
}
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!token_data || !signer || !token_data->mint_url ||
|
||||
!token_data->proofs || token_data->proof_count <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* payload = cJSON_CreateObject();
|
||||
if (!payload) return NULL;
|
||||
|
||||
cJSON_AddStringToObject(payload, "mint", token_data->mint_url);
|
||||
|
||||
cJSON* proofs = nostr_nip60_proofs_to_json(token_data->proofs, token_data->proof_count);
|
||||
if (!proofs) {
|
||||
cJSON_Delete(payload);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddItemToObject(payload, "proofs", proofs);
|
||||
|
||||
if (token_data->deleted_token_ids && token_data->deleted_count > 0) {
|
||||
cJSON* del = cJSON_CreateArray();
|
||||
if (!del) {
|
||||
cJSON_Delete(payload);
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < token_data->deleted_count; i++) {
|
||||
if (token_data->deleted_token_ids[i]) {
|
||||
cJSON_AddItemToArray(del, cJSON_CreateString(token_data->deleted_token_ids[i]));
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(payload, "del", del);
|
||||
}
|
||||
|
||||
char* plain = cJSON_PrintUnformatted(payload);
|
||||
cJSON_Delete(payload);
|
||||
if (!plain) return NULL;
|
||||
|
||||
char encrypted[131072];
|
||||
int rc = nip60_encrypt_self_with_signer(signer, plain, encrypted, sizeof(encrypted));
|
||||
free(plain);
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
return nostr_create_and_sign_event_with_signer(NOSTR_NIP60_TOKEN_KIND, encrypted, NULL, signer, timestamp);
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evt = nostr_nip60_create_token_event_with_signer(token_data, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip60_parse_token_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_token_data_t* token_data_out) {
|
||||
if (!event || !private_key || !token_data_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(token_data_out, 0, sizeof(*token_data_out));
|
||||
|
||||
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_TOKEN_KIND) {
|
||||
return NOSTR_ERROR_NIP60_INVALID_TOKEN;
|
||||
}
|
||||
|
||||
char decrypted[131072];
|
||||
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
cJSON* payload = cJSON_Parse(decrypted);
|
||||
if (!payload || !cJSON_IsObject(payload)) {
|
||||
cJSON_Delete(payload);
|
||||
return NOSTR_ERROR_NIP60_INVALID_TOKEN;
|
||||
}
|
||||
|
||||
cJSON* mint_item = cJSON_GetObjectItem(payload, "mint");
|
||||
cJSON* proofs_item = cJSON_GetObjectItem(payload, "proofs");
|
||||
if (!mint_item || !proofs_item || !cJSON_IsString(mint_item) || !cJSON_IsArray(proofs_item)) {
|
||||
cJSON_Delete(payload);
|
||||
return NOSTR_ERROR_NIP60_INVALID_TOKEN;
|
||||
}
|
||||
|
||||
const char* mint_url = cJSON_GetStringValue(mint_item);
|
||||
token_data_out->mint_url = nip60_strdup(mint_url ? mint_url : "");
|
||||
if (!token_data_out->mint_url) {
|
||||
cJSON_Delete(payload);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip60_proofs_from_json(proofs_item, &token_data_out->proofs, &token_data_out->proof_count);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(payload);
|
||||
nostr_nip60_free_token_data(token_data_out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
cJSON* del_item = cJSON_GetObjectItem(payload, "del");
|
||||
if (del_item && cJSON_IsArray(del_item)) {
|
||||
int del_count = cJSON_GetArraySize(del_item);
|
||||
if (del_count > 0) {
|
||||
token_data_out->deleted_token_ids = (char**)calloc((size_t)del_count, sizeof(char*));
|
||||
if (!token_data_out->deleted_token_ids) {
|
||||
cJSON_Delete(payload);
|
||||
nostr_nip60_free_token_data(token_data_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
for (int i = 0; i < del_count; i++) {
|
||||
cJSON* it = cJSON_GetArrayItem(del_item, i);
|
||||
if (!it || !cJSON_IsString(it)) continue;
|
||||
const char* s = cJSON_GetStringValue(it);
|
||||
if (!s) continue;
|
||||
token_data_out->deleted_token_ids[token_data_out->deleted_count] = nip60_strdup(s);
|
||||
if (!token_data_out->deleted_token_ids[token_data_out->deleted_count]) {
|
||||
cJSON_Delete(payload);
|
||||
nostr_nip60_free_token_data(token_data_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
token_data_out->deleted_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(payload);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data) {
|
||||
if (!data) return;
|
||||
|
||||
free(data->mint_url);
|
||||
nostr_nip60_free_proofs(data->proofs, data->proof_count);
|
||||
|
||||
if (data->deleted_token_ids) {
|
||||
for (int i = 0; i < data->deleted_count; i++) {
|
||||
free(data->deleted_token_ids[i]);
|
||||
}
|
||||
free(data->deleted_token_ids);
|
||||
}
|
||||
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!token_event_id || !signer) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
cJSON* e_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
|
||||
cJSON_AddItemToArray(e_tag, cJSON_CreateString(token_event_id));
|
||||
cJSON_AddItemToArray(tags, e_tag);
|
||||
|
||||
cJSON* k_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(k_tag, cJSON_CreateString("k"));
|
||||
cJSON_AddItemToArray(k_tag, cJSON_CreateString("7375"));
|
||||
cJSON_AddItemToArray(tags, k_tag);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(5, "NIP-60 token spent", tags, signer, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_token_deletion(const char* token_event_id,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip60_create_token_deletion_with_signer(token_event_id, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!remaining_proofs || !signer) return NULL;
|
||||
|
||||
nostr_nip60_token_data_t token = *remaining_proofs;
|
||||
token.deleted_token_ids = (char**)deleted_event_ids;
|
||||
token.deleted_count = deleted_count;
|
||||
|
||||
return nostr_nip60_create_token_event_with_signer(&token, signer, timestamp);
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_rollover_token(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip60_create_rollover_token_with_signer(remaining_proofs, deleted_event_ids, deleted_count,
|
||||
signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!history_data || !signer) return NULL;
|
||||
|
||||
cJSON* payload = cJSON_CreateArray();
|
||||
if (!payload) return NULL;
|
||||
|
||||
cJSON* dir = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(dir, cJSON_CreateString("direction"));
|
||||
cJSON_AddItemToArray(dir, cJSON_CreateString(nip60_direction_to_string(history_data->direction)));
|
||||
cJSON_AddItemToArray(payload, dir);
|
||||
|
||||
char amount_buf[32];
|
||||
snprintf(amount_buf, sizeof(amount_buf), "%llu", (unsigned long long)history_data->amount);
|
||||
cJSON* amt = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(amt, cJSON_CreateString("amount"));
|
||||
cJSON_AddItemToArray(amt, cJSON_CreateString(amount_buf));
|
||||
cJSON_AddItemToArray(payload, amt);
|
||||
|
||||
for (int i = 0; i < history_data->ref_count; i++) {
|
||||
cJSON* e = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].event_id));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].relay_hint));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString(nip60_marker_to_string(history_data->refs[i].marker)));
|
||||
cJSON_AddItemToArray(payload, e);
|
||||
}
|
||||
|
||||
char* plain = cJSON_PrintUnformatted(payload);
|
||||
cJSON_Delete(payload);
|
||||
if (!plain) return NULL;
|
||||
|
||||
char encrypted[65536];
|
||||
int rc = nip60_encrypt_self_with_signer(signer, plain, encrypted, sizeof(encrypted));
|
||||
free(plain);
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
for (int i = 0; i < history_data->ref_count; i++) {
|
||||
if (history_data->refs[i].marker != NOSTR_NIP60_REF_REDEEMED) continue;
|
||||
|
||||
cJSON* e = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].event_id));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].relay_hint));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString("redeemed"));
|
||||
cJSON_AddItemToArray(tags, e);
|
||||
}
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP60_HISTORY_KIND, encrypted, tags, signer, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip60_create_history_event_with_signer(history_data, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip60_parse_history_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_history_data_t* history_data_out) {
|
||||
if (!event || !private_key || !history_data_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(history_data_out, 0, sizeof(*history_data_out));
|
||||
|
||||
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_HISTORY_KIND) {
|
||||
return NOSTR_ERROR_NIP60_INVALID_HISTORY;
|
||||
}
|
||||
|
||||
char decrypted[65536];
|
||||
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
cJSON* payload = cJSON_Parse(decrypted);
|
||||
if (!payload || !cJSON_IsArray(payload)) {
|
||||
cJSON_Delete(payload);
|
||||
return NOSTR_ERROR_NIP60_INVALID_HISTORY;
|
||||
}
|
||||
|
||||
int ref_count = 0;
|
||||
cJSON* row = NULL;
|
||||
cJSON_ArrayForEach(row, payload) {
|
||||
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
|
||||
cJSON* k = cJSON_GetArrayItem(row, 0);
|
||||
if (!k || !cJSON_IsString(k)) continue;
|
||||
const char* key = cJSON_GetStringValue(k);
|
||||
if (key && strcmp(key, "e") == 0) ref_count++;
|
||||
}
|
||||
|
||||
if (ref_count > 0) {
|
||||
history_data_out->refs = (nostr_nip60_history_ref_t*)calloc((size_t)ref_count, sizeof(nostr_nip60_history_ref_t));
|
||||
if (!history_data_out->refs) {
|
||||
cJSON_Delete(payload);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int ref_idx = 0;
|
||||
cJSON_ArrayForEach(row, payload) {
|
||||
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
|
||||
|
||||
cJSON* k = cJSON_GetArrayItem(row, 0);
|
||||
cJSON* v = cJSON_GetArrayItem(row, 1);
|
||||
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
|
||||
|
||||
const char* key = cJSON_GetStringValue(k);
|
||||
const char* val = cJSON_GetStringValue(v);
|
||||
if (!key || !val) continue;
|
||||
|
||||
if (strcmp(key, "direction") == 0) {
|
||||
history_data_out->direction = nip60_string_to_direction(val);
|
||||
} else if (strcmp(key, "amount") == 0) {
|
||||
history_data_out->amount = (uint64_t)strtoull(val, NULL, 10);
|
||||
} else if (strcmp(key, "e") == 0 && ref_idx < ref_count) {
|
||||
strncpy(history_data_out->refs[ref_idx].event_id, val,
|
||||
sizeof(history_data_out->refs[ref_idx].event_id) - 1);
|
||||
|
||||
cJSON* relay_item = cJSON_GetArrayItem(row, 2);
|
||||
cJSON* marker_item = cJSON_GetArrayItem(row, 3);
|
||||
|
||||
if (relay_item && cJSON_IsString(relay_item)) {
|
||||
const char* relay = cJSON_GetStringValue(relay_item);
|
||||
if (relay) {
|
||||
strncpy(history_data_out->refs[ref_idx].relay_hint, relay,
|
||||
sizeof(history_data_out->refs[ref_idx].relay_hint) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (marker_item && cJSON_IsString(marker_item)) {
|
||||
const char* m = cJSON_GetStringValue(marker_item);
|
||||
history_data_out->refs[ref_idx].marker = nip60_string_to_marker(m);
|
||||
} else {
|
||||
history_data_out->refs[ref_idx].marker = NOSTR_NIP60_REF_CREATED;
|
||||
}
|
||||
|
||||
ref_idx++;
|
||||
}
|
||||
}
|
||||
|
||||
history_data_out->ref_count = ref_idx;
|
||||
cJSON_Delete(payload);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data) {
|
||||
if (!data) return;
|
||||
free(data->refs);
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!quote_id || !mint_url || !signer || expiration <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char encrypted[4096];
|
||||
int rc = nip60_encrypt_self_with_signer(signer, quote_id, encrypted, sizeof(encrypted));
|
||||
if (rc != NOSTR_SUCCESS) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
cJSON* exp_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(exp_tag, cJSON_CreateString("expiration"));
|
||||
|
||||
char exp_buf[32];
|
||||
snprintf(exp_buf, sizeof(exp_buf), "%lld", (long long)expiration);
|
||||
cJSON_AddItemToArray(exp_tag, cJSON_CreateString(exp_buf));
|
||||
cJSON_AddItemToArray(tags, exp_tag);
|
||||
|
||||
cJSON* mint_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(mint_tag, cJSON_CreateString("mint"));
|
||||
cJSON_AddItemToArray(mint_tag, cJSON_CreateString(mint_url));
|
||||
cJSON_AddItemToArray(tags, mint_tag);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP60_QUOTE_KIND, encrypted, tags, signer, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_quote_event(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip60_create_quote_event_with_signer(quote_id, mint_url, expiration, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip60_parse_quote_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
char* quote_id_out,
|
||||
size_t quote_id_size,
|
||||
char* mint_url_out,
|
||||
size_t mint_url_size,
|
||||
time_t* expiration_out) {
|
||||
if (!event || !private_key || !quote_id_out || quote_id_size == 0 ||
|
||||
!mint_url_out || mint_url_size == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
|
||||
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_QUOTE_KIND) {
|
||||
return NOSTR_ERROR_NIP60_INVALID_QUOTE;
|
||||
}
|
||||
|
||||
char decrypted[4096];
|
||||
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
strncpy(quote_id_out, decrypted, quote_id_size - 1);
|
||||
quote_id_out[quote_id_size - 1] = '\0';
|
||||
|
||||
mint_url_out[0] = '\0';
|
||||
if (expiration_out) *expiration_out = 0;
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (tags && cJSON_IsArray(tags)) {
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
||||
cJSON* k = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* v = cJSON_GetArrayItem(tag, 1);
|
||||
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
|
||||
|
||||
const char* key = cJSON_GetStringValue(k);
|
||||
const char* val = cJSON_GetStringValue(v);
|
||||
if (!key || !val) continue;
|
||||
|
||||
if (strcmp(key, "mint") == 0) {
|
||||
strncpy(mint_url_out, val, mint_url_size - 1);
|
||||
mint_url_out[mint_url_size - 1] = '\0';
|
||||
} else if (strcmp(key, "expiration") == 0 && expiration_out) {
|
||||
*expiration_out = (time_t)strtoll(val, NULL, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_wallet_filter(const char* pubkey_hex) {
|
||||
if (!pubkey_hex) return NULL;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
if (!filter) return NULL;
|
||||
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_WALLET_KIND));
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_TOKEN_KIND));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip60_create_history_filter(const char* pubkey_hex, time_t since) {
|
||||
if (!pubkey_hex) return NULL;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
if (!filter) return NULL;
|
||||
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_HISTORY_KIND));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
|
||||
if (since > 0) {
|
||||
cJSON_AddNumberToObject(filter, "since", (double)since);
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* NIP-60: Cashu Wallet
|
||||
* https://github.com/nostr-protocol/nips/blob/master/60.md
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_NIP060_H
|
||||
#define NOSTR_NIP060_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "nip001.h"
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define NOSTR_NIP60_WALLET_KIND 17375
|
||||
#define NOSTR_NIP60_TOKEN_KIND 7375
|
||||
#define NOSTR_NIP60_HISTORY_KIND 7376
|
||||
#define NOSTR_NIP60_QUOTE_KIND 7374
|
||||
|
||||
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 65
|
||||
#define NOSTR_CASHU_PUBKEY_HEX_SIZE 67
|
||||
#define NOSTR_CASHU_EVENT_ID_HEX_SIZE 65
|
||||
|
||||
typedef struct {
|
||||
char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE];
|
||||
uint64_t amount;
|
||||
char* secret;
|
||||
char* C;
|
||||
} nostr_cashu_proof_t;
|
||||
|
||||
typedef struct {
|
||||
char privkey[65];
|
||||
char** mint_urls;
|
||||
int mint_count;
|
||||
} nostr_nip60_wallet_data_t;
|
||||
|
||||
typedef struct {
|
||||
char* mint_url;
|
||||
nostr_cashu_proof_t* proofs;
|
||||
int proof_count;
|
||||
char** deleted_token_ids;
|
||||
int deleted_count;
|
||||
} nostr_nip60_token_data_t;
|
||||
|
||||
typedef enum {
|
||||
NOSTR_NIP60_DIRECTION_IN = 0,
|
||||
NOSTR_NIP60_DIRECTION_OUT = 1
|
||||
} nostr_nip60_direction_t;
|
||||
|
||||
typedef enum {
|
||||
NOSTR_NIP60_REF_CREATED = 0,
|
||||
NOSTR_NIP60_REF_DESTROYED = 1,
|
||||
NOSTR_NIP60_REF_REDEEMED = 2
|
||||
} nostr_nip60_ref_marker_t;
|
||||
|
||||
typedef struct {
|
||||
char event_id[NOSTR_CASHU_EVENT_ID_HEX_SIZE];
|
||||
char relay_hint[256];
|
||||
nostr_nip60_ref_marker_t marker;
|
||||
} nostr_nip60_history_ref_t;
|
||||
|
||||
typedef struct {
|
||||
nostr_nip60_direction_t direction;
|
||||
uint64_t amount;
|
||||
nostr_nip60_history_ref_t* refs;
|
||||
int ref_count;
|
||||
} nostr_nip60_history_data_t;
|
||||
|
||||
cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_wallet_event_with_signer(const nostr_nip60_wallet_data_t* wallet_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip60_parse_wallet_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_wallet_data_t* wallet_data_out);
|
||||
|
||||
void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data);
|
||||
|
||||
cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_token_event_with_signer(const nostr_nip60_token_data_t* token_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip60_parse_token_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_token_data_t* token_data_out);
|
||||
|
||||
void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data);
|
||||
|
||||
cJSON* nostr_nip60_create_token_deletion(const char* token_event_id,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_token_deletion_with_signer(const char* token_event_id,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
cJSON* nostr_nip60_create_rollover_token(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_rollover_token_with_signer(const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_history_event_with_signer(const nostr_nip60_history_data_t* history_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip60_parse_history_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_history_data_t* history_data_out);
|
||||
|
||||
void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data);
|
||||
|
||||
cJSON* nostr_nip60_create_quote_event(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip60_create_quote_event_with_signer(const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip60_parse_quote_event(cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
char* quote_id_out,
|
||||
size_t quote_id_size,
|
||||
char* mint_url_out,
|
||||
size_t mint_url_size,
|
||||
time_t* expiration_out);
|
||||
|
||||
uint64_t nostr_nip60_sum_proofs(const nostr_cashu_proof_t* proofs, int proof_count);
|
||||
|
||||
cJSON* nostr_nip60_proofs_to_json(const nostr_cashu_proof_t* proofs, int proof_count);
|
||||
|
||||
int nostr_nip60_proofs_from_json(cJSON* json_array,
|
||||
nostr_cashu_proof_t** proofs_out,
|
||||
int* proof_count_out);
|
||||
|
||||
void nostr_nip60_free_proofs(nostr_cashu_proof_t* proofs, int proof_count);
|
||||
|
||||
cJSON* nostr_nip60_create_wallet_filter(const char* pubkey_hex);
|
||||
|
||||
cJSON* nostr_nip60_create_history_filter(const char* pubkey_hex, time_t since);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_NIP060_H */
|
||||
@@ -0,0 +1,604 @@
|
||||
/*
|
||||
* NIP-61: Nutzaps Implementation
|
||||
* https://github.com/nostr-protocol/nips/blob/master/61.md
|
||||
*/
|
||||
|
||||
#include "nip061.h"
|
||||
#include "nip060.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static char* nip61_strdup(const char* s) {
|
||||
if (!s) return NULL;
|
||||
size_t len = strlen(s);
|
||||
char* out = (char*)malloc(len + 1);
|
||||
if (!out) return NULL;
|
||||
memcpy(out, s, len + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
static int nip61_mint_in_info(const char* mint_url, const nostr_nip61_nutzap_info_t* info) {
|
||||
if (!mint_url || !info) return 0;
|
||||
for (int i = 0; i < info->mint_count; i++) {
|
||||
if (info->mints[i].url && strcmp(info->mints[i].url, mint_url) == 0) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!info || !signer || info->mint_count <= 0 || info->relay_count <= 0 || info->pubkey[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
for (int i = 0; i < info->relay_count; i++) {
|
||||
if (!info->relay_urls || !info->relay_urls[i]) continue;
|
||||
cJSON* relay = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(relay, cJSON_CreateString("relay"));
|
||||
cJSON_AddItemToArray(relay, cJSON_CreateString(info->relay_urls[i]));
|
||||
cJSON_AddItemToArray(tags, relay);
|
||||
}
|
||||
|
||||
for (int i = 0; i < info->mint_count; i++) {
|
||||
if (!info->mints || !info->mints[i].url) continue;
|
||||
cJSON* mint = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(mint, cJSON_CreateString("mint"));
|
||||
cJSON_AddItemToArray(mint, cJSON_CreateString(info->mints[i].url));
|
||||
for (int u = 0; u < info->mints[i].unit_count; u++) {
|
||||
if (info->mints[i].units && info->mints[i].units[u]) {
|
||||
cJSON_AddItemToArray(mint, cJSON_CreateString(info->mints[i].units[u]));
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToArray(tags, mint);
|
||||
}
|
||||
|
||||
cJSON* pub = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(pub, cJSON_CreateString("pubkey"));
|
||||
cJSON_AddItemToArray(pub, cJSON_CreateString(info->pubkey));
|
||||
cJSON_AddItemToArray(tags, pub);
|
||||
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP61_NUTZAP_INFO_KIND, "", tags, signer, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip61_create_nutzap_info_event_with_signer(info, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip61_parse_nutzap_info_event(cJSON* event,
|
||||
nostr_nip61_nutzap_info_t* info_out) {
|
||||
if (!event || !info_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
memset(info_out, 0, sizeof(*info_out));
|
||||
|
||||
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP61_NUTZAP_INFO_KIND ||
|
||||
!tags || !cJSON_IsArray(tags)) {
|
||||
return NOSTR_ERROR_NIP61_INVALID_INFO;
|
||||
}
|
||||
|
||||
int relay_count = 0;
|
||||
int mint_count = 0;
|
||||
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
||||
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
|
||||
if (!t0 || !cJSON_IsString(t0)) continue;
|
||||
const char* key = cJSON_GetStringValue(t0);
|
||||
if (!key) continue;
|
||||
if (strcmp(key, "relay") == 0) relay_count++;
|
||||
if (strcmp(key, "mint") == 0) mint_count++;
|
||||
}
|
||||
|
||||
if (relay_count > 0) {
|
||||
info_out->relay_urls = (char**)calloc((size_t)relay_count, sizeof(char*));
|
||||
if (!info_out->relay_urls) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
if (mint_count > 0) {
|
||||
info_out->mints = (nostr_nip61_mint_entry_t*)calloc((size_t)mint_count, sizeof(nostr_nip61_mint_entry_t));
|
||||
if (!info_out->mints) {
|
||||
nostr_nip61_free_nutzap_info(info_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int relay_idx = 0;
|
||||
int mint_idx = 0;
|
||||
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
||||
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* t1 = cJSON_GetArrayItem(tag, 1);
|
||||
if (!t0 || !t1 || !cJSON_IsString(t0) || !cJSON_IsString(t1)) continue;
|
||||
|
||||
const char* key = cJSON_GetStringValue(t0);
|
||||
const char* val = cJSON_GetStringValue(t1);
|
||||
if (!key || !val) continue;
|
||||
|
||||
if (strcmp(key, "relay") == 0 && relay_idx < relay_count) {
|
||||
info_out->relay_urls[relay_idx] = nip61_strdup(val);
|
||||
if (!info_out->relay_urls[relay_idx]) {
|
||||
nostr_nip61_free_nutzap_info(info_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
relay_idx++;
|
||||
} else if (strcmp(key, "mint") == 0 && mint_idx < mint_count) {
|
||||
info_out->mints[mint_idx].url = nip61_strdup(val);
|
||||
if (!info_out->mints[mint_idx].url) {
|
||||
nostr_nip61_free_nutzap_info(info_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
int total = cJSON_GetArraySize(tag);
|
||||
int unit_count = (total > 2) ? (total - 2) : 0;
|
||||
if (unit_count > 0) {
|
||||
info_out->mints[mint_idx].units = (char**)calloc((size_t)unit_count, sizeof(char*));
|
||||
if (!info_out->mints[mint_idx].units) {
|
||||
nostr_nip61_free_nutzap_info(info_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
for (int u = 0; u < unit_count; u++) {
|
||||
cJSON* unit_item = cJSON_GetArrayItem(tag, u + 2);
|
||||
if (unit_item && cJSON_IsString(unit_item)) {
|
||||
const char* us = cJSON_GetStringValue(unit_item);
|
||||
if (us) {
|
||||
info_out->mints[mint_idx].units[info_out->mints[mint_idx].unit_count] = nip61_strdup(us);
|
||||
if (!info_out->mints[mint_idx].units[info_out->mints[mint_idx].unit_count]) {
|
||||
nostr_nip61_free_nutzap_info(info_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
info_out->mints[mint_idx].unit_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mint_idx++;
|
||||
} else if (strcmp(key, "pubkey") == 0) {
|
||||
strncpy(info_out->pubkey, val, sizeof(info_out->pubkey) - 1);
|
||||
info_out->pubkey[sizeof(info_out->pubkey) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
info_out->relay_count = relay_idx;
|
||||
info_out->mint_count = mint_idx;
|
||||
|
||||
if (info_out->pubkey[0] == '\0' || info_out->mint_count == 0) {
|
||||
nostr_nip61_free_nutzap_info(info_out);
|
||||
return NOSTR_ERROR_NIP61_INVALID_INFO;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info) {
|
||||
if (!info) return;
|
||||
|
||||
if (info->relay_urls) {
|
||||
for (int i = 0; i < info->relay_count; i++) {
|
||||
free(info->relay_urls[i]);
|
||||
}
|
||||
free(info->relay_urls);
|
||||
}
|
||||
|
||||
if (info->mints) {
|
||||
for (int i = 0; i < info->mint_count; i++) {
|
||||
free(info->mints[i].url);
|
||||
if (info->mints[i].units) {
|
||||
for (int u = 0; u < info->mints[i].unit_count; u++) {
|
||||
free(info->mints[i].units[u]);
|
||||
}
|
||||
free(info->mints[i].units);
|
||||
}
|
||||
}
|
||||
free(info->mints);
|
||||
}
|
||||
|
||||
memset(info, 0, sizeof(*info));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!nutzap_data || !signer || !nutzap_data->mint_url ||
|
||||
!nutzap_data->proofs || nutzap_data->proof_count <= 0 ||
|
||||
nutzap_data->recipient_pubkey[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* tags = cJSON_CreateArray();
|
||||
if (!tags) return NULL;
|
||||
|
||||
for (int i = 0; i < nutzap_data->proof_count; i++) {
|
||||
cJSON* proof_json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(proof_json, "id", nutzap_data->proofs[i].id);
|
||||
cJSON_AddNumberToObject(proof_json, "amount", (double)nutzap_data->proofs[i].amount);
|
||||
cJSON_AddStringToObject(proof_json, "secret", nutzap_data->proofs[i].secret ? nutzap_data->proofs[i].secret : "");
|
||||
cJSON_AddStringToObject(proof_json, "C", nutzap_data->proofs[i].C ? nutzap_data->proofs[i].C : "");
|
||||
|
||||
char* proof_str = cJSON_PrintUnformatted(proof_json);
|
||||
cJSON_Delete(proof_json);
|
||||
if (!proof_str) {
|
||||
cJSON_Delete(tags);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* proof_tag = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(proof_tag, cJSON_CreateString("proof"));
|
||||
cJSON_AddItemToArray(proof_tag, cJSON_CreateString(proof_str));
|
||||
cJSON_AddItemToArray(tags, proof_tag);
|
||||
free(proof_str);
|
||||
}
|
||||
|
||||
cJSON* u = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(u, cJSON_CreateString("u"));
|
||||
cJSON_AddItemToArray(u, cJSON_CreateString(nutzap_data->mint_url));
|
||||
cJSON_AddItemToArray(tags, u);
|
||||
|
||||
cJSON* p = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(p, cJSON_CreateString("p"));
|
||||
cJSON_AddItemToArray(p, cJSON_CreateString(nutzap_data->recipient_pubkey));
|
||||
cJSON_AddItemToArray(tags, p);
|
||||
|
||||
if (nutzap_data->nutzapped_event_id[0] != '\0') {
|
||||
cJSON* e = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_data->nutzapped_event_id));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_data->nutzapped_relay_hint));
|
||||
cJSON_AddItemToArray(tags, e);
|
||||
|
||||
if (nutzap_data->nutzapped_kind > 0) {
|
||||
cJSON* k = cJSON_CreateArray();
|
||||
char kind_buf[16];
|
||||
snprintf(kind_buf, sizeof(kind_buf), "%d", nutzap_data->nutzapped_kind);
|
||||
cJSON_AddItemToArray(k, cJSON_CreateString("k"));
|
||||
cJSON_AddItemToArray(k, cJSON_CreateString(kind_buf));
|
||||
cJSON_AddItemToArray(tags, k);
|
||||
}
|
||||
}
|
||||
|
||||
const char* content = nutzap_data->content ? nutzap_data->content : "";
|
||||
cJSON* evt = nostr_create_and_sign_event_with_signer(NOSTR_NIP61_NUTZAP_KIND, content, tags, signer, timestamp);
|
||||
cJSON_Delete(tags);
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
const unsigned char* sender_private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!sender_private_key) return NULL;
|
||||
|
||||
signer = nostr_signer_local(sender_private_key);
|
||||
if (!signer) return NULL;
|
||||
|
||||
evt = nostr_nip61_create_nutzap_event_with_signer(nutzap_data, signer, timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip61_parse_nutzap_event(cJSON* event,
|
||||
nostr_nip61_nutzap_data_t* nutzap_data_out) {
|
||||
if (!event || !nutzap_data_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
memset(nutzap_data_out, 0, sizeof(*nutzap_data_out));
|
||||
|
||||
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
cJSON* content = cJSON_GetObjectItem(event, "content");
|
||||
|
||||
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP61_NUTZAP_KIND ||
|
||||
!tags || !cJSON_IsArray(tags) || !content || !cJSON_IsString(content)) {
|
||||
return NOSTR_ERROR_NIP61_INVALID_NUTZAP;
|
||||
}
|
||||
|
||||
const char* content_str = cJSON_GetStringValue(content);
|
||||
nutzap_data_out->content = nip61_strdup(content_str ? content_str : "");
|
||||
if (!nutzap_data_out->content) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
int proof_count = 0;
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
||||
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
|
||||
if (t0 && cJSON_IsString(t0) && strcmp(cJSON_GetStringValue(t0), "proof") == 0) {
|
||||
proof_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (proof_count > 0) {
|
||||
nutzap_data_out->proofs = (nostr_cashu_proof_t*)calloc((size_t)proof_count, sizeof(nostr_cashu_proof_t));
|
||||
if (!nutzap_data_out->proofs) {
|
||||
nostr_nip61_free_nutzap_data(nutzap_data_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int proof_idx = 0;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
||||
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* t1 = cJSON_GetArrayItem(tag, 1);
|
||||
if (!t0 || !t1 || !cJSON_IsString(t0) || !cJSON_IsString(t1)) continue;
|
||||
|
||||
const char* key = cJSON_GetStringValue(t0);
|
||||
const char* val = cJSON_GetStringValue(t1);
|
||||
if (!key || !val) continue;
|
||||
|
||||
if (strcmp(key, "proof") == 0 && proof_idx < proof_count) {
|
||||
cJSON* proof_obj = cJSON_Parse(val);
|
||||
if (!proof_obj || !cJSON_IsObject(proof_obj)) {
|
||||
cJSON_Delete(proof_obj);
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* id = cJSON_GetObjectItem(proof_obj, "id");
|
||||
cJSON* amount = cJSON_GetObjectItem(proof_obj, "amount");
|
||||
cJSON* secret = cJSON_GetObjectItem(proof_obj, "secret");
|
||||
cJSON* C = cJSON_GetObjectItem(proof_obj, "C");
|
||||
|
||||
if (id && cJSON_IsString(id) && amount && cJSON_IsNumber(amount) &&
|
||||
secret && cJSON_IsString(secret) && C && cJSON_IsString(C)) {
|
||||
const char* id_s = cJSON_GetStringValue(id);
|
||||
const char* sec_s = cJSON_GetStringValue(secret);
|
||||
const char* c_s = cJSON_GetStringValue(C);
|
||||
|
||||
if (id_s && sec_s && c_s) {
|
||||
strncpy(nutzap_data_out->proofs[proof_idx].id, id_s,
|
||||
sizeof(nutzap_data_out->proofs[proof_idx].id) - 1);
|
||||
nutzap_data_out->proofs[proof_idx].amount = (uint64_t)cJSON_GetNumberValue(amount);
|
||||
nutzap_data_out->proofs[proof_idx].secret = nip61_strdup(sec_s);
|
||||
nutzap_data_out->proofs[proof_idx].C = nip61_strdup(c_s);
|
||||
if (!nutzap_data_out->proofs[proof_idx].secret || !nutzap_data_out->proofs[proof_idx].C) {
|
||||
cJSON_Delete(proof_obj);
|
||||
nostr_nip61_free_nutzap_data(nutzap_data_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
proof_idx++;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(proof_obj);
|
||||
} else if (strcmp(key, "u") == 0) {
|
||||
free(nutzap_data_out->mint_url);
|
||||
nutzap_data_out->mint_url = nip61_strdup(val);
|
||||
if (!nutzap_data_out->mint_url) {
|
||||
nostr_nip61_free_nutzap_data(nutzap_data_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
} else if (strcmp(key, "p") == 0) {
|
||||
strncpy(nutzap_data_out->recipient_pubkey, val, sizeof(nutzap_data_out->recipient_pubkey) - 1);
|
||||
} else if (strcmp(key, "e") == 0) {
|
||||
strncpy(nutzap_data_out->nutzapped_event_id, val, sizeof(nutzap_data_out->nutzapped_event_id) - 1);
|
||||
cJSON* t2 = cJSON_GetArrayItem(tag, 2);
|
||||
if (t2 && cJSON_IsString(t2)) {
|
||||
const char* relay = cJSON_GetStringValue(t2);
|
||||
if (relay) strncpy(nutzap_data_out->nutzapped_relay_hint, relay,
|
||||
sizeof(nutzap_data_out->nutzapped_relay_hint) - 1);
|
||||
}
|
||||
} else if (strcmp(key, "k") == 0) {
|
||||
nutzap_data_out->nutzapped_kind = atoi(val);
|
||||
}
|
||||
}
|
||||
|
||||
nutzap_data_out->proof_count = proof_idx;
|
||||
|
||||
if (!nutzap_data_out->mint_url || nutzap_data_out->recipient_pubkey[0] == '\0' || proof_idx == 0) {
|
||||
nostr_nip61_free_nutzap_data(nutzap_data_out);
|
||||
return NOSTR_ERROR_NIP61_INVALID_NUTZAP;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data) {
|
||||
if (!data) return;
|
||||
free(data->content);
|
||||
free(data->mint_url);
|
||||
nostr_nip60_free_proofs(data->proofs, data->proof_count);
|
||||
memset(data, 0, sizeof(*data));
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp) {
|
||||
if (!nutzap_event_id || !sender_pubkey || !created_token_event_id || !signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
nostr_nip60_history_ref_t refs[1];
|
||||
memset(refs, 0, sizeof(refs));
|
||||
strncpy(refs[0].event_id, created_token_event_id, sizeof(refs[0].event_id) - 1);
|
||||
if (created_token_relay_hint) {
|
||||
strncpy(refs[0].relay_hint, created_token_relay_hint, sizeof(refs[0].relay_hint) - 1);
|
||||
}
|
||||
refs[0].marker = NOSTR_NIP60_REF_CREATED;
|
||||
|
||||
nostr_nip60_history_data_t hist;
|
||||
memset(&hist, 0, sizeof(hist));
|
||||
hist.direction = NOSTR_NIP60_DIRECTION_IN;
|
||||
hist.amount = amount;
|
||||
hist.refs = refs;
|
||||
hist.ref_count = 1;
|
||||
|
||||
cJSON* evt = nostr_nip60_create_history_event_with_signer(&hist, signer, timestamp);
|
||||
if (!evt) return NULL;
|
||||
|
||||
cJSON* tags = cJSON_GetObjectItem(evt, "tags");
|
||||
if (!tags || !cJSON_IsArray(tags)) return evt;
|
||||
|
||||
cJSON* e = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_event_id));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_relay_hint ? nutzap_relay_hint : ""));
|
||||
cJSON_AddItemToArray(e, cJSON_CreateString("redeemed"));
|
||||
cJSON_AddItemToArray(tags, e);
|
||||
|
||||
cJSON* p = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(p, cJSON_CreateString("p"));
|
||||
cJSON_AddItemToArray(p, cJSON_CreateString(sender_pubkey));
|
||||
cJSON_AddItemToArray(tags, p);
|
||||
|
||||
return evt;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp) {
|
||||
nostr_signer_t* signer;
|
||||
cJSON* evt;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = nostr_signer_local(private_key);
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
evt = nostr_nip61_create_redemption_event_with_signer(nutzap_event_id,
|
||||
nutzap_relay_hint,
|
||||
sender_pubkey,
|
||||
created_token_event_id,
|
||||
created_token_relay_hint,
|
||||
amount,
|
||||
signer,
|
||||
timestamp);
|
||||
nostr_signer_free(signer);
|
||||
return evt;
|
||||
}
|
||||
|
||||
int nostr_nip61_verify_nutzap(cJSON* nutzap_event, cJSON* nutzap_info_event) {
|
||||
if (!nutzap_event || !nutzap_info_event) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
nostr_nip61_nutzap_data_t nutzap;
|
||||
memset(&nutzap, 0, sizeof(nutzap));
|
||||
int rc = nostr_nip61_parse_nutzap_event(nutzap_event, &nutzap);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
nostr_nip61_nutzap_info_t info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
rc = nostr_nip61_parse_nutzap_info_event(nutzap_info_event, &info);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
nostr_nip61_free_nutzap_data(&nutzap);
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (!nip61_mint_in_info(nutzap.mint_url, &info)) {
|
||||
nostr_nip61_free_nutzap_data(&nutzap);
|
||||
nostr_nip61_free_nutzap_info(&info);
|
||||
return NOSTR_ERROR_NIP61_MINT_MISMATCH;
|
||||
}
|
||||
|
||||
int key_match = 0;
|
||||
if (strlen(info.pubkey) == 64) {
|
||||
for (int i = 0; i < nutzap.proof_count; i++) {
|
||||
if (nutzap.proofs[i].secret && strstr(nutzap.proofs[i].secret, info.pubkey) != NULL) {
|
||||
key_match = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (strlen(info.pubkey) == 66 && strncmp(info.pubkey, "02", 2) == 0) {
|
||||
const char* xonly = info.pubkey + 2;
|
||||
for (int i = 0; i < nutzap.proof_count; i++) {
|
||||
if (nutzap.proofs[i].secret && strstr(nutzap.proofs[i].secret, xonly) != NULL) {
|
||||
key_match = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nostr_nip61_free_nutzap_data(&nutzap);
|
||||
nostr_nip61_free_nutzap_info(&info);
|
||||
|
||||
if (!key_match) {
|
||||
return NOSTR_ERROR_NIP61_PUBKEY_MISMATCH;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex) {
|
||||
if (!pubkey_hex) return NULL;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
if (!filter) return NULL;
|
||||
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP61_NUTZAP_INFO_KIND));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
cJSON* authors = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
|
||||
cJSON_AddItemToObject(filter, "authors", authors);
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_filter(const char* recipient_pubkey_hex,
|
||||
const char** mint_urls,
|
||||
int mint_count,
|
||||
time_t since) {
|
||||
if (!recipient_pubkey_hex) return NULL;
|
||||
|
||||
cJSON* filter = cJSON_CreateObject();
|
||||
if (!filter) return NULL;
|
||||
|
||||
cJSON* kinds = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP61_NUTZAP_KIND));
|
||||
cJSON_AddItemToObject(filter, "kinds", kinds);
|
||||
|
||||
cJSON* pvals = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(pvals, cJSON_CreateString(recipient_pubkey_hex));
|
||||
cJSON_AddItemToObject(filter, "#p", pvals);
|
||||
|
||||
if (mint_urls && mint_count > 0) {
|
||||
cJSON* uvals = cJSON_CreateArray();
|
||||
for (int i = 0; i < mint_count; i++) {
|
||||
if (mint_urls[i]) cJSON_AddItemToArray(uvals, cJSON_CreateString(mint_urls[i]));
|
||||
}
|
||||
cJSON_AddItemToObject(filter, "#u", uvals);
|
||||
}
|
||||
|
||||
if (since > 0) {
|
||||
cJSON_AddNumberToObject(filter, "since", (double)since);
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* NIP-61: Nutzaps
|
||||
* https://github.com/nostr-protocol/nips/blob/master/61.md
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_NIP061_H
|
||||
#define NOSTR_NIP061_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "nip001.h"
|
||||
#include "nip060.h"
|
||||
#include "nostr_common.h"
|
||||
#include "nostr_signer.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define NOSTR_NIP61_NUTZAP_INFO_KIND 10019
|
||||
#define NOSTR_NIP61_NUTZAP_KIND 9321
|
||||
|
||||
typedef struct {
|
||||
char* url;
|
||||
char** units;
|
||||
int unit_count;
|
||||
} nostr_nip61_mint_entry_t;
|
||||
|
||||
typedef struct {
|
||||
char** relay_urls;
|
||||
int relay_count;
|
||||
nostr_nip61_mint_entry_t* mints;
|
||||
int mint_count;
|
||||
char pubkey[NOSTR_CASHU_PUBKEY_HEX_SIZE];
|
||||
} nostr_nip61_nutzap_info_t;
|
||||
|
||||
typedef struct {
|
||||
char* content;
|
||||
nostr_cashu_proof_t* proofs;
|
||||
int proof_count;
|
||||
char* mint_url;
|
||||
char recipient_pubkey[65];
|
||||
char nutzapped_event_id[NOSTR_CASHU_EVENT_ID_HEX_SIZE];
|
||||
char nutzapped_relay_hint[256];
|
||||
int nutzapped_kind;
|
||||
} nostr_nip61_nutzap_data_t;
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_nutzap_info_event_with_signer(const nostr_nip61_nutzap_info_t* info,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip61_parse_nutzap_info_event(cJSON* event,
|
||||
nostr_nip61_nutzap_info_t* info_out);
|
||||
|
||||
void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info);
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
const unsigned char* sender_private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_nutzap_event_with_signer(const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip61_parse_nutzap_event(cJSON* event,
|
||||
nostr_nip61_nutzap_data_t* nutzap_data_out);
|
||||
|
||||
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data);
|
||||
|
||||
cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
cJSON* nostr_nip61_create_redemption_event_with_signer(const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
nostr_signer_t* signer,
|
||||
time_t timestamp);
|
||||
|
||||
int nostr_nip61_verify_nutzap(cJSON* nutzap_event, cJSON* nutzap_info_event);
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex);
|
||||
|
||||
cJSON* nostr_nip61_create_nutzap_filter(const char* recipient_pubkey_hex,
|
||||
const char** mint_urls,
|
||||
int mint_count,
|
||||
time_t since);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_NIP061_H */
|
||||
@@ -52,6 +52,38 @@ const char* nostr_strerror(int error_code) {
|
||||
case NOSTR_ERROR_NIP42_INVALID_MESSAGE_FORMAT: return "NIP-42: Invalid message format";
|
||||
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT: return "NIP-42: Challenge too short";
|
||||
case NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG: return "NIP-42: Challenge too long";
|
||||
case NOSTR_ERROR_NIP46_INVALID_BUNKER_URL: return "NIP-46: Invalid bunker URL";
|
||||
case NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT: return "NIP-46: Invalid nostrconnect URL";
|
||||
case NOSTR_ERROR_NIP46_INVALID_REQUEST: return "NIP-46: Invalid request payload";
|
||||
case NOSTR_ERROR_NIP46_INVALID_RESPONSE: return "NIP-46: Invalid response payload";
|
||||
case NOSTR_ERROR_NIP46_ENCRYPTION_FAILED: return "NIP-46: Encryption failed";
|
||||
case NOSTR_ERROR_NIP46_DECRYPTION_FAILED: return "NIP-46: Decryption failed";
|
||||
case NOSTR_ERROR_NIP46_CONNECTION_FAILED: return "NIP-46: Connection failed";
|
||||
case NOSTR_ERROR_NIP46_TIMEOUT: return "NIP-46: Timeout";
|
||||
case NOSTR_ERROR_NIP46_SECRET_MISMATCH: return "NIP-46: Secret mismatch";
|
||||
case NOSTR_ERROR_NIP46_UNKNOWN_METHOD: return "NIP-46: Unknown method";
|
||||
case NOSTR_ERROR_NIP46_AUTH_CHALLENGE: return "NIP-46: Auth challenge required";
|
||||
case NOSTR_ERROR_NIP46_NOT_CONNECTED: return "NIP-46: Not connected";
|
||||
case NOSTR_ERROR_NIP60_INVALID_WALLET: return "NIP-60: Invalid wallet event";
|
||||
case NOSTR_ERROR_NIP60_INVALID_TOKEN: return "NIP-60: Invalid token event";
|
||||
case NOSTR_ERROR_NIP60_INVALID_HISTORY: return "NIP-60: Invalid history event";
|
||||
case NOSTR_ERROR_NIP60_INVALID_QUOTE: return "NIP-60: Invalid quote event";
|
||||
case NOSTR_ERROR_NIP60_DECRYPT_FAILED: return "NIP-60: Decryption failed";
|
||||
case NOSTR_ERROR_NIP60_INVALID_PROOFS: return "NIP-60: Invalid proofs payload";
|
||||
case NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS: return "NIP-60: Insufficient funds";
|
||||
case NOSTR_ERROR_NIP61_INVALID_INFO: return "NIP-61: Invalid info event";
|
||||
case NOSTR_ERROR_NIP61_INVALID_NUTZAP: return "NIP-61: Invalid nutzap event";
|
||||
case NOSTR_ERROR_NIP61_MINT_MISMATCH: return "NIP-61: Mint mismatch";
|
||||
case NOSTR_ERROR_NIP61_PUBKEY_MISMATCH: return "NIP-61: Pubkey mismatch";
|
||||
case NOSTR_ERROR_NIP61_VERIFICATION_FAILED: return "NIP-61: Verification failed";
|
||||
case NOSTR_ERROR_CASHU_HTTP_FAILED: return "Cashu: HTTP request failed";
|
||||
case NOSTR_ERROR_CASHU_JSON_PARSE_FAILED: return "Cashu: JSON parsing failed";
|
||||
case NOSTR_ERROR_CASHU_MINT_ERROR: return "Cashu: Mint returned an error";
|
||||
case NOSTR_ERROR_CASHU_QUOTE_NOT_PAID: return "Cashu: Quote not paid";
|
||||
case NOSTR_ERROR_CASHU_QUOTE_EXPIRED: return "Cashu: Quote expired";
|
||||
case NOSTR_ERROR_CASHU_PROOFS_SPENT: return "Cashu: One or more proofs are already spent";
|
||||
case NOSTR_ERROR_CASHU_CRYPTO_FAILED: return "Cashu: Cryptographic operation failed";
|
||||
case NOSTR_ERROR_CASHU_INVALID_KEYSET: return "Cashu: Invalid keyset";
|
||||
default: return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
+47
-59
@@ -28,6 +28,9 @@
|
||||
#define NOSTR_ERROR_NIP05_NAME_NOT_FOUND -19
|
||||
#define NOSTR_ERROR_NIP05_PUBKEY_MISMATCH -20
|
||||
#define NOSTR_ERROR_EVENT_INVALID_STRUCTURE -30
|
||||
/* nsigner remote backend errors */
|
||||
#define NOSTR_ERROR_NSIGNER_POLICY_DENIED -2001
|
||||
#define NOSTR_ERROR_NSIGNER_INDEX_NOT_ALLOWED -2002
|
||||
#define NOSTR_ERROR_EVENT_INVALID_ID -31
|
||||
#define NOSTR_ERROR_EVENT_INVALID_PUBKEY -32
|
||||
#define NOSTR_ERROR_EVENT_INVALID_SIGNATURE -33
|
||||
@@ -62,6 +65,46 @@
|
||||
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_SHORT -207
|
||||
#define NOSTR_ERROR_NIP42_CHALLENGE_TOO_LONG -208
|
||||
|
||||
// NIP-46 Remote Signing error codes
|
||||
#define NOSTR_ERROR_NIP46_INVALID_BUNKER_URL -300
|
||||
#define NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT -301
|
||||
#define NOSTR_ERROR_NIP46_INVALID_REQUEST -302
|
||||
#define NOSTR_ERROR_NIP46_INVALID_RESPONSE -303
|
||||
#define NOSTR_ERROR_NIP46_ENCRYPTION_FAILED -304
|
||||
#define NOSTR_ERROR_NIP46_DECRYPTION_FAILED -305
|
||||
#define NOSTR_ERROR_NIP46_CONNECTION_FAILED -306
|
||||
#define NOSTR_ERROR_NIP46_TIMEOUT -307
|
||||
#define NOSTR_ERROR_NIP46_SECRET_MISMATCH -308
|
||||
#define NOSTR_ERROR_NIP46_UNKNOWN_METHOD -309
|
||||
#define NOSTR_ERROR_NIP46_AUTH_CHALLENGE -310
|
||||
#define NOSTR_ERROR_NIP46_NOT_CONNECTED -311
|
||||
|
||||
// NIP-60 Cashu Wallet error codes
|
||||
#define NOSTR_ERROR_NIP60_INVALID_WALLET -400
|
||||
#define NOSTR_ERROR_NIP60_INVALID_TOKEN -401
|
||||
#define NOSTR_ERROR_NIP60_INVALID_HISTORY -402
|
||||
#define NOSTR_ERROR_NIP60_INVALID_QUOTE -403
|
||||
#define NOSTR_ERROR_NIP60_DECRYPT_FAILED -404
|
||||
#define NOSTR_ERROR_NIP60_INVALID_PROOFS -405
|
||||
#define NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS -406
|
||||
|
||||
// NIP-61 Nutzap error codes
|
||||
#define NOSTR_ERROR_NIP61_INVALID_INFO -410
|
||||
#define NOSTR_ERROR_NIP61_INVALID_NUTZAP -411
|
||||
#define NOSTR_ERROR_NIP61_MINT_MISMATCH -412
|
||||
#define NOSTR_ERROR_NIP61_PUBKEY_MISMATCH -413
|
||||
#define NOSTR_ERROR_NIP61_VERIFICATION_FAILED -414
|
||||
|
||||
// Cashu Mint client error codes
|
||||
#define NOSTR_ERROR_CASHU_HTTP_FAILED -420
|
||||
#define NOSTR_ERROR_CASHU_JSON_PARSE_FAILED -421
|
||||
#define NOSTR_ERROR_CASHU_MINT_ERROR -422
|
||||
#define NOSTR_ERROR_CASHU_QUOTE_NOT_PAID -423
|
||||
#define NOSTR_ERROR_CASHU_QUOTE_EXPIRED -424
|
||||
#define NOSTR_ERROR_CASHU_PROOFS_SPENT -425
|
||||
#define NOSTR_ERROR_CASHU_CRYPTO_FAILED -426
|
||||
#define NOSTR_ERROR_CASHU_INVALID_KEYSET -427
|
||||
|
||||
// Constants
|
||||
#define NOSTR_PRIVATE_KEY_SIZE 32
|
||||
#define NOSTR_PUBLIC_KEY_SIZE 32
|
||||
@@ -72,75 +115,20 @@
|
||||
#define NIP05_DEFAULT_TIMEOUT 10
|
||||
|
||||
// NIP-04 Constants
|
||||
#define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 16777216 // 16MB
|
||||
#define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 1048576 // 1MB
|
||||
#define NOSTR_NIP04_MAX_ENCRYPTED_SIZE 22369621 // ~21.3MB (accounts for base64 overhead + IV)
|
||||
|
||||
// NIP-44 Constants
|
||||
#define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65536 // 64KB max plaintext (matches crypto header)
|
||||
// NIP-44 Constants
|
||||
#define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65535 // 64KB - 1 (NIP-44 spec compliant)
|
||||
|
||||
// Forward declaration for cJSON (to avoid requiring cJSON.h in header)
|
||||
struct cJSON;
|
||||
|
||||
// Relay query modes
|
||||
typedef enum {
|
||||
RELAY_QUERY_FIRST_RESULT, // Return as soon as first event is received
|
||||
RELAY_QUERY_MOST_RECENT, // Return the most recent event from all relays
|
||||
RELAY_QUERY_ALL_RESULTS // Return all unique events from all relays
|
||||
} relay_query_mode_t;
|
||||
|
||||
// Publish result types
|
||||
typedef enum {
|
||||
PUBLISH_SUCCESS, // Event was accepted by relay
|
||||
PUBLISH_REJECTED, // Event was rejected by relay
|
||||
PUBLISH_TIMEOUT, // No response within timeout
|
||||
PUBLISH_ERROR // Connection or other error
|
||||
} publish_result_t;
|
||||
|
||||
// Progress callback function types
|
||||
typedef void (*relay_progress_callback_t)(
|
||||
const char* relay_url,
|
||||
const char* status,
|
||||
const char* event_id,
|
||||
int events_received,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data);
|
||||
|
||||
typedef void (*publish_progress_callback_t)(
|
||||
const char* relay_url,
|
||||
const char* status,
|
||||
const char* message,
|
||||
int success_count,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data);
|
||||
|
||||
// Function declarations
|
||||
const char* nostr_strerror(int error_code);
|
||||
|
||||
// Library initialization functions
|
||||
// Library initialization functions
|
||||
int nostr_init(void);
|
||||
void nostr_cleanup(void);
|
||||
|
||||
// Relay query functions
|
||||
struct cJSON** synchronous_query_relays_with_progress(
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
struct cJSON* filter,
|
||||
relay_query_mode_t mode,
|
||||
int* result_count,
|
||||
int relay_timeout_seconds,
|
||||
relay_progress_callback_t callback,
|
||||
void* user_data);
|
||||
|
||||
// Relay publish functions
|
||||
publish_result_t* synchronous_publish_event_with_progress(
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
struct cJSON* event,
|
||||
int* success_count,
|
||||
int relay_timeout_seconds,
|
||||
publish_progress_callback_t callback,
|
||||
void* user_data);
|
||||
|
||||
#endif // NOSTR_COMMON_H
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
#ifndef NOSTR_CORE_H
|
||||
#define NOSTR_CORE_H
|
||||
|
||||
// Version information (auto-updated by increment_and_push.sh)
|
||||
#define VERSION "v0.6.11"
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 6
|
||||
#define VERSION_PATCH 11
|
||||
|
||||
/*
|
||||
* NOSTR Core Library - Complete API Reference
|
||||
*
|
||||
@@ -43,6 +49,28 @@
|
||||
* - nostr_nip44_encrypt_with_nonce() -> Encrypt with specific nonce (testing)
|
||||
* - nostr_nip44_decrypt() -> Decrypt ChaCha20 + HMAC messages
|
||||
*
|
||||
* NIP-46 REMOTE SIGNING:
|
||||
* - nostr_nip46_parse_bunker_url() -> Parse bunker:// connection tokens
|
||||
* - nostr_nip46_parse_nostrconnect_url() -> Parse nostrconnect:// connection tokens
|
||||
* - nostr_nip46_create_request_event() -> Create encrypted kind 24133 request events
|
||||
* - nostr_nip46_create_response_event() -> Create encrypted kind 24133 response events
|
||||
* - nostr_nip46_signer_handle_request() -> Handle signer-side RPC requests
|
||||
*
|
||||
* 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)
|
||||
@@ -82,6 +110,15 @@
|
||||
* - nostr_auth_rule_add() -> Add authentication rule
|
||||
* - nostr_auth_rule_remove() -> Remove authentication rule
|
||||
*
|
||||
* RELAY OPERATIONS:
|
||||
* - synchronous_query_relays_with_progress() -> One-off query from multiple relays
|
||||
* - synchronous_publish_event_with_progress() -> One-off publish to multiple relays
|
||||
* *
|
||||
* RELAY POOL OPERATIONS:
|
||||
* - nostr_relay_pool_create() -> Create relay pool for persistent connections
|
||||
* - nostr_relay_pool_subscribe() -> Subscribe to events with callbacks
|
||||
* - nostr_relay_pool_run() -> Run event loop for receiving events
|
||||
|
||||
* SYSTEM FUNCTIONS:
|
||||
* - nostr_crypto_init() -> Initialize crypto subsystem
|
||||
* - nostr_crypto_cleanup() -> Cleanup crypto subsystem
|
||||
@@ -117,6 +154,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);
|
||||
*
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
@@ -127,21 +174,255 @@ extern "C" {
|
||||
// Core common definitions and utilities
|
||||
#include "nostr_common.h"
|
||||
#include "utils.h"
|
||||
#include "nostr_signer.h"
|
||||
|
||||
// NIP implementations
|
||||
#include "nip001.h" // Basic Protocol
|
||||
#include "nip003.h" // OpenTimestamps
|
||||
#include "nip004.h" // Encryption (legacy)
|
||||
#include "nip005.h" // DNS-based identifiers
|
||||
#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 "nip021.h" // nostr: URI scheme
|
||||
#include "nip042.h" // Authentication of clients to relays
|
||||
#include "nip044.h" // Encryption (modern)
|
||||
#include "nip046.h" // Remote signing
|
||||
#include "nip059.h" // Gift Wrap
|
||||
#include "nip060.h" // Cashu Wallet
|
||||
#include "nip061.h" // Nutzaps
|
||||
#include "cashu_mint.h" // Cashu mint HTTP client
|
||||
|
||||
#include "nostr_http.h" // Shared HTTP client
|
||||
#include "blossom_client.h" // Blossom HTTP client
|
||||
|
||||
// Authentication and request validation system
|
||||
#include "request_validator.h" // Request validation and authentication rules
|
||||
|
||||
// Logging callback API
|
||||
#include "nostr_log.h"
|
||||
|
||||
// Relay pool types and functions
|
||||
typedef enum {
|
||||
NOSTR_POOL_RELAY_DISCONNECTED = 0,
|
||||
NOSTR_POOL_RELAY_CONNECTING = 1,
|
||||
NOSTR_POOL_RELAY_CONNECTED = 2,
|
||||
NOSTR_POOL_RELAY_ERROR = -1
|
||||
} nostr_pool_relay_status_t;
|
||||
|
||||
// EOSE result mode for subscriptions
|
||||
typedef enum {
|
||||
NOSTR_POOL_EOSE_FULL_SET, // Wait for all relays, return all events
|
||||
NOSTR_POOL_EOSE_MOST_RECENT, // Wait for all relays, return most recent event
|
||||
NOSTR_POOL_EOSE_FIRST // Return results on first EOSE (fastest response)
|
||||
} nostr_pool_eose_result_mode_t;
|
||||
|
||||
typedef struct {
|
||||
int connection_attempts;
|
||||
int connection_failures;
|
||||
int events_received;
|
||||
int events_published;
|
||||
int events_published_ok;
|
||||
int events_published_failed;
|
||||
time_t last_event_time;
|
||||
time_t connection_uptime_start;
|
||||
double ping_latency_avg;
|
||||
double ping_latency_min;
|
||||
double ping_latency_max;
|
||||
double ping_latency_current;
|
||||
int ping_samples;
|
||||
double query_latency_avg;
|
||||
double query_latency_min;
|
||||
double query_latency_max;
|
||||
int query_samples;
|
||||
double publish_latency_avg;
|
||||
int publish_samples;
|
||||
} nostr_relay_stats_t;
|
||||
|
||||
typedef struct nostr_relay_pool nostr_relay_pool_t;
|
||||
typedef struct nostr_pool_subscription nostr_pool_subscription_t;
|
||||
|
||||
// Reconnection configuration
|
||||
typedef struct {
|
||||
int enable_auto_reconnect; // 1 = enable, 0 = disable
|
||||
int max_reconnect_attempts; // Max attempts per relay
|
||||
int initial_reconnect_delay_ms; // Initial delay between attempts
|
||||
int max_reconnect_delay_ms; // Max delay (cap exponential backoff)
|
||||
int reconnect_backoff_multiplier; // Delay multiplier
|
||||
int reconnect_reset_stability_seconds; // Connected time required before resetting reconnect_attempts
|
||||
int ping_interval_seconds; // How often to ping (0 = disable)
|
||||
int pong_timeout_seconds; // How long to wait for pong before reconnecting
|
||||
} nostr_pool_reconnect_config_t;
|
||||
|
||||
// Relay pool management functions
|
||||
nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* config);
|
||||
nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void);
|
||||
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
|
||||
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool, const unsigned char* private_key, int enable);
|
||||
int nostr_relay_pool_set_auth_with_signer(nostr_relay_pool_t* pool, nostr_signer_t* signer, int enable);
|
||||
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
|
||||
|
||||
// Subscription management
|
||||
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(cJSON** events, int event_count, void* user_data),
|
||||
void* user_data,
|
||||
int close_on_eose,
|
||||
int enable_deduplication,
|
||||
nostr_pool_eose_result_mode_t result_mode,
|
||||
int relay_timeout_seconds,
|
||||
int eose_timeout_seconds);
|
||||
int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);
|
||||
|
||||
// Backward compatibility wrapper
|
||||
nostr_pool_subscription_t* nostr_relay_pool_subscribe_compat(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
|
||||
void (*on_eose)(void* user_data),
|
||||
void* user_data,
|
||||
int close_on_eose);
|
||||
|
||||
// Event loop functions
|
||||
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
|
||||
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
|
||||
|
||||
// Synchronous query/publish functions
|
||||
cJSON** nostr_relay_pool_query_sync(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int* event_count,
|
||||
int timeout_ms);
|
||||
// Same as above but also reports whether all relays sent EOSE (vs timed out).
|
||||
// out_got_eose is set to 1 if all connected relays sent EOSE, 0 if any timed
|
||||
// out. May be NULL if the caller doesn't care.
|
||||
cJSON** nostr_relay_pool_query_sync_with_eose(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int* event_count,
|
||||
int timeout_ms,
|
||||
int* out_got_eose);
|
||||
cJSON* nostr_relay_pool_get_event(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* filter,
|
||||
int timeout_ms);
|
||||
// Async publish callback typedef
|
||||
typedef void (*publish_response_callback_t)(
|
||||
const char* relay_url,
|
||||
const char* event_id,
|
||||
int success, // 1 for OK, 0 for rejection
|
||||
const char* message, // Error message if rejected, NULL if success
|
||||
void* user_data
|
||||
);
|
||||
|
||||
// Async publish function (only async version available)
|
||||
int nostr_relay_pool_publish_async(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
cJSON* event,
|
||||
publish_response_callback_t callback,
|
||||
void* user_data);
|
||||
|
||||
// Status and statistics functions
|
||||
nostr_pool_relay_status_t nostr_relay_pool_get_relay_status(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
int nostr_relay_pool_list_relays(
|
||||
nostr_relay_pool_t* pool,
|
||||
char*** relay_urls,
|
||||
nostr_pool_relay_status_t** statuses);
|
||||
const nostr_relay_stats_t* nostr_relay_pool_get_relay_stats(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
int nostr_relay_pool_reset_relay_stats(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
double nostr_relay_pool_get_relay_query_latency(
|
||||
nostr_relay_pool_t* pool,
|
||||
const char* relay_url);
|
||||
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);
|
||||
|
||||
// Synchronous relay operations (one-off queries/publishes)
|
||||
typedef enum {
|
||||
RELAY_QUERY_FIRST_RESULT, // Return as soon as first event is received
|
||||
RELAY_QUERY_MOST_RECENT, // Return the most recent event from all relays
|
||||
RELAY_QUERY_ALL_RESULTS // Return all unique events from all relays
|
||||
} relay_query_mode_t;
|
||||
|
||||
typedef enum {
|
||||
PUBLISH_SUCCESS, // Event was accepted by relay
|
||||
PUBLISH_REJECTED, // Event was rejected by relay
|
||||
PUBLISH_TIMEOUT, // No response within timeout
|
||||
PUBLISH_ERROR // Connection or other error
|
||||
} publish_result_t;
|
||||
|
||||
typedef void (*relay_progress_callback_t)(
|
||||
const char* relay_url,
|
||||
const char* status,
|
||||
const char* event_id,
|
||||
int events_received,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data);
|
||||
|
||||
typedef void (*publish_progress_callback_t)(
|
||||
const char* relay_url,
|
||||
const char* status,
|
||||
const char* message,
|
||||
int success_count,
|
||||
int total_relays,
|
||||
int completed_relays,
|
||||
void* user_data);
|
||||
|
||||
// Synchronous relay query functions
|
||||
struct cJSON** synchronous_query_relays_with_progress(
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
struct cJSON* filter,
|
||||
relay_query_mode_t mode,
|
||||
int* result_count,
|
||||
int relay_timeout_seconds,
|
||||
relay_progress_callback_t callback,
|
||||
void* user_data,
|
||||
int nip42_enabled,
|
||||
const unsigned char* private_key);
|
||||
|
||||
// Synchronous relay publish functions
|
||||
publish_result_t* synchronous_publish_event_with_progress(
|
||||
const char** relay_urls,
|
||||
int relay_count,
|
||||
struct cJSON* event,
|
||||
int* success_count,
|
||||
int relay_timeout_seconds,
|
||||
publish_progress_callback_t callback,
|
||||
void* user_data,
|
||||
int nip42_enabled,
|
||||
const unsigned char* private_key);
|
||||
|
||||
// Relay communication functions are defined in nostr_common.h
|
||||
// WebSocket functions are defined in nostr_common.h
|
||||
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* NOSTR Core Library - Shared HTTP Client
|
||||
*/
|
||||
|
||||
#include "nostr_http.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
size_t max_bytes;
|
||||
int truncated;
|
||||
} nostr_http_buffer_t;
|
||||
|
||||
static char* nostr_http_strdup_local(const char* s) {
|
||||
if (!s) return NULL;
|
||||
size_t n = strlen(s);
|
||||
char* out = (char*)malloc(n + 1U);
|
||||
if (!out) return NULL;
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static char g_ca_bundle_path[512] = {0};
|
||||
|
||||
static int append_bytes(nostr_http_buffer_t* b, const void* src, size_t n) {
|
||||
if (!b || !src || n == 0) return 1;
|
||||
|
||||
if (b->max_bytes > 0 && b->len >= b->max_bytes) {
|
||||
b->truncated = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t to_copy = n;
|
||||
if (b->max_bytes > 0) {
|
||||
size_t remaining = b->max_bytes - b->len;
|
||||
if (to_copy > remaining) {
|
||||
to_copy = remaining;
|
||||
b->truncated = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (b->len + to_copy + 1U > b->cap) {
|
||||
size_t new_cap = b->cap == 0 ? 1024U : b->cap;
|
||||
while (new_cap < b->len + to_copy + 1U) {
|
||||
new_cap *= 2U;
|
||||
}
|
||||
char* p = (char*)realloc(b->data, new_cap);
|
||||
if (!p) return 0;
|
||||
b->data = p;
|
||||
b->cap = new_cap;
|
||||
}
|
||||
|
||||
memcpy(b->data + b->len, src, to_copy);
|
||||
b->len += to_copy;
|
||||
b->data[b->len] = '\0';
|
||||
return 1;
|
||||
}
|
||||
|
||||
static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
nostr_http_buffer_t* rb = (nostr_http_buffer_t*)userp;
|
||||
size_t total = size * nmemb;
|
||||
if (!rb || total == 0) return total;
|
||||
if (!append_bytes(rb, contents, total)) return 0;
|
||||
return total;
|
||||
}
|
||||
|
||||
static size_t header_cb(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
nostr_http_buffer_t* hb = (nostr_http_buffer_t*)userp;
|
||||
size_t total = size * nmemb;
|
||||
if (!hb || total == 0) return total;
|
||||
if (!append_bytes(hb, contents, total)) return 0;
|
||||
return total;
|
||||
}
|
||||
|
||||
void nostr_http_set_ca_bundle(const char* ca_bundle_path) {
|
||||
if (!ca_bundle_path || ca_bundle_path[0] == '\0') {
|
||||
g_ca_bundle_path[0] = '\0';
|
||||
return;
|
||||
}
|
||||
strncpy(g_ca_bundle_path, ca_bundle_path, sizeof(g_ca_bundle_path) - 1);
|
||||
g_ca_bundle_path[sizeof(g_ca_bundle_path) - 1] = '\0';
|
||||
}
|
||||
|
||||
const char* nostr_http_detect_ca_bundle(void) {
|
||||
const char* env = getenv("SSL_CERT_FILE");
|
||||
if (env && env[0] != '\0' && access(env, R_OK) == 0) {
|
||||
return env;
|
||||
}
|
||||
|
||||
static const char* candidates[] = {
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
"/etc/ssl/cert.pem",
|
||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/ssl/ca-bundle.pem"
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
||||
if (access(candidates[i], R_OK) == 0) {
|
||||
return candidates[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp) {
|
||||
if (!req || !req->url || !resp) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
memset(resp, 0, sizeof(*resp));
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) return NOSTR_ERROR_NETWORK_FAILED;
|
||||
|
||||
const char* method = (req->method && req->method[0] != '\0') ? req->method : "GET";
|
||||
int timeout = req->timeout_seconds > 0 ? req->timeout_seconds : 30;
|
||||
int follow = req->follow_redirects;
|
||||
if (follow != 0 && follow != 1) follow = 1;
|
||||
int max_redirs = req->max_redirects > 0 ? req->max_redirects : 3;
|
||||
const char* user_agent = (req->user_agent && req->user_agent[0] != '\0') ? req->user_agent : "nostr-core/1.0";
|
||||
|
||||
nostr_http_buffer_t body = {0};
|
||||
body.max_bytes = req->max_response_bytes;
|
||||
|
||||
nostr_http_buffer_t headers = {0};
|
||||
|
||||
struct curl_slist* header_list = NULL;
|
||||
if (req->headers) {
|
||||
for (const char** h = req->headers; *h; h++) {
|
||||
if ((*h)[0] != '\0') header_list = curl_slist_append(header_list, *h);
|
||||
}
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, req->url);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, (long)follow);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, (long)max_redirs);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
|
||||
|
||||
if (req->capture_headers) {
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headers);
|
||||
}
|
||||
|
||||
if (header_list) {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
|
||||
}
|
||||
|
||||
if (g_ca_bundle_path[0] != '\0') {
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, g_ca_bundle_path);
|
||||
}
|
||||
|
||||
if (strcasecmp(method, "GET") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
} else if (strcasecmp(method, "POST") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
if (req->body && req->body_len > 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, (const char*)req->body);
|
||||
} else {
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L);
|
||||
}
|
||||
// Disable Expect: 100-continue
|
||||
header_list = curl_slist_append(header_list, "Expect:");
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
|
||||
} else if (strcasecmp(method, "HEAD") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "HEAD");
|
||||
} else {
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method);
|
||||
if (req->body && req->body_len > 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, (const char*)req->body);
|
||||
}
|
||||
}
|
||||
|
||||
CURLcode rc = curl_easy_perform(curl);
|
||||
long status = 0;
|
||||
char* content_type = NULL;
|
||||
char* content_type_copy = NULL;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
|
||||
curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, &content_type);
|
||||
if (content_type && content_type[0] != '\0') {
|
||||
content_type_copy = nostr_http_strdup_local(content_type);
|
||||
}
|
||||
|
||||
if (header_list) curl_slist_free_all(header_list);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (rc != CURLE_OK) {
|
||||
free(body.data);
|
||||
free(headers.data);
|
||||
free(content_type_copy);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
resp->status_code = status;
|
||||
resp->body = body.data ? body.data : nostr_http_strdup_local("");
|
||||
resp->body_len = body.data ? body.len : 0;
|
||||
resp->headers_raw = headers.data;
|
||||
resp->content_type = content_type_copy;
|
||||
resp->truncated = body.truncated;
|
||||
|
||||
if (!resp->body) {
|
||||
free(resp->headers_raw);
|
||||
free(resp->content_type);
|
||||
memset(resp, 0, sizeof(*resp));
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void nostr_http_response_free(nostr_http_response_t* resp) {
|
||||
if (!resp) return;
|
||||
free(resp->body);
|
||||
free(resp->content_type);
|
||||
free(resp->headers_raw);
|
||||
memset(resp, 0, sizeof(*resp));
|
||||
}
|
||||
|
||||
int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out) {
|
||||
if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
*body_out = NULL;
|
||||
if (status_out) *status_out = 0;
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = url;
|
||||
req.timeout_seconds = timeout_seconds;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (status_out) *status_out = resp.status_code;
|
||||
*body_out = resp.body;
|
||||
free(resp.content_type);
|
||||
free(resp.headers_raw);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_http_post_json(const char* url,
|
||||
const char* json_body,
|
||||
int timeout_seconds,
|
||||
char** body_out,
|
||||
long* status_out) {
|
||||
if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
*body_out = NULL;
|
||||
if (status_out) *status_out = 0;
|
||||
|
||||
const char* headers[] = {
|
||||
"Accept: application/json",
|
||||
"Content-Type: application/json",
|
||||
NULL
|
||||
};
|
||||
|
||||
const char* payload = json_body ? json_body : "{}";
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "POST";
|
||||
req.url = url;
|
||||
req.headers = headers;
|
||||
req.body = (const unsigned char*)payload;
|
||||
req.body_len = strlen(payload);
|
||||
req.timeout_seconds = timeout_seconds;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (status_out) *status_out = resp.status_code;
|
||||
*body_out = resp.body;
|
||||
free(resp.content_type);
|
||||
free(resp.headers_raw);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* NOSTR Core Library - Shared HTTP Client
|
||||
*/
|
||||
|
||||
#ifndef NOSTR_HTTP_H
|
||||
#define NOSTR_HTTP_H
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char* body;
|
||||
size_t body_len;
|
||||
long status_code;
|
||||
char* content_type;
|
||||
char* headers_raw;
|
||||
int truncated;
|
||||
} nostr_http_response_t;
|
||||
|
||||
typedef struct {
|
||||
const char* method;
|
||||
const char* url;
|
||||
const char** headers;
|
||||
const unsigned char* body;
|
||||
size_t body_len;
|
||||
int timeout_seconds;
|
||||
size_t max_response_bytes;
|
||||
int follow_redirects;
|
||||
int max_redirects;
|
||||
const char* user_agent;
|
||||
int capture_headers;
|
||||
} nostr_http_request_t;
|
||||
|
||||
void nostr_http_set_ca_bundle(const char* ca_bundle_path);
|
||||
const char* nostr_http_detect_ca_bundle(void);
|
||||
|
||||
int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp);
|
||||
void nostr_http_response_free(nostr_http_response_t* resp);
|
||||
|
||||
int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out);
|
||||
int nostr_http_post_json(const char* url,
|
||||
const char* json_body,
|
||||
int timeout_seconds,
|
||||
char** body_out,
|
||||
long* status_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_HTTP_H */
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "nostr_log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define NOSTR_LOG_BUFFER_SIZE 4096
|
||||
|
||||
static nostr_log_callback_t g_log_callback = NULL;
|
||||
static void* g_log_user_data = NULL;
|
||||
static int g_log_min_level = NOSTR_LOG_LEVEL_ERROR;
|
||||
|
||||
void nostr_set_log_callback(nostr_log_callback_t cb, void* user_data) {
|
||||
g_log_callback = cb;
|
||||
g_log_user_data = user_data;
|
||||
}
|
||||
|
||||
void nostr_set_log_level(nostr_log_level_t min_level) {
|
||||
if (min_level < NOSTR_LOG_LEVEL_ERROR) {
|
||||
g_log_min_level = NOSTR_LOG_LEVEL_ERROR;
|
||||
return;
|
||||
}
|
||||
if (min_level > NOSTR_LOG_LEVEL_TRACE) {
|
||||
g_log_min_level = NOSTR_LOG_LEVEL_TRACE;
|
||||
return;
|
||||
}
|
||||
g_log_min_level = (int)min_level;
|
||||
}
|
||||
|
||||
void nostr_log_vemitf(int level, const char* component, const char* format, va_list args) {
|
||||
if (!g_log_callback || !format) {
|
||||
return;
|
||||
}
|
||||
if (level < g_log_min_level) {
|
||||
return;
|
||||
}
|
||||
|
||||
char message[NOSTR_LOG_BUFFER_SIZE];
|
||||
int written = vsnprintf(message, sizeof(message), format, args);
|
||||
if (written < 0) {
|
||||
return;
|
||||
}
|
||||
message[sizeof(message) - 1] = '\0';
|
||||
|
||||
g_log_callback(level,
|
||||
component ? component : "unknown",
|
||||
message,
|
||||
g_log_user_data);
|
||||
}
|
||||
|
||||
void nostr_log_emitf(int level, const char* component, const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
nostr_log_vemitf(level, component, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef NOSTR_LOG_H
|
||||
#define NOSTR_LOG_H
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
NOSTR_LOG_LEVEL_ERROR = 1,
|
||||
NOSTR_LOG_LEVEL_WARN = 2,
|
||||
NOSTR_LOG_LEVEL_INFO = 3,
|
||||
NOSTR_LOG_LEVEL_DEBUG = 4,
|
||||
NOSTR_LOG_LEVEL_TRACE = 5
|
||||
} nostr_log_level_t;
|
||||
|
||||
typedef void (*nostr_log_callback_t)(
|
||||
int level,
|
||||
const char* component,
|
||||
const char* message,
|
||||
void* user_data
|
||||
);
|
||||
|
||||
void nostr_set_log_callback(nostr_log_callback_t cb, void* user_data);
|
||||
void nostr_set_log_level(nostr_log_level_t min_level);
|
||||
|
||||
void nostr_log_emitf(int level, const char* component, const char* format, ...);
|
||||
void nostr_log_vemitf(int level, const char* component, const char* format, va_list args);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_LOG_H */
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef NOSTR_PLATFORM_H
|
||||
#define NOSTR_PLATFORM_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Fill buffer with cryptographically secure random bytes.
|
||||
* Returns 0 on success, -1 on failure.
|
||||
*/
|
||||
int nostr_platform_random(unsigned char *buf, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_PLATFORM_H */
|
||||
@@ -0,0 +1,880 @@
|
||||
#include "nostr_signer.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "utils.h"
|
||||
#include "nip004.h"
|
||||
#include "nip044.h"
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
#include "nsigner_transport.h"
|
||||
#include "nsigner_client.h"
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
NOSTR_SIGNER_BACKEND_LOCAL = 1,
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE = 2,
|
||||
#endif
|
||||
} nostr_signer_backend_t;
|
||||
|
||||
struct nostr_signer {
|
||||
nostr_signer_backend_t backend;
|
||||
union {
|
||||
struct {
|
||||
unsigned char private_key[32];
|
||||
} local;
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
struct {
|
||||
nsigner_client_t* client;
|
||||
char role[64];
|
||||
int nostr_index; /* -1 = not set; when set, overrides role */
|
||||
int has_nostr_index; /* 1 if nostr_index is set */
|
||||
} remote;
|
||||
#endif
|
||||
} u;
|
||||
};
|
||||
|
||||
static int signer_hex_pubkey_to_bytes(const char* hex, unsigned char out[32]) {
|
||||
if (!hex || strlen(hex) != 64 || !out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
if (nostr_hex_to_bytes(hex, out, 32) != 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]) {
|
||||
unsigned char pubkey[32];
|
||||
|
||||
if (!signer || !out_pubkey_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (nostr_ec_public_key_from_private_key(signer->u.local.private_key, pubkey) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(pubkey, 32, out_pubkey_hex);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out) {
|
||||
cJSON* pubkey_item;
|
||||
cJSON* created_at_item;
|
||||
cJSON* kind_item;
|
||||
cJSON* tags_item;
|
||||
cJSON* content_item;
|
||||
cJSON* serialize_array;
|
||||
cJSON* signed_event = NULL;
|
||||
char* serialize_string = NULL;
|
||||
unsigned char event_hash[32];
|
||||
unsigned char signature[64];
|
||||
char event_id[65];
|
||||
char sig_hex[129];
|
||||
|
||||
if (!signer || !unsigned_event || !signed_event_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*signed_event_out = NULL;
|
||||
|
||||
pubkey_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "pubkey");
|
||||
created_at_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "created_at");
|
||||
kind_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "kind");
|
||||
tags_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "tags");
|
||||
content_item = cJSON_GetObjectItem((cJSON*)unsigned_event, "content");
|
||||
|
||||
if (!pubkey_item || !created_at_item || !kind_item || !tags_item || !content_item) {
|
||||
return NOSTR_ERROR_EVENT_INVALID_STRUCTURE;
|
||||
}
|
||||
|
||||
serialize_array = cJSON_CreateArray();
|
||||
if (!serialize_array) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
serialize_string = cJSON_PrintUnformatted(serialize_array);
|
||||
cJSON_Delete(serialize_array);
|
||||
|
||||
if (!serialize_string) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
if (nostr_sha256((const unsigned char*)serialize_string, strlen(serialize_string), event_hash) != 0) {
|
||||
free(serialize_string);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(event_hash, 32, event_id);
|
||||
|
||||
if (nostr_ec_sign(signer->u.local.private_key, event_hash, signature) != 0) {
|
||||
free(serialize_string);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(signature, 64, sig_hex);
|
||||
|
||||
signed_event = cJSON_Duplicate((cJSON*)unsigned_event, 1);
|
||||
if (!signed_event) {
|
||||
free(serialize_string);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(signed_event, "id");
|
||||
cJSON_DeleteItemFromObjectCaseSensitive(signed_event, "sig");
|
||||
cJSON_AddStringToObject(signed_event, "id", event_id);
|
||||
cJSON_AddStringToObject(signed_event, "sig", sig_hex);
|
||||
|
||||
*signed_event_out = signed_event;
|
||||
free(serialize_string);
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
unsigned char peer_pubkey[32];
|
||||
char* out;
|
||||
int rc;
|
||||
|
||||
if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*ciphertext_out = NULL;
|
||||
|
||||
rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
out = (char*)malloc(NOSTR_NIP04_MAX_ENCRYPTED_SIZE + 1);
|
||||
if (!out) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip04_encrypt(signer->u.local.private_key, peer_pubkey, plaintext, out, NOSTR_NIP04_MAX_ENCRYPTED_SIZE + 1);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*ciphertext_out = out;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
unsigned char peer_pubkey[32];
|
||||
char* out;
|
||||
int rc;
|
||||
|
||||
if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*plaintext_out = NULL;
|
||||
|
||||
rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
out = (char*)malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1);
|
||||
if (!out) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip04_decrypt(signer->u.local.private_key, peer_pubkey, ciphertext, out, NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*plaintext_out = out;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
unsigned char peer_pubkey[32];
|
||||
char* out;
|
||||
size_t out_size;
|
||||
int rc;
|
||||
|
||||
if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*ciphertext_out = NULL;
|
||||
|
||||
rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
out_size = (strlen(plaintext) * 4) + 4096;
|
||||
if (out_size < 8192) {
|
||||
out_size = 8192;
|
||||
}
|
||||
|
||||
out = (char*)malloc(out_size);
|
||||
if (!out) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip44_encrypt(signer->u.local.private_key, peer_pubkey, plaintext, out, out_size);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*ciphertext_out = out;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
unsigned char peer_pubkey[32];
|
||||
char* out;
|
||||
int rc;
|
||||
|
||||
if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*plaintext_out = NULL;
|
||||
|
||||
rc = signer_hex_pubkey_to_bytes(peer_pubkey_hex, peer_pubkey);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
out = (char*)malloc(NOSTR_NIP44_MAX_PLAINTEXT_SIZE + 1);
|
||||
if (!out) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nostr_nip44_decrypt(signer->u.local.private_key, peer_pubkey, ciphertext, out, NOSTR_NIP44_MAX_PLAINTEXT_SIZE + 1);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
free(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
*plaintext_out = out;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_local_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
unsigned char mac[32];
|
||||
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (nostr_hmac_sha256(signer->u.local.private_key, 32,
|
||||
(const unsigned char*)data, strlen(data),
|
||||
mac) != 0) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(mac, 32, out_digest_hex);
|
||||
memset(mac, 0, sizeof(mac));
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
|
||||
static cJSON* signer_remote_params_with_selector(cJSON* params, const char* role, int has_nostr_index, int nostr_index) {
|
||||
cJSON* selector;
|
||||
|
||||
if (params == NULL) {
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* nostr_index takes precedence over role when set */
|
||||
if (has_nostr_index) {
|
||||
selector = cJSON_CreateObject();
|
||||
if (selector == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddNumberToObject(selector, "nostr_index", nostr_index);
|
||||
cJSON_AddItemToArray(params, selector);
|
||||
return params;
|
||||
}
|
||||
|
||||
if (role == NULL || role[0] == '\0') {
|
||||
return params;
|
||||
}
|
||||
|
||||
selector = cJSON_CreateObject();
|
||||
if (selector == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(selector, "role", role);
|
||||
cJSON_AddItemToArray(params, selector);
|
||||
return params;
|
||||
}
|
||||
|
||||
static int signer_remote_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]) {
|
||||
cJSON* params;
|
||||
cJSON* result = NULL;
|
||||
const char* result_str;
|
||||
int rc;
|
||||
|
||||
if (!signer || !out_pubkey_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
params = signer_remote_params_with_selector(NULL, signer->u.remote.role,
|
||||
signer->u.remote.has_nostr_index,
|
||||
signer->u.remote.nostr_index);
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, "nostr_get_public_key", params, &result);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (!cJSON_IsString(result) || result->valuestring == NULL || strlen(result->valuestring) != 64) {
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
result_str = result->valuestring;
|
||||
memcpy(out_pubkey_hex, result_str, 64);
|
||||
out_pubkey_hex[64] = '\0';
|
||||
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_remote_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out) {
|
||||
cJSON* params;
|
||||
cJSON* result = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
char* event_json;
|
||||
int rc;
|
||||
|
||||
if (!signer || !unsigned_event || !signed_event_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*signed_event_out = NULL;
|
||||
|
||||
event_json = cJSON_PrintUnformatted((cJSON*)unsigned_event);
|
||||
if (event_json == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
free(event_json);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(event_json));
|
||||
free(event_json);
|
||||
|
||||
params = signer_remote_params_with_selector(params, signer->u.remote.role,
|
||||
signer->u.remote.has_nostr_index,
|
||||
signer->u.remote.nostr_index);
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, "nostr_sign_event", params, &result);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (cJSON_IsString(result) && result->valuestring != NULL) {
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
cJSON_Delete(result);
|
||||
if (parsed == NULL || !cJSON_IsObject(parsed)) {
|
||||
cJSON_Delete(parsed);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
*signed_event_out = parsed;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
if (cJSON_IsObject(result)) {
|
||||
*signed_event_out = cJSON_Duplicate(result, 1);
|
||||
cJSON_Delete(result);
|
||||
if (*signed_event_out == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
static int signer_remote_encrypt_decrypt(nostr_signer_t* signer,
|
||||
const char* method,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* in,
|
||||
char** out) {
|
||||
cJSON* params;
|
||||
cJSON* result = NULL;
|
||||
const char* result_str;
|
||||
int rc;
|
||||
|
||||
if (!signer || !method || !peer_pubkey_hex || !in || !out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*out = NULL;
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(peer_pubkey_hex));
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(in));
|
||||
|
||||
params = signer_remote_params_with_selector(params, signer->u.remote.role,
|
||||
signer->u.remote.has_nostr_index,
|
||||
signer->u.remote.nostr_index);
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, method, params, &result);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (!cJSON_IsString(result) || result->valuestring == NULL) {
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
result_str = result->valuestring;
|
||||
{
|
||||
size_t n = strlen(result_str);
|
||||
*out = (char*)malloc(n + 1);
|
||||
if (*out == NULL) {
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(*out, result_str, n + 1);
|
||||
}
|
||||
cJSON_Delete(result);
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int signer_remote_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
return signer_remote_encrypt_decrypt(signer, "nostr_nip04_encrypt", peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
|
||||
static int signer_remote_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
return signer_remote_encrypt_decrypt(signer, "nostr_nip04_decrypt", peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
|
||||
static int signer_remote_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
return signer_remote_encrypt_decrypt(signer, "nostr_nip44_encrypt", peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
|
||||
static int signer_remote_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
return signer_remote_encrypt_decrypt(signer, "nostr_nip44_decrypt", peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
|
||||
static int signer_remote_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
cJSON* params;
|
||||
cJSON* opts = NULL;
|
||||
cJSON* result = NULL;
|
||||
cJSON* parsed = NULL;
|
||||
cJSON* digest_item = NULL;
|
||||
const char* digest_str = NULL;
|
||||
int rc;
|
||||
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
params = cJSON_CreateArray();
|
||||
if (params == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
cJSON_AddItemToArray(params, cJSON_CreateString(data));
|
||||
|
||||
/* Build the options object with algorithm:"secp256k1" and the selector
|
||||
* (role or nostr_index). The derive verb requires index, so when
|
||||
* has_nostr_index is set we include it; otherwise we include role (the
|
||||
* nsigner derive handler requires index, so callers using the remote
|
||||
* backend must set nostr_index via nostr_signer_nsigner_set_nostr_index
|
||||
* before calling derive_hmac). */
|
||||
opts = cJSON_CreateObject();
|
||||
if (opts == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
cJSON_AddStringToObject(opts, "algorithm", "secp256k1");
|
||||
if (signer->u.remote.has_nostr_index) {
|
||||
cJSON_AddNumberToObject(opts, "index", signer->u.remote.nostr_index);
|
||||
} else if (signer->u.remote.role[0] != '\0') {
|
||||
cJSON_AddStringToObject(opts, "role", signer->u.remote.role);
|
||||
}
|
||||
cJSON_AddItemToArray(params, opts);
|
||||
|
||||
rc = nsigner_client_call(signer->u.remote.client, "derive", params, &result);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* result is a JSON string: {"algorithm":"secp256k1","key_id":"...","digest":"<64hex>"} */
|
||||
if (!cJSON_IsString(result) || result->valuestring == NULL) {
|
||||
cJSON_Delete(result);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
parsed = cJSON_Parse(result->valuestring);
|
||||
cJSON_Delete(result);
|
||||
if (parsed == NULL) {
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
digest_item = cJSON_GetObjectItemCaseSensitive(parsed, "digest");
|
||||
if (!cJSON_IsString(digest_item) || digest_item->valuestring == NULL ||
|
||||
strlen(digest_item->valuestring) != 64) {
|
||||
cJSON_Delete(parsed);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
digest_str = digest_item->valuestring;
|
||||
memcpy(out_digest_hex, digest_str, 64);
|
||||
out_digest_hex[64] = '\0';
|
||||
cJSON_Delete(parsed);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
|
||||
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]) {
|
||||
nostr_signer_t* signer;
|
||||
|
||||
if (!private_key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = (nostr_signer_t*)calloc(1, sizeof(nostr_signer_t));
|
||||
if (!signer) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer->backend = NOSTR_SIGNER_BACKEND_LOCAL;
|
||||
memcpy(signer->u.local.private_key, private_key, 32);
|
||||
return signer;
|
||||
}
|
||||
|
||||
void nostr_signer_free(nostr_signer_t* signer) {
|
||||
if (!signer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
memset(signer->u.local.private_key, 0, sizeof(signer->u.local.private_key));
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
else if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
if (signer->u.remote.client != NULL) {
|
||||
nsigner_client_free(signer->u.remote.client);
|
||||
signer->u.remote.client = NULL;
|
||||
}
|
||||
memset(signer->u.remote.role, 0, sizeof(signer->u.remote.role));
|
||||
}
|
||||
#endif
|
||||
|
||||
free(signer);
|
||||
}
|
||||
|
||||
int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]) {
|
||||
if (!signer || !out_pubkey_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_get_public_key(signer, out_pubkey_hex);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_get_public_key(signer, out_pubkey_hex);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out) {
|
||||
if (!signer || !unsigned_event || !signed_event_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_sign_event(signer, unsigned_event, signed_event_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_sign_event(signer, unsigned_event, signed_event_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_nip04_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_nip04_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_nip04_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_nip04_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out) {
|
||||
if (!signer || !peer_pubkey_hex || !plaintext || !ciphertext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_nip44_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_nip44_encrypt(signer, peer_pubkey_hex, plaintext, ciphertext_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out) {
|
||||
if (!signer || !peer_pubkey_hex || !ciphertext || !plaintext_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_nip44_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_nip44_decrypt(signer, peer_pubkey_hex, ciphertext, plaintext_out);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int nostr_signer_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]) {
|
||||
if (!signer || !data || !out_digest_hex) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_LOCAL) {
|
||||
return signer_local_derive_hmac(signer, data, out_digest_hex);
|
||||
}
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
if (signer->backend == NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE) {
|
||||
return signer_remote_derive_hmac(signer, data, out_digest_hex);
|
||||
}
|
||||
#endif
|
||||
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
static nostr_signer_t* nostr_signer_nsigner_from_transport(nsigner_transport_t* transport, const char* role) {
|
||||
nsigner_client_t* client = NULL;
|
||||
nostr_signer_t* signer = NULL;
|
||||
|
||||
if (transport == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
client = nsigner_client_new(transport);
|
||||
if (client == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer = (nostr_signer_t*)calloc(1, sizeof(*signer));
|
||||
if (signer == NULL) {
|
||||
nsigner_client_free(client);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
signer->backend = NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE;
|
||||
signer->u.remote.client = client;
|
||||
if (role != NULL) {
|
||||
strncpy(signer->u.remote.role, role, sizeof(signer->u.remote.role) - 1);
|
||||
signer->u.remote.role[sizeof(signer->u.remote.role) - 1] = '\0';
|
||||
}
|
||||
|
||||
return signer;
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport = NULL;
|
||||
char discovered[8][64];
|
||||
const char* effective_name = socket_name;
|
||||
|
||||
if (effective_name == NULL || effective_name[0] == '\0') {
|
||||
int count = nsigner_transport_list_unix(discovered, 8);
|
||||
if (count != 1) {
|
||||
return NULL;
|
||||
}
|
||||
effective_name = discovered[0];
|
||||
}
|
||||
|
||||
transport = nsigner_transport_open_unix(effective_name, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport;
|
||||
|
||||
transport = nsigner_transport_open_serial(device_path, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport;
|
||||
|
||||
transport = nsigner_transport_open_tcp(host, port, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport;
|
||||
|
||||
transport = nsigner_transport_open_fds(read_fd, write_fd, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
int nostr_signer_nsigner_set_auth(nostr_signer_t* signer,
|
||||
const unsigned char auth_privkey[32],
|
||||
const char* label) {
|
||||
if (signer == NULL || auth_privkey == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend != NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE || signer->u.remote.client == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
return nsigner_client_set_auth(signer->u.remote.client, auth_privkey, label);
|
||||
}
|
||||
|
||||
nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms) {
|
||||
nsigner_transport_t* transport;
|
||||
|
||||
transport = nsigner_transport_open_qrexec(target_qube, service_name, timeout_ms);
|
||||
return nostr_signer_nsigner_from_transport(transport, role);
|
||||
}
|
||||
|
||||
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index) {
|
||||
if (signer == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (signer->backend != NOSTR_SIGNER_BACKEND_NSIGNER_REMOTE || signer->u.remote.client == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (nostr_index < 0) {
|
||||
/* Clear the nostr_index selector, fall back to role */
|
||||
signer->u.remote.has_nostr_index = 0;
|
||||
signer->u.remote.nostr_index = -1;
|
||||
} else {
|
||||
/* Set nostr_index; clear role to avoid ambiguous selector */
|
||||
signer->u.remote.has_nostr_index = 1;
|
||||
signer->u.remote.nostr_index = nostr_index;
|
||||
memset(signer->u.remote.role, 0, sizeof(signer->u.remote.role));
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,73 @@
|
||||
#ifndef NOSTR_SIGNER_H
|
||||
#define NOSTR_SIGNER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include "nostr_common.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct nostr_signer nostr_signer_t;
|
||||
|
||||
/* Lifecycle */
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]);
|
||||
void nostr_signer_free(nostr_signer_t* signer);
|
||||
|
||||
/* Core verbs */
|
||||
int nostr_signer_get_public_key(nostr_signer_t* signer, char out_pubkey_hex[65]);
|
||||
int nostr_signer_sign_event(nostr_signer_t* signer, const cJSON* unsigned_event, cJSON** signed_event_out);
|
||||
|
||||
/* Derive a deterministic MAC over caller-supplied data using the signer's
|
||||
* secp256k1 private key: out = HMAC-SHA256(privkey, data).
|
||||
* data is a NUL-terminated UTF-8 string. out_digest_hex receives 64 lowercase
|
||||
* hex chars + NUL. For the local backend this computes directly; for the
|
||||
* nsigner remote backend it calls the "derive" verb (algorithm:"secp256k1").
|
||||
* Returns NOSTR_SUCCESS or an error code. */
|
||||
int nostr_signer_derive_hmac(nostr_signer_t* signer,
|
||||
const char* data,
|
||||
char out_digest_hex[65]);
|
||||
|
||||
/* Encryption verbs (for parity with upcoming remote backends) */
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* plaintext,
|
||||
char** ciphertext_out);
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t* signer,
|
||||
const char* peer_pubkey_hex,
|
||||
const char* ciphertext,
|
||||
char** plaintext_out);
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* device_path, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_fds(int read_fd, int write_fd, const char* role, int timeout_ms);
|
||||
nostr_signer_t* nostr_signer_nsigner_qrexec(const char* target_qube, const char* service_name, const char* role, int timeout_ms);
|
||||
/* TCP mode requires auth envelope per n_signer; set this before making calls. */
|
||||
int nostr_signer_nsigner_set_auth(nostr_signer_t* signer,
|
||||
const unsigned char auth_privkey[32],
|
||||
const char* label);
|
||||
/*
|
||||
* Set the nostr_index selector for the remote nsigner backend. When set,
|
||||
* requests use {"nostr_index":N} instead of {"role":"..."}. This is mutually
|
||||
* exclusive with the role parameter (setting index clears role).
|
||||
* Returns NOSTR_SUCCESS or an error code.
|
||||
*/
|
||||
int nostr_signer_nsigner_set_nostr_index(nostr_signer_t* signer, int nostr_index);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_SIGNER_H */
|
||||
@@ -0,0 +1,383 @@
|
||||
#define _POSIX_C_SOURCE 200112L
|
||||
|
||||
#include "nsigner_client.h"
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "nip001.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* Ensure a broken connection returns EPIPE instead of killing the process. */
|
||||
static void nsigner_client_ignore_sigpipe_once(void) {
|
||||
static int done = 0;
|
||||
if (!done) {
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = SIG_IGN;
|
||||
(void)sigaction(SIGPIPE, &sa, NULL);
|
||||
done = 1;
|
||||
}
|
||||
}
|
||||
|
||||
struct nsigner_client {
|
||||
nsigner_transport_t* transport;
|
||||
unsigned long long next_id;
|
||||
int auth_enabled;
|
||||
int connected; /* whether the transport has an open connection ready for a request */
|
||||
unsigned char auth_privkey[32];
|
||||
char auth_label[64];
|
||||
char last_error[256];
|
||||
};
|
||||
|
||||
static void nsigner_client_set_error(nsigner_client_t* client, const char* msg) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
if (msg == NULL) {
|
||||
msg = "unknown";
|
||||
}
|
||||
strncpy(client->last_error, msg, sizeof(client->last_error) - 1);
|
||||
client->last_error[sizeof(client->last_error) - 1] = '\0';
|
||||
}
|
||||
|
||||
int nsigner_client_compute_body_hash_hex(const cJSON* params, char out_hash_hex[65]) {
|
||||
unsigned char hash[32];
|
||||
char* compact;
|
||||
|
||||
if (out_hash_hex == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (params == NULL) {
|
||||
compact = (char*)malloc(5);
|
||||
if (compact == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(compact, "null", 5);
|
||||
} else {
|
||||
compact = cJSON_PrintUnformatted((cJSON*)params);
|
||||
if (compact == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
if (nostr_sha256((const unsigned char*)compact, strlen(compact), hash) != 0) {
|
||||
free(compact);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
nostr_bytes_to_hex(hash, sizeof(hash), out_hash_hex);
|
||||
out_hash_hex[64] = '\0';
|
||||
free(compact);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static cJSON* nsigner_client_make_tag_pair(const char* key, const char* value) {
|
||||
cJSON* pair;
|
||||
|
||||
if (key == NULL || value == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pair = cJSON_CreateArray();
|
||||
if (pair == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(pair, cJSON_CreateString(key));
|
||||
cJSON_AddItemToArray(pair, cJSON_CreateString(value));
|
||||
return pair;
|
||||
}
|
||||
|
||||
static int nsigner_client_build_auth(nsigner_client_t* client,
|
||||
const char* request_id,
|
||||
const char* method,
|
||||
const cJSON* params,
|
||||
cJSON** out_auth) {
|
||||
cJSON* tags = NULL;
|
||||
cJSON* auth = NULL;
|
||||
char hash_hex[65];
|
||||
|
||||
if (client == NULL || request_id == NULL || method == NULL || out_auth == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*out_auth = NULL;
|
||||
|
||||
if (nsigner_client_compute_body_hash_hex(params, hash_hex) != NOSTR_SUCCESS) {
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
tags = cJSON_CreateArray();
|
||||
if (tags == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(tags, nsigner_client_make_tag_pair("nsigner_rpc", request_id));
|
||||
cJSON_AddItemToArray(tags, nsigner_client_make_tag_pair("nsigner_method", method));
|
||||
cJSON_AddItemToArray(tags, nsigner_client_make_tag_pair("nsigner_body_hash", hash_hex));
|
||||
|
||||
auth = nostr_create_and_sign_event(27235,
|
||||
client->auth_label,
|
||||
tags,
|
||||
client->auth_privkey,
|
||||
time(NULL));
|
||||
if (auth == NULL) {
|
||||
cJSON_Delete(tags);
|
||||
return NOSTR_ERROR_CRYPTO_FAILED;
|
||||
}
|
||||
|
||||
*out_auth = auth;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int nsigner_client_next_id(nsigner_client_t* client, char out_id[32]) {
|
||||
if (client == NULL || out_id == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
snprintf(out_id, 32, "%llu", client->next_id);
|
||||
client->next_id++;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int nsigner_client_map_rpc_error(int rpc_code, const char* rpc_message) {
|
||||
if (rpc_code == 2010 || rpc_code == 2011 || rpc_code == 2012 || rpc_code == 2013 ||
|
||||
rpc_code == 2014 || rpc_code == 2015 || rpc_code == 2016 || rpc_code == 2017) {
|
||||
return NOSTR_ERROR_NIP46_AUTH_CHALLENGE;
|
||||
}
|
||||
|
||||
if (rpc_message != NULL) {
|
||||
if (strcmp(rpc_message, "approval_denied") == 0 || strcmp(rpc_message, "unauthorized") == 0) {
|
||||
return NOSTR_ERROR_AUTH_RULES_DENIED;
|
||||
}
|
||||
if (strcmp(rpc_message, "method_not_found") == 0) {
|
||||
return NOSTR_ERROR_NIP46_UNKNOWN_METHOD;
|
||||
}
|
||||
if (strcmp(rpc_message, "invalid_request") == 0) {
|
||||
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
|
||||
}
|
||||
if (strcmp(rpc_message, "ambiguous_role_selector") == 0 || strcmp(rpc_message, "unknown_role") == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
}
|
||||
|
||||
if (rpc_code >= 1000 && rpc_code < 2000) {
|
||||
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
|
||||
}
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
nsigner_client_t* nsigner_client_new(nsigner_transport_t* transport) {
|
||||
nsigner_client_t* client;
|
||||
|
||||
if (transport == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
client = (nsigner_client_t*)calloc(1, sizeof(*client));
|
||||
if (client == NULL) {
|
||||
transport->close(transport);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
client->transport = transport;
|
||||
client->next_id = 1;
|
||||
client->connected = 1; /* transport was opened connected by its constructor */
|
||||
nsigner_client_set_error(client, "ok");
|
||||
return client;
|
||||
}
|
||||
|
||||
void nsigner_client_free(nsigner_client_t* client) {
|
||||
if (client == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->transport != NULL) {
|
||||
client->transport->close(client->transport);
|
||||
client->transport = NULL;
|
||||
}
|
||||
|
||||
memset(client->auth_privkey, 0, sizeof(client->auth_privkey));
|
||||
memset(client->auth_label, 0, sizeof(client->auth_label));
|
||||
free(client);
|
||||
}
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t* client, const unsigned char privkey[32], const char* label) {
|
||||
if (client == NULL || privkey == NULL) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memcpy(client->auth_privkey, privkey, 32);
|
||||
client->auth_enabled = 1;
|
||||
|
||||
if (label == NULL) {
|
||||
label = "";
|
||||
}
|
||||
|
||||
strncpy(client->auth_label, label, sizeof(client->auth_label) - 1);
|
||||
client->auth_label[sizeof(client->auth_label) - 1] = '\0';
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nsigner_client_call(nsigner_client_t* client,
|
||||
const char* method,
|
||||
cJSON* params,
|
||||
cJSON** out_result) {
|
||||
cJSON* req = NULL;
|
||||
cJSON* res = NULL;
|
||||
cJSON* result_item;
|
||||
cJSON* error_item;
|
||||
cJSON* code_item;
|
||||
cJSON* message_item;
|
||||
cJSON* params_item;
|
||||
cJSON* auth = NULL;
|
||||
char id_buf[32];
|
||||
char* req_json = NULL;
|
||||
char* res_json = NULL;
|
||||
size_t res_len = 0;
|
||||
int rc;
|
||||
|
||||
if (client == NULL || method == NULL || out_result == NULL || client->transport == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*out_result = NULL;
|
||||
|
||||
nsigner_client_ignore_sigpipe_once();
|
||||
|
||||
/*
|
||||
* The n_signer server closes the connection after handling a single framed
|
||||
* request (one-request-per-connection model). Reconnect before every call
|
||||
* except the first (the transport was opened connected by the constructor).
|
||||
* Transports that do not support reconnection (fd-pair) skip this.
|
||||
*/
|
||||
if (client->connected && client->transport->reconnect != NULL) {
|
||||
if (client->transport->reconnect(client->transport) != 0) {
|
||||
nsigner_client_set_error(client, "transport reconnect failed");
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
}
|
||||
client->connected = 1;
|
||||
|
||||
rc = nsigner_client_next_id(client, id_buf);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(params);
|
||||
return rc;
|
||||
}
|
||||
|
||||
req = cJSON_CreateObject();
|
||||
if (req == NULL) {
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(req, "id", id_buf);
|
||||
cJSON_AddStringToObject(req, "method", method);
|
||||
|
||||
if (params == NULL) {
|
||||
params_item = cJSON_CreateNull();
|
||||
} else {
|
||||
params_item = params;
|
||||
params = NULL;
|
||||
}
|
||||
|
||||
if (params_item == NULL) {
|
||||
cJSON_Delete(req);
|
||||
cJSON_Delete(params);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(req, "params", params_item);
|
||||
|
||||
if (client->auth_enabled) {
|
||||
rc = nsigner_client_build_auth(client, id_buf, method, params_item, &auth);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
nsigner_client_set_error(client, "failed to build auth envelope");
|
||||
cJSON_Delete(req);
|
||||
return rc;
|
||||
}
|
||||
cJSON_AddItemToObject(req, "auth", auth);
|
||||
auth = NULL;
|
||||
}
|
||||
|
||||
req_json = cJSON_PrintUnformatted(req);
|
||||
cJSON_Delete(req);
|
||||
if (req_json == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
rc = client->transport->send_framed(client->transport, req_json, strlen(req_json));
|
||||
free(req_json);
|
||||
if (rc != 0) {
|
||||
nsigner_client_set_error(client, "transport send failed");
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
rc = client->transport->recv_framed(client->transport, &res_json, &res_len);
|
||||
if (rc != 0 || res_json == NULL || res_len == 0) {
|
||||
free(res_json);
|
||||
nsigner_client_set_error(client, "transport receive failed");
|
||||
return NOSTR_ERROR_IO_FAILED;
|
||||
}
|
||||
|
||||
res = cJSON_Parse(res_json);
|
||||
free(res_json);
|
||||
if (res == NULL) {
|
||||
nsigner_client_set_error(client, "invalid JSON response");
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
error_item = cJSON_GetObjectItemCaseSensitive(res, "error");
|
||||
if (cJSON_IsObject(error_item)) {
|
||||
int rpc_code = 0;
|
||||
const char* rpc_msg = "rpc_error";
|
||||
|
||||
code_item = cJSON_GetObjectItemCaseSensitive(error_item, "code");
|
||||
message_item = cJSON_GetObjectItemCaseSensitive(error_item, "message");
|
||||
if (cJSON_IsNumber(code_item)) {
|
||||
rpc_code = code_item->valueint;
|
||||
}
|
||||
if (cJSON_IsString(message_item) && message_item->valuestring != NULL) {
|
||||
rpc_msg = message_item->valuestring;
|
||||
}
|
||||
|
||||
nsigner_client_set_error(client, rpc_msg);
|
||||
cJSON_Delete(res);
|
||||
return nsigner_client_map_rpc_error(rpc_code, rpc_msg);
|
||||
}
|
||||
|
||||
result_item = cJSON_GetObjectItemCaseSensitive(res, "result");
|
||||
if (result_item == NULL) {
|
||||
nsigner_client_set_error(client, "missing result field");
|
||||
cJSON_Delete(res);
|
||||
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
|
||||
}
|
||||
|
||||
*out_result = cJSON_Duplicate(result_item, 1);
|
||||
cJSON_Delete(res);
|
||||
|
||||
if (*out_result == NULL) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
nsigner_client_set_error(client, "ok");
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
const char* nsigner_client_last_error(const nsigner_client_t* client) {
|
||||
if (client == NULL) {
|
||||
return "invalid_client";
|
||||
}
|
||||
return client->last_error;
|
||||
}
|
||||
|
||||
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef NSIGNER_CLIENT_H
|
||||
#define NSIGNER_CLIENT_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "nostr_common.h"
|
||||
#include "nsigner_transport.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
|
||||
typedef struct nsigner_client nsigner_client_t;
|
||||
|
||||
nsigner_client_t* nsigner_client_new(nsigner_transport_t* transport); /* takes ownership of transport */
|
||||
void nsigner_client_free(nsigner_client_t* client);
|
||||
|
||||
int nsigner_client_set_auth(nsigner_client_t* client, const unsigned char privkey[32], const char* label);
|
||||
|
||||
/*
|
||||
* low-level: build request JSON, attach auth envelope if enabled, send, recv, parse result/error
|
||||
* params may be NULL and ownership is transferred to the call.
|
||||
*/
|
||||
int nsigner_client_call(nsigner_client_t* client,
|
||||
const char* method,
|
||||
cJSON* params,
|
||||
cJSON** out_result);
|
||||
|
||||
const char* nsigner_client_last_error(const nsigner_client_t* client);
|
||||
|
||||
/* Exposed for offline tests and parity checks with n_signer hashing behavior. */
|
||||
int nsigner_client_compute_body_hash_hex(const cJSON* params, char out_hash_hex[65]);
|
||||
|
||||
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NSIGNER_CLIENT_H */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
#ifndef NSIGNER_TRANSPORT_H
|
||||
#define NSIGNER_TRANSPORT_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
|
||||
typedef struct nsigner_transport nsigner_transport_t;
|
||||
|
||||
struct nsigner_transport {
|
||||
int (*send_framed)(nsigner_transport_t* t, const char* json, size_t len);
|
||||
int (*recv_framed)(nsigner_transport_t* t, char** out_json, size_t* out_len); /* caller frees *out_json */
|
||||
/*
|
||||
* Re-establish the underlying connection. The n_signer server closes the
|
||||
* connection after handling a single framed request, so the client must
|
||||
* reconnect before each call. Returns 0 on success, non-zero on failure.
|
||||
* May be NULL for transports that do not support reconnection (e.g. fd-pair);
|
||||
* callers should treat a NULL reconnect as "not supported".
|
||||
*/
|
||||
int (*reconnect)(nsigner_transport_t* t);
|
||||
void (*close)(nsigner_transport_t* t);
|
||||
void* ctx;
|
||||
};
|
||||
|
||||
/* Raw framed I/O helpers for existing connected file descriptors. */
|
||||
int nsigner_transport_send_framed_fd(int fd, const char* json, size_t len);
|
||||
int nsigner_transport_recv_framed_fd(int fd, char** out_json, size_t* out_len);
|
||||
|
||||
/* UNIX abstract socket transport (socket_name without leading '@'). */
|
||||
nsigner_transport_t* nsigner_transport_open_unix(const char* socket_name, int timeout_ms);
|
||||
|
||||
/* TCP transport (host + port), with IPv4/IPv6 resolution via getaddrinfo. */
|
||||
nsigner_transport_t* nsigner_transport_open_tcp(const char* host, int port, int timeout_ms);
|
||||
|
||||
/* USB CDC-ACM serial transport (device path like /dev/ttyACM0). */
|
||||
nsigner_transport_t* nsigner_transport_open_serial(const char* device_path, int timeout_ms);
|
||||
|
||||
/*
|
||||
* Framed transport over existing file descriptors (read_fd, write_fd).
|
||||
* close() closes both owned fds. If passing STDIN_FILENO/STDOUT_FILENO and you do not
|
||||
* want them closed, pass dup()'d descriptors instead.
|
||||
*/
|
||||
nsigner_transport_t* nsigner_transport_open_fds(int read_fd, int write_fd, int timeout_ms);
|
||||
|
||||
/*
|
||||
* Qubes qrexec transport: spawns `qrexec-client-vm <target_qube> <service_name>`
|
||||
* as a subprocess and pipes framed I/O through its stdin/stdout. Each reconnect
|
||||
* re-spawns the subprocess (qrexec handles one request per invocation).
|
||||
* Only works on Qubes OS AppVMs; on other hosts the exec will fail.
|
||||
*/
|
||||
nsigner_transport_t* nsigner_transport_open_qrexec(const char* target_qube, const char* service_name, int timeout_ms);
|
||||
|
||||
/* Enumerate /proc/net/unix abstract sockets beginning with @nsigner, returns count copied. */
|
||||
int nsigner_transport_list_unix(char names[][64], int max_names);
|
||||
|
||||
/* Best-effort enumerate serial devices (e.g. /dev/ttyACM*), returns count copied. */
|
||||
int nsigner_transport_list_serial(char paths[][64], int max_paths);
|
||||
|
||||
#endif /* NOSTR_ENABLE_NSIGNER_CLIENT */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NSIGNER_TRANSPORT_H */
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sqlite3.h>
|
||||
#include <time.h>
|
||||
|
||||
@@ -241,8 +242,10 @@ void nostr_request_validator_cleanup(void) {
|
||||
// CONVENIENCE FUNCTIONS
|
||||
//=============================================================================
|
||||
|
||||
int nostr_auth_check_upload(const char* pubkey, const char* auth_header,
|
||||
int nostr_auth_check_upload(const char* pubkey, const char* auth_header,
|
||||
const char* hash, const char* mime_type, long file_size) {
|
||||
(void)pubkey; // Parameter not used in this convenience function
|
||||
|
||||
nostr_request_t request = {
|
||||
.operation = "upload",
|
||||
.auth_header = auth_header,
|
||||
@@ -265,6 +268,8 @@ int nostr_auth_check_upload(const char* pubkey, const char* auth_header,
|
||||
}
|
||||
|
||||
int nostr_auth_check_delete(const char* pubkey, const char* auth_header, const char* hash) {
|
||||
(void)pubkey; // Parameter not used in this convenience function
|
||||
|
||||
nostr_request_t request = {
|
||||
.operation = "delete",
|
||||
.auth_header = auth_header,
|
||||
@@ -287,6 +292,8 @@ int nostr_auth_check_delete(const char* pubkey, const char* auth_header, const c
|
||||
}
|
||||
|
||||
int nostr_auth_check_publish(const char* pubkey, struct cJSON* event) {
|
||||
(void)pubkey; // Parameter not used in this convenience function
|
||||
|
||||
if (!event) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
@@ -459,6 +466,8 @@ static int validate_nostr_event(struct cJSON* event, const char* expected_hash,
|
||||
//=============================================================================
|
||||
|
||||
static int sqlite_auth_init(const char* db_path, const char* app_name) {
|
||||
(void)app_name; // Parameter not used in this implementation
|
||||
|
||||
if (g_auth_db) {
|
||||
return NOSTR_SUCCESS; // Already initialized
|
||||
}
|
||||
@@ -765,13 +774,15 @@ static int sqlite_auth_rule_update(const nostr_auth_rule_t* rule) {
|
||||
}
|
||||
|
||||
static int sqlite_auth_rule_list(const char* operation, nostr_auth_rule_t** rules, int* count) {
|
||||
(void)operation; // Parameter not used in this implementation
|
||||
|
||||
if (!g_auth_db || !rules || !count) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
|
||||
*rules = NULL;
|
||||
*count = 0;
|
||||
|
||||
|
||||
// For now, return empty list - would implement full rule listing
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
@@ -1130,19 +1141,21 @@ static int evaluate_auth_rules(const char* pubkey, const char* operation, const
|
||||
static void generate_auth_cache_key(const char* pubkey, const char* operation, const char* hash,
|
||||
const char* mime_type, long file_size, char* cache_key, size_t key_size) {
|
||||
char temp_buffer[1024];
|
||||
snprintf(temp_buffer, sizeof(temp_buffer), "%s|%s|%s|%s|%ld",
|
||||
pubkey ? pubkey : "", operation ? operation : "",
|
||||
hash ? hash : "", mime_type ? mime_type : "", file_size);
|
||||
|
||||
int written = snprintf(temp_buffer, sizeof(temp_buffer), "%s|%s|%s|%s|%ld",
|
||||
pubkey ? pubkey : "", operation ? operation : "",
|
||||
hash ? hash : "", mime_type ? mime_type : "", file_size);
|
||||
|
||||
// Generate SHA-256 hash of the key components for consistent cache keys
|
||||
unsigned char hash_bytes[32];
|
||||
if (nostr_sha256((unsigned char*)temp_buffer, strlen(temp_buffer), hash_bytes) == NOSTR_SUCCESS) {
|
||||
size_t hash_len = (written >= 0 && (size_t)written < sizeof(temp_buffer)) ? (size_t)written : sizeof(temp_buffer) - 1;
|
||||
if (nostr_sha256((unsigned char*)temp_buffer, hash_len, hash_bytes) == NOSTR_SUCCESS) {
|
||||
nostr_bytes_to_hex(hash_bytes, 32, cache_key);
|
||||
cache_key[64] = '\0'; // Ensure null termination
|
||||
} else {
|
||||
// Fallback if hashing fails
|
||||
strncpy(cache_key, temp_buffer, key_size - 1);
|
||||
cache_key[key_size - 1] = '\0';
|
||||
// Fallback if hashing fails - safely copy up to key_size - 1 characters
|
||||
size_t copy_len = (written >= 0 && (size_t)written < key_size - 1) ? (size_t)written : key_size - 1;
|
||||
memcpy(cache_key, temp_buffer, copy_len);
|
||||
cache_key[copy_len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1179,6 +1192,8 @@ int nostr_auth_rule_list(const char* operation, nostr_auth_rule_t** rules, int*
|
||||
}
|
||||
|
||||
void nostr_auth_rules_free(nostr_auth_rule_t* rules, int count) {
|
||||
(void)count; // Parameter not used in this implementation
|
||||
|
||||
if (rules) {
|
||||
free(rules);
|
||||
}
|
||||
@@ -1200,6 +1215,8 @@ int nostr_auth_cache_stats(int* hit_count, int* miss_count, int* entries) {
|
||||
}
|
||||
|
||||
int nostr_auth_register_db_backend(const nostr_auth_db_interface_t* backend) {
|
||||
(void)backend; // Parameter not used in this implementation
|
||||
|
||||
// For now, only SQLite backend is supported
|
||||
return NOSTR_ERROR_AUTH_RULES_BACKEND_NOT_FOUND;
|
||||
}
|
||||
|
||||
+17
-6
@@ -433,32 +433,38 @@ int nostr_sha256_final(nostr_sha256_ctx_t* ctx, unsigned char* hash) {
|
||||
}
|
||||
|
||||
int nostr_sha256_file_stream(const char* filename, unsigned char* hash) {
|
||||
#ifndef NOSTR_NO_FILESYSTEM
|
||||
if (!filename || !hash) return -1;
|
||||
|
||||
|
||||
FILE* file = fopen(filename, "rb");
|
||||
if (!file) return -1;
|
||||
|
||||
|
||||
nostr_sha256_ctx_t ctx;
|
||||
if (nostr_sha256_init(&ctx) != 0) {
|
||||
fclose(file);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
// Process file in 4KB chunks for memory efficiency
|
||||
unsigned char buffer[4096];
|
||||
size_t bytes_read;
|
||||
|
||||
|
||||
while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
|
||||
if (nostr_sha256_update(&ctx, buffer, bytes_read) != 0) {
|
||||
fclose(file);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fclose(file);
|
||||
|
||||
|
||||
// Finalize and return result
|
||||
return nostr_sha256_final(&ctx, hash);
|
||||
#else
|
||||
(void)filename;
|
||||
(void)hash;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -1325,6 +1331,11 @@ static int find_word_index(const char* word) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char * const *nostr_bip39_get_wordlist(int *count) {
|
||||
if (count) *count = 2048;
|
||||
return BIP39_WORDLIST;
|
||||
}
|
||||
|
||||
int nostr_bip39_mnemonic_from_bytes(const unsigned char* entropy, size_t entropy_len,
|
||||
char* mnemonic) {
|
||||
if (!entropy || !mnemonic || entropy_len < 16 || entropy_len > 32) return -1;
|
||||
|
||||
@@ -24,6 +24,11 @@ void nostr_bytes_to_hex(const unsigned char *bytes, size_t len, char *hex);
|
||||
// Convert hexadecimal string to bytes
|
||||
int nostr_hex_to_bytes(const char *hex, unsigned char *bytes, size_t len);
|
||||
|
||||
// Get the BIP-39 English wordlist (2048 words). Returns a pointer to a
|
||||
// static array; count is set to 2048 if non-NULL. The pointer is valid
|
||||
// for the lifetime of the process.
|
||||
const char * const *nostr_bip39_get_wordlist(int *count);
|
||||
|
||||
// Base64 encoding function
|
||||
size_t base64_encode(const unsigned char *data, size_t len, char *output,
|
||||
size_t output_size);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"git.ignoreLimitWarning": true
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#define _GNU_SOURCE
|
||||
#include "nostr_websocket_tls.h"
|
||||
#include "../nostr_core/nostr_log.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -11,6 +12,18 @@
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <sys/select.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
static int ws_connect_debug_enabled(void) {
|
||||
static int initialized = 0;
|
||||
static int enabled = 0;
|
||||
if (!initialized) {
|
||||
const char* env = getenv("NOSTR_WS_CONNECT_DEBUG");
|
||||
enabled = (env && env[0] != '\0' && strcmp(env, "0") != 0);
|
||||
initialized = 1;
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// OpenSSL headers
|
||||
#include <openssl/ssl.h>
|
||||
@@ -22,15 +35,7 @@
|
||||
#define WS_MAGIC_STRING "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
#define WS_KEY_LEN 24
|
||||
#define MAX_HEADER_SIZE 4096
|
||||
#define MAX_FRAME_SIZE 65536
|
||||
|
||||
// Debug logging (conditional compilation)
|
||||
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
|
||||
static FILE* debug_log_file = NULL;
|
||||
static void debug_log_init(void);
|
||||
static void debug_log_message(const char* direction, const char* host, int port, const char* message);
|
||||
static const char* get_timestamp(void);
|
||||
#endif
|
||||
#define MAX_FRAME_SIZE 262144
|
||||
|
||||
// Transport layer abstraction
|
||||
typedef struct {
|
||||
@@ -52,6 +57,15 @@ typedef struct {
|
||||
SSL_CTX* ssl_ctx;
|
||||
SSL* ssl;
|
||||
int ssl_connected;
|
||||
/* Internal read buffer: SSL_read may return more bytes than the
|
||||
* caller requested (a full TLS record at once). Without buffering
|
||||
* the excess, those bytes are lost — which breaks ws_receive_frame
|
||||
* when it asks for a 2-byte header but SSL_read returns a full
|
||||
* event message. This caused the caching backfill to only receive
|
||||
* 1 event per query from TLS relays. */
|
||||
char read_buf[65536];
|
||||
size_t read_buf_len; /* total bytes in read_buf */
|
||||
size_t read_buf_pos; /* offset of next byte to consume */
|
||||
} tls_transport_t;
|
||||
|
||||
// WebSocket client structure
|
||||
@@ -131,7 +145,7 @@ static void init_openssl(void) {
|
||||
|
||||
nostr_ws_client_t* nostr_ws_connect(const char* url) {
|
||||
if (!url) return NULL;
|
||||
|
||||
|
||||
// Initialize OpenSSL
|
||||
init_openssl();
|
||||
|
||||
@@ -425,35 +439,159 @@ const char* nostr_ws_strerror(int error_code) {
|
||||
|
||||
static int tcp_connect(void* ctx, const char* host, int port) {
|
||||
tcp_transport_t* tcp = (tcp_transport_t*)ctx;
|
||||
|
||||
// Create socket
|
||||
tcp->socket_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
struct addrinfo hints;
|
||||
struct addrinfo* result = NULL;
|
||||
struct addrinfo* rp = NULL;
|
||||
char port_str[16];
|
||||
int ret;
|
||||
int addr_idx = 0;
|
||||
time_t connect_start = time(NULL);
|
||||
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] tcp_connect_start host=%s port=%d\n", host ? host : "(null)", port);
|
||||
}
|
||||
|
||||
tcp->socket_fd = -1;
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC; // IPv4 or IPv6
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_protocol = IPPROTO_TCP;
|
||||
|
||||
snprintf(port_str, sizeof(port_str), "%d", port);
|
||||
ret = getaddrinfo(host, port_str, &hints, &result);
|
||||
if (ret != 0 || !result) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] getaddrinfo_failed host=%s port=%d ret=%d err=%s\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
ret,
|
||||
gai_strerror(ret));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (rp = result; rp != NULL; rp = rp->ai_next) {
|
||||
int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
addr_idx++;
|
||||
if (fd < 0) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] socket_failed host=%s port=%d addr_idx=%d family=%d errno=%d\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
rp->ai_family,
|
||||
errno);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_attempt host=%s port=%d addr_idx=%d family=%d\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
rp->ai_family);
|
||||
}
|
||||
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
if (flags >= 0) {
|
||||
(void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
}
|
||||
|
||||
int connect_rc = connect(fd, rp->ai_addr, rp->ai_addrlen);
|
||||
if (connect_rc == 0) {
|
||||
tcp->socket_fd = fd;
|
||||
if (flags >= 0) {
|
||||
(void)fcntl(fd, F_SETFL, flags);
|
||||
}
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_success host=%s port=%d addr_idx=%d elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (connect_rc < 0 && errno == EINPROGRESS) {
|
||||
fd_set wfds;
|
||||
struct timeval tv;
|
||||
int sel_rc;
|
||||
int so_error = 0;
|
||||
socklen_t so_error_len = sizeof(so_error);
|
||||
|
||||
FD_ZERO(&wfds);
|
||||
FD_SET(fd, &wfds);
|
||||
tv.tv_sec = 5;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
sel_rc = select(fd + 1, NULL, &wfds, NULL, &tv);
|
||||
if (sel_rc > 0 && FD_ISSET(fd, &wfds) &&
|
||||
getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &so_error_len) == 0 &&
|
||||
so_error == 0) {
|
||||
tcp->socket_fd = fd;
|
||||
if (flags >= 0) {
|
||||
(void)fcntl(fd, F_SETFL, flags);
|
||||
}
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_success_after_wait host=%s port=%d addr_idx=%d elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_timeout_or_error host=%s port=%d addr_idx=%d select_rc=%d so_error=%d elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
sel_rc,
|
||||
so_error,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
} else if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] connect_failed host=%s port=%d addr_idx=%d errno=%d elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
addr_idx,
|
||||
errno,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
|
||||
close(fd);
|
||||
}
|
||||
|
||||
freeaddrinfo(result);
|
||||
|
||||
if (tcp->socket_fd < 0) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] tcp_connect_end host=%s port=%d rc=-1 elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Resolve hostname
|
||||
struct hostent* he = gethostbyname(host);
|
||||
if (!he) {
|
||||
close(tcp->socket_fd);
|
||||
tcp->socket_fd = -1;
|
||||
return -1;
|
||||
|
||||
// Safety timeout: prevent indefinite blocking in recv/send on half-broken sockets.
|
||||
struct timeval io_timeout;
|
||||
io_timeout.tv_sec = 5;
|
||||
io_timeout.tv_usec = 0;
|
||||
(void)setsockopt(tcp->socket_fd, SOL_SOCKET, SO_RCVTIMEO, &io_timeout, sizeof(io_timeout));
|
||||
(void)setsockopt(tcp->socket_fd, SOL_SOCKET, SO_SNDTIMEO, &io_timeout, sizeof(io_timeout));
|
||||
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr, "[nostr_ws][connect] tcp_connect_end host=%s port=%d rc=0 elapsed_s=%ld\n",
|
||||
host ? host : "(null)",
|
||||
port,
|
||||
(long)(time(NULL) - connect_start));
|
||||
}
|
||||
|
||||
// Set up address
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
|
||||
|
||||
// Connect
|
||||
if (connect(tcp->socket_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
close(tcp->socket_fd);
|
||||
tcp->socket_fd = -1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -579,24 +717,47 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
|
||||
if (!tls->ssl_connected || !tls->ssl) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check if SSL has pending data first
|
||||
if (SSL_pending(tls->ssl) == 0 && timeout_ms > 0) {
|
||||
// Only use select() if no data is pending in SSL buffers
|
||||
|
||||
/* Serve from the internal read buffer first. SSL_read may have
|
||||
* returned more bytes than the caller requested on a previous call
|
||||
* (a full TLS record), and we buffered the excess. Without this,
|
||||
* ws_receive_frame loses the extra bytes when it asks for a 2-byte
|
||||
* header but SSL_read returns a full event message. */
|
||||
if (tls->read_buf_pos < tls->read_buf_len) {
|
||||
size_t avail = tls->read_buf_len - tls->read_buf_pos;
|
||||
size_t to_copy = (avail < len) ? avail : len;
|
||||
memcpy(data, tls->read_buf + tls->read_buf_pos, to_copy);
|
||||
tls->read_buf_pos += to_copy;
|
||||
if (tls->read_buf_pos >= tls->read_buf_len) {
|
||||
tls->read_buf_len = 0;
|
||||
tls->read_buf_pos = 0;
|
||||
}
|
||||
return (int)to_copy;
|
||||
}
|
||||
|
||||
/* Buffer is empty — refill it with a single SSL_read, then serve
|
||||
* the requested bytes from it. This ensures excess bytes are
|
||||
* preserved for subsequent tls_recv calls. */
|
||||
if (timeout_ms > 0) {
|
||||
fd_set readfds;
|
||||
struct timeval tv;
|
||||
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(tls->socket_fd, &readfds);
|
||||
|
||||
tv.tv_sec = timeout_ms / 1000;
|
||||
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
||||
if (SSL_pending(tls->ssl) > 0) {
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 0;
|
||||
} else {
|
||||
tv.tv_sec = timeout_ms / 1000;
|
||||
tv.tv_usec = (timeout_ms % 1000) * 1000;
|
||||
}
|
||||
|
||||
int result = select(tls->socket_fd + 1, &readfds, NULL, NULL, &tv);
|
||||
if (result < 0) {
|
||||
return -1;
|
||||
} else if (result == 0) {
|
||||
return -1; // Timeout
|
||||
} else if (result == 0 && SSL_pending(tls->ssl) == 0) {
|
||||
return -1; // Timeout with no pending data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,16 +766,26 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
|
||||
const int max_attempts = 10;
|
||||
|
||||
while (attempts < max_attempts) {
|
||||
int ret = SSL_read(tls->ssl, data, len);
|
||||
/* Read into the internal buffer (up to its capacity), then copy
|
||||
* the requested number of bytes to the caller's output. */
|
||||
int ret = SSL_read(tls->ssl, tls->read_buf, sizeof(tls->read_buf));
|
||||
|
||||
if (ret > 0) {
|
||||
return ret; // Success
|
||||
tls->read_buf_len = (size_t)ret;
|
||||
tls->read_buf_pos = 0;
|
||||
size_t to_copy = (tls->read_buf_len < len) ? tls->read_buf_len : len;
|
||||
memcpy(data, tls->read_buf, to_copy);
|
||||
tls->read_buf_pos = to_copy;
|
||||
if (tls->read_buf_pos >= tls->read_buf_len) {
|
||||
tls->read_buf_len = 0;
|
||||
tls->read_buf_pos = 0;
|
||||
}
|
||||
return (int)to_copy;
|
||||
}
|
||||
|
||||
int err = SSL_get_error(tls->ssl, ret);
|
||||
|
||||
if (err == SSL_ERROR_WANT_READ) {
|
||||
// Check if more data is available on socket
|
||||
fd_set readfds;
|
||||
struct timeval tv = {0, 100000}; // 100ms timeout
|
||||
|
||||
@@ -627,7 +798,7 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
|
||||
}
|
||||
|
||||
attempts++;
|
||||
continue; // Retry the SSL read
|
||||
continue;
|
||||
|
||||
} else if (err == SSL_ERROR_WANT_WRITE) {
|
||||
return -1;
|
||||
@@ -781,32 +952,35 @@ static int ws_perform_handshake(nostr_ws_client_t* client, const char* key) {
|
||||
// Read response
|
||||
char response[MAX_HEADER_SIZE];
|
||||
int total_received = 0;
|
||||
|
||||
|
||||
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);
|
||||
int received = client->transport->recv(&client->transport_ctx,
|
||||
response + total_received,
|
||||
sizeof(response) - total_received - 1,
|
||||
client->timeout_ms);
|
||||
if (received <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
total_received += received;
|
||||
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
|
||||
if (strncmp(response, "HTTP/1.1 101 Switching Protocols\r\n", 34) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!strstr(response, "Upgrade: websocket") && !strstr(response, "upgrade: websocket")) {
|
||||
|
||||
if (!strstr(response, "Upgrade: websocket") && !strstr(response, "upgrade: websocket") &&
|
||||
!strstr(response, "Upgrade: WebSocket") && !strstr(response, "upgrade: WebSocket")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -850,10 +1024,15 @@ static int ws_send_frame(nostr_ws_client_t* client, ws_opcode_t opcode, const ch
|
||||
frame_len += payload_len;
|
||||
}
|
||||
|
||||
// Log outgoing message to debug.log
|
||||
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
|
||||
// Emit outgoing message to consumer callback logger
|
||||
#if defined(ENABLE_WEBSOCKET_LOGGING)
|
||||
if (opcode == WS_OPCODE_TEXT && payload && payload_len > 0) {
|
||||
debug_log_message("SEND", client->host, client->port, payload);
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_TRACE,
|
||||
"websocket",
|
||||
"SEND %s:%d: %s",
|
||||
client->host ? client->host : "",
|
||||
client->port,
|
||||
payload);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -866,13 +1045,22 @@ static int ws_send_frame(nostr_ws_client_t* client, ws_opcode_t opcode, const ch
|
||||
static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char* payload, size_t* payload_len, int timeout_ms) {
|
||||
if (!client || !opcode || !payload || !payload_len) return -1;
|
||||
|
||||
char header[14]; // Max header size
|
||||
unsigned char header[14]; // Max header size
|
||||
size_t header_len = 2; // Minimum header size
|
||||
|
||||
// Read basic header
|
||||
int header_result = client->transport->recv(&client->transport_ctx, header, 2, timeout_ms);
|
||||
|
||||
if (header_result != 2) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] short_header host=%s port=%d got=%d expected=2 timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
header_result,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -884,12 +1072,28 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
||||
// Extended length
|
||||
if (len == 126) {
|
||||
if (client->transport->recv(&client->transport_ctx, header + 2, 2, timeout_ms) != 2) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] short_ext16 host=%s port=%d timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
len = ((uint16_t)header[2] << 8) | (uint8_t)header[3];
|
||||
header_len = 4;
|
||||
} else if (len == 127) {
|
||||
if (client->transport->recv(&client->transport_ctx, header + 2, 8, timeout_ms) != 8) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] short_ext64 host=%s port=%d timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
// For simplicity, we don't support 64-bit lengths
|
||||
@@ -898,15 +1102,18 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
||||
header_len = 10;
|
||||
}
|
||||
|
||||
// Check payload length
|
||||
if (len > *payload_len) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read mask (if present)
|
||||
uint32_t mask = 0;
|
||||
if (masked) {
|
||||
if (client->transport->recv(&client->transport_ctx, header + header_len, 4, timeout_ms) != 4) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] short_mask host=%s port=%d timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
mask = ((uint32_t)header[header_len] << 24) |
|
||||
@@ -915,6 +1122,41 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
||||
header[header_len + 3];
|
||||
header_len += 4;
|
||||
}
|
||||
|
||||
// Check payload length; if too large for caller buffer, drain frame to keep stream aligned
|
||||
if (len > *payload_len) {
|
||||
char discard[1024];
|
||||
uint64_t remaining = len;
|
||||
int drain_timeout_ms = timeout_ms < 5000 ? 5000 : timeout_ms;
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] payload_too_large host=%s port=%d frame_len=%llu buffer_cap=%zu drain_timeout_ms=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
(unsigned long long)len,
|
||||
*payload_len,
|
||||
drain_timeout_ms);
|
||||
}
|
||||
while (remaining > 0) {
|
||||
size_t chunk = remaining > sizeof(discard) ? sizeof(discard) : (size_t)remaining;
|
||||
int got = client->transport->recv(&client->transport_ctx, discard, chunk, drain_timeout_ms);
|
||||
if (got <= 0) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] discard_read_failed host=%s port=%d got=%d drain_timeout_ms=%d errno=%d remaining=%llu\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
got,
|
||||
drain_timeout_ms,
|
||||
errno,
|
||||
(unsigned long long)remaining);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
remaining -= (uint64_t)got;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read payload
|
||||
if (len > 0) {
|
||||
@@ -924,7 +1166,19 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
||||
payload + received,
|
||||
len - received,
|
||||
timeout_ms);
|
||||
if (chunk <= 0) return -1;
|
||||
if (chunk <= 0) {
|
||||
if (ws_connect_debug_enabled()) {
|
||||
fprintf(stderr,
|
||||
"[nostr_ws][frame] payload_read_failed host=%s port=%d got=%d need_remaining=%zu timeout_ms=%d errno=%d\n",
|
||||
client->host ? client->host : "(null)",
|
||||
client->port,
|
||||
chunk,
|
||||
len - received,
|
||||
timeout_ms,
|
||||
errno);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
received += chunk;
|
||||
}
|
||||
|
||||
@@ -933,14 +1187,19 @@ static int ws_receive_frame(nostr_ws_client_t* client, ws_opcode_t* opcode, char
|
||||
ws_mask_payload(payload, len, mask);
|
||||
}
|
||||
|
||||
// Log incoming text messages to debug.log
|
||||
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
|
||||
// Emit incoming text messages to consumer callback logger
|
||||
#if defined(ENABLE_WEBSOCKET_LOGGING)
|
||||
if (*opcode == WS_OPCODE_TEXT && len > 0) {
|
||||
// Null terminate for logging
|
||||
char temp_payload[len + 1];
|
||||
memcpy(temp_payload, payload, len);
|
||||
temp_payload[len] = '\0';
|
||||
debug_log_message("RECV", client->host, client->port, temp_payload);
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_TRACE,
|
||||
"websocket",
|
||||
"RECV %s:%d: %s",
|
||||
client->host ? client->host : "",
|
||||
client->port,
|
||||
temp_payload);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -971,44 +1230,3 @@ static uint32_t ws_generate_mask(void) {
|
||||
return ((uint32_t)rand() << 16) | ((uint32_t)rand() & 0xFFFF);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Debug Logging Functions
|
||||
// ============================================================================
|
||||
|
||||
#if defined(ENABLE_FILE_LOGGING) && defined(ENABLE_WEBSOCKET_LOGGING)
|
||||
static void debug_log_init(void) {
|
||||
if (!debug_log_file) {
|
||||
debug_log_file = fopen("debug.log", "a");
|
||||
if (debug_log_file) {
|
||||
fprintf(debug_log_file, "\n=== NOSTR WebSocket Debug Log Started ===\n");
|
||||
fflush(debug_log_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const char* get_timestamp(void) {
|
||||
static char timestamp[32];
|
||||
struct timespec ts;
|
||||
struct tm *timeinfo;
|
||||
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
timeinfo = localtime(&ts.tv_sec);
|
||||
|
||||
// Format: HH:MM:SS.mmm (with milliseconds)
|
||||
strftime(timestamp, sizeof(timestamp), "%H:%M:%S", timeinfo);
|
||||
snprintf(timestamp + 8, sizeof(timestamp) - 8, ".%03ld", ts.tv_nsec / 1000000);
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
static void debug_log_message(const char* direction, const char* host, int port, const char* message) {
|
||||
debug_log_init();
|
||||
|
||||
if (debug_log_file) {
|
||||
fprintf(debug_log_file, "[%s] %s %s:%d: %s\n",
|
||||
get_timestamp(), direction, host, port, message);
|
||||
fflush(debug_log_file);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Logging Delegation Plan for `nostr_core_lib`
|
||||
|
||||
## Objective
|
||||
|
||||
Refactor `nostr_core_lib` so it does not own log destinations such as `debug.log`, and instead emits logs through a consumer-provided callback. This allows host applications such as [`didactyl`](../README.md) to route all library logs into their own logging system with consistent formatting, level control, and file policy.
|
||||
|
||||
## Current State and Problem
|
||||
|
||||
The library currently writes directly to `debug.log` in multiple places:
|
||||
|
||||
- [`nostr_core_lib/nostr_websocket/nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:1008)
|
||||
- [`nostr_core_lib/nostr_core/nip013.c`](../nostr_core/nip013.c:178)
|
||||
- [`nostr_core_lib/nostr_core/nip013.c`](../nostr_core/nip013.c:242)
|
||||
- [`nostr_core_lib/nostr_core/nip013.c`](../nostr_core/nip013.c:270)
|
||||
|
||||
This creates collisions with consumer logs when the host app also uses `debug.log` such as [`didactyl/src/debug.c`](../../src/debug.c:18).
|
||||
|
||||
## Target Design
|
||||
|
||||
Adopt a callback-based logging API inside `nostr_core_lib`:
|
||||
|
||||
1. Library owns no file handle for logs.
|
||||
2. Library emits structured log events by level and source.
|
||||
3. Consumer registers a callback once during startup.
|
||||
4. If no callback is set, logs are dropped by default.
|
||||
|
||||
### High-Level Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[nostr_core_lib internal code] --> B[nostr_log_emit level source message]
|
||||
B --> C{callback configured}
|
||||
C -->|Yes| D[consumer callback]
|
||||
C -->|No| E[drop log event]
|
||||
D --> F[didactyl debug_log routing]
|
||||
F --> G[didactyl log file or stdout]
|
||||
```
|
||||
|
||||
## API Additions
|
||||
|
||||
Add a small public logging API in [`nostr_core_lib/nostr_core/nostr_core.h`](../nostr_core/nostr_core.h):
|
||||
|
||||
- `nostr_log_level_t` enum with levels compatible with host systems
|
||||
- `nostr_log_callback_t` callback type
|
||||
- `void nostr_set_log_callback(nostr_log_callback_t cb, void* user_data);`
|
||||
- `void nostr_set_log_level(nostr_log_level_t min_level);`
|
||||
|
||||
Recommended callback shape:
|
||||
|
||||
```c
|
||||
typedef void (*nostr_log_callback_t)(
|
||||
int level,
|
||||
const char* component,
|
||||
const char* message,
|
||||
void* user_data
|
||||
);
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Keep ABI simple by formatting message inside the library before callback invocation.
|
||||
- Include `component` values such as `websocket` and `nip013`.
|
||||
|
||||
## Internal Implementation Plan
|
||||
|
||||
## Phase 1: Logging Core
|
||||
|
||||
1. Create `nostr_core_lib/nostr_core/nostr_log.h` and `nostr_core_lib/nostr_core/nostr_log.c`.
|
||||
2. Add internal singleton state:
|
||||
- callback pointer
|
||||
- callback user data
|
||||
- minimum log level
|
||||
3. Add internal helpers:
|
||||
- `nostr_log_emitf level component fmt ...`
|
||||
- stack buffer formatting with truncation safety
|
||||
4. Default behavior when no callback set: no-op.
|
||||
|
||||
## Phase 2: Replace File Logging in WebSocket Layer
|
||||
|
||||
1. Remove file-specific static globals from [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:27).
|
||||
2. Remove `debug_log_init` and `debug_log_message` path at [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:1005).
|
||||
3. Replace call sites:
|
||||
- outgoing frame logging near [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:871)
|
||||
- incoming frame logging near [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:964)
|
||||
4. Emit through `nostr_log_emitf` with component `websocket`.
|
||||
|
||||
## Phase 3: Replace File Logging in NIP-13
|
||||
|
||||
1. Replace `fopen`/`fprintf` blocks in [`nip013.c`](../nostr_core/nip013.c:178), [`nip013.c`](../nostr_core/nip013.c:242), and [`nip013.c`](../nostr_core/nip013.c:270).
|
||||
2. Emit equivalent debug lines via `nostr_log_emitf` with component `nip013`.
|
||||
3. Preserve existing compile-time verbosity guards if needed, but route output through callback.
|
||||
|
||||
## Phase 4: Wire Public API
|
||||
|
||||
1. Export new functions in [`nostr_core.h`](../nostr_core/nostr_core.h:1).
|
||||
2. Ensure object file inclusion in `nostr_core_lib` build scripts and static archive.
|
||||
3. Document behavior in [`nostr_core_lib/README.md`](../README.md).
|
||||
|
||||
## Phase 5: Consumer Integration in Didactyl
|
||||
|
||||
1. In didactyl initialization path near [`src/nostr_handler.c`](../../src/nostr_handler.c:1519), register `nostr_set_log_callback`.
|
||||
2. Adapter callback maps library levels to didactyl levels from [`src/debug.h`](../../src/debug.h:7).
|
||||
3. Prefix messages with source namespace such as `[nostr:websocket]` and `[nostr:nip013]`.
|
||||
|
||||
## Backward Compatibility Strategy
|
||||
|
||||
- Keep existing compile guards but change behavior from file output to callback emission.
|
||||
- If callback not set, behavior remains safe and silent.
|
||||
- No breaking changes for consumers not using logging.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Unit Tests in `nostr_core_lib`
|
||||
|
||||
1. Add tests for callback registration and null behavior.
|
||||
2. Add tests for level filtering.
|
||||
3. Add tests that message formatting truncates safely and remains null terminated.
|
||||
|
||||
### Integration Checks in Didactyl
|
||||
|
||||
1. Start didactyl with debug enabled and callback wired.
|
||||
2. Verify websocket SEND and RECV lines appear through didactyl logger format from [`src/debug.c`](../../src/debug.c:31).
|
||||
3. Verify no separate websocket-only banner line from old path near [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:1010).
|
||||
4. Verify LLM request log lines and nostr logs coexist in one log destination.
|
||||
|
||||
## Rollout Steps
|
||||
|
||||
1. Implement callback infrastructure and migrate websocket logging first.
|
||||
2. Validate no direct file writes remain in websocket module.
|
||||
3. Migrate nip013 logging.
|
||||
4. Add documentation and examples.
|
||||
5. Update didactyl wiring.
|
||||
6. Remove obsolete references to `ENABLE_FILE_LOGGING` for websocket logging behavior if no longer needed.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- Risk: excessive callback volume from websocket trace traffic.
|
||||
- Mitigation: implement minimum level filter in library and map websocket traffic to trace level.
|
||||
- Risk: callback reentrancy or thread concerns.
|
||||
- Mitigation: document callback must be fast and thread-safe, avoid heavy blocking in callback.
|
||||
- Risk: API drift across consumers.
|
||||
- Mitigation: keep callback signature minimal and stable.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- No direct `fopen("debug.log", ...)` remains in production logging paths in `nostr_core_lib`.
|
||||
- Consumer can set callback and receive websocket and nip013 logs.
|
||||
- Didactyl routes nostr_core_lib logs through its logger and single configured destination.
|
||||
- Documentation updated with migration and usage examples.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Plan: n_signer Verb Name Migration for nostr_core_lib
|
||||
|
||||
## Context
|
||||
|
||||
`n_signer` has completed a verb-name migration (see [`n_signer/plans/legacy_verb_aliases.md`](../../n_signer/plans/legacy_verb_aliases.md)). The legacy verb names are gone from the n_signer wire protocol. This document covers the fixes needed in `nostr_core_lib` — the shared C client library that many other projects vendor.
|
||||
|
||||
**Reference:** the new n_signer API is documented in [`n_signer/README.md`](../../n_signer/README.md) §4 (API).
|
||||
|
||||
## What changed in n_signer
|
||||
|
||||
The Nostr protocol verbs gained a `nostr_` prefix. The algorithm-based verbs (`sign`, `verify`, `encapsulate`, `decapsulate`, `derive_shared_secret`, `get_public_key`) are unchanged. The old alias verbs (`sign_data`, `ssh_sign`, `verify_signature`, `kem_encapsulate`, `kem_decapsulate`, `otp_encrypt`, `otp_decrypt`) are gone.
|
||||
|
||||
| Legacy n_signer verb | New n_signer verb |
|
||||
|----------------------|-------------------|
|
||||
| `sign_event` | `nostr_sign_event` |
|
||||
| `mine_event` | `nostr_mine_event` |
|
||||
| `nip04_encrypt` | `nostr_nip04_encrypt` |
|
||||
| `nip04_decrypt` | `nostr_nip04_decrypt` |
|
||||
| `nip44_encrypt` | `nostr_nip44_encrypt` |
|
||||
| `nip44_decrypt` | `nostr_nip44_decrypt` |
|
||||
| `get_public_key` (role-based) | `nostr_get_public_key` |
|
||||
|
||||
**Important distinction:** `get_public_key` is now **algorithm-based** (takes `algorithm`+`index`). The role-based Nostr pubkey verb is `nostr_get_public_key` (takes `nostr_index`/`role`). Since `nostr_core_lib`'s remote signer always uses role-based selectors (`nostr_index`/`role`), the correct replacement is `nostr_get_public_key`.
|
||||
|
||||
## What does NOT change
|
||||
|
||||
- **NIP-46 standard method names** in [`nostr_core/nip046.c`](../nostr_core/nip046.c) — the strings `"sign_event"`, `"get_public_key"`, `"nip04_encrypt"`, `"nip44_encrypt"`, etc. at lines 111-131 are the **NIP-46 wire protocol** (the standard that relays and other bunkers speak). These must stay as-is. They are NOT n_signer verbs.
|
||||
- **High-level C API function names** in [`nostr_core/nostr_signer.h`](../nostr_core/nostr_signer.h) — `nostr_signer_get_public_key()`, `nostr_signer_sign_event()`, `nostr_signer_nip04_encrypt()`, etc. are the nostr_core_lib's own API surface. These names are fine — they're C function names, not wire verbs. Renaming them would break every consumer for no protocol reason.
|
||||
- **The `nostr_signer_local` backend** — local signing doesn't call n_signer at all; it uses libsecp256k1 directly. No changes needed.
|
||||
|
||||
## Files to change
|
||||
|
||||
### 1. `nostr_core/nostr_signer.c` — the remote signer backend
|
||||
|
||||
This is the only file that calls n_signer verbs via `nsigner_client_call()`. All calls use role-based selectors (`nostr_index`/`role`), so all map to the `nostr_*` verbs.
|
||||
|
||||
| Line | Current verb string | New verb string |
|
||||
|------|---------------------|-----------------|
|
||||
| 343 | `"get_public_key"` | `"nostr_get_public_key"` |
|
||||
| 395 | `"sign_event"` | `"nostr_sign_event"` |
|
||||
| 484 | `"nip04_encrypt"` | `"nostr_nip04_encrypt"` |
|
||||
| 491 | `"nip04_decrypt"` | `"nostr_nip04_decrypt"` |
|
||||
| 498 | `"nip44_encrypt"` | `"nostr_nip44_encrypt"` |
|
||||
| 505 | `"nip44_decrypt"` | `"nostr_nip44_decrypt"` |
|
||||
|
||||
These are simple string-literal replacements. The `signer_remote_params_with_selector()` function (line 289) builds `{"nostr_index": N}` or `{"role": "..."}` selectors — these are still valid for the `nostr_*` verbs, so no selector changes needed.
|
||||
|
||||
**Response format note:** `nostr_get_public_key` returns a plain 64-hex-char string by default (same as the old role-based `get_public_key`). The existing parsing at line 348 (`strlen(result->valuestring) != 64`) continues to work. If the caller ever passes `{"format":"structured"}`, the response would be a JSON object string — but the current code doesn't request structured format, so no change needed.
|
||||
|
||||
### 2. `tests/nsigner_client_test.c` — the mock client test
|
||||
|
||||
| Line | Current verb string | New verb string |
|
||||
|------|---------------------|-----------------|
|
||||
| 296 | `"get_public_key"` | `"nostr_get_public_key"` |
|
||||
|
||||
This test uses a mock transport (fds pair) that returns a canned response — it doesn't actually call n_signer. The verb string just needs to match what the real client would send. Update it for consistency.
|
||||
|
||||
### 3. `nostr_core/NSIGNER_INTEGRATION.md` — integration doc
|
||||
|
||||
This doc references the high-level C API function names (`nostr_signer_get_public_key`, `nostr_signer_sign_event`, etc.) which are NOT changing. However, if it documents the underlying n_signer wire verbs anywhere, those references should be updated to the `nostr_*` names. Scan for any wire-verb documentation and update.
|
||||
|
||||
### 4. `plans/nsigner_integration_plan.md` — historical plan
|
||||
|
||||
This is a historical planning doc. Update any wire-verb references for accuracy, or add a note pointing to this migration plan.
|
||||
|
||||
## Files that do NOT need changes
|
||||
|
||||
- `nostr_core/nip046.c` — NIP-46 standard method names (stay as-is, see above).
|
||||
- `nostr_core/nsigner_client.c` — generic transport/client, doesn't hardcode verb names.
|
||||
- `nostr_core/nsigner_transport.c` — transport layer, no verb names.
|
||||
- `nostr_core/nostr_signer.h` — high-level API declarations (function names stay).
|
||||
- `tests/signer_modules_test.c` — tests the local signer, not n_signer verbs.
|
||||
- `tests/nip01_test.c` — NIP-01 tests, no n_signer verb calls.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `grep -rn '"sign_event"\|"nip04_encrypt"\|"nip04_decrypt"\|"nip44_encrypt"\|"nip44_decrypt"\|"mine_event"\|"sign_data"\|"ssh_sign"\|"verify_signature"\|"kem_encapsulate"\|"kem_decapsulate"\|"otp_encrypt"\|"otp_decrypt"' nostr_core/nostr_signer.c` returns no matches.
|
||||
2. `grep -rn '"get_public_key"' nostr_core/nostr_signer.c` returns no matches (replaced by `nostr_get_public_key`).
|
||||
3. `grep -rn '"sign_event"\|"nip04_encrypt"\|"nip44_encrypt"' nostr_core/nip046.c` still returns matches (NIP-46 standard names — these stay).
|
||||
4. Build clean: `bash build.sh` or the project's build target.
|
||||
5. Tests pass: `./build/nsigner_client_test` (or equivalent).
|
||||
|
||||
## Downstream consumers (inherit the fix)
|
||||
|
||||
Once `nostr_core_lib` is updated, these projects that vendor it will pick up the fix when they update their vendored copy:
|
||||
|
||||
- `sovereign_browser/nostr_core_lib/`
|
||||
- `didactyl/nostr_core_lib/`
|
||||
- `fips/nostr_core_lib/`
|
||||
- `fips_access_point/` (if it vendors nostr_core_lib)
|
||||
- `screen_capture/nostr_core_lib/`
|
||||
- `n_os_tr/includes/nostr_core_lib/` and `n_os_tr/includes/didactyl/nostr_core_lib/`
|
||||
|
||||
Each downstream project should update its vendored `nostr_core/nostr_signer.c` with the same 6 string-literal changes. The `nip046.c` file in each vendored copy should NOT be changed (NIP-46 standard names).
|
||||
|
||||
## Projects with their own n_signer clients (separate fixes)
|
||||
|
||||
These projects have their own n_signer client code (not vendored from nostr_core_lib) and need separate fixes:
|
||||
|
||||
- **`nostr_terminal/src/nsigner_client.c`** — many call sites (lines 1054-1452): `"get_public_key"`→`"nostr_get_public_key"`, `"sign_event"`→`"nostr_sign_event"`, `"nip04_encrypt"`→`"nostr_nip04_encrypt"`, `"nip44_encrypt"`→`"nostr_nip44_encrypt"`. Also vendored into `n_os_tr/includes/nostr_terminal/`.
|
||||
- **`codium_nostr_extension/src/signer/nsignerBackend.ts`** — `"get_public_key"` (line 65)→`"nostr_get_public_key"`, `"sign_event"` (line 76)→`"nostr_sign_event"`.
|
||||
- **`nostr_push/nostr_push`** (bash script) — `"sign_event"` (line 394)→`"nostr_sign_event"`, `"get_public_key"`→`"nostr_get_public_key"`.
|
||||
- **`laantungir_website/scripts/get_nsigner_pubkey.js`** — `"get_public_key"` (line 68)→`"nostr_get_public_key"`.
|
||||
- **`laantungir_website/scripts/publish_nostr.js`** — `"get_public_key"` (line 102)→`"nostr_get_public_key"`, `"sign_event"` (line 112)→`"nostr_sign_event"`.
|
||||
- **`sovereign_browser/src/nostr_bridge.c`** — `"nip04_encrypt"` (line 4525)→`"nostr_nip04_encrypt"`, `"nip44_encrypt"` (line 4529)→`"nostr_nip44_encrypt"`.
|
||||
- **`nostr_login_lite`** — check `src/signers/nsigner-webserial.js`, `nsigner-webusb.js`, and the built `www/` bundle for verb calls.
|
||||
@@ -0,0 +1,135 @@
|
||||
# NIP-03 OpenTimestamps Fix Plan
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
The NIP-03 timestamping implementation in `nostr_core/nip003.c` was never working properly due to several critical bugs in the OTS proof parsing and upgrade logic.
|
||||
|
||||
## OTS Binary Format (Verified against real .ots file)
|
||||
|
||||
### File Structure
|
||||
1. **Header magic** (31 bytes): `\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94`
|
||||
2. **Major version** (LEB128 varuint): typically `0x01`
|
||||
3. **File hash op** (1 byte): `0x08` = SHA256
|
||||
4. **File digest** (32 bytes for SHA256)
|
||||
5. **Timestamp tree** (recursive structure)
|
||||
|
||||
### Timestamp Tree Format
|
||||
- Read a tag byte
|
||||
- `0xff` = separator (read next tag, process it, continue looping)
|
||||
- `0x00` = attestation follows
|
||||
- Any other byte = op code (process op, then recursively parse sub-timestamp)
|
||||
- The last tag (not preceded by `0xff`) is the final item
|
||||
|
||||
### Attestation Format (after `0x00` tag)
|
||||
- 8-byte attestation tag:
|
||||
- `83dfe30d2ef90c8e` = PendingAttestation
|
||||
- `0588960d73d71901` = BitcoinBlockHeaderAttestation
|
||||
- `06856e0d73d71901` = LitecoinBlockHeaderAttestation
|
||||
- varuint payload length
|
||||
- payload bytes:
|
||||
- Pending: varbytes (URI string like "https://alice.btc.calendar.opentimestamps.org")
|
||||
- Bitcoin: varuint (block height)
|
||||
- Litecoin: varuint (block height)
|
||||
|
||||
### Op Codes
|
||||
- `0x02` = SHA1 (CryptOp, 20-byte digest)
|
||||
- `0x03` = RIPEMD160 (CryptOp, 20-byte digest)
|
||||
- `0x08` = SHA256 (CryptOp, 32-byte digest)
|
||||
- `0x67` = KECCAK256 (CryptOp, 32-byte digest)
|
||||
- `0xf0` = Append (BinaryOp: varbytes argument)
|
||||
- `0xf1` = Prepend (BinaryOp: varbytes argument)
|
||||
- `0xf2` = Reverse (UnaryOp)
|
||||
- `0xf3` = Hexlify (UnaryOp)
|
||||
|
||||
### LEB128 Varuint Format
|
||||
- Read bytes, each contributes 7 bits (low bits)
|
||||
- High bit (0x80) = continue reading
|
||||
- Low bit = final byte
|
||||
|
||||
### Varbytes Format
|
||||
- varuint length prefix
|
||||
- followed by that many bytes
|
||||
|
||||
## Bugs Found
|
||||
|
||||
### Bug 1: Broken `is_proof_complete` (nip003.c:128)
|
||||
```c
|
||||
if (len > 150) {
|
||||
complete = 1;
|
||||
}
|
||||
```
|
||||
Uses a size heuristic instead of parsing the OTS format. A pending proof can be >150 bytes, and a complete proof might be smaller. Should parse the binary format and look for Bitcoin/Litecoin attestation tags.
|
||||
|
||||
### Bug 2: Flawed `upgrade_proof` (nip003.c:184)
|
||||
```c
|
||||
if (resp.body_len <= len) {
|
||||
nostr_http_response_free(&resp);
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
Rejects any response that isn't strictly larger. A calendar may return the same proof if nothing changed, or a differently-serialized proof. Should compare content, not just length.
|
||||
|
||||
### Bug 3: CMakeLists.txt missing NIP-03
|
||||
The CMake build only includes a subset of NIPs. `nip003.c` and many other NIP source files are missing from the library build target.
|
||||
|
||||
### Bug 4: No proof verification function
|
||||
No way to verify a proof is cryptographically valid against the Bitcoin blockchain.
|
||||
|
||||
### Bug 5: No signer variant
|
||||
All other NIPs have `_with_signer` variants but NIP-03 only has raw private key version.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Add OTS parser to nip003.c
|
||||
Add static helper functions:
|
||||
- `ots_read_varuint(data, pos)` - read LEB128 varuint
|
||||
- `ots_read_varbytes(data, pos)` - read varbytes
|
||||
- `ots_parse_timestamp(data, pos, len, results)` - recursively parse timestamp tree
|
||||
- `ots_find_attestations(data, len)` - top-level parser that finds all attestations
|
||||
|
||||
### Step 2: Fix `nostr_nip03_is_proof_complete`
|
||||
Replace the size heuristic with proper parsing:
|
||||
- Parse the OTS file format
|
||||
- Walk the timestamp tree
|
||||
- Return 1 if any Bitcoin (`0588960d73d71901`) or Litecoin (`06856e0d73d71901`) attestation is found
|
||||
- Return 0 if only pending attestations found
|
||||
- Return -1 on parse error
|
||||
|
||||
### Step 3: Fix `nostr_nip03_upgrade_proof`
|
||||
- Remove the `resp.body_len <= len` check
|
||||
- Accept any valid 200 response with non-empty body
|
||||
- Return the base64-encoded response
|
||||
|
||||
### Step 4: Add `nostr_nip03_verify_proof`
|
||||
New function that:
|
||||
- Parses the OTS proof
|
||||
- Extracts Bitcoin attestation(s) with block height(s)
|
||||
- Optionally fetches the Bitcoin block header from a blockchain API
|
||||
- Verifies the merkle root matches the computed digest
|
||||
- Returns the timestamp (block time) on success
|
||||
|
||||
### Step 5: Add `nostr_nip03_create_proof_event_with_signer`
|
||||
Mirror the pattern used by other NIPs for signer support.
|
||||
|
||||
### Step 6: Fix CMakeLists.txt
|
||||
Add all missing NIP source files to the library build target.
|
||||
|
||||
### Step 7: Add example program
|
||||
Create `examples/timestamping.c` demonstrating the full workflow:
|
||||
- Create a Nostr event
|
||||
- Submit to OTS calendar
|
||||
- Poll for completion
|
||||
- Create proof event
|
||||
- Verify proof
|
||||
|
||||
### Step 8: Fix live test
|
||||
Fix the broken curl debug command in `tests/nip03_live_test.c`.
|
||||
|
||||
### Step 9: Add unit tests
|
||||
Add tests for:
|
||||
- OTS parsing with known test vectors
|
||||
- Proof completeness detection (pending vs complete)
|
||||
- Varuint/varbytes encoding/decoding
|
||||
|
||||
### Step 10: Update documentation
|
||||
Update `README.md` and `nostr_core.h` with NIP-03 function documentation.
|
||||
@@ -0,0 +1,209 @@
|
||||
# NIP-42 Authentication Support for core_relay_pool.c
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The relay pool ([`core_relay_pool.c`](nostr_core/core_relay_pool.c)) does not handle NIP-42 authentication. When a relay sends an `AUTH` challenge message, the pool silently ignores it, causing:
|
||||
|
||||
- Subscriptions to fail on auth-required relays (events never delivered)
|
||||
- Publishes to be rejected with `auth-required:` errors
|
||||
- No visibility into authentication state per relay
|
||||
|
||||
The synchronous relay functions in [`core_relays.c`](nostr_core/core_relays.c) already have full NIP-42 support (lines 58-63, 211-239, 595-624). The NIP-42 primitives in [`nip042.c`](nostr_core/nip042.c) / [`nip042.h`](nostr_core/nip042.h) are production-ready. This plan wires them into the pool.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Relay sends AUTH challenge] --> B[process_relay_message parses AUTH]
|
||||
B --> C{pool has private_key?}
|
||||
C -->|No| D[Log warning, mark relay auth_state = CHALLENGE_RECEIVED]
|
||||
C -->|Yes| E[nostr_nip42_create_auth_event]
|
||||
E --> F[nostr_nip42_create_auth_message]
|
||||
F --> G[nostr_ws_send_text to relay]
|
||||
G --> H[Mark relay auth_state = AUTHENTICATING]
|
||||
H --> I[Relay sends OK for auth event]
|
||||
I --> J[Mark relay auth_state = AUTHENTICATED]
|
||||
|
||||
K[Relay sends OK with auth-required] --> L[process_relay_message detects prefix]
|
||||
L --> M{relay already authenticated?}
|
||||
M -->|No| N[Trigger auth flow if private_key available]
|
||||
M -->|Yes| O[Surface as publish failure]
|
||||
|
||||
P[Relay sends NOTICE] --> Q{Contains auth-required?}
|
||||
Q -->|Yes| R[Log auth-required notice]
|
||||
Q -->|No| S[Ignore or log]
|
||||
```
|
||||
|
||||
## Detailed Changes
|
||||
|
||||
### 1. Add NIP-42 include to core_relay_pool.c
|
||||
|
||||
Add `#include "nip042.h"` alongside the existing includes at the top of [`core_relay_pool.c`](nostr_core/core_relay_pool.c:27).
|
||||
|
||||
### 2. Add auth fields to `relay_connection_t` struct
|
||||
|
||||
Modeled after [`core_relays.c:58-63`](nostr_core/core_relays.c:58), add to the [`relay_connection_t`](nostr_core/core_relay_pool.c:76) struct:
|
||||
|
||||
```c
|
||||
// NIP-42 Authentication fields
|
||||
nostr_auth_state_t auth_state;
|
||||
char auth_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
time_t auth_challenge_time;
|
||||
int nip42_enabled;
|
||||
```
|
||||
|
||||
These go after the existing `last_connection_error_time` field (around line 106).
|
||||
|
||||
### 3. Add private key and NIP-42 config to `nostr_relay_pool` struct
|
||||
|
||||
Add to the [`nostr_relay_pool`](nostr_core/core_relay_pool.c:146) struct:
|
||||
|
||||
```c
|
||||
// NIP-42 Authentication configuration
|
||||
int nip42_enabled;
|
||||
unsigned char private_key[32];
|
||||
int has_private_key;
|
||||
```
|
||||
|
||||
### 4. Add public API: `nostr_relay_pool_set_auth()`
|
||||
|
||||
Add to [`nostr_core.h`](nostr_core/nostr_core.h) after the existing pool management functions (around line 252):
|
||||
|
||||
```c
|
||||
// NIP-42 Authentication configuration for relay pool
|
||||
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool,
|
||||
const unsigned char* private_key,
|
||||
int enable);
|
||||
```
|
||||
|
||||
Implement in [`core_relay_pool.c`](nostr_core/core_relay_pool.c):
|
||||
- Copies the 32-byte private key into the pool struct
|
||||
- Sets `nip42_enabled` flag
|
||||
- Propagates `nip42_enabled` to all existing relay connections
|
||||
|
||||
### 5. Handle AUTH in `process_relay_message()`
|
||||
|
||||
Add an `AUTH` handler in [`process_relay_message()`](nostr_core/core_relay_pool.c:912) between the existing `OK` and `PONG` handlers (around line 1097). The logic mirrors [`core_relays.c:211-239`](nostr_core/core_relays.c:211):
|
||||
|
||||
```c
|
||||
} else if (strcmp(msg_type, "AUTH") == 0) {
|
||||
// Handle AUTH challenge: ["AUTH", <challenge-string>]
|
||||
if (pool->nip42_enabled && pool->has_private_key &&
|
||||
cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
|
||||
if (cJSON_IsString(challenge_json)) {
|
||||
const char* challenge = cJSON_GetStringValue(challenge_json);
|
||||
|
||||
// Store challenge
|
||||
strncpy(relay->auth_challenge, challenge,
|
||||
sizeof(relay->auth_challenge) - 1);
|
||||
relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0';
|
||||
relay->auth_challenge_time = time(NULL);
|
||||
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
|
||||
|
||||
// Create and send auth event
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(
|
||||
challenge, relay->url, pool->private_key, 0);
|
||||
if (auth_event) {
|
||||
char* auth_message = nostr_nip42_create_auth_message(auth_event);
|
||||
if (auth_message) {
|
||||
if (nostr_ws_send_text(relay->ws_client, auth_message) >= 0) {
|
||||
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
|
||||
}
|
||||
free(auth_message);
|
||||
}
|
||||
cJSON_Delete(auth_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Handle AUTH in `nostr_relay_pool_query_sync()` inline loop
|
||||
|
||||
The [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:1107) function has its own inline message parsing loop (lines 1178-1227) that bypasses `process_relay_message()`. Add AUTH handling there too, similar to the pattern above.
|
||||
|
||||
### 7. Handle NOTICE messages
|
||||
|
||||
Add a `NOTICE` handler in [`process_relay_message()`](nostr_core/core_relay_pool.c:912):
|
||||
|
||||
```c
|
||||
} else if (strcmp(msg_type, "NOTICE") == 0) {
|
||||
// Handle NOTICE: ["NOTICE", <message>]
|
||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* notice_msg = cJSON_GetArrayItem(parsed, 1);
|
||||
if (cJSON_IsString(notice_msg)) {
|
||||
const char* notice = cJSON_GetStringValue(notice_msg);
|
||||
// Detect auth-required notices
|
||||
if (strstr(notice, "auth-required") != NULL) {
|
||||
relay->auth_state = NOSTR_AUTH_STATE_NONE; // Reset for re-auth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Handle OK auth-required rejections
|
||||
|
||||
Enhance the existing [`OK` handler](nostr_core/core_relay_pool.c:1048) to detect `auth-required:` prefix in error messages. When detected:
|
||||
- If the pool has a private key and the relay hasn't been authenticated yet, trigger the auth flow
|
||||
- Surface the rejection through the publish callback with the auth-required message
|
||||
|
||||
### 9. Initialize auth fields in `nostr_relay_pool_add_relay()`
|
||||
|
||||
In [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:605), initialize the new auth fields:
|
||||
|
||||
```c
|
||||
// Initialize NIP-42 authentication fields
|
||||
relay->auth_state = NOSTR_AUTH_STATE_NONE;
|
||||
memset(relay->auth_challenge, 0, sizeof(relay->auth_challenge));
|
||||
relay->auth_challenge_time = 0;
|
||||
relay->nip42_enabled = pool->nip42_enabled;
|
||||
```
|
||||
|
||||
### 10. Secure cleanup in `nostr_relay_pool_destroy()`
|
||||
|
||||
In [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:684), zero the private key before freeing:
|
||||
|
||||
```c
|
||||
// Securely zero private key
|
||||
if (pool->has_private_key) {
|
||||
memset(pool->private_key, 0, sizeof(pool->private_key));
|
||||
pool->has_private_key = 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Test file
|
||||
|
||||
Create `tests/nip42_pool_test.c` that:
|
||||
- Creates a pool with auth configured
|
||||
- Verifies `nostr_relay_pool_set_auth()` stores the key
|
||||
- Simulates AUTH challenge parsing logic (unit-level, no live relay needed)
|
||||
- Verifies auth event creation with the pool's private key
|
||||
|
||||
### 12. Documentation updates
|
||||
|
||||
- Update [`POOL_API.md`](POOL_API.md) with `nostr_relay_pool_set_auth()` documentation
|
||||
- Update [`README.md`](README.md) with a relay pool NIP-42 usage example
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Change Type | Description |
|
||||
|------|-------------|-------------|
|
||||
| [`nostr_core/core_relay_pool.c`](nostr_core/core_relay_pool.c) | Modified | Add auth fields, AUTH/NOTICE handlers, auth config API |
|
||||
| [`nostr_core/nostr_core.h`](nostr_core/nostr_core.h) | Modified | Add `nostr_relay_pool_set_auth()` declaration |
|
||||
| [`tests/nip42_pool_test.c`](tests/nip42_pool_test.c) | New | NIP-42 pool authentication tests |
|
||||
| [`POOL_API.md`](POOL_API.md) | Modified | Document new auth API |
|
||||
| [`README.md`](README.md) | Modified | Add pool auth usage example |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Private key stored in pool, not per-relay**: A single identity authenticates to all relays in the pool. This matches the common use case and mirrors how `core_relays.c` accepts a single `private_key` parameter.
|
||||
|
||||
2. **Auth state tracked per-relay**: Each relay connection tracks its own `nostr_auth_state_t` since different relays may challenge at different times.
|
||||
|
||||
3. **Automatic re-auth on reconnect**: When a relay reconnects (via the existing reconnection logic), the auth state resets to `NOSTR_AUTH_STATE_NONE`, allowing the relay to re-challenge.
|
||||
|
||||
4. **No blocking on auth**: The AUTH response is sent asynchronously. If a relay requires auth before accepting subscriptions, the relay will re-send events after authentication succeeds. The pool does not block waiting for auth confirmation.
|
||||
|
||||
5. **Reuse existing NIP-42 primitives**: All crypto and message formatting uses [`nostr_nip42_create_auth_event()`](nostr_core/nip042.c:26) and [`nostr_nip42_create_auth_message()`](nostr_core/nip042.c:84) — no new crypto code needed.
|
||||
@@ -0,0 +1,350 @@
|
||||
# NIP-46 Remote Signing — Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
NIP-46 defines a protocol for **Nostr Remote Signing**, enabling 2-way communication between a client application and a remote signer (bunker) over Nostr relays. The remote signer holds the user's private keys and performs cryptographic operations on behalf of the client, reducing the attack surface by keeping keys off the client device.
|
||||
|
||||
This plan covers implementing **both sides** of the protocol:
|
||||
- **Client-side**: sends requests to a remote signer and processes responses
|
||||
- **Remote-signer-side**: receives requests and produces responses (for building bunker applications)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Protocol Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant R as Relay
|
||||
participant S as Remote Signer
|
||||
|
||||
Note over C: Generate client-keypair
|
||||
C->>R: Subscribe to kind:24133 p-tagged to client-pubkey
|
||||
C->>R: Publish connect request, kind:24133, p-tag remote-signer-pubkey
|
||||
R->>S: Deliver connect request
|
||||
S->>R: Publish connect response, kind:24133, p-tag client-pubkey
|
||||
R->>C: Deliver connect response
|
||||
Note over C: Connection established
|
||||
|
||||
C->>R: Publish get_public_key request
|
||||
R->>S: Deliver request
|
||||
S->>R: Publish response with user-pubkey
|
||||
R->>C: Deliver response
|
||||
Note over C: Now knows user-pubkey
|
||||
|
||||
C->>R: Publish sign_event request
|
||||
R->>S: Deliver request
|
||||
S->>R: Publish signed event response
|
||||
R->>C: Deliver signed event
|
||||
```
|
||||
|
||||
### Connection Initiation
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Connection Initiation] --> B{Who initiates?}
|
||||
B -->|Remote Signer| C[bunker:// URL]
|
||||
B -->|Client| D[nostrconnect:// URL]
|
||||
|
||||
C --> E[Client parses bunker URL]
|
||||
E --> F[Extract remote-signer-pubkey + relays + secret]
|
||||
F --> G[Client sends connect request to remote-signer]
|
||||
|
||||
D --> H[Remote signer parses nostrconnect URL]
|
||||
H --> I[Extract client-pubkey + relays + secret + perms]
|
||||
I --> J[Remote signer sends connect response to client]
|
||||
|
||||
G --> K[Connection Established]
|
||||
J --> K
|
||||
```
|
||||
|
||||
### Component Dependency Map
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
NIP46[nip046.c/h] --> NIP44[nip044 - NIP-44 Encryption]
|
||||
NIP46 --> NIP04[nip004 - NIP-04 Encryption]
|
||||
NIP46 --> NIP01[nip001 - Event Creation/Signing]
|
||||
NIP46 --> POOL[core_relay_pool - Relay Communication]
|
||||
NIP46 --> UTILS[utils - Hex/Bytes/Random]
|
||||
NIP46 --> CJSON[cJSON - JSON Handling]
|
||||
NIP46 --> COMMON[nostr_common - Error Codes/Constants]
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
### Core Types
|
||||
|
||||
```c
|
||||
// NIP-46 event kind
|
||||
#define NOSTR_NIP46_REQUEST_KIND 24133
|
||||
|
||||
// Connection types
|
||||
typedef enum {
|
||||
NOSTR_NIP46_CONN_BUNKER, // bunker:// initiated by remote-signer
|
||||
NOSTR_NIP46_CONN_NOSTRCONNECT // nostrconnect:// initiated by client
|
||||
} nostr_nip46_connection_type_t;
|
||||
|
||||
// RPC Method types
|
||||
typedef enum {
|
||||
NOSTR_NIP46_METHOD_CONNECT,
|
||||
NOSTR_NIP46_METHOD_SIGN_EVENT,
|
||||
NOSTR_NIP46_METHOD_PING,
|
||||
NOSTR_NIP46_METHOD_GET_PUBLIC_KEY,
|
||||
NOSTR_NIP46_METHOD_NIP04_ENCRYPT,
|
||||
NOSTR_NIP46_METHOD_NIP04_DECRYPT,
|
||||
NOSTR_NIP46_METHOD_NIP44_ENCRYPT,
|
||||
NOSTR_NIP46_METHOD_NIP44_DECRYPT
|
||||
} nostr_nip46_method_t;
|
||||
|
||||
// Parsed bunker:// URL
|
||||
typedef struct {
|
||||
char remote_signer_pubkey[65]; // hex pubkey
|
||||
char relays[8][256]; // up to 8 relay URLs
|
||||
int relay_count;
|
||||
char secret[128]; // optional secret
|
||||
} nostr_nip46_bunker_url_t;
|
||||
|
||||
// Parsed nostrconnect:// URL
|
||||
typedef struct {
|
||||
char client_pubkey[65]; // hex pubkey
|
||||
char relays[8][256]; // relay URLs
|
||||
int relay_count;
|
||||
char secret[128]; // required secret
|
||||
char perms[512]; // optional permissions
|
||||
char name[128]; // optional app name
|
||||
char url[256]; // optional app URL
|
||||
char image[256]; // optional app image
|
||||
} nostr_nip46_nostrconnect_url_t;
|
||||
|
||||
// JSON-RPC Request
|
||||
typedef struct {
|
||||
char id[65]; // random request ID
|
||||
nostr_nip46_method_t method;
|
||||
char method_str[32]; // string form of method
|
||||
char** params; // array of string params
|
||||
int param_count;
|
||||
} nostr_nip46_request_t;
|
||||
|
||||
// JSON-RPC Response
|
||||
typedef struct {
|
||||
char id[65]; // matching request ID
|
||||
char* result; // result string
|
||||
char* error; // error string, NULL if success
|
||||
} nostr_nip46_response_t;
|
||||
|
||||
// Client session state
|
||||
typedef struct {
|
||||
unsigned char client_private_key[32];
|
||||
char client_pubkey_hex[65];
|
||||
char remote_signer_pubkey_hex[65];
|
||||
unsigned char remote_signer_pubkey[32];
|
||||
char user_pubkey_hex[65]; // learned via get_public_key
|
||||
char relays[8][256];
|
||||
int relay_count;
|
||||
int connected;
|
||||
} nostr_nip46_client_session_t;
|
||||
|
||||
// Signer session state
|
||||
typedef struct {
|
||||
unsigned char signer_private_key[32];
|
||||
char signer_pubkey_hex[65];
|
||||
unsigned char user_private_key[32];
|
||||
char user_pubkey_hex[65];
|
||||
char client_pubkey_hex[65];
|
||||
unsigned char client_pubkey[32];
|
||||
char relays[8][256];
|
||||
int relay_count;
|
||||
int connected;
|
||||
} nostr_nip46_signer_session_t;
|
||||
```
|
||||
|
||||
## Public API Functions
|
||||
|
||||
### URL Parsing
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_parse_bunker_url()` | Parse `bunker://<pubkey>?relay=...&secret=...` |
|
||||
| `nostr_nip46_parse_nostrconnect_url()` | Parse `nostrconnect://<pubkey>?relay=...&secret=...&perms=...` |
|
||||
| `nostr_nip46_create_bunker_url()` | Generate a bunker:// connection token |
|
||||
| `nostr_nip46_create_nostrconnect_url()` | Generate a nostrconnect:// connection token |
|
||||
|
||||
### JSON-RPC Message Construction
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_create_request()` | Build a JSON-RPC request object |
|
||||
| `nostr_nip46_create_response()` | Build a JSON-RPC response object |
|
||||
| `nostr_nip46_parse_request()` | Parse decrypted JSON into request struct |
|
||||
| `nostr_nip46_parse_response()` | Parse decrypted JSON into response struct |
|
||||
| `nostr_nip46_free_request()` | Free request struct memory |
|
||||
| `nostr_nip46_free_response()` | Free response struct memory |
|
||||
|
||||
### Event Construction (kind: 24133)
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_create_request_event()` | Create NIP-44 encrypted kind:24133 request event |
|
||||
| `nostr_nip46_create_response_event()` | Create NIP-44 encrypted kind:24133 response event |
|
||||
| `nostr_nip46_decrypt_event()` | Decrypt a kind:24133 event content |
|
||||
|
||||
### Client-Side Operations
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_client_session_init()` | Initialize client session from bunker URL |
|
||||
| `nostr_nip46_client_session_destroy()` | Clean up client session |
|
||||
| `nostr_nip46_client_connect()` | Send connect request to remote signer |
|
||||
| `nostr_nip46_client_get_public_key()` | Request user pubkey from remote signer |
|
||||
| `nostr_nip46_client_sign_event()` | Request event signing from remote signer |
|
||||
| `nostr_nip46_client_ping()` | Send ping to remote signer |
|
||||
| `nostr_nip46_client_nip04_encrypt()` | Request NIP-04 encryption |
|
||||
| `nostr_nip46_client_nip04_decrypt()` | Request NIP-04 decryption |
|
||||
| `nostr_nip46_client_nip44_encrypt()` | Request NIP-44 encryption |
|
||||
| `nostr_nip46_client_nip44_decrypt()` | Request NIP-44 decryption |
|
||||
|
||||
### Remote-Signer-Side Operations
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_signer_session_init()` | Initialize signer session |
|
||||
| `nostr_nip46_signer_session_destroy()` | Clean up signer session |
|
||||
| `nostr_nip46_signer_handle_request()` | Process an incoming request and produce a response |
|
||||
| `nostr_nip46_signer_create_bunker_url()` | Generate bunker URL for distribution |
|
||||
|
||||
### Utility
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_generate_request_id()` | Generate random request ID string |
|
||||
| `nostr_nip46_method_to_string()` | Convert method enum to string |
|
||||
| `nostr_nip46_string_to_method()` | Convert string to method enum |
|
||||
|
||||
## Error Codes
|
||||
|
||||
New error codes to add to `nostr_common.h`:
|
||||
|
||||
```c
|
||||
// NIP-46 Remote Signing error codes
|
||||
#define NOSTR_ERROR_NIP46_INVALID_BUNKER_URL -300
|
||||
#define NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT -301
|
||||
#define NOSTR_ERROR_NIP46_INVALID_REQUEST -302
|
||||
#define NOSTR_ERROR_NIP46_INVALID_RESPONSE -303
|
||||
#define NOSTR_ERROR_NIP46_ENCRYPTION_FAILED -304
|
||||
#define NOSTR_ERROR_NIP46_DECRYPTION_FAILED -305
|
||||
#define NOSTR_ERROR_NIP46_CONNECTION_FAILED -306
|
||||
#define NOSTR_ERROR_NIP46_TIMEOUT -307
|
||||
#define NOSTR_ERROR_NIP46_SECRET_MISMATCH -308
|
||||
#define NOSTR_ERROR_NIP46_UNKNOWN_METHOD -309
|
||||
#define NOSTR_ERROR_NIP46_AUTH_CHALLENGE -310
|
||||
#define NOSTR_ERROR_NIP46_NOT_CONNECTED -311
|
||||
```
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `nostr_core/nip046.h` | Header with all NIP-46 types, constants, and function declarations |
|
||||
| `nostr_core/nip046.c` | Implementation of all NIP-46 functions |
|
||||
| `tests/nip46_test.c` | Comprehensive test suite |
|
||||
| `examples/nip46_remote_signer.c` | Example showing both client and signer usage |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `nostr_core/nostr_core.h` | Add `#include "nip046.h"` and NIP-46 API docs in header comment |
|
||||
| `nostr_core/nostr_common.h` | Add NIP-46 error code defines |
|
||||
| `build.sh` | Add NIP-046 to auto-detection, `--nips=all` list, and description mapping |
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### URL Parsing Strategy
|
||||
|
||||
The `bunker://` and `nostrconnect://` URLs use standard URI format with query parameters. Implementation will:
|
||||
1. Validate the scheme prefix
|
||||
2. Extract the pubkey from the authority section
|
||||
3. Parse query parameters using simple string splitting — no external URL parsing library needed
|
||||
4. URL-decode relay values since they contain `://` characters
|
||||
|
||||
### NIP-44 Encryption Integration
|
||||
|
||||
All kind:24133 event content is NIP-44 encrypted. The implementation will use the existing `nostr_nip44_encrypt()` and `nostr_nip44_decrypt()` functions from `nip044.h`. The flow:
|
||||
1. Build JSON-RPC payload as a string
|
||||
2. Encrypt with `nostr_nip44_encrypt(client_privkey, remote_signer_pubkey, payload, ...)`
|
||||
3. Set as event content
|
||||
4. On receive: `nostr_nip44_decrypt(my_privkey, sender_pubkey, content, ...)`
|
||||
5. Parse decrypted JSON-RPC payload
|
||||
|
||||
### Relay Communication
|
||||
|
||||
For the initial implementation, the NIP-46 module will provide **event construction and parsing only** — it will not manage relay connections directly. Users will use the existing relay pool API to:
|
||||
1. Subscribe to kind:24133 events p-tagged to their pubkey
|
||||
2. Publish kind:24133 request/response events
|
||||
|
||||
This keeps the module focused and composable, matching the pattern used by NIP-59 and NIP-17.
|
||||
|
||||
A higher-level convenience layer could be added later that wraps the relay pool for a fully managed NIP-46 session.
|
||||
|
||||
### Auth Challenge Handling
|
||||
|
||||
When a response has `result: "auth_url"`, the error field contains a URL. The client-side API will return a specific error code `NOSTR_ERROR_NIP46_AUTH_CHALLENGE` and provide the URL in the response struct's error field, allowing the application to handle it appropriately.
|
||||
|
||||
## Test Plan
|
||||
|
||||
The test suite will cover:
|
||||
|
||||
1. **URL Parsing Tests**
|
||||
- Parse valid bunker:// URLs with single and multiple relays
|
||||
- Parse valid nostrconnect:// URLs with all optional fields
|
||||
- Reject malformed URLs
|
||||
- Handle URL-encoded relay values
|
||||
|
||||
2. **URL Generation Tests**
|
||||
- Generate bunker:// URLs and verify round-trip parsing
|
||||
- Generate nostrconnect:// URLs and verify round-trip parsing
|
||||
|
||||
3. **JSON-RPC Message Tests**
|
||||
- Create and parse each method type request
|
||||
- Create and parse success responses
|
||||
- Create and parse error responses
|
||||
- Verify request ID matching
|
||||
|
||||
4. **Event Construction Tests**
|
||||
- Create kind:24133 request events with proper encryption
|
||||
- Create kind:24133 response events with proper encryption
|
||||
- Decrypt events and verify content matches
|
||||
|
||||
5. **Method Enum Conversion Tests**
|
||||
- Round-trip all method enum values through string conversion
|
||||
|
||||
6. **Signer Request Handling Tests**
|
||||
- Handle connect request and produce ack response
|
||||
- Handle sign_event request and produce signed event response
|
||||
- Handle ping request and produce pong response
|
||||
- Handle get_public_key and return correct pubkey
|
||||
- Handle NIP-04/NIP-44 encrypt/decrypt requests
|
||||
|
||||
7. **End-to-End Flow Tests**
|
||||
- Client creates connect request → Signer handles → Client processes response
|
||||
- Client creates sign_event request → Signer signs → Client gets signed event
|
||||
- Full bunker:// connection flow
|
||||
- Secret validation on connect
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Error codes in `nostr_common.h`
|
||||
2. Header file `nip046.h` with all declarations
|
||||
3. URL parsing functions
|
||||
4. JSON-RPC request/response construction and parsing
|
||||
5. Kind:24133 event creation and decryption
|
||||
6. Client session management
|
||||
7. Signer session and request handling
|
||||
8. Utility functions
|
||||
9. Build system updates
|
||||
10. Test suite
|
||||
11. Example program
|
||||
12. Documentation updates in `nostr_core.h`
|
||||
@@ -0,0 +1,778 @@
|
||||
# NIP-60 Cashu Wallet & NIP-61 Nutzaps — Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
**NIP-60** defines a protocol for storing Cashu wallet state on Nostr relays, enabling cross-application wallet portability. The wallet stores unspent proofs as encrypted events, tracks spending history, and manages mint relationships.
|
||||
|
||||
**NIP-61** defines "Nutzaps" — P2PK-locked Cashu tokens sent as zaps between Nostr users, where the payment itself serves as the receipt.
|
||||
|
||||
This implementation covers:
|
||||
- **NIP-60**: Wallet event creation/parsing (kind:17375), token events (kind:7375), spending history (kind:7376), quote events (kind:7374)
|
||||
- **NIP-61**: Nutzap info events (kind:10019), nutzap events (kind:9321), nutzap redemption
|
||||
- **Cashu Mint HTTP Client**: Mint info, token swap, melt, and mint operations via curl
|
||||
|
||||
## Architecture
|
||||
|
||||
### Component Dependency Map
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
NIP60[nip060.c/h - Cashu Wallet Events] --> NIP44[nip044 - NIP-44 Encryption]
|
||||
NIP60 --> NIP01[nip001 - Event Creation/Signing]
|
||||
NIP60 --> CJSON[cJSON - JSON Handling]
|
||||
NIP60 --> UTILS[utils - Hex/Bytes/Base64]
|
||||
NIP60 --> COMMON[nostr_common - Error Codes]
|
||||
|
||||
NIP61[nip061.c/h - Nutzaps] --> NIP44
|
||||
NIP61 --> NIP01
|
||||
NIP61 --> CJSON
|
||||
NIP61 --> UTILS
|
||||
NIP61 --> COMMON
|
||||
|
||||
CASHU[cashu_mint.c/h - Mint HTTP Client] --> CJSON
|
||||
CASHU --> CURL[libcurl - HTTP]
|
||||
CASHU --> COMMON
|
||||
|
||||
NIP60 -.->|optional| CASHU
|
||||
NIP61 -.->|optional| CASHU
|
||||
```
|
||||
|
||||
### NIP-60 High-Level Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as Application
|
||||
participant W as NIP-60 Wallet Lib
|
||||
participant R as Relay
|
||||
participant M as Cashu Mint
|
||||
|
||||
Note over App: Initialize wallet
|
||||
App->>W: nostr_nip60_create_wallet_event
|
||||
W->>R: Publish kind:17375
|
||||
|
||||
Note over App: Receive tokens
|
||||
App->>M: Mint tokens via cashu_mint_mint_tokens
|
||||
M-->>App: Cashu proofs
|
||||
App->>W: nostr_nip60_create_token_event
|
||||
W->>R: Publish kind:7375
|
||||
|
||||
Note over App: Spend tokens
|
||||
App->>W: nostr_nip60_spend_token
|
||||
W->>M: Swap proofs via cashu_mint_swap
|
||||
M-->>W: Change proofs
|
||||
W->>R: Delete old kind:7375
|
||||
W->>R: Publish new kind:7375 with change
|
||||
W->>R: Publish kind:7376 history
|
||||
```
|
||||
|
||||
### NIP-61 Nutzap Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice as Alice - Sender
|
||||
participant R as Relay
|
||||
participant Bob as Bob - Recipient
|
||||
participant M as Cashu Mint
|
||||
|
||||
Note over Bob: Setup
|
||||
Bob->>R: Publish kind:10019 with mints + p2pk pubkey
|
||||
|
||||
Note over Alice: Send nutzap
|
||||
Alice->>R: Fetch Bobs kind:10019
|
||||
Alice->>M: Mint/swap P2PK-locked tokens
|
||||
Alice->>R: Publish kind:9321 nutzap
|
||||
|
||||
Note over Bob: Receive nutzap
|
||||
Bob->>R: Fetch kind:9321 events
|
||||
Bob->>M: Swap P2PK tokens into wallet
|
||||
Bob->>R: Publish kind:7376 redemption history
|
||||
```
|
||||
|
||||
### Token State Transition
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Created: Mint/Receive tokens
|
||||
Created --> Published: Publish kind 7375
|
||||
Published --> Spending: Spend some proofs
|
||||
Spending --> Deleted: NIP-09 delete old event
|
||||
Spending --> RolledOver: New kind 7375 with unspent + change
|
||||
RolledOver --> Published
|
||||
Deleted --> [*]
|
||||
Published --> Redeemed: All proofs spent
|
||||
Redeemed --> Deleted
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
nostr_core/
|
||||
nip060.h # NIP-60 Cashu wallet types and function declarations
|
||||
nip060.c # NIP-60 implementation
|
||||
nip061.h # NIP-61 Nutzap types and function declarations
|
||||
nip061.c # NIP-61 implementation
|
||||
cashu_mint.h # Cashu mint HTTP client types and functions
|
||||
cashu_mint.c # Cashu mint HTTP client implementation
|
||||
tests/
|
||||
nip60_test.c # NIP-60 unit tests
|
||||
nip61_test.c # NIP-61 unit tests
|
||||
cashu_mint_test.c # Cashu mint client tests
|
||||
examples/
|
||||
cashu_wallet.c # Complete wallet example
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
### NIP-60 Core Types
|
||||
|
||||
```c
|
||||
// Event kinds
|
||||
#define NOSTR_NIP60_WALLET_KIND 17375
|
||||
#define NOSTR_NIP60_TOKEN_KIND 7375
|
||||
#define NOSTR_NIP60_HISTORY_KIND 7376
|
||||
#define NOSTR_NIP60_QUOTE_KIND 7374
|
||||
|
||||
// A single Cashu proof
|
||||
typedef struct {
|
||||
char id[17]; // Keyset ID, e.g. "005c2502034d4f12"
|
||||
uint64_t amount; // Denomination amount
|
||||
char* secret; // Proof secret (base64 or P2PK JSON)
|
||||
char* C; // Blinded signature (hex compressed point)
|
||||
} nostr_cashu_proof_t;
|
||||
|
||||
// Token content - decrypted payload of kind:7375
|
||||
typedef struct {
|
||||
char* mint_url; // Mint URL
|
||||
nostr_cashu_proof_t* proofs; // Array of proofs
|
||||
int proof_count; // Number of proofs
|
||||
char** deleted_token_ids; // Event IDs of tokens destroyed
|
||||
int deleted_count; // Number of deleted token IDs
|
||||
} nostr_nip60_token_data_t;
|
||||
|
||||
// Wallet content - decrypted payload of kind:17375
|
||||
typedef struct {
|
||||
char privkey[65]; // Hex private key for P2PK
|
||||
char** mint_urls; // Array of mint URLs
|
||||
int mint_count; // Number of mints
|
||||
} nostr_nip60_wallet_data_t;
|
||||
|
||||
// Spending history entry direction
|
||||
typedef enum {
|
||||
NOSTR_NIP60_DIRECTION_IN, // Received funds
|
||||
NOSTR_NIP60_DIRECTION_OUT // Sent funds
|
||||
} nostr_nip60_direction_t;
|
||||
|
||||
// History event reference
|
||||
typedef enum {
|
||||
NOSTR_NIP60_REF_CREATED, // New token event created
|
||||
NOSTR_NIP60_REF_DESTROYED, // Token event destroyed
|
||||
NOSTR_NIP60_REF_REDEEMED // Nutzap redeemed
|
||||
} nostr_nip60_ref_marker_t;
|
||||
|
||||
typedef struct {
|
||||
char event_id[65]; // Referenced event ID
|
||||
char relay_hint[256]; // Optional relay hint
|
||||
nostr_nip60_ref_marker_t marker; // Reference type
|
||||
} nostr_nip60_history_ref_t;
|
||||
|
||||
// Spending history data - decrypted payload of kind:7376
|
||||
typedef struct {
|
||||
nostr_nip60_direction_t direction; // in or out
|
||||
uint64_t amount; // Amount in sats
|
||||
nostr_nip60_history_ref_t* refs; // Event references
|
||||
int ref_count; // Number of references
|
||||
} nostr_nip60_history_data_t;
|
||||
```
|
||||
|
||||
### NIP-61 Core Types
|
||||
|
||||
```c
|
||||
// Event kinds
|
||||
#define NOSTR_NIP61_NUTZAP_INFO_KIND 10019
|
||||
#define NOSTR_NIP61_NUTZAP_KIND 9321
|
||||
|
||||
// Mint entry in kind:10019
|
||||
typedef struct {
|
||||
char* url; // Mint URL
|
||||
char** units; // Supported units, e.g. "sat", "usd"
|
||||
int unit_count; // Number of units
|
||||
} nostr_nip61_mint_entry_t;
|
||||
|
||||
// Nutzap info - parsed kind:10019
|
||||
typedef struct {
|
||||
char** relay_urls; // Relays for receiving nutzaps
|
||||
int relay_count;
|
||||
nostr_nip61_mint_entry_t* mints; // Trusted mints
|
||||
int mint_count;
|
||||
char pubkey[67]; // P2PK pubkey (with 02 prefix)
|
||||
} nostr_nip61_nutzap_info_t;
|
||||
|
||||
// Nutzap event data - parsed kind:9321
|
||||
typedef struct {
|
||||
char* content; // Optional comment
|
||||
nostr_cashu_proof_t* proofs; // P2PK-locked proofs
|
||||
int proof_count;
|
||||
char* mint_url; // Mint URL
|
||||
char recipient_pubkey[65]; // Recipient Nostr pubkey
|
||||
char nutzapped_event_id[65]; // Event being nutzapped (optional)
|
||||
char nutzapped_relay_hint[256]; // Relay hint for nutzapped event
|
||||
int nutzapped_kind; // Kind of nutzapped event
|
||||
} nostr_nip61_nutzap_data_t;
|
||||
```
|
||||
|
||||
### Cashu Mint Client Types
|
||||
|
||||
```c
|
||||
// Cashu protocol version
|
||||
#define CASHU_API_VERSION "v1"
|
||||
|
||||
// Mint keyset
|
||||
typedef struct {
|
||||
char id[17]; // Keyset ID
|
||||
char unit[8]; // Unit, e.g. "sat"
|
||||
int active; // Whether keyset is active
|
||||
uint64_t* amounts; // Denomination amounts
|
||||
char** pubkeys; // Corresponding public keys
|
||||
int key_count; // Number of keys
|
||||
} cashu_keyset_t;
|
||||
|
||||
// Mint info
|
||||
typedef struct {
|
||||
char* name;
|
||||
char* description;
|
||||
char* version;
|
||||
char** supported_nuts; // NUT numbers supported
|
||||
int nut_count;
|
||||
cashu_keyset_t* keysets;
|
||||
int keyset_count;
|
||||
} cashu_mint_info_t;
|
||||
|
||||
// Mint quote (for minting new tokens)
|
||||
typedef struct {
|
||||
char quote_id[128];
|
||||
char payment_request[1024]; // Lightning invoice
|
||||
int paid;
|
||||
uint64_t amount;
|
||||
time_t expiry;
|
||||
} cashu_mint_quote_t;
|
||||
|
||||
// Melt quote (for paying Lightning invoices)
|
||||
typedef struct {
|
||||
char quote_id[128];
|
||||
uint64_t amount;
|
||||
uint64_t fee_reserve;
|
||||
int paid;
|
||||
char* payment_preimage;
|
||||
time_t expiry;
|
||||
} cashu_melt_quote_t;
|
||||
|
||||
// Blind message for minting/swapping
|
||||
typedef struct {
|
||||
uint64_t amount;
|
||||
char* B_; // Blinded secret (hex)
|
||||
char id[17]; // Keyset ID
|
||||
} cashu_blinded_message_t;
|
||||
|
||||
// Blind signature from mint
|
||||
typedef struct {
|
||||
uint64_t amount;
|
||||
char* C_; // Blinded signature (hex)
|
||||
char id[17]; // Keyset ID
|
||||
} cashu_blind_signature_t;
|
||||
```
|
||||
|
||||
## API Functions
|
||||
|
||||
### NIP-60 Wallet Functions
|
||||
|
||||
```c
|
||||
// === Wallet Event (kind:17375) ===
|
||||
|
||||
// Create a wallet event with encrypted content
|
||||
cJSON* nostr_nip60_create_wallet_event(
|
||||
const nostr_nip60_wallet_data_t* wallet_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse and decrypt a wallet event
|
||||
int nostr_nip60_parse_wallet_event(
|
||||
cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_wallet_data_t* wallet_data_out);
|
||||
|
||||
// Free wallet data
|
||||
void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data);
|
||||
|
||||
// === Token Event (kind:7375) ===
|
||||
|
||||
// Create a token event with encrypted proofs
|
||||
cJSON* nostr_nip60_create_token_event(
|
||||
const nostr_nip60_token_data_t* token_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse and decrypt a token event
|
||||
int nostr_nip60_parse_token_event(
|
||||
cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_token_data_t* token_data_out);
|
||||
|
||||
// Free token data
|
||||
void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data);
|
||||
|
||||
// === Token Spending ===
|
||||
|
||||
// Create a NIP-09 deletion event for a spent token
|
||||
cJSON* nostr_nip60_create_token_deletion(
|
||||
const char* token_event_id,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Create a rollover token event (unspent proofs + change from old token)
|
||||
cJSON* nostr_nip60_create_rollover_token(
|
||||
const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// === Spending History (kind:7376) ===
|
||||
|
||||
// Create a spending history event
|
||||
cJSON* nostr_nip60_create_history_event(
|
||||
const nostr_nip60_history_data_t* history_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse and decrypt a spending history event
|
||||
int nostr_nip60_parse_history_event(
|
||||
cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_history_data_t* history_data_out);
|
||||
|
||||
// Free history data
|
||||
void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data);
|
||||
|
||||
// === Quote Event (kind:7374) ===
|
||||
|
||||
// Create a mint quote event
|
||||
cJSON* nostr_nip60_create_quote_event(
|
||||
const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse a quote event
|
||||
int nostr_nip60_parse_quote_event(
|
||||
cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
char* quote_id_out,
|
||||
size_t quote_id_size,
|
||||
char* mint_url_out,
|
||||
size_t mint_url_size);
|
||||
|
||||
// === Utility: Proof helpers ===
|
||||
|
||||
// Calculate total amount from an array of proofs
|
||||
uint64_t nostr_nip60_sum_proofs(
|
||||
const nostr_cashu_proof_t* proofs,
|
||||
int proof_count);
|
||||
|
||||
// Serialize proofs to JSON array
|
||||
cJSON* nostr_nip60_proofs_to_json(
|
||||
const nostr_cashu_proof_t* proofs,
|
||||
int proof_count);
|
||||
|
||||
// Parse proofs from JSON array
|
||||
int nostr_nip60_proofs_from_json(
|
||||
cJSON* json_array,
|
||||
nostr_cashu_proof_t** proofs_out,
|
||||
int* proof_count_out);
|
||||
|
||||
// Free proof array
|
||||
void nostr_nip60_free_proofs(
|
||||
nostr_cashu_proof_t* proofs,
|
||||
int proof_count);
|
||||
|
||||
// === Filter helpers ===
|
||||
|
||||
// Create a filter to fetch wallet and token events
|
||||
cJSON* nostr_nip60_create_wallet_filter(const char* pubkey_hex);
|
||||
|
||||
// Create a filter to fetch spending history
|
||||
cJSON* nostr_nip60_create_history_filter(const char* pubkey_hex, time_t since);
|
||||
```
|
||||
|
||||
### NIP-61 Nutzap Functions
|
||||
|
||||
```c
|
||||
// === Nutzap Info Event (kind:10019) ===
|
||||
|
||||
// Create nutzap info event
|
||||
cJSON* nostr_nip61_create_nutzap_info_event(
|
||||
const nostr_nip61_nutzap_info_t* info,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse nutzap info event
|
||||
int nostr_nip61_parse_nutzap_info_event(
|
||||
cJSON* event,
|
||||
nostr_nip61_nutzap_info_t* info_out);
|
||||
|
||||
// Free nutzap info
|
||||
void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info);
|
||||
|
||||
// === Nutzap Event (kind:9321) ===
|
||||
|
||||
// Create a nutzap event
|
||||
cJSON* nostr_nip61_create_nutzap_event(
|
||||
const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
const unsigned char* sender_private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse a nutzap event
|
||||
int nostr_nip61_parse_nutzap_event(
|
||||
cJSON* event,
|
||||
nostr_nip61_nutzap_data_t* nutzap_data_out);
|
||||
|
||||
// Free nutzap data
|
||||
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data);
|
||||
|
||||
// === Nutzap Redemption ===
|
||||
|
||||
// Create a redemption history event (kind:7376 with redeemed marker)
|
||||
cJSON* nostr_nip61_create_redemption_event(
|
||||
const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// === Nutzap Verification ===
|
||||
|
||||
// Verify a nutzap against recipients kind:10019
|
||||
int nostr_nip61_verify_nutzap(
|
||||
cJSON* nutzap_event,
|
||||
cJSON* nutzap_info_event);
|
||||
|
||||
// === Filter helpers ===
|
||||
|
||||
// Create filter to fetch nutzap info for a user
|
||||
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex);
|
||||
|
||||
// Create filter to fetch incoming nutzaps
|
||||
cJSON* nostr_nip61_create_nutzap_filter(
|
||||
const char* recipient_pubkey_hex,
|
||||
const char** mint_urls,
|
||||
int mint_count,
|
||||
time_t since);
|
||||
```
|
||||
|
||||
### Cashu Mint HTTP Client Functions
|
||||
|
||||
```c
|
||||
// === Mint Connection ===
|
||||
|
||||
// Get mint info
|
||||
int cashu_mint_get_info(
|
||||
const char* mint_url,
|
||||
cashu_mint_info_t* info_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Free mint info
|
||||
void cashu_mint_free_info(cashu_mint_info_t* info);
|
||||
|
||||
// === Keysets ===
|
||||
|
||||
// Get active keysets from mint
|
||||
int cashu_mint_get_keysets(
|
||||
const char* mint_url,
|
||||
cashu_keyset_t** keysets_out,
|
||||
int* keyset_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Get specific keyset keys
|
||||
int cashu_mint_get_keys(
|
||||
const char* mint_url,
|
||||
const char* keyset_id,
|
||||
cashu_keyset_t* keyset_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Free keysets
|
||||
void cashu_mint_free_keysets(cashu_keyset_t* keysets, int count);
|
||||
|
||||
// === Minting (receiving Lightning) ===
|
||||
|
||||
// Request a mint quote (get Lightning invoice)
|
||||
int cashu_mint_request_mint_quote(
|
||||
const char* mint_url,
|
||||
uint64_t amount,
|
||||
const char* unit,
|
||||
cashu_mint_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Check mint quote status
|
||||
int cashu_mint_check_mint_quote(
|
||||
const char* mint_url,
|
||||
const char* quote_id,
|
||||
cashu_mint_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Mint tokens (after quote is paid)
|
||||
int cashu_mint_mint_tokens(
|
||||
const char* mint_url,
|
||||
const char* quote_id,
|
||||
const cashu_blinded_message_t* blinded_messages,
|
||||
int message_count,
|
||||
cashu_blind_signature_t** signatures_out,
|
||||
int* signature_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// === Melting (paying Lightning) ===
|
||||
|
||||
// Request a melt quote (estimate fee for paying invoice)
|
||||
int cashu_mint_request_melt_quote(
|
||||
const char* mint_url,
|
||||
const char* payment_request,
|
||||
const char* unit,
|
||||
cashu_melt_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Check melt quote status
|
||||
int cashu_mint_check_melt_quote(
|
||||
const char* mint_url,
|
||||
const char* quote_id,
|
||||
cashu_melt_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Melt tokens (pay Lightning invoice)
|
||||
int cashu_mint_melt_tokens(
|
||||
const char* mint_url,
|
||||
const char* quote_id,
|
||||
const nostr_cashu_proof_t* proofs,
|
||||
int proof_count,
|
||||
cashu_melt_quote_t* result_out,
|
||||
cashu_blind_signature_t** change_out,
|
||||
int* change_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// === Swapping ===
|
||||
|
||||
// Swap proofs (split/combine denominations)
|
||||
int cashu_mint_swap(
|
||||
const char* mint_url,
|
||||
const nostr_cashu_proof_t* inputs,
|
||||
int input_count,
|
||||
const cashu_blinded_message_t* outputs,
|
||||
int output_count,
|
||||
cashu_blind_signature_t** signatures_out,
|
||||
int* signature_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// === Token State ===
|
||||
|
||||
// Check if proofs have been spent
|
||||
int cashu_mint_check_proofs_state(
|
||||
const char* mint_url,
|
||||
const nostr_cashu_proof_t* proofs,
|
||||
int proof_count,
|
||||
int* states_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// === Crypto Helpers ===
|
||||
|
||||
// Generate blinded messages for a target amount
|
||||
int cashu_create_blinded_messages(
|
||||
uint64_t amount,
|
||||
const char* keyset_id,
|
||||
cashu_blinded_message_t** messages_out,
|
||||
int* message_count_out,
|
||||
char*** secrets_out,
|
||||
char*** rs_out);
|
||||
|
||||
// Unblind signatures to get proofs
|
||||
int cashu_unblind_signatures(
|
||||
const cashu_blind_signature_t* signatures,
|
||||
int signature_count,
|
||||
const char** secrets,
|
||||
const char** rs,
|
||||
const cashu_keyset_t* keyset,
|
||||
nostr_cashu_proof_t** proofs_out,
|
||||
int* proof_count_out);
|
||||
|
||||
// Create P2PK-locked blinded messages
|
||||
int cashu_create_p2pk_blinded_messages(
|
||||
uint64_t amount,
|
||||
const char* keyset_id,
|
||||
const char* recipient_pubkey_hex,
|
||||
cashu_blinded_message_t** messages_out,
|
||||
int* message_count_out,
|
||||
char*** secrets_out,
|
||||
char*** rs_out);
|
||||
|
||||
// Free blinded messages
|
||||
void cashu_free_blinded_messages(cashu_blinded_message_t* messages, int count);
|
||||
|
||||
// Free blind signatures
|
||||
void cashu_free_blind_signatures(cashu_blind_signature_t* signatures, int count);
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
New error codes to add to `nostr_common.h`:
|
||||
|
||||
```c
|
||||
// NIP-60 Cashu Wallet error codes
|
||||
#define NOSTR_ERROR_NIP60_INVALID_WALLET -400
|
||||
#define NOSTR_ERROR_NIP60_INVALID_TOKEN -401
|
||||
#define NOSTR_ERROR_NIP60_INVALID_HISTORY -402
|
||||
#define NOSTR_ERROR_NIP60_INVALID_QUOTE -403
|
||||
#define NOSTR_ERROR_NIP60_DECRYPT_FAILED -404
|
||||
#define NOSTR_ERROR_NIP60_INVALID_PROOFS -405
|
||||
#define NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS -406
|
||||
|
||||
// NIP-61 Nutzap error codes
|
||||
#define NOSTR_ERROR_NIP61_INVALID_INFO -410
|
||||
#define NOSTR_ERROR_NIP61_INVALID_NUTZAP -411
|
||||
#define NOSTR_ERROR_NIP61_MINT_MISMATCH -412
|
||||
#define NOSTR_ERROR_NIP61_PUBKEY_MISMATCH -413
|
||||
#define NOSTR_ERROR_NIP61_VERIFICATION_FAILED -414
|
||||
|
||||
// Cashu Mint client error codes
|
||||
#define NOSTR_ERROR_CASHU_HTTP_FAILED -420
|
||||
#define NOSTR_ERROR_CASHU_JSON_PARSE_FAILED -421
|
||||
#define NOSTR_ERROR_CASHU_MINT_ERROR -422
|
||||
#define NOSTR_ERROR_CASHU_QUOTE_NOT_PAID -423
|
||||
#define NOSTR_ERROR_CASHU_QUOTE_EXPIRED -424
|
||||
#define NOSTR_ERROR_CASHU_PROOFS_SPENT -425
|
||||
#define NOSTR_ERROR_CASHU_CRYPTO_FAILED -426
|
||||
#define NOSTR_ERROR_CASHU_INVALID_KEYSET -427
|
||||
```
|
||||
|
||||
## Build System Changes
|
||||
|
||||
### build.sh Updates
|
||||
|
||||
1. Add `060` and `061` to the NIP detection patterns
|
||||
2. Add `cashu_mint.c` as a source when NIP-60 or NIP-61 is detected
|
||||
3. Add NIP descriptions: `NIP-060(Cashu-Wallet)`, `NIP-061(Nutzaps)`
|
||||
4. Update the `--nips=all` list to include `060` and `061`
|
||||
5. Update the help text with new NIP descriptions
|
||||
|
||||
### nostr_core.h Updates
|
||||
|
||||
Add includes:
|
||||
```c
|
||||
#include "nip060.h" // Cashu Wallet
|
||||
#include "nip061.h" // Nutzaps
|
||||
#include "cashu_mint.h" // Cashu Mint HTTP Client
|
||||
```
|
||||
|
||||
Add NIP-60/61 functions to the quick reference comment block.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
The implementation should proceed in this order due to dependencies:
|
||||
|
||||
1. **Error codes** in `nostr_common.h` — no dependencies
|
||||
2. **Cashu proof types** — shared between NIP-60 and NIP-61
|
||||
3. **`cashu_mint.h/c`** — Cashu mint HTTP client (depends on curl, cJSON)
|
||||
4. **`nip060.h/c`** — NIP-60 wallet events (depends on NIP-44, NIP-01, cashu types)
|
||||
5. **`nip061.h/c`** — NIP-61 nutzaps (depends on NIP-60 types, cashu types)
|
||||
6. **Build system** — update `build.sh` and `nostr_core.h`
|
||||
7. **Tests** — `nip60_test.c`, `nip61_test.c`, `cashu_mint_test.c`
|
||||
8. **Example** — `cashu_wallet.c`
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Self-encryption for NIP-44
|
||||
NIP-60 encrypts content to the user's own key (sender = recipient). The NIP-44 encrypt/decrypt functions take sender_private_key and recipient_public_key. For self-encryption, we derive the user's public key from their private key and use that as the recipient.
|
||||
|
||||
### 2. Cashu Crypto (Blinding)
|
||||
The Cashu protocol requires elliptic curve blinding operations (hash-to-curve, blind/unblind). These use secp256k1 which is already a dependency. The blinding math is:
|
||||
- `B_ = Y + r*G` where Y = hash_to_curve(secret), r = random blinding factor
|
||||
- `C_ = k*B_` (mint signs)
|
||||
- `C = C_ - r*K` where K = mint's public key (unblind)
|
||||
|
||||
### 3. P2PK Secrets
|
||||
For NIP-61 nutzaps, proof secrets use the P2PK format:
|
||||
```json
|
||||
["P2PK", {"nonce": "<random>", "data": "02<pubkey>"}]
|
||||
```
|
||||
The `02` prefix is required for nostr-cashu compatibility.
|
||||
|
||||
### 4. Curl Usage
|
||||
Following the same pattern as `nip005.c` — direct curl usage with write callbacks. No abstraction layer needed since curl is already a project dependency.
|
||||
|
||||
## Test Strategy
|
||||
|
||||
### NIP-60 Tests
|
||||
- Create and parse wallet events (round-trip encryption)
|
||||
- Create and parse token events with multiple proofs
|
||||
- Token deletion event creation
|
||||
- Rollover token creation with del references
|
||||
- History event creation and parsing
|
||||
- Quote event creation and parsing
|
||||
- Proof sum calculation
|
||||
- Filter creation helpers
|
||||
|
||||
### NIP-61 Tests
|
||||
- Create and parse nutzap info events
|
||||
- Create and parse nutzap events
|
||||
- Nutzap verification against kind:10019
|
||||
- Redemption history event creation
|
||||
- Filter creation helpers
|
||||
|
||||
### Cashu Mint Tests
|
||||
- Mint info parsing from JSON
|
||||
- Keyset parsing
|
||||
- Blinded message creation
|
||||
- Signature unblinding
|
||||
- P2PK secret generation
|
||||
- Proof state checking (mock or live mint)
|
||||
|
||||
## Example Usage
|
||||
|
||||
```c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
// Create a wallet
|
||||
nostr_nip60_wallet_data_t wallet = {0};
|
||||
strcpy(wallet.privkey, "<generated-hex-privkey>");
|
||||
wallet.mint_urls = (char*[]){"https://mint.example.com"};
|
||||
wallet.mint_count = 1;
|
||||
|
||||
cJSON* wallet_event = nostr_nip60_create_wallet_event(&wallet, my_privkey, 0);
|
||||
// Publish wallet_event to relays...
|
||||
|
||||
// Store tokens
|
||||
nostr_cashu_proof_t proofs[2] = {
|
||||
{.id = "00ad268c4d1f5826", .amount = 1, .secret = "...", .C = "02..."},
|
||||
{.id = "00ad268c4d1f5826", .amount = 4, .secret = "...", .C = "02..."}
|
||||
};
|
||||
nostr_nip60_token_data_t token = {
|
||||
.mint_url = "https://mint.example.com",
|
||||
.proofs = proofs,
|
||||
.proof_count = 2
|
||||
};
|
||||
|
||||
cJSON* token_event = nostr_nip60_create_token_event(&token, my_privkey, 0);
|
||||
// Publish token_event to relays...
|
||||
|
||||
// Send a nutzap
|
||||
nostr_nip61_nutzap_data_t nutzap = {
|
||||
.content = "Great post!",
|
||||
.proofs = p2pk_locked_proofs,
|
||||
.proof_count = 1,
|
||||
.mint_url = "https://mint.example.com",
|
||||
.recipient_pubkey = "<bob-pubkey>",
|
||||
.nutzapped_event_id = "<event-id>"
|
||||
};
|
||||
|
||||
cJSON* nutzap_event = nostr_nip61_create_nutzap_event(&nutzap, my_privkey, 0);
|
||||
// Publish nutzap_event to relays...
|
||||
```
|
||||
@@ -0,0 +1,216 @@
|
||||
# n_signer Integration into nostr_core_lib — Architecture Plan
|
||||
|
||||
> Status: M1–M6 implemented. This plan remains as architecture history; for current usage/integration contract see:
|
||||
> - `README.md` → "Signer abstraction & nsigner integration"
|
||||
> - `nostr_core/NSIGNER_INTEGRATION.md`
|
||||
> - `plans/n_signer_verb_migration.md` for the completed Nostr wire-method prefix migration
|
||||
>
|
||||
> **Wire-method update:** n_signer's role-based Nostr methods are now
|
||||
> `nostr_get_public_key`, `nostr_sign_event`, `nostr_nip04_encrypt`,
|
||||
> `nostr_nip04_decrypt`, `nostr_nip44_encrypt`, and `nostr_nip44_decrypt`.
|
||||
> The unprefixed `get_public_key` method is algorithm-based and is not used by the
|
||||
> role/`nostr_index` selectors in this library. Public `nostr_signer_*` C API names
|
||||
> and standard NIP-46 method names are unchanged.
|
||||
>
|
||||
> This plan originally described pulling the **caller-side** signer-integration glue out of per-project implementations and into `nostr_core_lib`,
|
||||
> so any project that already links the library can sign **locally**, via a **running
|
||||
> n_signer process**, or via a **USB hardware signer** — with the same code.
|
||||
|
||||
## 1. Problem
|
||||
|
||||
Many projects (`nostr_terminal`, `nostr_push`, `n_os_tr`, `nostr_login_lite`) each
|
||||
re-implement the *same* caller-side integration with n_signer:
|
||||
|
||||
- 4-byte big-endian length-prefixed framing
|
||||
- kind-27235 auth envelope (`nsigner_rpc` / `nsigner_method` / `nsigner_body_hash`)
|
||||
- the JSON-RPC methods (`nostr_get_public_key`, `nostr_sign_event`, `nostr_nip04_*`, `nostr_nip44_*`)
|
||||
- a per-project `signer.h` abstraction wrapping local-vs-remote signing
|
||||
|
||||
Meanwhile, `nostr_core_lib` exposes ~30+ functions that take a raw
|
||||
`const unsigned char* private_key` and sign/encrypt in-process (e.g.
|
||||
[`nostr_create_and_sign_event()`](../nostr_core/nip001.h:16)), with no seam to
|
||||
substitute a remote signer.
|
||||
|
||||
The dependency direction is already **n_signer → nostr_core_lib** (n_signer vendors
|
||||
nostr_core_lib and uses its crypto). So the natural home for the shared, **key-free**,
|
||||
caller-side glue is `nostr_core_lib` itself. This is not recursive: nothing
|
||||
key-bearing and nothing from the signer *program* moves down.
|
||||
|
||||
## 2. What moves vs. what stays
|
||||
|
||||
**Moves into `nostr_core_lib`** (caller-side, key-free, shared):
|
||||
- `nostr_signer_t` abstraction (the backend-agnostic seam).
|
||||
- Local backend (wraps today's raw-privkey path; zero behavior change).
|
||||
- Shared nsigner client: framing + kind-27235 auth envelope + JSON-RPC verbs + discovery.
|
||||
- Caller-side transports: AF_UNIX abstract socket, TCP, stdio, **USB-CDC serial**.
|
||||
- Parallel `*_with_signer` entry points.
|
||||
|
||||
**Stays in `n_signer`** (the program + host/device specifics):
|
||||
- The signer **binary** itself: mnemonic, role derivation, purpose/curve enforcement,
|
||||
attended TUI, approval prompts — the **trust boundary**. Must not move.
|
||||
- Spawn/embed helper (memfd + fexecve + terminal launch) — desktop-Linux deployment.
|
||||
- **Hardware-signer firmware** (ESP32/Feather, TinyUSB) — developing the device.
|
||||
- Browser WebUSB/WebSerial drivers (JS) — stay in `nostr_login_lite`.
|
||||
- Qubes packaging, FIPS, Docker.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph CORE[nostr_core_lib]
|
||||
ABS[nostr_signer_t abstraction]
|
||||
LOCAL[Local backend]
|
||||
CLIENT[nsigner client: framing + kind-27235 auth + discovery]
|
||||
WS[signer-aware *_with_signer entry points]
|
||||
TRANS[Transports: unix / tcp / stdio / usb-cdc serial]
|
||||
end
|
||||
subgraph NS[n_signer]
|
||||
BIN[Signer binary: mnemonic, roles, TUI, approvals]
|
||||
SPAWN[Spawn/embed helper]
|
||||
FW[Hardware firmware ESP32/Feather]
|
||||
end
|
||||
PROJ[Downstream project] --> CORE
|
||||
WS -.wire.-> BIN
|
||||
TRANS -.usb-cdc.-> FW
|
||||
SPAWN -.launches.-> BIN
|
||||
NS --> CORE
|
||||
```
|
||||
|
||||
## 3. Design decisions (locked)
|
||||
|
||||
- **Parallel APIs (Option A).** Add `*_with_signer` variants; leave all existing
|
||||
raw-privkey APIs byte-for-byte untouched. Existing projects keep compiling and
|
||||
behaving identically; remote signing is opt-in per call site.
|
||||
- **Scope = Option 2.** Abstraction + local backend + shared nsigner client (incl.
|
||||
transports) live in `nostr_core_lib`. The spawn/embed helper and the device firmware
|
||||
remain in n_signer.
|
||||
- **USB hardware signer is a caller-side transport.** Desktop C apps reach a USB-CDC
|
||||
hardware signer over `/dev/ttyACM*` using the *same* framing/auth/verbs as the socket
|
||||
transports. The browser WebUSB path and the firmware are out of scope here.
|
||||
- **Build-flag gated.** The remote client + desktop transports compile only when
|
||||
`NOSTR_ENABLE_NSIGNER_CLIENT` is set — ON for desktop, OFF for ESP32 builds and OFF
|
||||
for n_signer's own build. The abstraction + local backend compile everywhere.
|
||||
|
||||
## 4. Module layout (new files)
|
||||
|
||||
- `nostr_core/nostr_signer.{h,c}` — `nostr_signer_t` + vtable + local backend + the
|
||||
six verb entry points operating on a handle.
|
||||
- `nostr_core/nsigner_client.{h,c}` — framing, JSON-RPC verbs, kind-27235 auth envelope
|
||||
builder, error mapping, discovery; remote backend that adapts a connection into a
|
||||
`nostr_signer_t`. (Guarded by `NOSTR_ENABLE_NSIGNER_CLIENT`.)
|
||||
- `nostr_core/nsigner_transport.{h,c}` — pluggable transport vtable
|
||||
(`open / send_framed / recv_framed / close`) with unix-abstract, tcp, stdio, and
|
||||
usb-cdc serial implementations.
|
||||
|
||||
## 5. Signer abstraction (sketch)
|
||||
|
||||
```c
|
||||
typedef struct nostr_signer nostr_signer_t;
|
||||
|
||||
/* Verbs mirror n_signer's wire contract */
|
||||
int nostr_signer_get_public_key(nostr_signer_t*, char out_pubkey_hex[65]);
|
||||
int nostr_signer_sign_event(nostr_signer_t*, cJSON* unsigned_event, cJSON** signed_out);
|
||||
int nostr_signer_nip04_encrypt(nostr_signer_t*, const char* peer_hex, const char* pt, char** ct);
|
||||
int nostr_signer_nip04_decrypt(nostr_signer_t*, const char* peer_hex, const char* ct, char** pt);
|
||||
int nostr_signer_nip44_encrypt(nostr_signer_t*, const char* peer_hex, const char* pt, char** ct);
|
||||
int nostr_signer_nip44_decrypt(nostr_signer_t*, const char* peer_hex, const char* ct, char** pt);
|
||||
void nostr_signer_free(nostr_signer_t*);
|
||||
|
||||
/* Local backend (always available) */
|
||||
nostr_signer_t* nostr_signer_local(const unsigned char private_key[32]);
|
||||
|
||||
/* Remote backends (only with NOSTR_ENABLE_NSIGNER_CLIENT) */
|
||||
nostr_signer_t* nostr_signer_nsigner_unix(const char* socket_name, const char* role,
|
||||
const unsigned char caller_key[32]);
|
||||
nostr_signer_t* nostr_signer_nsigner_tcp(const char* host, int port, const char* role,
|
||||
const unsigned char caller_key[32]);
|
||||
nostr_signer_t* nostr_signer_nsigner_serial(const char* dev_path, const char* role,
|
||||
const unsigned char caller_key[32]);
|
||||
```
|
||||
|
||||
The local backend simply calls the existing in-process crypto; the remote backends
|
||||
serialize each verb into a framed JSON-RPC request, attach the kind-27235 auth envelope,
|
||||
and parse the response.
|
||||
|
||||
## 6. Signer-aware entry points (Phase 3 targets)
|
||||
|
||||
Add parallel functions, highest value first:
|
||||
1. `nostr_create_and_sign_event_with_signer(...)` — the workhorse.
|
||||
2. NIP-17 DM creation (needs nip44 encrypt + sign).
|
||||
3. NIP-46 client session signing.
|
||||
4. NIP-60 / NIP-61 wallet/token/nutzap events.
|
||||
5. blossom auth header creation.
|
||||
6. relay-pool NIP-42 auth (`nostr_relay_pool_set_auth` signer-aware variant).
|
||||
|
||||
Every one keeps its existing raw-privkey sibling unchanged.
|
||||
|
||||
## 7. Phasing
|
||||
|
||||
- **Phase 0** — this doc.
|
||||
- **Phase 1** — `nostr_signer_t` + local backend (portable, ESP32-safe).
|
||||
- **Phase 2** — transport vtable; shared nsigner client (framing/auth/verbs/discovery);
|
||||
unix/tcp/stdio/usb-cdc transports; remote backend.
|
||||
- **Phase 3** — `*_with_signer` entry points (order in §6).
|
||||
- **Phase 4** — build-flag gating; verify n_signer + ESP32 exclude remote pieces;
|
||||
confirm dependency stays n_signer → nostr_core_lib only.
|
||||
- **Phase 5** — example: identical code signing local vs running nsigner (unix) vs USB
|
||||
hardware signer; tests against a live nsigner instance.
|
||||
- **Phase 6** — README/integration docs; cross-link the per-project plans this supersedes.
|
||||
- **Phase 7** (optional) — migrate n_signer's `client/` and the per-project hand-rolled
|
||||
clients onto the shared module; design spawn helper as a clean add-on for later
|
||||
promotion into the core if desired.
|
||||
|
||||
## 8. Risks / open items
|
||||
|
||||
- **Auth envelope canonicalization (JCS).** Must match n_signer exactly
|
||||
(`SHA-256(JCS(params))`, `SHA-256("null")` for no-params). Verify against
|
||||
[`caller_token_identity.md`](../../n_signer/plans/caller_token_identity.md:96).
|
||||
- **Serial transport identity.** USB-CDC is an "asserted caller"; approval prompts on the
|
||||
device still apply. Client must treat `approval_denied` as a normal outcome.
|
||||
- **Memory ownership.** Document free() responsibilities for returned JSON/strings, and
|
||||
zeroization of any caller key buffers.
|
||||
- **Versioning of the wire contract.** Centralizing it here makes nostr_core_lib the
|
||||
single source of truth; n_signer should track it rather than diverge.
|
||||
|
||||
## 9. Driver / acceptance app: `examples/note_poster`
|
||||
|
||||
The work is driven by the simplest possible real Nostr app: **sign in, then post one
|
||||
kind-1 note**. It is the acceptance test for the abstraction — the app is written once
|
||||
and only the signer construction at sign-in changes.
|
||||
|
||||
### Behavior
|
||||
1. Sign in → `nostr_signer_get_public_key()` resolves the npub.
|
||||
2. Prompt for note text → `nostr_create_and_sign_event_with_signer(kind=1, ...)`.
|
||||
3. Publish the signed event to **`wss://relay.laantungir.net`** using the existing relay
|
||||
pool (see [`examples/relay_pool.c`](../examples/relay_pool.c)).
|
||||
4. Print the event id and the relay's OK response.
|
||||
|
||||
### The whole point (one line differs per backend)
|
||||
```c
|
||||
nostr_signer_t *signer;
|
||||
switch (mode) {
|
||||
case LOCAL: signer = nostr_signer_local(privkey); break;
|
||||
case UNIX: signer = nostr_signer_nsigner_unix(socket_name, "main", caller_key); break;
|
||||
case SERIAL: signer = nostr_signer_nsigner_serial("/dev/ttyACM0", "main", caller_key);break;
|
||||
}
|
||||
/* everything below is identical regardless of backend */
|
||||
```
|
||||
|
||||
### Milestones (each independently demoable with the same app)
|
||||
- **M1 — local key, real publish.** `nostr_signer_t` + local backend +
|
||||
`nostr_create_and_sign_event_with_signer`; app posts a kind-1 to
|
||||
`wss://relay.laantungir.net` using a local nsec. No remote/transport/auth code yet.
|
||||
- **M2 — running nsigner over unix socket.** Add framing + kind-27235 auth + unix
|
||||
transport + remote backend. Same app, `--signer unix:<name>`, posts the note via a
|
||||
running `nsigner`.
|
||||
- **M3 — USB hardware signer.** Add the USB-CDC serial transport. Same app,
|
||||
`--signer serial:/dev/ttyACM0`, posts the note via the hardware signer.
|
||||
|
||||
### CLI shape
|
||||
```
|
||||
note_poster --signer local --nsec <hex>
|
||||
note_poster --signer unix:<socket_name> --caller-nsec <hex> [--role main]
|
||||
note_poster --signer serial:/dev/ttyACM0 --caller-nsec <hex> [--role main]
|
||||
```
|
||||
|
||||
### Acceptance criteria
|
||||
A kind-1 note authored by the same code reaches `wss://relay.laantungir.net` and returns
|
||||
a relay OK, across all three signer backends, with only the sign-in line differing.
|
||||
@@ -0,0 +1,312 @@
|
||||
#include "../../nostr_core/nostr_http.h"
|
||||
|
||||
#include "esp_http_client.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
size_t max_bytes;
|
||||
int truncated;
|
||||
} nostr_http_buffer_t;
|
||||
|
||||
static char g_ca_bundle_path[512] = {0};
|
||||
|
||||
static char* nostr_http_strdup_local(const char* s) {
|
||||
if (!s) return NULL;
|
||||
size_t n = strlen(s);
|
||||
char* out = (char*)malloc(n + 1U);
|
||||
if (!out) return NULL;
|
||||
memcpy(out, s, n + 1U);
|
||||
return out;
|
||||
}
|
||||
|
||||
static int append_bytes(nostr_http_buffer_t* b, const void* src, size_t n) {
|
||||
if (!b || !src || n == 0) return 1;
|
||||
|
||||
if (b->max_bytes > 0 && b->len >= b->max_bytes) {
|
||||
b->truncated = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t to_copy = n;
|
||||
if (b->max_bytes > 0) {
|
||||
size_t remaining = b->max_bytes - b->len;
|
||||
if (to_copy > remaining) {
|
||||
to_copy = remaining;
|
||||
b->truncated = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (b->len + to_copy + 1U > b->cap) {
|
||||
size_t new_cap = b->cap == 0 ? 1024U : b->cap;
|
||||
while (new_cap < b->len + to_copy + 1U) {
|
||||
new_cap *= 2U;
|
||||
}
|
||||
char* p = (char*)realloc(b->data, new_cap);
|
||||
if (!p) return 0;
|
||||
b->data = p;
|
||||
b->cap = new_cap;
|
||||
}
|
||||
|
||||
memcpy(b->data + b->len, src, to_copy);
|
||||
b->len += to_copy;
|
||||
b->data[b->len] = '\0';
|
||||
return 1;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
nostr_http_buffer_t* body;
|
||||
nostr_http_buffer_t* headers;
|
||||
int capture_headers;
|
||||
} nostr_http_event_ctx_t;
|
||||
|
||||
static esp_err_t http_event_handler(esp_http_client_event_t* evt) {
|
||||
if (!evt || !evt->user_data) return ESP_OK;
|
||||
|
||||
nostr_http_event_ctx_t* ctx = (nostr_http_event_ctx_t*)evt->user_data;
|
||||
|
||||
switch (evt->event_id) {
|
||||
case HTTP_EVENT_ON_DATA:
|
||||
if (evt->data && evt->data_len > 0 && ctx->body) {
|
||||
if (!append_bytes(ctx->body, evt->data, (size_t)evt->data_len)) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case HTTP_EVENT_ON_HEADER:
|
||||
if (ctx->capture_headers && ctx->headers && evt->header_key && evt->header_value) {
|
||||
if (!append_bytes(ctx->headers, evt->header_key, strlen(evt->header_key)) ||
|
||||
!append_bytes(ctx->headers, ": ", 2) ||
|
||||
!append_bytes(ctx->headers, evt->header_value, strlen(evt->header_value)) ||
|
||||
!append_bytes(ctx->headers, "\r\n", 2)) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void nostr_http_set_ca_bundle(const char* ca_bundle_path) {
|
||||
if (!ca_bundle_path || ca_bundle_path[0] == '\0') {
|
||||
g_ca_bundle_path[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
strncpy(g_ca_bundle_path, ca_bundle_path, sizeof(g_ca_bundle_path) - 1);
|
||||
g_ca_bundle_path[sizeof(g_ca_bundle_path) - 1] = '\0';
|
||||
}
|
||||
|
||||
const char* nostr_http_detect_ca_bundle(void) {
|
||||
if (g_ca_bundle_path[0] != '\0') {
|
||||
return g_ca_bundle_path;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int nostr_http_request(const nostr_http_request_t* req, nostr_http_response_t* resp) {
|
||||
if (!req || !req->url || !resp) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
memset(resp, 0, sizeof(*resp));
|
||||
|
||||
const char* method = (req->method && req->method[0] != '\0') ? req->method : "GET";
|
||||
int timeout_ms = (req->timeout_seconds > 0 ? req->timeout_seconds : 30) * 1000;
|
||||
|
||||
nostr_http_buffer_t body = {0};
|
||||
body.max_bytes = req->max_response_bytes;
|
||||
|
||||
nostr_http_buffer_t headers = {0};
|
||||
|
||||
nostr_http_event_ctx_t evt_ctx = {
|
||||
.body = &body,
|
||||
.headers = &headers,
|
||||
.capture_headers = req->capture_headers
|
||||
};
|
||||
|
||||
esp_http_client_config_t cfg = {
|
||||
.url = req->url,
|
||||
.timeout_ms = timeout_ms,
|
||||
.event_handler = http_event_handler,
|
||||
.user_data = &evt_ctx,
|
||||
.disable_auto_redirect = req->follow_redirects ? false : true,
|
||||
.max_redirection_count = req->max_redirects > 0 ? req->max_redirects : 3,
|
||||
};
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&cfg);
|
||||
if (!client) {
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
if (req->user_agent && req->user_agent[0] != '\0') {
|
||||
esp_http_client_set_header(client, "User-Agent", req->user_agent);
|
||||
} else {
|
||||
esp_http_client_set_header(client, "User-Agent", "nostr-core/1.0");
|
||||
}
|
||||
|
||||
if (req->headers) {
|
||||
for (const char** h = req->headers; *h; h++) {
|
||||
const char* line = *h;
|
||||
if (!line || line[0] == '\0') continue;
|
||||
const char* colon = strchr(line, ':');
|
||||
if (!colon) continue;
|
||||
|
||||
size_t key_len = (size_t)(colon - line);
|
||||
if (key_len == 0) continue;
|
||||
|
||||
char* key = (char*)malloc(key_len + 1U);
|
||||
if (!key) {
|
||||
esp_http_client_cleanup(client);
|
||||
free(body.data);
|
||||
free(headers.data);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
memcpy(key, line, key_len);
|
||||
key[key_len] = '\0';
|
||||
|
||||
const char* value = colon + 1;
|
||||
while (*value == ' ') value++;
|
||||
|
||||
esp_http_client_set_header(client, key, value);
|
||||
free(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (strcasecmp(method, "GET") == 0) {
|
||||
esp_http_client_set_method(client, HTTP_METHOD_GET);
|
||||
} else if (strcasecmp(method, "POST") == 0) {
|
||||
esp_http_client_set_method(client, HTTP_METHOD_POST);
|
||||
if (req->body && req->body_len > 0) {
|
||||
esp_http_client_set_post_field(client, (const char*)req->body, (int)req->body_len);
|
||||
} else {
|
||||
esp_http_client_set_post_field(client, "", 0);
|
||||
}
|
||||
} else if (strcasecmp(method, "HEAD") == 0) {
|
||||
esp_http_client_set_method(client, HTTP_METHOD_HEAD);
|
||||
} else if (strcasecmp(method, "PUT") == 0) {
|
||||
esp_http_client_set_method(client, HTTP_METHOD_PUT);
|
||||
if (req->body && req->body_len > 0) {
|
||||
esp_http_client_set_post_field(client, (const char*)req->body, (int)req->body_len);
|
||||
}
|
||||
} else if (strcasecmp(method, "DELETE") == 0) {
|
||||
esp_http_client_set_method(client, HTTP_METHOD_DELETE);
|
||||
} else {
|
||||
esp_http_client_cleanup(client);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
esp_err_t err = esp_http_client_perform(client);
|
||||
if (err != ESP_OK) {
|
||||
esp_http_client_cleanup(client);
|
||||
free(body.data);
|
||||
free(headers.data);
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
resp->status_code = esp_http_client_get_status_code(client);
|
||||
resp->body = body.data ? body.data : nostr_http_strdup_local("");
|
||||
resp->body_len = body.data ? body.len : 0;
|
||||
resp->headers_raw = headers.data;
|
||||
resp->truncated = body.truncated;
|
||||
|
||||
char* ct = NULL;
|
||||
char* ct_raw = NULL;
|
||||
if (esp_http_client_get_header(client, "Content-Type", &ct_raw) == ESP_OK && ct_raw && ct_raw[0] != '\0') {
|
||||
ct = nostr_http_strdup_local(ct_raw);
|
||||
}
|
||||
resp->content_type = ct;
|
||||
|
||||
esp_http_client_cleanup(client);
|
||||
|
||||
if (!resp->body) {
|
||||
free(resp->headers_raw);
|
||||
free(resp->content_type);
|
||||
memset(resp, 0, sizeof(*resp));
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void nostr_http_response_free(nostr_http_response_t* resp) {
|
||||
if (!resp) return;
|
||||
free(resp->body);
|
||||
free(resp->content_type);
|
||||
free(resp->headers_raw);
|
||||
memset(resp, 0, sizeof(*resp));
|
||||
}
|
||||
|
||||
int nostr_http_get(const char* url, int timeout_seconds, char** body_out, long* status_out) {
|
||||
if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
*body_out = NULL;
|
||||
if (status_out) *status_out = 0;
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "GET";
|
||||
req.url = url;
|
||||
req.timeout_seconds = timeout_seconds;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (status_out) *status_out = resp.status_code;
|
||||
*body_out = resp.body;
|
||||
free(resp.content_type);
|
||||
free(resp.headers_raw);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_http_post_json(const char* url,
|
||||
const char* json_body,
|
||||
int timeout_seconds,
|
||||
char** body_out,
|
||||
long* status_out) {
|
||||
if (!url || !body_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
*body_out = NULL;
|
||||
if (status_out) *status_out = 0;
|
||||
|
||||
const char* headers[] = {
|
||||
"Accept: application/json",
|
||||
"Content-Type: application/json",
|
||||
NULL
|
||||
};
|
||||
|
||||
const char* payload = json_body ? json_body : "{}";
|
||||
|
||||
nostr_http_request_t req;
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.method = "POST";
|
||||
req.url = url;
|
||||
req.headers = headers;
|
||||
req.body = (const unsigned char*)payload;
|
||||
req.body_len = strlen(payload);
|
||||
req.timeout_seconds = timeout_seconds;
|
||||
req.follow_redirects = 1;
|
||||
req.max_redirects = 3;
|
||||
|
||||
nostr_http_response_t resp;
|
||||
int rc = nostr_http_request(&req, &resp);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
if (status_out) *status_out = resp.status_code;
|
||||
*body_out = resp.body;
|
||||
free(resp.content_type);
|
||||
free(resp.headers_raw);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "../../nostr_core/nostr_platform.h"
|
||||
|
||||
#include "esp_random.h"
|
||||
|
||||
int nostr_platform_random(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL || len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_fill_random(buf, len);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
#include "../../nostr_websocket/nostr_websocket_tls.h"
|
||||
|
||||
#include "esp_crt_bundle.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <esp_transport.h>
|
||||
#include <esp_transport_ssl.h>
|
||||
#include <esp_transport_tcp.h>
|
||||
#include <esp_transport_ws.h>
|
||||
|
||||
typedef struct {
|
||||
char* host;
|
||||
int port;
|
||||
char* path;
|
||||
int use_tls;
|
||||
} ws_url_parts_t;
|
||||
|
||||
struct nostr_ws_client {
|
||||
esp_transport_list_handle_t list;
|
||||
esp_transport_handle_t transport;
|
||||
nostr_ws_state_t state;
|
||||
int timeout_ms;
|
||||
char* host;
|
||||
int port;
|
||||
char* path;
|
||||
};
|
||||
|
||||
static void ws_free_url_parts(ws_url_parts_t* p) {
|
||||
if (!p) return;
|
||||
free(p->host);
|
||||
free(p->path);
|
||||
p->host = NULL;
|
||||
p->path = NULL;
|
||||
p->port = 0;
|
||||
p->use_tls = 0;
|
||||
}
|
||||
|
||||
static int ws_parse_url(const char* url, ws_url_parts_t* out) {
|
||||
if (!url || !out) return -1;
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
const char* p = NULL;
|
||||
if (strncmp(url, "ws://", 5) == 0) {
|
||||
out->use_tls = 0;
|
||||
out->port = 80;
|
||||
p = url + 5;
|
||||
} else if (strncmp(url, "wss://", 6) == 0) {
|
||||
out->use_tls = 1;
|
||||
out->port = 443;
|
||||
p = url + 6;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* slash = strchr(p, '/');
|
||||
const char* host_end = slash ? slash : (p + strlen(p));
|
||||
|
||||
const char* colon = NULL;
|
||||
for (const char* it = p; it < host_end; ++it) {
|
||||
if (*it == ':') {
|
||||
colon = it;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
size_t host_len;
|
||||
if (colon) {
|
||||
host_len = (size_t)(colon - p);
|
||||
char portbuf[8] = {0};
|
||||
size_t port_len = (size_t)(host_end - colon - 1);
|
||||
if (port_len == 0 || port_len >= sizeof(portbuf)) return -1;
|
||||
memcpy(portbuf, colon + 1, port_len);
|
||||
for (size_t i = 0; i < port_len; i++) {
|
||||
if (!isdigit((unsigned char)portbuf[i])) return -1;
|
||||
}
|
||||
out->port = atoi(portbuf);
|
||||
if (out->port <= 0 || out->port > 65535) return -1;
|
||||
} else {
|
||||
host_len = (size_t)(host_end - p);
|
||||
}
|
||||
|
||||
if (host_len == 0) return -1;
|
||||
|
||||
out->host = (char*)malloc(host_len + 1U);
|
||||
if (!out->host) return -1;
|
||||
memcpy(out->host, p, host_len);
|
||||
out->host[host_len] = '\0';
|
||||
|
||||
out->path = slash ? strdup(slash) : strdup("/");
|
||||
if (!out->path) {
|
||||
ws_free_url_parts(out);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void ws_client_destroy(struct nostr_ws_client* c) {
|
||||
if (!c) return;
|
||||
|
||||
if (c->transport) {
|
||||
esp_transport_close(c->transport);
|
||||
c->transport = NULL;
|
||||
}
|
||||
|
||||
if (c->list) {
|
||||
esp_transport_list_destroy(c->list);
|
||||
c->list = NULL;
|
||||
}
|
||||
|
||||
free(c->host);
|
||||
free(c->path);
|
||||
free(c);
|
||||
}
|
||||
|
||||
static esp_transport_handle_t ws_build_transport(const ws_url_parts_t* up,
|
||||
esp_transport_list_handle_t list) {
|
||||
if (!up || !list) return NULL;
|
||||
|
||||
if (up->use_tls) {
|
||||
esp_transport_handle_t ssl = esp_transport_ssl_init();
|
||||
if (!ssl) return NULL;
|
||||
|
||||
esp_transport_ssl_crt_bundle_attach(ssl, esp_crt_bundle_attach);
|
||||
esp_transport_set_default_port(ssl, 443);
|
||||
if (esp_transport_list_add(list, ssl, "ssl") != ESP_OK) {
|
||||
esp_transport_destroy(ssl);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
esp_transport_handle_t wss = esp_transport_ws_init(ssl);
|
||||
if (!wss) return NULL;
|
||||
|
||||
esp_transport_set_default_port(wss, up->port);
|
||||
esp_transport_ws_set_path(wss, up->path);
|
||||
if (esp_transport_ws_set_user_agent(wss, "nostr_core_lib/esp32") != ESP_OK) {
|
||||
esp_transport_destroy(wss);
|
||||
return NULL;
|
||||
}
|
||||
if (esp_transport_list_add(list, wss, "wss") != ESP_OK) {
|
||||
esp_transport_destroy(wss);
|
||||
return NULL;
|
||||
}
|
||||
return wss;
|
||||
}
|
||||
|
||||
esp_transport_handle_t tcp = esp_transport_tcp_init();
|
||||
if (!tcp) return NULL;
|
||||
|
||||
esp_transport_set_default_port(tcp, 80);
|
||||
if (esp_transport_list_add(list, tcp, "tcp") != ESP_OK) {
|
||||
esp_transport_destroy(tcp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
esp_transport_handle_t ws = esp_transport_ws_init(tcp);
|
||||
if (!ws) return NULL;
|
||||
|
||||
esp_transport_set_default_port(ws, up->port);
|
||||
esp_transport_ws_set_path(ws, up->path);
|
||||
if (esp_transport_ws_set_user_agent(ws, "nostr_core_lib/esp32") != ESP_OK) {
|
||||
esp_transport_destroy(ws);
|
||||
return NULL;
|
||||
}
|
||||
if (esp_transport_list_add(list, ws, "ws") != ESP_OK) {
|
||||
esp_transport_destroy(ws);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
nostr_ws_client_t* nostr_ws_connect(const char* url) {
|
||||
if (!url) return NULL;
|
||||
|
||||
ws_url_parts_t up;
|
||||
if (ws_parse_url(url, &up) != 0) return NULL;
|
||||
|
||||
struct nostr_ws_client* c = (struct nostr_ws_client*)calloc(1, sizeof(struct nostr_ws_client));
|
||||
if (!c) {
|
||||
ws_free_url_parts(&up);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
c->list = esp_transport_list_init();
|
||||
if (!c->list) {
|
||||
ws_free_url_parts(&up);
|
||||
free(c);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
c->transport = ws_build_transport(&up, c->list);
|
||||
if (!c->transport) {
|
||||
ws_free_url_parts(&up);
|
||||
ws_client_destroy(c);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
c->timeout_ms = 30000;
|
||||
c->state = NOSTR_WS_CONNECTING;
|
||||
c->host = up.host;
|
||||
c->port = up.port;
|
||||
c->path = up.path;
|
||||
|
||||
up.host = NULL;
|
||||
up.path = NULL;
|
||||
|
||||
int cr = esp_transport_connect(c->transport, c->host, c->port, c->timeout_ms);
|
||||
if (cr < 0) {
|
||||
c->state = NOSTR_WS_ERROR;
|
||||
ws_client_destroy(c);
|
||||
ws_free_url_parts(&up);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int hs = esp_transport_ws_get_upgrade_request_status(c->transport);
|
||||
if (hs != 101) {
|
||||
c->state = NOSTR_WS_ERROR;
|
||||
ws_client_destroy(c);
|
||||
ws_free_url_parts(&up);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
c->state = NOSTR_WS_CONNECTED;
|
||||
ws_free_url_parts(&up);
|
||||
return c;
|
||||
}
|
||||
|
||||
int nostr_ws_close(nostr_ws_client_t* client) {
|
||||
if (!client) return NOSTR_WS_ERROR_INVALID;
|
||||
|
||||
if (client->state == NOSTR_WS_CONNECTED) {
|
||||
(void)esp_transport_ws_send_raw(client->transport,
|
||||
(ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_CLOSE | WS_TRANSPORT_OPCODES_FIN),
|
||||
NULL,
|
||||
0,
|
||||
client->timeout_ms);
|
||||
}
|
||||
|
||||
client->state = NOSTR_WS_CLOSED;
|
||||
ws_client_destroy(client);
|
||||
return NOSTR_WS_SUCCESS;
|
||||
}
|
||||
|
||||
nostr_ws_state_t nostr_ws_get_state(nostr_ws_client_t* client) {
|
||||
if (!client) return NOSTR_WS_ERROR;
|
||||
return client->state;
|
||||
}
|
||||
|
||||
int nostr_ws_send_text(nostr_ws_client_t* client, const char* message) {
|
||||
if (!client || !message || client->state != NOSTR_WS_CONNECTED) {
|
||||
return NOSTR_WS_ERROR_INVALID;
|
||||
}
|
||||
|
||||
int len = (int)strlen(message);
|
||||
int wr = esp_transport_ws_send_raw(client->transport,
|
||||
(ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_TEXT | WS_TRANSPORT_OPCODES_FIN),
|
||||
message,
|
||||
len,
|
||||
client->timeout_ms);
|
||||
return (wr == len) ? NOSTR_WS_SUCCESS : NOSTR_WS_ERROR_NETWORK;
|
||||
}
|
||||
|
||||
int nostr_ws_receive(nostr_ws_client_t* client, char* buffer, size_t buffer_size, int timeout_ms) {
|
||||
if (!client || !buffer || buffer_size == 0) return NOSTR_WS_ERROR_INVALID;
|
||||
if (client->state != NOSTR_WS_CONNECTED && client->state != NOSTR_WS_CLOSING) {
|
||||
return NOSTR_WS_ERROR_INVALID;
|
||||
}
|
||||
|
||||
int tmo = (timeout_ms > 0) ? timeout_ms : client->timeout_ms;
|
||||
int n = esp_transport_read(client->transport, buffer, (int)buffer_size - 1, tmo);
|
||||
if (n < 0) {
|
||||
return NOSTR_WS_ERROR_NETWORK;
|
||||
}
|
||||
if (n == 0) {
|
||||
/* timeout or internally-handled control frame; not a hard network failure */
|
||||
buffer[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
ws_transport_opcodes_t opcode = esp_transport_ws_get_read_opcode(client->transport);
|
||||
|
||||
if (opcode == WS_TRANSPORT_OPCODES_TEXT || opcode == WS_TRANSPORT_OPCODES_BINARY) {
|
||||
buffer[n] = '\0';
|
||||
return n;
|
||||
}
|
||||
|
||||
if (opcode == WS_TRANSPORT_OPCODES_PING) {
|
||||
(void)esp_transport_ws_send_raw(client->transport,
|
||||
(ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_PONG | WS_TRANSPORT_OPCODES_FIN),
|
||||
buffer,
|
||||
n,
|
||||
tmo);
|
||||
return nostr_ws_receive(client, buffer, buffer_size, timeout_ms);
|
||||
}
|
||||
|
||||
if (opcode == WS_TRANSPORT_OPCODES_PONG) {
|
||||
buffer[n] = '\0';
|
||||
return n;
|
||||
}
|
||||
|
||||
if (opcode == WS_TRANSPORT_OPCODES_CLOSE) {
|
||||
client->state = NOSTR_WS_CLOSING;
|
||||
buffer[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
return NOSTR_WS_ERROR_PROTOCOL;
|
||||
}
|
||||
|
||||
int nostr_ws_ping(nostr_ws_client_t* client) {
|
||||
if (!client || client->state != NOSTR_WS_CONNECTED) {
|
||||
return NOSTR_WS_ERROR_INVALID;
|
||||
}
|
||||
|
||||
int wr = esp_transport_ws_send_raw(client->transport,
|
||||
(ws_transport_opcodes_t)(WS_TRANSPORT_OPCODES_PING | WS_TRANSPORT_OPCODES_FIN),
|
||||
"ping",
|
||||
4,
|
||||
client->timeout_ms);
|
||||
return (wr == 4) ? NOSTR_WS_SUCCESS : NOSTR_WS_ERROR_NETWORK;
|
||||
}
|
||||
|
||||
int nostr_ws_set_timeout(nostr_ws_client_t* client, int timeout_ms) {
|
||||
if (!client || timeout_ms < 0) return NOSTR_WS_ERROR_INVALID;
|
||||
client->timeout_ms = timeout_ms;
|
||||
return NOSTR_WS_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_relay_send_req(nostr_ws_client_t* client, const char* subscription_id, cJSON* filters) {
|
||||
if (!client || !subscription_id || !filters) {
|
||||
return NOSTR_WS_ERROR_INVALID;
|
||||
}
|
||||
|
||||
cJSON* req_array = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(req_array, cJSON_CreateString("REQ"));
|
||||
cJSON_AddItemToArray(req_array, cJSON_CreateString(subscription_id));
|
||||
|
||||
if (cJSON_IsArray(filters)) {
|
||||
cJSON* filter;
|
||||
cJSON_ArrayForEach(filter, filters) {
|
||||
cJSON_AddItemToArray(req_array, cJSON_Duplicate(filter, 1));
|
||||
}
|
||||
} else {
|
||||
cJSON_AddItemToArray(req_array, cJSON_Duplicate(filters, 1));
|
||||
}
|
||||
|
||||
char* req_string = cJSON_PrintUnformatted(req_array);
|
||||
if (!req_string) {
|
||||
cJSON_Delete(req_array);
|
||||
return NOSTR_WS_ERROR_MEMORY;
|
||||
}
|
||||
|
||||
int result = nostr_ws_send_text(client, req_string);
|
||||
|
||||
free(req_string);
|
||||
cJSON_Delete(req_array);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int nostr_relay_send_event(nostr_ws_client_t* client, cJSON* event) {
|
||||
if (!client || !event) {
|
||||
return NOSTR_WS_ERROR_INVALID;
|
||||
}
|
||||
|
||||
cJSON* event_array = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(event_array, cJSON_CreateString("EVENT"));
|
||||
cJSON_AddItemToArray(event_array, cJSON_Duplicate(event, 1));
|
||||
|
||||
char* event_string = cJSON_PrintUnformatted(event_array);
|
||||
if (!event_string) {
|
||||
cJSON_Delete(event_array);
|
||||
return NOSTR_WS_ERROR_MEMORY;
|
||||
}
|
||||
|
||||
int result = nostr_ws_send_text(client, event_string);
|
||||
|
||||
free(event_string);
|
||||
cJSON_Delete(event_array);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int nostr_relay_send_close(nostr_ws_client_t* client, const char* subscription_id) {
|
||||
if (!client || !subscription_id) {
|
||||
return NOSTR_WS_ERROR_INVALID;
|
||||
}
|
||||
|
||||
cJSON* close_array = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(close_array, cJSON_CreateString("CLOSE"));
|
||||
cJSON_AddItemToArray(close_array, cJSON_CreateString(subscription_id));
|
||||
|
||||
char* close_string = cJSON_PrintUnformatted(close_array);
|
||||
if (!close_string) {
|
||||
cJSON_Delete(close_array);
|
||||
return NOSTR_WS_ERROR_MEMORY;
|
||||
}
|
||||
|
||||
int result = nostr_ws_send_text(client, close_string);
|
||||
|
||||
free(close_string);
|
||||
cJSON_Delete(close_array);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int nostr_parse_relay_message(const char* message, char** message_type, cJSON** parsed_json) {
|
||||
if (!message || !message_type || !parsed_json) {
|
||||
return NOSTR_WS_ERROR_INVALID;
|
||||
}
|
||||
|
||||
*message_type = NULL;
|
||||
*parsed_json = NULL;
|
||||
|
||||
cJSON* json = cJSON_Parse(message);
|
||||
if (!json || !cJSON_IsArray(json)) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_WS_ERROR_PROTOCOL;
|
||||
}
|
||||
|
||||
cJSON* type_item = cJSON_GetArrayItem(json, 0);
|
||||
if (!type_item || !cJSON_IsString(type_item)) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_WS_ERROR_PROTOCOL;
|
||||
}
|
||||
|
||||
*message_type = strdup(type_item->valuestring);
|
||||
if (!*message_type) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_WS_ERROR_MEMORY;
|
||||
}
|
||||
|
||||
*parsed_json = json;
|
||||
return NOSTR_WS_SUCCESS;
|
||||
}
|
||||
|
||||
const char* nostr_ws_strerror(int error_code) {
|
||||
switch (error_code) {
|
||||
case NOSTR_WS_SUCCESS: return "Success";
|
||||
case NOSTR_WS_ERROR_INVALID: return "Invalid parameter";
|
||||
case NOSTR_WS_ERROR_NETWORK: return "Network error";
|
||||
case NOSTR_WS_ERROR_PROTOCOL: return "Protocol error";
|
||||
case NOSTR_WS_ERROR_MEMORY: return "Memory allocation error";
|
||||
case NOSTR_WS_ERROR_TLS: return "TLS error";
|
||||
default: return "Unknown error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "../nostr_core/nostr_platform.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int nostr_platform_random(unsigned char *buf, size_t len) {
|
||||
if (buf == NULL || len == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t total = 0;
|
||||
while (total < len) {
|
||||
ssize_t n = read(fd, buf + total, len - total);
|
||||
if (n <= 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
total += (size_t)n;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user