Compare commits

...
35 Commits
Author SHA1 Message Date
laantungir b512be79ab Fix Cashu hash_to_curve to NUT-00 spec and align live helper for send-proof compatibility 2026-03-20 15:30:09 -04:00
laantungir 5760a3a7d0 Confirm live testnut-based outbound token generation test run and documentation update 2026-03-20 11:26:07 -04:00
laantungir 2b80abc3ae Add cashu_encode_token with cashuA/cashuB formats and live encode-decode validation 2026-03-20 10:59:13 -04:00
laantungir c81db98e3f Add Cashu keyset endpoints support and per-amount unblinding with live checkstate validation 2026-03-20 10:32:43 -04:00
laantungir afe3e4787f Add live NIP-60 Cashu receive integration test using nofee.testnut mint 2026-03-20 10:23:21 -04:00
Your Name e3b9248004 Preserve Cashu mint error JSON on non-2xx responses for downstream diagnostics 2026-03-20 09:48:03 -04:00
Your Name fcb5abdccf Add Cashu blinding helpers and secp256k1 point ops 2026-03-19 18:45:17 -04:00
Your Name 92d5927392 Add Cashu token decode support for cashuA and cashuB 2026-03-19 18:19:45 -04:00
Your Name 6c6bcba650 Added cashu functionality. 2026-03-19 14:52:03 -04:00
Your Name 7aea4b07df Added cashu functionality. 2026-03-19 14:32:14 -04:00
Your Name 84257b2f58 Added cashu functionality. 2026-03-19 14:30:44 -04:00
Your Name af4f7cace9 Added cashu functionality. 2026-03-19 14:25:53 -04:00
laantungir 363b7e9adf Release v0.5.0 2026-03-19 09:13:59 -04:00
laantungir 3186c2ead6 just want to increment 2026-03-19 09:09:59 -04:00
laantungir f07433578a just want to increment 2026-03-19 09:09:46 -04:00
laantungir d264d9f5ed nip60 2026-03-19 09:01:11 -04:00
Your Name 98094cac86 fix relay pool sync query repeat behavior and add regression test 2026-03-16 05:27:23 -04:00
Your Name 79d98587d3 Delegate nostr_core_lib logging via callback and integrate single-file didactyl logging 2026-03-13 13:57:20 -04:00
Your Name 81814e8518 Various changes 2026-03-05 15:18:44 -04:00
Your Name 784cfc09c3 Add relay state transition cause capture in poll loop 2026-03-05 15:18:13 -04:00
Your Name c87f8db6d8 Fix oversized ws frame handling and increase receive buffers 2026-03-05 14:42:14 -04:00
Your Name c871c40219 query_sync: forward non-matching relay messages to pool dispatch 2026-03-05 14:07:11 -04:00
Your Name 2b3113dafc relay_pool: skip reconnect attempts in publish_async 2026-03-05 11:01:21 -04:00
Your Name 5c51ab1f9f Add socket recv/send timeouts after connect to prevent websocket poll hangs on half-broken connections 2026-03-05 10:39:51 -04:00
laantungir df50ca1273 Add NIP-42 auth support to relay pool with verbose auth diagnostics test and docs updates 2026-03-01 08:49:55 -04:00
laantungir 2ce2ecbbfb Implement NIP-46 remote signing API, tests/example, build integration, and refresh README docs 2026-02-28 09:53:35 -04:00
Your Name 57ecbf0c11 Fix SSL_pending busy-wait causing 100% CPU usage
The tls_recv() function was skipping the select() call when SSL_pending()
returned non-zero, causing a tight busy-wait loop. This fix ensures select()
is always called, using a zero timeout when SSL has buffered data to return
immediately, or the full timeout to properly block when waiting for new data.

This prevents the relay client from consuming 100% CPU while maintaining
the WebSocket connection.
2026-02-03 14:16:00 -04:00
laantungir e87d7364ff Rollback of nip44 changes, and added ability to modify timestamps in giftwraps. 2025-10-27 13:16:06 -04:00
Your Name 0b2cca6549 Add configurable timestamp randomization for NIP-59 gift wraps 2025-10-27 12:57:25 -04:00
laantungir fca1a8077d Revert NIP-44 to spec compliance (65535 bytes), keep NIP-04 at 1MB 2025-10-27 12:51:50 -04:00
laantungir 2c128a6295 Upgrade nip4 and nip44 to be able to handle 1MB payloads. 2025-10-24 18:56:30 -04:00
laantungir 24903b8c90 Adding async publish to relay pool 2025-10-09 09:19:11 -04:00
laantungir c6896eebb4 added nip 17 2025-10-04 18:32:28 -04:00
laantungir 4e06d8f723 Implement NIP-21: nostr: URI scheme with full support for note, nprofile, nevent, and naddr URIs including TLV encoding/decoding and comprehensive test suite 2025-10-03 06:10:56 -04:00
laantungir d7a8aa1dc4 nip 17, and 59 2025-10-03 04:25:10 -04:00
58 changed files with 13208 additions and 935 deletions
+7
View File
@@ -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"
+33 -11
View File
@@ -6,17 +6,18 @@ This document describes the public API for the Nostr Relay Pool implementation i
| Function | Description |
|----------|-------------|
| [`nostr_relay_pool_create()`](nostr_core/core_relay_pool.c:219) | Create and initialize a new relay pool |
| [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:304) | Destroy pool and cleanup all resources |
| [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:229) | Add a relay URL to the pool |
| [`nostr_relay_pool_remove_relay()`](nostr_core/core_relay_pool.c:273) | Remove a relay URL from the pool |
| [`nostr_relay_pool_subscribe()`](nostr_core/core_relay_pool.c:399) | Create async subscription with callbacks |
| [`nostr_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()`](nostr_core/core_relay_pool.c:866) | Publish event and wait for OK responses |
| [`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 |
@@ -71,6 +72,26 @@ void cleanup_pool(nostr_relay_pool_t* pool) {
## 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
@@ -438,9 +459,9 @@ int get_latest_note_from_pubkey(nostr_relay_pool_t* pool, const char* pubkey_hex
```
### Publish Event
**Function:** [`nostr_relay_pool_publish()`](nostr_core/core_relay_pool.c:866)
**Function:** [`nostr_relay_pool_publish_async()`](nostr_core/core_relay_pool.c:866)
```c
int nostr_relay_pool_publish(
int nostr_relay_pool_publish_async(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
@@ -471,8 +492,8 @@ int publish_text_note(nostr_relay_pool_t* pool, const char* content) {
printf("Publishing note: %s\n", content);
int success_count = nostr_relay_pool_publish(
pool, relay_urls, 3, event);
int success_count = nostr_relay_pool_publish_async(
pool, relay_urls, 3, event, my_callback, user_data);
cJSON_Delete(event);
@@ -751,5 +772,6 @@ int main() {
- **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:1192) or [`nostr_relay_pool_poll()`](nostr_core/core_relay_pool.c:1232) to receive events.
- **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.
+99 -70
View File
@@ -2,7 +2,7 @@
A C library for NOSTR protocol implementation. Work in progress.
[![Version](https://img.shields.io/badge/version-0.2.1-blue.svg)](VERSION)
[![Version](https://img.shields.io/badge/version-0.4.8-blue.svg)](VERSION)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](#license)
[![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg)](#building)
@@ -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
@@ -66,7 +66,7 @@ A C library for NOSTR protocol implementation. Work in progress.
- [ ] [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
@@ -96,7 +96,7 @@ A C library for NOSTR protocol implementation. Work in progress.
**Legend:** ✅ Fully Implemented | ⚠️ Partial Implementation | ❌ Not Implemented
**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
@@ -306,6 +292,7 @@ publish_result_t* synchronous_publish_event_with_progress(const char** relay_url
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 (with auto-reconnection)
@@ -314,7 +301,12 @@ nostr_pool_subscription_t* nostr_relay_pool_subscribe(
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);
// Run event loop (handles reconnection automatically)
// 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);
@@ -364,11 +356,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/
```
@@ -377,28 +372,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
# Interactive relay pool testing
cd tests && ./pool_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`
- **Interactive Pool Testing**: `pool_test` (menu-driven interface with reconnection testing)
**Current test binaries live in [`tests/`](tests/) and are generated from `*_test.c` sources.**
## 🏗️ Integration
@@ -408,7 +394,7 @@ cd tests && ./pool_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/
```
@@ -443,7 +429,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:**
@@ -471,8 +457,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
@@ -481,6 +467,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)
@@ -490,24 +510,24 @@ make arm64
## 📄 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.4.8**
The library uses automatic semantic versioning based on Git tags. Each build increments the patch version automatically.
@@ -520,10 +540,11 @@ The library uses automatic semantic versioning based on Git tags. Each build inc
- **Comprehensive Testing**: Extensive test suite and error handling
**Version Timeline:**
- `v0.2.x` - Current development releases with enhanced NIP support
- `v0.4.x` - Current 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
@@ -531,21 +552,29 @@ 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
+1 -1
View File
@@ -1 +1 @@
0.4.3
0.5.9
+77 -8
View File
@@ -58,6 +58,7 @@ FORCE_NIPS=""
VERBOSE=false
HELP=false
BUILD_TESTS=false
BUILD_EXAMPLES=false
NO_COLOR_FLAG=false
# Parse command line arguments
@@ -83,6 +84,10 @@ while [[ $# -gt 0 ]]; do
BUILD_TESTS=true
shift
;;
--examples|-e)
BUILD_EXAMPLES=true
shift
;;
--no-color)
NO_COLOR_FLAG=true
shift
@@ -119,6 +124,7 @@ if [ "$HELP" = true ]; then
echo " --nips=1,5,6,19 Force specific NIPs (comma-separated)"
echo " --nips=all Include all available NIPs"
echo " --tests, -t Build all test programs in tests/ directory"
echo " --examples, -e Build all example programs in examples/ directory"
echo " --verbose, -v Verbose output"
echo " --no-color Disable colored output"
echo " --help, -h Show this help"
@@ -134,9 +140,15 @@ if [ "$HELP" = true ]; then
echo " 006 - Key derivation from mnemonic"
echo " 011 - Relay information document"
echo " 013 - Proof of Work"
echo " 017 - Private Direct Messages"
echo " 019 - Bech32 encoding (nsec/npub)"
echo " 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 +179,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 +197,7 @@ print_info "Auto-detecting needed NIPs from your source code..."
NEEDED_NIPS=""
if [ -n "$FORCE_NIPS" ]; then
if [ "$FORCE_NIPS" = "all" ]; then
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
NEEDED_NIPS="001 004 005 006 011 013 017 019 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 +216,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 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 +232,10 @@ else
fi
fi
# If building tests, include all NIPs to ensure test compatibility
if [ "$BUILD_TESTS" = true ] && [ -z "$FORCE_NIPS" ]; then
NEEDED_NIPS="001 004 005 006 011 013 019 042 044"
print_info "Building tests - including all available NIPs for test compatibility"
# If building tests or examples, include all NIPs to ensure compatibility
if ([ "$BUILD_TESTS" = true ] || [ "$BUILD_EXAMPLES" = true ]) && [ -z "$FORCE_NIPS" ]; then
NEEDED_NIPS="001 004 005 006 011 013 017 019 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)
@@ -492,6 +504,7 @@ SOURCES="$SOURCES nostr_core/utils.c"
SOURCES="$SOURCES nostr_core/nostr_common.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"
@@ -508,15 +521,25 @@ for nip in $NEEDED_NIPS; do
006) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-006(Keys)" ;;
011) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-011(Relay-Info)" ;;
013) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-013(PoW)" ;;
017) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-017(DMs)" ;;
019) NIP_DESCRIPTIONS="$NIP_DESCRIPTIONS NIP-019(Bech32)" ;;
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
# Build flags
CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"
CFLAGS="$CFLAGS -DENABLE_FILE_LOGGING -DENABLE_WEBSOCKET_LOGGING -DENABLE_DEBUG_LOGGING"
@@ -667,7 +690,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 ""
+121
View File
@@ -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;
}
@@ -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)
-186
View File
@@ -1,186 +0,0 @@
# NOSTR Core Integration Example
This directory contains a complete example showing how to integrate the NOSTR Core library into your own projects.
## What This Example Demonstrates
- **Library Initialization**: Proper setup and cleanup of the NOSTR library
- **Identity Management**: Key generation, bech32 encoding, and format detection
- **Event Creation**: Creating and signing different types of NOSTR events
- **Input Handling**: Processing various input formats (mnemonic, hex, bech32)
- **Utility Functions**: Using helper functions for hex conversion and error handling
- **CMake Integration**: How to integrate the library in your CMake-based project
## Building and Running
### Method 1: Using CMake
```bash
# Create build directory
mkdir build && cd build
# Configure with CMake
cmake ..
# Build
make
# Run the example
./my_nostr_app
```
### Method 2: Manual Compilation
```bash
# Compile directly (assuming you're in the c_nostr root directory)
gcc -I. examples/integration_example/main.c nostr_core.c nostr_crypto.c cjson/cJSON.c -lm -o integration_example
# Run
./integration_example
```
## Expected Output
The example will demonstrate:
1. **Identity Management Demo**
- Generate a new keypair
- Display keys in hex and bech32 format
2. **Event Creation Demo**
- Create a text note event
- Create a profile event
- Display the JSON for both events
3. **Input Handling Demo**
- Process different input formats
- Show format detection and decoding
4. **Utility Functions Demo**
- Hex conversion round-trip
- Error message display
## Integration Patterns
### Pattern 1: CMake Find Package
If NOSTR Core is installed system-wide:
```cmake
find_package(nostr_core REQUIRED)
target_link_libraries(your_app nostr_core::static)
```
### Pattern 2: CMake Subdirectory
If NOSTR Core is a subdirectory of your project:
```cmake
add_subdirectory(nostr_core)
target_link_libraries(your_app nostr_core_static)
```
### Pattern 3: pkg-config
If using pkg-config:
```cmake
find_package(PkgConfig REQUIRED)
pkg_check_modules(NOSTR_CORE REQUIRED nostr_core)
target_include_directories(your_app PRIVATE ${NOSTR_CORE_INCLUDE_DIRS})
target_link_libraries(your_app ${NOSTR_CORE_LIBRARIES})
```
### Pattern 4: Direct Source Integration
Copy the essential files to your project:
```bash
cp nostr_core.{c,h} nostr_crypto.{c,h} your_project/src/
cp -r cjson/ your_project/src/
```
Then compile them with your project sources.
## Code Structure
### main.c Structure
The example is organized into clear demonstration functions:
- `demo_identity_management()` - Key generation and encoding
- `demo_event_creation()` - Creating different event types
- `demo_input_handling()` - Processing various input formats
- `demo_utilities()` - Using utility functions
Each function demonstrates specific aspects of the library while maintaining proper error handling and resource cleanup.
### Key Integration Points
1. **Initialization**
```c
int ret = nostr_init();
if (ret != NOSTR_SUCCESS) {
// Handle error
}
```
2. **Resource Cleanup**
```c
// Always clean up JSON objects
cJSON_Delete(event);
// Clean up library on exit
nostr_cleanup();
```
3. **Error Handling**
```c
if (ret != NOSTR_SUCCESS) {
printf("Error: %s\n", nostr_strerror(ret));
return ret;
}
```
## Customization
You can modify this example for your specific needs:
- Change the `app_config_t` structure to match your application's configuration
- Add additional event types or custom event creation logic
- Integrate with your existing error handling and logging systems
- Add networking functionality using the WebSocket layer
## Dependencies
This example requires:
- C99 compiler (gcc, clang)
- CMake 3.12+ (for CMake build)
- NOSTR Core library and its dependencies
## Testing
You can test different input formats by passing them as command line arguments:
```bash
# Test with mnemonic
./my_nostr_app "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
# Test with hex private key
./my_nostr_app "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
# Test with bech32 nsec
./my_nostr_app "nsec1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
## Next Steps
After studying this example, you can:
1. Integrate the patterns into your own application
2. Explore the WebSocket functionality for relay communication
3. Add support for additional NOSTR event types
4. Implement your own identity persistence layer
5. Add networking and relay management features
For more examples, see the other files in the `examples/` directory.
-271
View File
@@ -1,271 +0,0 @@
/*
* Example application demonstrating how to integrate nostr_core into other projects
* This shows a complete workflow from key generation to event publishing
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nostr_core.h"
// Example application configuration
typedef struct {
char* app_name;
char* version;
int debug_mode;
} app_config_t;
static app_config_t g_config = {
.app_name = "My NOSTR App",
.version = "1.0.0",
.debug_mode = 1
};
// Helper function to print hex data
static void print_hex(const char* label, const unsigned char* data, size_t len) {
if (g_config.debug_mode) {
printf("%s: ", label);
for (size_t i = 0; i < len; i++) {
printf("%02x", data[i]);
}
printf("\n");
}
}
// Helper function to print JSON nicely
static void print_event(const char* label, cJSON* event) {
if (!event) {
printf("%s: NULL\n", label);
return;
}
char* json_string = cJSON_Print(event);
if (json_string) {
printf("%s:\n%s\n", label, json_string);
free(json_string);
}
}
// Example: Generate and manage identity
static int demo_identity_management(void) {
printf("\n=== Identity Management Demo ===\n");
unsigned char private_key[32], public_key[32];
char nsec[100], npub[100];
// Generate a new keypair
printf("Generating new keypair...\n");
int ret = nostr_generate_keypair(private_key, public_key);
if (ret != NOSTR_SUCCESS) {
printf("Error generating keypair: %s\n", nostr_strerror(ret));
return ret;
}
print_hex("Private Key", private_key, 32);
print_hex("Public Key", public_key, 32);
// Convert to bech32 format
ret = nostr_key_to_bech32(private_key, "nsec", nsec);
if (ret != NOSTR_SUCCESS) {
printf("Error encoding nsec: %s\n", nostr_strerror(ret));
return ret;
}
ret = nostr_key_to_bech32(public_key, "npub", npub);
if (ret != NOSTR_SUCCESS) {
printf("Error encoding npub: %s\n", nostr_strerror(ret));
return ret;
}
printf("nsec: %s\n", nsec);
printf("npub: %s\n", npub);
return NOSTR_SUCCESS;
}
// Example: Create different types of events
static int demo_event_creation(const unsigned char* private_key) {
printf("\n=== Event Creation Demo ===\n");
// Create a text note
printf("Creating text note...\n");
cJSON* text_event = nostr_create_text_event("Hello from my NOSTR app!", private_key);
if (!text_event) {
printf("Error creating text event\n");
return NOSTR_ERROR_JSON_PARSE;
}
print_event("Text Event", text_event);
// Create a profile event
printf("\nCreating profile event...\n");
cJSON* profile_event = nostr_create_profile_event(
g_config.app_name,
"A sample application demonstrating NOSTR integration",
private_key
);
if (!profile_event) {
printf("Error creating profile event\n");
cJSON_Delete(text_event);
return NOSTR_ERROR_JSON_PARSE;
}
print_event("Profile Event", profile_event);
// Cleanup
cJSON_Delete(text_event);
cJSON_Delete(profile_event);
return NOSTR_SUCCESS;
}
// Example: Handle different input formats
static int demo_input_handling(const char* user_input) {
printf("\n=== Input Handling Demo ===\n");
printf("Processing input: %s\n", user_input);
// Detect input type
int input_type = nostr_detect_input_type(user_input);
switch (input_type) {
case NOSTR_INPUT_MNEMONIC:
printf("Detected: BIP39 Mnemonic\n");
{
unsigned char priv[32], pub[32];
int ret = nostr_derive_keys_from_mnemonic(user_input, 0, priv, pub);
if (ret == NOSTR_SUCCESS) {
print_hex("Derived Private Key", priv, 32);
print_hex("Derived Public Key", pub, 32);
}
}
break;
case NOSTR_INPUT_NSEC_HEX:
printf("Detected: Hex-encoded private key\n");
{
unsigned char decoded[32];
int ret = nostr_decode_nsec(user_input, decoded);
if (ret == NOSTR_SUCCESS) {
print_hex("Decoded Private Key", decoded, 32);
}
}
break;
case NOSTR_INPUT_NSEC_BECH32:
printf("Detected: Bech32-encoded private key (nsec)\n");
{
unsigned char decoded[32];
int ret = nostr_decode_nsec(user_input, decoded);
if (ret == NOSTR_SUCCESS) {
print_hex("Decoded Private Key", decoded, 32);
}
}
break;
default:
printf("Unknown input format\n");
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}
// Example: Demonstrate utility functions
static int demo_utilities(void) {
printf("\n=== Utility Functions Demo ===\n");
// Hex conversion
const char* test_hex = "deadbeef";
unsigned char bytes[4];
char hex_result[9];
printf("Testing hex conversion with: %s\n", test_hex);
int ret = nostr_hex_to_bytes(test_hex, bytes, 4);
if (ret != NOSTR_SUCCESS) {
printf("Error in hex_to_bytes: %s\n", nostr_strerror(ret));
return ret;
}
nostr_bytes_to_hex(bytes, 4, hex_result);
printf("Round-trip result: %s\n", hex_result);
// Error message testing
printf("\nTesting error messages:\n");
for (int i = 0; i >= -10; i--) {
const char* msg = nostr_strerror(i);
if (msg && strlen(msg) > 0) {
printf(" %d: %s\n", i, msg);
}
}
return NOSTR_SUCCESS;
}
int main(int argc, char* argv[]) {
printf("%s v%s\n", g_config.app_name, g_config.version);
printf("NOSTR Core Integration Example\n");
printf("=====================================\n");
// Initialize the NOSTR library
printf("Initializing NOSTR core library...\n");
int ret = nostr_init();
if (ret != NOSTR_SUCCESS) {
printf("Failed to initialize NOSTR library: %s\n", nostr_strerror(ret));
return 1;
}
// Run demonstrations
unsigned char demo_private_key[32];
// 1. Identity management
ret = demo_identity_management();
if (ret != NOSTR_SUCCESS) {
goto cleanup;
}
// Generate a key for other demos
nostr_generate_keypair(demo_private_key, NULL);
// 2. Event creation
ret = demo_event_creation(demo_private_key);
if (ret != NOSTR_SUCCESS) {
goto cleanup;
}
// 3. Input handling (use command line argument if provided)
const char* test_input = (argc > 1) ? argv[1] :
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
ret = demo_input_handling(test_input);
if (ret != NOSTR_SUCCESS && ret != NOSTR_ERROR_INVALID_INPUT) {
goto cleanup;
}
// 4. Utility functions
ret = demo_utilities();
if (ret != NOSTR_SUCCESS) {
goto cleanup;
}
printf("\n=====================================\n");
printf("All demonstrations completed successfully!\n");
printf("\nThis example shows how to:\n");
printf(" • Initialize the NOSTR library\n");
printf(" • Generate and manage keypairs\n");
printf(" • Create and sign different event types\n");
printf(" • Handle various input formats\n");
printf(" • Use utility functions\n");
printf(" • Clean up resources properly\n");
ret = NOSTR_SUCCESS;
cleanup:
// Clean up the NOSTR library
printf("\nCleaning up NOSTR library...\n");
nostr_cleanup();
if (ret == NOSTR_SUCCESS) {
printf("Example completed successfully.\n");
return 0;
} else {
printf("Example failed with error: %s\n", nostr_strerror(ret));
return 1;
}
}
+125
View File
@@ -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;
}
+207 -10
View File
@@ -8,6 +8,7 @@
*/
#define _POSIX_C_SOURCE 200809L
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -109,7 +110,7 @@ void* poll_thread_func(void* arg) {
// Print menu
void print_menu() {
printf("\n=== NOSTR Relay Pool Test Menu ===\n");
printf("1. Start Pool (wss://relay.laantungir.net)\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");
@@ -117,7 +118,8 @@ void print_menu() {
printf("6. Remove subscription\n");
printf("7. Show pool status\n");
printf("8. Test reconnection (simulate disconnect)\n");
printf("9. Exit\n");
printf("9. Publish Event\n");
printf("0. Exit\n");
printf("Choice: ");
}
@@ -405,11 +407,24 @@ void show_pool_status() {
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);
}
@@ -422,6 +437,148 @@ void show_pool_status() {
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);
@@ -481,22 +638,22 @@ int main() {
}
// Create pool with custom reconnection configuration for faster testing
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
config->ping_interval_seconds = 5; // Ping every 5 seconds for testing
pool = nostr_relay_pool_create(config);
nostr_pool_reconnect_config_t config = *nostr_pool_reconnect_config_default();
config.ping_interval_seconds = 5; // Ping every 5 seconds for testing
pool = nostr_relay_pool_create(&config);
if (!pool) {
printf("❌ Failed to create pool\n");
break;
}
if (nostr_relay_pool_add_relay(pool, "wss://relay.laantungir.net") != NOSTR_SUCCESS) {
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 wss://relay.laantungir.net\n");
printf("✅ Pool started with ws://localhost:7555\n");
now = time(NULL);
ctime_r(&now, timestamp);
@@ -544,13 +701,49 @@ int main() {
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\n\n", timestamp, url);
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\n");
printf("❌ Failed to add relay to pool\n");
}
}
free(url);
@@ -655,7 +848,11 @@ int main() {
break;
}
case '9': // Exit
case '9': // Publish Event
publish_event();
break;
case '0': // Exit
running = 0;
break;
+242
View File
@@ -0,0 +1,242 @@
/*
* NIP-17 Private Direct Messages - Command Line Application
*
* This example demonstrates how to send NIP-17 private direct messages
* using the Nostr Core Library.
*
* Usage:
* ./send_nip17_dm <recipient_pubkey> <message> [sender_nsec]
*
* Arguments:
* recipient_pubkey: The npub or hex public key of the recipient
* message: The message to send
* sender_nsec: (optional) The nsec private key to use for sending.
* If not provided, uses a default test key.
*
* Example:
* ./send_nip17_dm npub1example... "Hello from NIP-17!" nsec1test...
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "../nostr_core/nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// Default test private key (for demonstration - DO NOT USE IN PRODUCTION)
#define DEFAULT_SENDER_NSEC "nsec12kgt0dv2k2safv6s32w8f89z9uw27e68hjaa0d66c5xvk70ezpwqncd045"
// Default relay for sending DMs
#define DEFAULT_RELAY "wss://relay.laantungir.net"
// Progress callback for publishing
void publish_progress_callback(const char* relay_url, const char* status,
const char* message, int success_count,
int total_relays, int completed_relays, void* user_data) {
(void)user_data;
if (relay_url) {
printf("📡 [%s]: %s", relay_url, status);
if (message) {
printf(" - %s", message);
}
printf(" (%d/%d completed, %d successful)\n", completed_relays, total_relays, success_count);
} else {
printf("📡 PUBLISH COMPLETE: %d/%d successful\n", success_count, total_relays);
}
}
/**
* Convert npub to hex if needed
*/
int convert_pubkey_to_hex(const char* input_pubkey, char* output_hex) {
// Check if it's already hex (64 characters)
if (strlen(input_pubkey) == 64) {
// Assume it's already hex
strcpy(output_hex, input_pubkey);
return 0;
}
// Check if it's an npub (starts with "npub1")
if (strncmp(input_pubkey, "npub1", 5) == 0) {
// Convert npub to hex
unsigned char pubkey_bytes[32];
if (nostr_decode_npub(input_pubkey, pubkey_bytes) != 0) {
fprintf(stderr, "Error: Invalid npub format\n");
return -1;
}
nostr_bytes_to_hex(pubkey_bytes, 32, output_hex);
return 0;
}
fprintf(stderr, "Error: Public key must be 64-character hex or valid npub\n");
return -1;
}
/**
* Convert nsec to private key bytes if needed
*/
int convert_nsec_to_private_key(const char* input_nsec, unsigned char* private_key) {
// Check if it's already hex (64 characters)
if (strlen(input_nsec) == 64) {
// Convert hex to bytes
if (nostr_hex_to_bytes(input_nsec, private_key, 32) != 0) {
fprintf(stderr, "Error: Invalid hex private key\n");
return -1;
}
return 0;
}
// Check if it's an nsec (starts with "nsec1")
if (strncmp(input_nsec, "nsec1", 5) == 0) {
// Convert nsec directly to private key bytes
if (nostr_decode_nsec(input_nsec, private_key) != 0) {
fprintf(stderr, "Error: Invalid nsec format\n");
return -1;
}
return 0;
}
fprintf(stderr, "Error: Private key must be 64-character hex or valid nsec\n");
return -1;
}
/**
* Main function
*/
int main(int argc, char* argv[]) {
if (argc < 3 || argc > 4) {
fprintf(stderr, "Usage: %s <recipient_pubkey> <message> [sender_nsec]\n\n", argv[0]);
fprintf(stderr, "Arguments:\n");
fprintf(stderr, " recipient_pubkey: npub or hex public key of recipient\n");
fprintf(stderr, " message: The message to send\n");
fprintf(stderr, " sender_nsec: (optional) nsec private key. Uses test key if not provided.\n\n");
fprintf(stderr, "Example:\n");
fprintf(stderr, " %s npub1example... \"Hello!\" nsec1test...\n", argv[0]);
return 1;
}
const char* recipient_pubkey_input = argv[1];
const char* message = argv[2];
const char* sender_nsec_input = (argc >= 4) ? argv[3] : DEFAULT_SENDER_NSEC;
printf("🧪 NIP-17 Private Direct Message Sender\n");
printf("======================================\n\n");
// Initialize crypto
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize crypto\n");
return 1;
}
// Convert recipient pubkey
char recipient_pubkey_hex[65];
if (convert_pubkey_to_hex(recipient_pubkey_input, recipient_pubkey_hex) != 0) {
return 1;
}
// Convert sender private key
unsigned char sender_privkey[32];
if (convert_nsec_to_private_key(sender_nsec_input, sender_privkey) != 0) {
return 1;
}
// Derive sender public key for display
unsigned char sender_pubkey_bytes[32];
char sender_pubkey_hex[65];
if (nostr_ec_public_key_from_private_key(sender_privkey, sender_pubkey_bytes) != 0) {
fprintf(stderr, "Failed to derive sender public key\n");
return 1;
}
nostr_bytes_to_hex(sender_pubkey_bytes, 32, sender_pubkey_hex);
printf("📤 Sender: %s\n", sender_pubkey_hex);
printf("📥 Recipient: %s\n", recipient_pubkey_hex);
printf("💬 Message: %s\n", message);
printf("🌐 Relay: %s\n\n", DEFAULT_RELAY);
// Create DM event
printf("💬 Creating DM event...\n");
const char* recipient_pubkeys[] = {recipient_pubkey_hex};
cJSON* dm_event = nostr_nip17_create_chat_event(
message,
recipient_pubkeys,
1,
"NIP-17 CLI", // subject
NULL, // no reply
DEFAULT_RELAY, // relay hint
sender_pubkey_hex
);
if (!dm_event) {
fprintf(stderr, "Failed to create DM event\n");
return 1;
}
printf("✅ Created DM event (kind 14)\n");
// Send DM (create gift wraps)
printf("🎁 Creating gift wraps...\n");
cJSON* gift_wraps[10]; // Max 10 gift wraps
int gift_wrap_count = nostr_nip17_send_dm(
dm_event,
recipient_pubkeys,
1,
sender_privkey,
gift_wraps,
10
);
cJSON_Delete(dm_event); // Original DM event no longer needed
if (gift_wrap_count <= 0) {
fprintf(stderr, "Failed to create gift wraps\n");
return 1;
}
printf("✅ Created %d gift wrap(s)\n", gift_wrap_count);
// Publish the gift wrap to relay
printf("\n📤 Publishing gift wrap to relay...\n");
const char* relay_urls[] = {DEFAULT_RELAY};
int success_count = 0;
publish_result_t* publish_results = synchronous_publish_event_with_progress(
relay_urls,
1, // single relay
gift_wraps[0], // Send the first gift wrap
&success_count,
10, // 10 second timeout
publish_progress_callback,
NULL, // no user data
0, // NIP-42 disabled
NULL // no private key for auth
);
if (!publish_results || success_count != 1) {
fprintf(stderr, "\n❌ Failed to publish gift wrap (success_count: %d)\n", success_count);
// Clean up gift wraps
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
if (publish_results) free(publish_results);
return 1;
}
printf("\n✅ Successfully published NIP-17 DM!\n");
// Clean up
free(publish_results);
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
nostr_cleanup();
printf("\n🎉 DM sent successfully! The recipient can now decrypt it using their private key.\n");
return 0;
}
+2 -1
View File
@@ -139,7 +139,8 @@ print_info "Creating git tag: $NEW_VERSION"
git tag "$NEW_VERSION"
print_info "Pushing commits and tags..."
git push origin main
CURRENT_BRANCH=$(git branch --show-current)
git push origin "$CURRENT_BRANCH"
git push origin "$NEW_VERSION"
print_success "Version $NEW_VERSION successfully released!"
File diff suppressed because it is too large Load Diff
+272
View File
@@ -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 */
+610 -98
View File
@@ -19,6 +19,7 @@
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <stdarg.h>
// Our production-ready WebSocket implementation
#include "../nostr_websocket/nostr_websocket_tls.h"
@@ -26,8 +27,8 @@
// cJSON for JSON handling
#include "../cjson/cJSON.h"
// NIP-42 Authentication
#include "nip042.h"
// =============================================================================
// RELAY POOL MANAGEMENT
@@ -41,6 +42,37 @@
#define NOSTR_POOL_SUBSCRIPTION_ID_SIZE 32
#define NOSTR_POOL_PING_INTERVAL 59 // 59 seconds - keeps connections alive
#define NOSTR_POOL_MAX_PENDING_SUBSCRIPTIONS 8 // Max concurrent subscription timings per relay
#define NOSTR_POOL_MAX_PENDING_PUBLISHES 32 // Max concurrent publish operations
static int g_query_sync_debug_cached = -1;
static int query_sync_debug_enabled(void) {
if (g_query_sync_debug_cached >= 0) {
return g_query_sync_debug_cached;
}
const char* env = getenv("NOSTR_POOL_QUERY_SYNC_DEBUG");
if (!env || env[0] == '\0' || strcmp(env, "0") == 0 || strcasecmp(env, "false") == 0) {
g_query_sync_debug_cached = 0;
} else {
g_query_sync_debug_cached = 1;
}
return g_query_sync_debug_cached;
}
static void query_sync_debug_log(const char* format, ...) {
if (!query_sync_debug_enabled() || !format) {
return;
}
va_list args;
va_start(args, format);
fprintf(stderr, "[nostr_core][query_sync] ");
vfprintf(stderr, format, args);
fprintf(stderr, "\n");
va_end(args);
}
// High-resolution timing helper
static double get_current_time_ms(void) {
@@ -60,6 +92,17 @@ typedef struct subscription_timing {
int active;
} subscription_timing_t;
// Publish operation tracking for async callbacks
typedef struct publish_operation {
char event_id[65]; // Event ID being published
publish_response_callback_t callback; // User callback function
void* user_data; // User data for callback
time_t publish_time; // When publish was initiated
int pending_relay_count; // Number of relays still pending response
char** pending_relay_urls; // URLs of relays still pending
int total_relay_count; // Total number of relays for this publish
} publish_operation_t;
// Internal structures
typedef struct relay_connection {
char* url;
@@ -85,6 +128,20 @@ typedef struct relay_connection {
subscription_timing_t pending_subscriptions[NOSTR_POOL_MAX_PENDING_SUBSCRIPTIONS];
int pending_subscription_count;
// Error reporting for publish operations
char last_publish_error[512]; // Last error message from relay publish response
time_t last_publish_error_time; // When the error occurred
// Error reporting for connection operations
char last_connection_error[512]; // Last error message from relay connection
time_t last_connection_error_time; // When the connection error occurred
// 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
// Statistics
nostr_relay_stats_t stats;
} relay_connection_t;
@@ -139,8 +196,17 @@ struct nostr_relay_pool {
nostr_pool_subscription_t* subscriptions[NOSTR_POOL_MAX_SUBSCRIPTIONS];
int subscription_count;
// Active publish operations for async callbacks
publish_operation_t* publish_operations[NOSTR_POOL_MAX_PENDING_PUBLISHES];
int publish_operation_count;
// Pool-wide settings
int default_timeout_ms;
// NIP-42 Authentication configuration
int nip42_enabled;
unsigned char private_key[32];
int has_private_key;
};
// Helper function to generate unique subscription IDs
@@ -209,6 +275,122 @@ static double remove_subscription_timing(relay_connection_t* relay, const char*
return -1.0; // Not found
}
// Helper functions for managing publish operations
static int add_publish_operation(nostr_relay_pool_t* pool, const char* event_id,
const char** relay_urls, int relay_count,
publish_response_callback_t callback, void* user_data) {
if (!pool || !event_id || !relay_urls || relay_count <= 0) return -1;
// Check if we have space for another operation
if (pool->publish_operation_count >= NOSTR_POOL_MAX_PENDING_PUBLISHES) {
return -1; // No space available
}
// Create new publish operation
publish_operation_t* op = calloc(1, sizeof(publish_operation_t));
if (!op) return -1;
// Copy event ID
strncpy(op->event_id, event_id, sizeof(op->event_id) - 1);
op->event_id[sizeof(op->event_id) - 1] = '\0';
// Set callback and user data
op->callback = callback;
op->user_data = user_data;
op->publish_time = time(NULL);
op->total_relay_count = relay_count;
op->pending_relay_count = relay_count;
// Copy relay URLs
op->pending_relay_urls = malloc(relay_count * sizeof(char*));
if (!op->pending_relay_urls) {
free(op);
return -1;
}
for (int i = 0; i < relay_count; i++) {
op->pending_relay_urls[i] = strdup(relay_urls[i]);
if (!op->pending_relay_urls[i]) {
// Cleanup on failure
for (int j = 0; j < i; j++) {
free(op->pending_relay_urls[j]);
}
free(op->pending_relay_urls);
free(op);
return -1;
}
}
// Add to pool
pool->publish_operations[pool->publish_operation_count++] = op;
return 0;
}
static publish_operation_t* find_publish_operation(nostr_relay_pool_t* pool, const char* event_id) {
if (!pool || !event_id) return NULL;
for (int i = 0; i < pool->publish_operation_count; i++) {
if (pool->publish_operations[i] &&
strcmp(pool->publish_operations[i]->event_id, event_id) == 0) {
return pool->publish_operations[i];
}
}
return NULL;
}
static void remove_publish_operation(nostr_relay_pool_t* pool, const char* event_id) {
if (!pool || !event_id) return;
for (int i = 0; i < pool->publish_operation_count; i++) {
if (pool->publish_operations[i] &&
strcmp(pool->publish_operations[i]->event_id, event_id) == 0) {
publish_operation_t* op = pool->publish_operations[i];
// Free relay URLs (only non-NULL ones)
if (op->pending_relay_urls) {
for (int j = 0; j < op->total_relay_count; j++) {
if (op->pending_relay_urls[j]) {
free(op->pending_relay_urls[j]);
op->pending_relay_urls[j] = NULL;
}
}
free(op->pending_relay_urls);
op->pending_relay_urls = NULL;
}
free(op);
// Shift remaining operations
for (int j = i; j < pool->publish_operation_count - 1; j++) {
pool->publish_operations[j] = pool->publish_operations[j + 1];
}
pool->publish_operations[--pool->publish_operation_count] = NULL;
break;
}
}
}
static int remove_relay_from_publish_operation(publish_operation_t* op, const char* relay_url) {
if (!op || !relay_url) return -1;
for (int i = 0; i < op->pending_relay_count; i++) {
if (op->pending_relay_urls[i] && strcmp(op->pending_relay_urls[i], relay_url) == 0) {
// Free this relay URL
free(op->pending_relay_urls[i]);
op->pending_relay_urls[i] = NULL; // Mark as freed
// Shift remaining URLs
for (int j = i; j < op->pending_relay_count - 1; j++) {
op->pending_relay_urls[j] = op->pending_relay_urls[j + 1];
}
op->pending_relay_urls[op->pending_relay_count - 1] = NULL; // Clear the last slot
op->pending_relay_count--;
return op->pending_relay_count; // Return remaining count
}
}
return -1; // Relay not found
}
// Helper function to ensure relay connection
static int ensure_relay_connection(relay_connection_t* relay) {
if (!relay) {
@@ -236,6 +418,10 @@ static int ensure_relay_connection(relay_connection_t* relay) {
relay->status = NOSTR_POOL_RELAY_ERROR;
relay->reconnect_attempts++;
relay->stats.connection_failures++;
// Set connection error message
snprintf(relay->last_connection_error, sizeof(relay->last_connection_error),
"Failed to create WebSocket client for relay %s", relay->url);
relay->last_connection_error_time = time(NULL);
return -1;
}
@@ -257,6 +443,11 @@ static int ensure_relay_connection(relay_connection_t* relay) {
relay->reconnect_attempts++;
relay->stats.connection_failures++;
// Set connection error message
snprintf(relay->last_connection_error, sizeof(relay->last_connection_error),
"WebSocket connection failed for relay %s (state: %d)", relay->url, state);
relay->last_connection_error_time = time(NULL);
// Close the failed connection
nostr_ws_close(relay->ws_client);
relay->ws_client = NULL;
@@ -448,6 +639,43 @@ nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* confi
return pool;
}
// Compatibility wrapper for old API
nostr_relay_pool_t* nostr_relay_pool_create_compat(void) {
return nostr_relay_pool_create(NULL);
}
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool,
const unsigned char* private_key,
int enable) {
if (!pool) {
return NOSTR_ERROR_INVALID_INPUT;
}
pool->nip42_enabled = enable ? 1 : 0;
if (private_key) {
memcpy(pool->private_key, private_key, sizeof(pool->private_key));
pool->has_private_key = 1;
} else {
memset(pool->private_key, 0, sizeof(pool->private_key));
pool->has_private_key = 0;
}
// Propagate to existing relay connections
for (int i = 0; i < pool->relay_count; i++) {
if (pool->relays[i]) {
pool->relays[i]->nip42_enabled = pool->nip42_enabled;
if (!pool->nip42_enabled) {
pool->relays[i]->auth_state = NOSTR_AUTH_STATE_NONE;
memset(pool->relays[i]->auth_challenge, 0, sizeof(pool->relays[i]->auth_challenge));
pool->relays[i]->auth_challenge_time = 0;
}
}
}
return NOSTR_SUCCESS;
}
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url) {
if (!pool || !relay_url || pool->relay_count >= NOSTR_POOL_MAX_RELAYS) {
return NOSTR_ERROR_INVALID_INPUT;
@@ -486,6 +714,12 @@ int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url)
relay->ping_pending = 0;
relay->pending_ping_start_ms = 0.0;
// 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;
// Initialize statistics
memset(&relay->stats, 0, sizeof(relay->stats));
relay->stats.connection_uptime_start = time(NULL);
@@ -537,6 +771,20 @@ void nostr_relay_pool_destroy(nostr_relay_pool_t* pool) {
}
}
// Clean up all pending publish operations
for (int i = 0; i < pool->publish_operation_count; i++) {
if (pool->publish_operations[i]) {
publish_operation_t* op = pool->publish_operations[i];
// Free relay URLs
for (int j = 0; j < op->total_relay_count; j++) {
free(op->pending_relay_urls[j]);
}
free(op->pending_relay_urls);
free(op);
}
}
// Close all relay connections
for (int i = 0; i < pool->relay_count; i++) {
if (pool->relays[i]) {
@@ -548,6 +796,11 @@ void nostr_relay_pool_destroy(nostr_relay_pool_t* pool) {
}
}
if (pool->has_private_key) {
memset(pool->private_key, 0, sizeof(pool->private_key));
pool->has_private_key = 0;
}
free(pool);
}
@@ -754,7 +1007,7 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
}
relay->stats.last_event_time = time(NULL);
if (strcmp(msg_type, "EVENT") == 0) {
// Handle EVENT message: ["EVENT", subscription_id, event]
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
@@ -880,13 +1133,122 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
} else if (strcmp(msg_type, "OK") == 0) {
// Handle OK response: ["OK", event_id, true/false, message]
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* event_id_json = cJSON_GetArrayItem(parsed, 1);
cJSON* success_flag = cJSON_GetArrayItem(parsed, 2);
if (cJSON_IsBool(success_flag)) {
if (cJSON_IsTrue(success_flag)) {
if (cJSON_IsString(event_id_json) && cJSON_IsBool(success_flag)) {
const char* event_id = cJSON_GetStringValue(event_id_json);
int success = cJSON_IsTrue(success_flag);
const char* error_message = NULL;
// Extract error message if available
if (!success && cJSON_GetArraySize(parsed) >= 4) {
cJSON* error_msg = cJSON_GetArrayItem(parsed, 3);
if (cJSON_IsString(error_msg)) {
error_message = cJSON_GetStringValue(error_msg);
}
}
// Update relay statistics
if (success) {
relay->stats.events_published_ok++;
if (relay->auth_state == NOSTR_AUTH_STATE_AUTHENTICATING) {
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATED;
}
} else {
relay->stats.events_published_failed++;
if (error_message && strstr(error_message, "auth-required") != NULL) {
relay->auth_state = NOSTR_AUTH_STATE_REJECTED;
// If we already have a challenge, immediately attempt re-authentication
if (relay->nip42_enabled && pool->has_private_key && relay->auth_challenge[0] != '\0') {
cJSON* auth_event = nostr_nip42_create_auth_event(
relay->auth_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);
}
}
}
// Store error message for legacy API
if (error_message) {
strncpy(relay->last_publish_error, error_message,
sizeof(relay->last_publish_error) - 1);
relay->last_publish_error[sizeof(relay->last_publish_error) - 1] = '\0';
relay->last_publish_error_time = time(NULL);
}
}
// Check for async publish operation callback
publish_operation_t* op = find_publish_operation(pool, event_id);
if (op && op->callback) {
// Call the user's callback
op->callback(relay->url, event_id, success, error_message, op->user_data);
// Remove this relay from the pending list
int remaining = remove_relay_from_publish_operation(op, relay->url);
// If no more relays pending, remove the operation
if (remaining == 0) {
remove_publish_operation(pool, event_id);
}
}
}
}
} else if (strcmp(msg_type, "AUTH") == 0) {
// Handle AUTH challenge message: ["AUTH", <challenge-string>]
if (relay->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);
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;
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);
}
}
}
} else if (strcmp(msg_type, "NOTICE") == 0) {
// Handle NOTICE message: ["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);
if (notice && strstr(notice, "auth-required") != NULL) {
relay->auth_state = NOSTR_AUTH_STATE_REJECTED;
strncpy(relay->last_publish_error, notice, sizeof(relay->last_publish_error) - 1);
relay->last_publish_error[sizeof(relay->last_publish_error) - 1] = '\0';
relay->last_publish_error_time = time(NULL);
}
}
}
@@ -916,6 +1278,11 @@ cJSON** nostr_relay_pool_query_sync(
cJSON** events = NULL;
*event_count = 0;
int events_capacity = 0;
// Query-local event deduplication (do not reuse pool-global seen cache)
char** query_seen_ids = NULL;
int query_seen_count = 0;
int query_seen_capacity = 0;
// Generate unique subscription ID
char subscription_id[NOSTR_POOL_SUBSCRIPTION_ID_SIZE];
@@ -941,7 +1308,9 @@ cJSON** nostr_relay_pool_query_sync(
add_subscription_timing(relay, subscription_id);
// Send REQ message
if (nostr_relay_send_req(relay->ws_client, subscription_id, filter) < 0) {
int send_rc = nostr_relay_send_req(relay->ws_client, subscription_id, filter);
query_sync_debug_log("send_req relay=%s sub_id=%s rc=%d", relay->url, subscription_id, send_rc);
if (send_rc < 0) {
// Remove timing if send failed
remove_subscription_timing(relay, subscription_id);
connected_count--; // Don't count failed connections
@@ -950,19 +1319,25 @@ cJSON** nostr_relay_pool_query_sync(
}
if (connected_count == 0) {
free(query_seen_ids);
return NULL;
}
// Wait for responses
time_t start_time = time(NULL);
int eose_count = 0;
while (time(NULL) - start_time < (timeout_ms / 1000) && eose_count < connected_count) {
int timeout_seconds = (timeout_ms + 999) / 1000;
if (timeout_seconds <= 0) timeout_seconds = 1;
query_sync_debug_log("query_loop_start sub_id=%s timeout_ms=%d timeout_seconds=%d connected_relays=%d", subscription_id, timeout_ms, timeout_seconds, connected_count);
while (time(NULL) - start_time < timeout_seconds && eose_count < connected_count) {
for (int i = 0; i < connected_count; i++) {
relay_connection_t* relay = connected_relays[i];
char buffer[8192];
char buffer[65536];
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 100);
query_sync_debug_log("receive relay=%s len=%d", relay->url, len);
if (len > 0) {
buffer[len] = '\0';
@@ -971,60 +1346,141 @@ cJSON** nostr_relay_pool_query_sync(
// Handle pong response for connection health monitoring
handle_pong_response(relay);
} else {
// Process as regular NOSTR message
// Parse once: query-sync consumes only its own sub-id messages;
// everything else is forwarded to normal pool dispatch.
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
int handled_for_query = 0;
const char* parsed_sub_id = NULL;
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* parsed_sub_id_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(parsed_sub_id_json)) {
parsed_sub_id = cJSON_GetStringValue(parsed_sub_id_json);
}
}
query_sync_debug_log("parsed relay=%s type=%s sub_id=%s", relay->url, msg_type ? msg_type : "(null)", parsed_sub_id ? parsed_sub_id : "(none)");
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), 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
if (!is_event_seen(pool, event_id)) {
mark_event_seen(pool, event_id);
relay->stats.events_received++;
// Add to results array
if (*event_count >= events_capacity) {
events_capacity = events_capacity == 0 ? 10 : events_capacity * 2;
events = realloc(events, events_capacity * sizeof(cJSON*));
if (!events) {
*event_count = 0;
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), subscription_id) == 0) {
handled_for_query = 1;
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);
int duplicate_in_query = 0;
for (int q = 0; q < query_seen_count; q++) {
if (query_seen_ids[q] && strcmp(query_seen_ids[q], event_id) == 0) {
duplicate_in_query = 1;
break;
}
}
events[(*event_count)++] = cJSON_Duplicate(event, 1);
if (!duplicate_in_query) {
if (query_seen_count >= query_seen_capacity) {
int new_capacity = (query_seen_capacity == 0) ? 16 : query_seen_capacity * 2;
char** grown_seen = realloc(query_seen_ids, (size_t)new_capacity * sizeof(char*));
if (!grown_seen) {
*event_count = 0;
break;
}
query_seen_ids = grown_seen;
query_seen_capacity = new_capacity;
}
query_seen_ids[query_seen_count] = strdup(event_id);
if (!query_seen_ids[query_seen_count]) {
*event_count = 0;
break;
}
query_seen_count++;
relay->stats.events_received++;
if (*event_count >= events_capacity) {
events_capacity = events_capacity == 0 ? 10 : events_capacity * 2;
events = realloc(events, events_capacity * sizeof(cJSON*));
if (!events) {
*event_count = 0;
break;
}
}
events[(*event_count)++] = cJSON_Duplicate(event, 1);
}
}
}
}
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(sub_id_json) &&
strcmp(cJSON_GetStringValue(sub_id_json), subscription_id) == 0) {
handled_for_query = 1;
eose_count++;
query_sync_debug_log("eose relay=%s sub_id=%s eose_count=%d/%d event_count=%d", relay->url, cJSON_GetStringValue(sub_id_json), eose_count, connected_count, *event_count);
}
}
} else if (msg_type && strcmp(msg_type, "AUTH") == 0) {
if (relay->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)) {
handled_for_query = 1;
const char* challenge = cJSON_GetStringValue(challenge_json);
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;
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);
}
}
}
}
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
// Handle EOSE message
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(sub_id_json) &&
strcmp(cJSON_GetStringValue(sub_id_json), subscription_id) == 0) {
eose_count++;
}
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
if (!handled_for_query) {
query_sync_debug_log("forward_to_pool relay=%s type=%s", relay->url, msg_type ? msg_type : "(null)");
process_relay_message(pool, relay, buffer);
}
}
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
}
}
}
}
}
int elapsed_sec = (int)(time(NULL) - start_time);
if (eose_count >= connected_count) {
query_sync_debug_log("query_loop_end reason=all_eose sub_id=%s elapsed_s=%d eose=%d/%d events=%d", subscription_id, elapsed_sec, eose_count, connected_count, *event_count);
} else {
query_sync_debug_log("query_loop_end reason=timeout sub_id=%s elapsed_s=%d timeout_s=%d eose=%d/%d events=%d", subscription_id, elapsed_sec, timeout_seconds, eose_count, connected_count, *event_count);
}
// Send CLOSE messages
for (int i = 0; i < connected_count; i++) {
@@ -1078,16 +1534,35 @@ cJSON* nostr_relay_pool_get_event(
return result;
}
int nostr_relay_pool_publish(
int nostr_relay_pool_publish_async(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* event) {
cJSON* event,
publish_response_callback_t callback,
void* user_data) {
if (!pool || !relay_urls || relay_count <= 0 || !event) {
return -1;
}
// Extract event ID for tracking
cJSON* event_id_json = cJSON_GetObjectItem(event, "id");
if (!event_id_json || !cJSON_IsString(event_id_json)) {
return -1; // Event must have an ID
}
const char* event_id = cJSON_GetStringValue(event_id_json);
// Add publish operation for tracking (only if callback provided)
publish_operation_t* op = NULL;
if (callback) {
if (add_publish_operation(pool, event_id, relay_urls, relay_count, callback, user_data) != 0) {
return -1; // Failed to add operation
}
op = find_publish_operation(pool, event_id);
}
int success_count = 0;
for (int i = 0; i < relay_count; i++) {
@@ -1099,56 +1574,41 @@ int nostr_relay_pool_publish(
}
}
if (relay && ensure_relay_connection(relay) == 0) {
double start_time_ms = get_current_time_ms();
// Send EVENT message
int relay_connected = 0;
if (relay && relay->ws_client) {
relay_connected = (nostr_ws_get_state(relay->ws_client) == NOSTR_WS_CONNECTED);
}
if (relay_connected) {
// Send EVENT message only on already-connected relays
if (nostr_relay_send_event(relay->ws_client, event) >= 0) {
relay->stats.events_published++;
// Wait for OK response
char buffer[1024];
time_t wait_start = time(NULL);
int got_response = 0;
while (time(NULL) - wait_start < 5 && !got_response) { // 5 second timeout
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 1000);
if (len > 0) {
buffer[len] = '\0';
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) {
// Handle OK response
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* success_flag = cJSON_GetArrayItem(parsed, 2);
if (cJSON_IsBool(success_flag) && cJSON_IsTrue(success_flag)) {
success_count++;
relay->stats.events_published_ok++;
// Update publish latency statistics
double latency_ms = get_current_time_ms() - start_time_ms;
if (relay->stats.publish_samples == 0) {
relay->stats.publish_latency_avg = latency_ms;
} else {
relay->stats.publish_latency_avg =
(relay->stats.publish_latency_avg * relay->stats.publish_samples + latency_ms) /
(relay->stats.publish_samples + 1);
}
relay->stats.publish_samples++;
} else {
relay->stats.events_published_failed++;
}
}
got_response = 1;
}
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
}
success_count++;
} else {
// If send failed and we have a callback, notify immediately
if (callback && op) {
callback(relay_urls[i], event_id, 0, "Failed to send event to relay", user_data);
// Remove this relay from the pending operation
int remaining = remove_relay_from_publish_operation(op, relay_urls[i]);
if (remaining == 0) {
remove_publish_operation(pool, event_id);
op = NULL; // Mark as removed to prevent double-free
}
}
}
} else {
// Relay not currently connected - publish is skipped; reconnection is handled by poll loop
if (callback && op) {
callback(relay_urls[i], event_id, 0, "Relay not connected; publish skipped", user_data);
// Remove this relay from the pending operation
int remaining = remove_relay_from_publish_operation(op, relay_urls[i]);
if (remaining == 0) {
remove_publish_operation(pool, event_id);
op = NULL; // Mark as removed to prevent double-free
}
}
}
}
@@ -1258,21 +1718,65 @@ double nostr_relay_pool_get_relay_ping_latency(
}
double nostr_relay_pool_get_relay_query_latency(
nostr_relay_pool_t* pool,
nostr_relay_pool_t* pool,
const char* relay_url) {
if (!pool || !relay_url) {
return -1.0;
}
relay_connection_t* relay = find_relay_by_url(pool, relay_url);
if (!relay) {
return -1.0;
}
return relay->stats.query_latency_avg;
}
const char* nostr_relay_pool_get_relay_last_publish_error(
nostr_relay_pool_t* pool,
const char* relay_url) {
if (!pool || !relay_url) {
return NULL;
}
relay_connection_t* relay = find_relay_by_url(pool, relay_url);
if (!relay) {
return NULL;
}
// Return error message only if it's recent (within last 60 seconds)
if (relay->last_publish_error_time > 0 &&
time(NULL) - relay->last_publish_error_time < 60) {
return relay->last_publish_error[0] != '\0' ? relay->last_publish_error : NULL;
}
return NULL;
}
const char* nostr_relay_pool_get_relay_last_connection_error(
nostr_relay_pool_t* pool,
const char* relay_url) {
if (!pool || !relay_url) {
return NULL;
}
relay_connection_t* relay = find_relay_by_url(pool, relay_url);
if (!relay) {
return NULL;
}
// Return error message only if it's recent (within last 60 seconds)
if (relay->last_connection_error_time > 0 &&
time(NULL) - relay->last_connection_error_time < 60) {
return relay->last_connection_error[0] != '\0' ? relay->last_connection_error : NULL;
}
return NULL;
}
int nostr_relay_pool_ping_relay(
nostr_relay_pool_t* pool,
const char* relay_url) {
@@ -1476,7 +1980,15 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
nostr_ws_state_t state = nostr_ws_get_state(relay->ws_client);
if (state != NOSTR_WS_CONNECTED) {
nostr_pool_relay_status_t prev_status = relay->status;
relay->status = (state == NOSTR_WS_ERROR) ? NOSTR_POOL_RELAY_ERROR : NOSTR_POOL_RELAY_DISCONNECTED;
// Capture transition cause for observability/debugging
if (prev_status == NOSTR_POOL_RELAY_CONNECTED || relay->last_connection_error_time == 0) {
snprintf(relay->last_connection_error, sizeof(relay->last_connection_error),
"Connection state transitioned to %d during poll", (int)state);
relay->last_connection_error_time = time(NULL);
}
continue;
}
@@ -1486,7 +1998,7 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
check_connection_health(relay);
// Process incoming messages
char buffer[8192];
char buffer[65536];
int timeout_per_relay = timeout_ms / pool->relay_count;
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, timeout_per_relay);
+71
View File
@@ -237,6 +237,77 @@ 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;
+1 -1
View File
@@ -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)
+28 -23
View File
@@ -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
+407
View File
@@ -0,0 +1,407 @@
/*
* NIP-17: Private Direct Messages Implementation
* https://github.com/nostr-protocol/nips/blob/master/17.md
*/
#define _GNU_SOURCE
#include "nip017.h"
#include "nip059.h"
#include "nip001.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
/**
* Create tags array for DM events
*/
static cJSON* create_dm_tags(const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url) {
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
// Add "p" tags for each recipient
for (int i = 0; i < num_recipients; i++) {
cJSON* p_tag = cJSON_CreateArray();
if (!p_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(p_tag, cJSON_CreateString("p"));
cJSON_AddItemToArray(p_tag, cJSON_CreateString(recipient_pubkeys[i]));
// Add relay URL if provided (recommended)
if (reply_relay_url) {
cJSON_AddItemToArray(p_tag, cJSON_CreateString(reply_relay_url));
}
cJSON_AddItemToArray(tags, p_tag);
}
// Add subject tag if provided
if (subject && strlen(subject) > 0) {
cJSON* subject_tag = cJSON_CreateArray();
if (!subject_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(subject_tag, cJSON_CreateString("subject"));
cJSON_AddItemToArray(subject_tag, cJSON_CreateString(subject));
cJSON_AddItemToArray(tags, subject_tag);
}
// Add reply reference if provided
if (reply_to_event_id && strlen(reply_to_event_id) > 0) {
cJSON* e_tag = cJSON_CreateArray();
if (!e_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_to_event_id));
if (reply_relay_url) {
cJSON_AddItemToArray(e_tag, cJSON_CreateString(reply_relay_url));
}
// For replies, add "reply" marker as per NIP-17
cJSON_AddItemToArray(e_tag, cJSON_CreateString("reply"));
cJSON_AddItemToArray(tags, e_tag);
}
return tags;
}
/**
* NIP-17: Create a chat message event (kind 14)
*/
cJSON* nostr_nip17_create_chat_event(const char* message,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex) {
if (!message || !recipient_pubkeys || num_recipients <= 0 || !sender_pubkey_hex) {
return NULL;
}
// Create tags
cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject,
reply_to_event_id, reply_relay_url);
if (!tags) {
return NULL;
}
// Create the chat rumor (kind 14, unsigned)
cJSON* chat_event = nostr_nip59_create_rumor(14, message, tags, sender_pubkey_hex, 0);
cJSON_Delete(tags); // Tags are duplicated in create_rumor
return chat_event;
}
/**
* NIP-17: Create a file message event (kind 15)
*/
cJSON* nostr_nip17_create_file_event(const char* file_url,
const char* file_type,
const char* encryption_algorithm,
const char* decryption_key,
const char* decryption_nonce,
const char* file_hash,
const char* original_file_hash,
size_t file_size,
const char* dimensions,
const char* blurhash,
const char* thumbnail_url,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex) {
if (!file_url || !file_type || !encryption_algorithm || !decryption_key ||
!decryption_nonce || !file_hash || !recipient_pubkeys ||
num_recipients <= 0 || !sender_pubkey_hex) {
return NULL;
}
// Create base tags
cJSON* tags = create_dm_tags(recipient_pubkeys, num_recipients, subject,
reply_to_event_id, reply_relay_url);
if (!tags) {
return NULL;
}
// Add file-specific tags
cJSON* file_type_tag = cJSON_CreateArray();
cJSON_AddItemToArray(file_type_tag, cJSON_CreateString("file-type"));
cJSON_AddItemToArray(file_type_tag, cJSON_CreateString(file_type));
cJSON_AddItemToArray(tags, file_type_tag);
cJSON* encryption_tag = cJSON_CreateArray();
cJSON_AddItemToArray(encryption_tag, cJSON_CreateString("encryption-algorithm"));
cJSON_AddItemToArray(encryption_tag, cJSON_CreateString(encryption_algorithm));
cJSON_AddItemToArray(tags, encryption_tag);
cJSON* key_tag = cJSON_CreateArray();
cJSON_AddItemToArray(key_tag, cJSON_CreateString("decryption-key"));
cJSON_AddItemToArray(key_tag, cJSON_CreateString(decryption_key));
cJSON_AddItemToArray(tags, key_tag);
cJSON* nonce_tag = cJSON_CreateArray();
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString("decryption-nonce"));
cJSON_AddItemToArray(nonce_tag, cJSON_CreateString(decryption_nonce));
cJSON_AddItemToArray(tags, nonce_tag);
cJSON* x_tag = cJSON_CreateArray();
cJSON_AddItemToArray(x_tag, cJSON_CreateString("x"));
cJSON_AddItemToArray(x_tag, cJSON_CreateString(file_hash));
cJSON_AddItemToArray(tags, x_tag);
// Optional tags
if (original_file_hash && strlen(original_file_hash) > 0) {
cJSON* ox_tag = cJSON_CreateArray();
cJSON_AddItemToArray(ox_tag, cJSON_CreateString("ox"));
cJSON_AddItemToArray(ox_tag, cJSON_CreateString(original_file_hash));
cJSON_AddItemToArray(tags, ox_tag);
}
if (file_size > 0) {
char size_str[32];
snprintf(size_str, sizeof(size_str), "%zu", file_size);
cJSON* size_tag = cJSON_CreateArray();
cJSON_AddItemToArray(size_tag, cJSON_CreateString("size"));
cJSON_AddItemToArray(size_tag, cJSON_CreateString(size_str));
cJSON_AddItemToArray(tags, size_tag);
}
if (dimensions && strlen(dimensions) > 0) {
cJSON* dim_tag = cJSON_CreateArray();
cJSON_AddItemToArray(dim_tag, cJSON_CreateString("dim"));
cJSON_AddItemToArray(dim_tag, cJSON_CreateString(dimensions));
cJSON_AddItemToArray(tags, dim_tag);
}
if (blurhash && strlen(blurhash) > 0) {
cJSON* blurhash_tag = cJSON_CreateArray();
cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString("blurhash"));
cJSON_AddItemToArray(blurhash_tag, cJSON_CreateString(blurhash));
cJSON_AddItemToArray(tags, blurhash_tag);
}
if (thumbnail_url && strlen(thumbnail_url) > 0) {
cJSON* thumb_tag = cJSON_CreateArray();
cJSON_AddItemToArray(thumb_tag, cJSON_CreateString("thumb"));
cJSON_AddItemToArray(thumb_tag, cJSON_CreateString(thumbnail_url));
cJSON_AddItemToArray(tags, thumb_tag);
}
// Create the file rumor (kind 15, unsigned)
cJSON* file_event = nostr_nip59_create_rumor(15, file_url, tags, sender_pubkey_hex, 0);
cJSON_Delete(tags); // Tags are duplicated in create_rumor
return file_event;
}
/**
* NIP-17: Create a relay list event (kind 10050)
*/
cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
int num_relays,
const unsigned char* private_key) {
if (!relay_urls || num_relays <= 0 || !private_key) {
return NULL;
}
// Get public key
unsigned char public_key[32];
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
return NULL;
}
char pubkey_hex[65];
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
// Create tags with relay URLs
cJSON* tags = cJSON_CreateArray();
if (!tags) {
return NULL;
}
for (int i = 0; i < num_relays; i++) {
cJSON* relay_tag = cJSON_CreateArray();
if (!relay_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(relay_tag, cJSON_CreateString("relay"));
cJSON_AddItemToArray(relay_tag, cJSON_CreateString(relay_urls[i]));
cJSON_AddItemToArray(tags, relay_tag);
}
// Create and sign the event
cJSON* relay_event = nostr_create_and_sign_event(10050, "", tags, private_key, time(NULL));
cJSON_Delete(tags); // Tags are duplicated in create_and_sign_event
return relay_event;
}
/**
* NIP-17: Send a direct message to recipients
*/
int nostr_nip17_send_dm(cJSON* dm_event,
const char** recipient_pubkeys,
int num_recipients,
const unsigned char* sender_private_key,
cJSON** gift_wraps_out,
int max_gift_wraps,
long max_delay_sec) {
if (!dm_event || !recipient_pubkeys || num_recipients <= 0 ||
!sender_private_key || !gift_wraps_out || max_gift_wraps <= 0) {
return -1;
}
int created_wraps = 0;
for (int i = 0; i < num_recipients && created_wraps < max_gift_wraps; i++) {
// Convert recipient pubkey hex to bytes
unsigned char recipient_public_key[32];
if (nostr_hex_to_bytes(recipient_pubkeys[i], recipient_public_key, 32) != 0) {
continue; // Skip invalid pubkeys
}
// Create seal for this recipient
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key, max_delay_sec);
if (!seal) {
continue; // Skip if sealing fails
}
// Create gift wrap for this recipient
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i], max_delay_sec);
cJSON_Delete(seal); // Seal is now wrapped
if (!gift_wrap) {
continue; // Skip if wrapping fails
}
gift_wraps_out[created_wraps++] = gift_wrap;
}
// Also create a gift wrap for the sender (so they can see their own messages)
if (created_wraps < max_gift_wraps) {
// Get sender's public key
unsigned char sender_public_key[32];
if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) == 0) {
char sender_pubkey_hex[65];
nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex);
// Create seal for sender
cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key, max_delay_sec);
if (sender_seal) {
// Create gift wrap for sender
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) {
if (!gift_wrap || !recipient_private_key) {
return NULL;
}
// Unwrap the gift wrap to get the seal
cJSON* seal = nostr_nip59_unwrap_gift(gift_wrap, recipient_private_key);
if (!seal) {
return NULL;
}
// Get sender's public key from the seal
cJSON* seal_pubkey_item = cJSON_GetObjectItem(seal, "pubkey");
if (!seal_pubkey_item || !cJSON_IsString(seal_pubkey_item)) {
cJSON_Delete(seal);
return NULL;
}
const char* sender_pubkey_hex = cJSON_GetStringValue(seal_pubkey_item);
// Convert sender pubkey hex to bytes
unsigned char sender_public_key[32];
if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) {
cJSON_Delete(seal);
return NULL;
}
// Unseal the rumor
cJSON* rumor = nostr_nip59_unseal_rumor(seal, sender_public_key, recipient_private_key);
cJSON_Delete(seal); // Seal is no longer needed
return rumor;
}
/**
* NIP-17: Extract DM relay URLs from a user's kind 10050 event
*/
int nostr_nip17_extract_dm_relays(cJSON* relay_list_event,
char** relay_urls_out,
int max_relays) {
if (!relay_list_event || !relay_urls_out || max_relays <= 0) {
return -1;
}
// Check if this is a kind 10050 event
cJSON* kind_item = cJSON_GetObjectItem(relay_list_event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || cJSON_GetNumberValue(kind_item) != 10050) {
return -1;
}
// Get tags array
cJSON* tags_item = cJSON_GetObjectItem(relay_list_event, "tags");
if (!tags_item || !cJSON_IsArray(tags_item)) {
return -1;
}
int extracted = 0;
cJSON* tag_item;
cJSON_ArrayForEach(tag_item, tags_item) {
if (!cJSON_IsArray(tag_item) || cJSON_GetArraySize(tag_item) < 2) {
continue;
}
// Check if this is a "relay" tag
cJSON* tag_name = cJSON_GetArrayItem(tag_item, 0);
cJSON* relay_url = cJSON_GetArrayItem(tag_item, 1);
if (cJSON_IsString(tag_name) && cJSON_IsString(relay_url) &&
strcmp(cJSON_GetStringValue(tag_name), "relay") == 0) {
if (extracted < max_relays) {
relay_urls_out[extracted] = strdup(cJSON_GetStringValue(relay_url));
if (relay_urls_out[extracted]) {
extracted++;
}
}
}
}
return extracted;
}
+139
View File
@@ -0,0 +1,139 @@
/*
* NIP-17: Private Direct Messages
* https://github.com/nostr-protocol/nips/blob/master/17.md
*/
#ifndef NOSTR_NIP017_H
#define NOSTR_NIP017_H
#include <stddef.h>
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* NIP-17: Create a chat message event (kind 14)
*
* @param message Plain text message content
* @param recipient_pubkeys Array of recipient public keys (hex strings)
* @param num_recipients Number of recipients
* @param subject Optional conversation subject/title (can be NULL)
* @param reply_to_event_id Optional event ID this message replies to (can be NULL)
* @param reply_relay_url Optional relay URL for reply reference (can be NULL)
* @param sender_pubkey_hex Sender's public key in hex format
* @return cJSON object representing the unsigned chat event, or NULL on error
*/
cJSON* nostr_nip17_create_chat_event(const char* message,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex);
/**
* NIP-17: Create a file message event (kind 15)
*
* @param file_url URL of the encrypted file
* @param file_type MIME type of the original file (e.g., "image/jpeg")
* @param encryption_algorithm Encryption algorithm used ("aes-gcm")
* @param decryption_key Base64-encoded decryption key
* @param decryption_nonce Base64-encoded decryption nonce
* @param file_hash SHA-256 hash of the encrypted file (hex)
* @param original_file_hash SHA-256 hash of the original file before encryption (hex, optional)
* @param file_size Size of encrypted file in bytes (optional, 0 to skip)
* @param dimensions Image dimensions in "WxH" format (optional, NULL to skip)
* @param blurhash Blurhash for preview (optional, NULL to skip)
* @param thumbnail_url URL of encrypted thumbnail (optional, NULL to skip)
* @param recipient_pubkeys Array of recipient public keys (hex strings)
* @param num_recipients Number of recipients
* @param subject Optional conversation subject/title (can be NULL)
* @param reply_to_event_id Optional event ID this message replies to (can be NULL)
* @param reply_relay_url Optional relay URL for reply reference (can be NULL)
* @param sender_pubkey_hex Sender's public key in hex format
* @return cJSON object representing the unsigned file event, or NULL on error
*/
cJSON* nostr_nip17_create_file_event(const char* file_url,
const char* file_type,
const char* encryption_algorithm,
const char* decryption_key,
const char* decryption_nonce,
const char* file_hash,
const char* original_file_hash,
size_t file_size,
const char* dimensions,
const char* blurhash,
const char* thumbnail_url,
const char** recipient_pubkeys,
int num_recipients,
const char* subject,
const char* reply_to_event_id,
const char* reply_relay_url,
const char* sender_pubkey_hex);
/**
* NIP-17: Create a relay list event (kind 10050)
*
* @param relay_urls Array of relay URLs for DM delivery
* @param num_relays Number of relay URLs
* @param private_key Sender's private key for signing
* @return cJSON object representing the signed relay list event, or NULL on error
*/
cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
int num_relays,
const unsigned char* private_key);
/**
* NIP-17: Send a direct message to recipients
*
* This function creates the appropriate rumor, seals it, gift wraps it,
* and returns the final gift wrap events ready for publishing.
*
* @param dm_event The unsigned DM event (kind 14 or 15)
* @param recipient_pubkeys Array of recipient public keys (hex strings)
* @param num_recipients Number of recipients
* @param sender_private_key 32-byte sender private key
* @param gift_wraps_out Array to store resulting gift wrap events (caller must free)
* @param max_gift_wraps Maximum number of gift wraps to create
* @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);
/**
* NIP-17: Receive and decrypt a direct message
*
* This function unwraps a gift wrap, unseals the rumor, and returns the original DM event.
*
* @param gift_wrap The received gift wrap event (kind 1059)
* @param recipient_private_key 32-byte recipient private key
* @return cJSON object representing the decrypted DM event, or NULL on error
*/
cJSON* nostr_nip17_receive_dm(cJSON* gift_wrap,
const unsigned char* recipient_private_key);
/**
* NIP-17: Extract DM relay URLs from a user's kind 10050 event
*
* @param relay_list_event The kind 10050 event
* @param relay_urls_out Array to store extracted relay URLs (caller must free)
* @param max_relays Maximum number of relays to extract
* @return Number of relay URLs extracted, or -1 on error
*/
int nostr_nip17_extract_dm_relays(cJSON* relay_list_event,
char** relay_urls_out,
int max_relays);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_NIP017_H
+855
View File
@@ -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);
}
+81
View File
@@ -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
+1 -1
View File
@@ -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
+987
View File
@@ -0,0 +1,987 @@
/*
* NIP-46: Nostr Remote Signing Implementation
* https://github.com/nostr-protocol/nips/blob/master/46.md
*/
#define _GNU_SOURCE
#include "nip046.h"
#include "nip044.h"
#include "nip004.h"
#include "utils.h"
#include "nostr_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
// Forward declarations for crypto/private APIs
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
static char* nip46_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 void safe_copy(char* dst, size_t dst_size, const char* src) {
if (!dst || dst_size == 0) return;
if (!src) {
dst[0] = '\0';
return;
}
size_t src_len = strlen(src);
size_t copy_len = (src_len < (dst_size - 1U)) ? src_len : (dst_size - 1U);
memcpy(dst, src, copy_len);
dst[copy_len] = '\0';
}
static int is_hex_64(const char* s) {
if (!s || strlen(s) != 64) return 0;
for (int i = 0; i < 64; i++) {
if (!isxdigit((unsigned char)s[i])) return 0;
}
return 1;
}
static int hex_val(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
static int url_decode(const char* in, char* out, size_t out_size) {
if (!in || !out || out_size == 0) return NOSTR_ERROR_INVALID_INPUT;
size_t oi = 0;
for (size_t i = 0; in[i] != '\0'; i++) {
if (oi + 1 >= out_size) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
if (in[i] == '%' && in[i + 1] && in[i + 2]) {
int hi = hex_val(in[i + 1]);
int lo = hex_val(in[i + 2]);
if (hi < 0 || lo < 0) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
out[oi++] = (char)((hi << 4) | lo);
i += 2;
} else if (in[i] == '+') {
out[oi++] = ' ';
} else {
out[oi++] = in[i];
}
}
out[oi] = '\0';
return NOSTR_SUCCESS;
}
static int is_unreserved(char c) {
return (isalnum((unsigned char)c) || c == '-' || c == '_' || c == '.' || c == '~');
}
static int url_encode(const char* in, char* out, size_t out_size) {
if (!in || !out || out_size == 0) return NOSTR_ERROR_INVALID_INPUT;
static const char* HEX = "0123456789ABCDEF";
size_t oi = 0;
for (size_t i = 0; in[i] != '\0'; i++) {
unsigned char c = (unsigned char)in[i];
if (is_unreserved((char)c)) {
if (oi + 1 >= out_size) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
out[oi++] = (char)c;
} else {
if (oi + 3 >= out_size) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
out[oi++] = '%';
out[oi++] = HEX[(c >> 4) & 0x0F];
out[oi++] = HEX[c & 0x0F];
}
}
out[oi] = '\0';
return NOSTR_SUCCESS;
}
const char* nostr_nip46_method_to_string(nostr_nip46_method_t method) {
switch (method) {
case NOSTR_NIP46_METHOD_CONNECT: return "connect";
case NOSTR_NIP46_METHOD_SIGN_EVENT: return "sign_event";
case NOSTR_NIP46_METHOD_PING: return "ping";
case NOSTR_NIP46_METHOD_GET_PUBLIC_KEY: return "get_public_key";
case NOSTR_NIP46_METHOD_NIP04_ENCRYPT: return "nip04_encrypt";
case NOSTR_NIP46_METHOD_NIP04_DECRYPT: return "nip04_decrypt";
case NOSTR_NIP46_METHOD_NIP44_ENCRYPT: return "nip44_encrypt";
case NOSTR_NIP46_METHOD_NIP44_DECRYPT: return "nip44_decrypt";
default: return "unknown";
}
}
nostr_nip46_method_t nostr_nip46_string_to_method(const char* method) {
if (!method) return NOSTR_NIP46_METHOD_UNKNOWN;
if (strcmp(method, "connect") == 0) return NOSTR_NIP46_METHOD_CONNECT;
if (strcmp(method, "sign_event") == 0) return NOSTR_NIP46_METHOD_SIGN_EVENT;
if (strcmp(method, "ping") == 0) return NOSTR_NIP46_METHOD_PING;
if (strcmp(method, "get_public_key") == 0) return NOSTR_NIP46_METHOD_GET_PUBLIC_KEY;
if (strcmp(method, "nip04_encrypt") == 0) return NOSTR_NIP46_METHOD_NIP04_ENCRYPT;
if (strcmp(method, "nip04_decrypt") == 0) return NOSTR_NIP46_METHOD_NIP04_DECRYPT;
if (strcmp(method, "nip44_encrypt") == 0) return NOSTR_NIP46_METHOD_NIP44_ENCRYPT;
if (strcmp(method, "nip44_decrypt") == 0) return NOSTR_NIP46_METHOD_NIP44_DECRYPT;
return NOSTR_NIP46_METHOD_UNKNOWN;
}
int nostr_nip46_generate_request_id(char* output, size_t output_size) {
if (!output || output_size < NOSTR_NIP46_MAX_REQUEST_ID_LEN) {
return NOSTR_ERROR_INVALID_INPUT;
}
unsigned char rnd[16];
if (nostr_secp256k1_get_random_bytes(rnd, sizeof(rnd)) != 1) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
nostr_bytes_to_hex(rnd, sizeof(rnd), output);
return NOSTR_SUCCESS;
}
int nostr_nip46_create_request(const char* id,
nostr_nip46_method_t method,
const char** params,
int param_count,
nostr_nip46_request_t* out) {
if (!id || !out || param_count < 0) return NOSTR_ERROR_INVALID_INPUT;
if (strlen(id) >= sizeof(out->id)) return NOSTR_ERROR_NIP46_INVALID_REQUEST;
memset(out, 0, sizeof(*out));
safe_copy(out->id, sizeof(out->id), id);
out->method = method;
safe_copy(out->method_str, sizeof(out->method_str), nostr_nip46_method_to_string(method));
out->param_count = param_count;
if (param_count == 0) {
out->params = NULL;
return NOSTR_SUCCESS;
}
out->params = (char**)calloc((size_t)param_count, sizeof(char*));
if (!out->params) return NOSTR_ERROR_MEMORY_FAILED;
for (int i = 0; i < param_count; i++) {
const char* p = (params && params[i]) ? params[i] : "";
out->params[i] = nip46_strdup(p);
if (!out->params[i]) {
for (int j = 0; j < i; j++) free(out->params[j]);
free(out->params);
out->params = NULL;
out->param_count = 0;
return NOSTR_ERROR_MEMORY_FAILED;
}
}
return NOSTR_SUCCESS;
}
int nostr_nip46_create_response(const char* id,
const char* result,
const char* error,
nostr_nip46_response_t* out) {
if (!id || !out) return NOSTR_ERROR_INVALID_INPUT;
if (strlen(id) >= sizeof(out->id)) return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
memset(out, 0, sizeof(*out));
safe_copy(out->id, sizeof(out->id), id);
if (result) {
out->result = nip46_strdup(result);
if (!out->result) return NOSTR_ERROR_MEMORY_FAILED;
}
if (error) {
out->error = nip46_strdup(error);
if (!out->error) {
free(out->result);
out->result = NULL;
return NOSTR_ERROR_MEMORY_FAILED;
}
}
return NOSTR_SUCCESS;
}
void nostr_nip46_free_request(nostr_nip46_request_t* request) {
if (!request) return;
if (request->params) {
for (int i = 0; i < request->param_count; i++) {
free(request->params[i]);
}
free(request->params);
}
memset(request, 0, sizeof(*request));
}
void nostr_nip46_free_response(nostr_nip46_response_t* response) {
if (!response) return;
free(response->result);
free(response->error);
memset(response, 0, sizeof(*response));
}
int nostr_nip46_request_to_json(const nostr_nip46_request_t* request, char** output_json) {
if (!request || !output_json) return NOSTR_ERROR_INVALID_INPUT;
cJSON* root = cJSON_CreateObject();
if (!root) return NOSTR_ERROR_MEMORY_FAILED;
cJSON_AddStringToObject(root, "id", request->id);
cJSON_AddStringToObject(root, "method", request->method_str);
cJSON* params = cJSON_CreateArray();
if (!params) {
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < request->param_count; i++) {
cJSON_AddItemToArray(params, cJSON_CreateString(request->params[i] ? request->params[i] : ""));
}
cJSON_AddItemToObject(root, "params", params);
char* js = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!js) return NOSTR_ERROR_MEMORY_FAILED;
*output_json = js;
return NOSTR_SUCCESS;
}
int nostr_nip46_response_to_json(const nostr_nip46_response_t* response, char** output_json) {
if (!response || !output_json) return NOSTR_ERROR_INVALID_INPUT;
cJSON* root = cJSON_CreateObject();
if (!root) return NOSTR_ERROR_MEMORY_FAILED;
cJSON_AddStringToObject(root, "id", response->id);
cJSON_AddStringToObject(root, "result", response->result ? response->result : "");
if (response->error) {
cJSON_AddStringToObject(root, "error", response->error);
}
char* js = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!js) return NOSTR_ERROR_MEMORY_FAILED;
*output_json = js;
return NOSTR_SUCCESS;
}
int nostr_nip46_parse_request(const char* json_payload, nostr_nip46_request_t* out) {
if (!json_payload || !out) return NOSTR_ERROR_INVALID_INPUT;
memset(out, 0, sizeof(*out));
cJSON* root = cJSON_Parse(json_payload);
if (!root) return NOSTR_ERROR_NIP46_INVALID_REQUEST;
cJSON* id = cJSON_GetObjectItem(root, "id");
cJSON* method = cJSON_GetObjectItem(root, "method");
cJSON* params = cJSON_GetObjectItem(root, "params");
if (!cJSON_IsString(id) || !cJSON_IsString(method) || !cJSON_IsArray(params)) {
cJSON_Delete(root);
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
}
safe_copy(out->id, sizeof(out->id), cJSON_GetStringValue(id));
safe_copy(out->method_str, sizeof(out->method_str), cJSON_GetStringValue(method));
out->method = nostr_nip46_string_to_method(out->method_str);
out->param_count = cJSON_GetArraySize(params);
if (out->param_count > 0) {
out->params = (char**)calloc((size_t)out->param_count, sizeof(char*));
if (!out->params) {
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < out->param_count; i++) {
cJSON* p = cJSON_GetArrayItem(params, i);
if (!cJSON_IsString(p)) {
nostr_nip46_free_request(out);
cJSON_Delete(root);
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
}
out->params[i] = nip46_strdup(cJSON_GetStringValue(p));
if (!out->params[i]) {
nostr_nip46_free_request(out);
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
}
cJSON_Delete(root);
return NOSTR_SUCCESS;
}
int nostr_nip46_parse_response(const char* json_payload, nostr_nip46_response_t* out) {
if (!json_payload || !out) return NOSTR_ERROR_INVALID_INPUT;
memset(out, 0, sizeof(*out));
cJSON* root = cJSON_Parse(json_payload);
if (!root) return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
cJSON* id = cJSON_GetObjectItem(root, "id");
cJSON* result = cJSON_GetObjectItem(root, "result");
cJSON* error = cJSON_GetObjectItem(root, "error");
if (!cJSON_IsString(id) || !cJSON_IsString(result)) {
cJSON_Delete(root);
return NOSTR_ERROR_NIP46_INVALID_RESPONSE;
}
safe_copy(out->id, sizeof(out->id), cJSON_GetStringValue(id));
out->result = nip46_strdup(cJSON_GetStringValue(result));
if (!out->result) {
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
if (error && cJSON_IsString(error)) {
out->error = nip46_strdup(cJSON_GetStringValue(error));
if (!out->error) {
nostr_nip46_free_response(out);
cJSON_Delete(root);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
cJSON_Delete(root);
return NOSTR_SUCCESS;
}
static cJSON* create_nip46_event(int kind,
const char* encrypted_content,
const unsigned char* sender_private_key,
const unsigned char* recipient_public_key,
time_t timestamp) {
if (!encrypted_content || !sender_private_key || !recipient_public_key) return NULL;
char recipient_hex[65];
nostr_bytes_to_hex(recipient_public_key, 32, recipient_hex);
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
cJSON* ptag = cJSON_CreateArray();
if (!ptag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(ptag, cJSON_CreateString("p"));
cJSON_AddItemToArray(ptag, cJSON_CreateString(recipient_hex));
cJSON_AddItemToArray(tags, ptag);
cJSON* evt = nostr_create_and_sign_event(kind, encrypted_content, tags, sender_private_key, timestamp);
cJSON_Delete(tags);
return evt;
}
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) {
if (!request || !sender_private_key || !recipient_public_key) return NULL;
char* json_payload = NULL;
if (nostr_nip46_request_to_json(request, &json_payload) != NOSTR_SUCCESS) return NULL;
char encrypted[NOSTR_NIP46_MAX_PAYLOAD_LEN];
int rc = nostr_nip44_encrypt(sender_private_key, recipient_public_key, json_payload,
encrypted, sizeof(encrypted));
free(json_payload);
if (rc != NOSTR_SUCCESS) return NULL;
return create_nip46_event(NOSTR_NIP46_EVENT_KIND, encrypted,
sender_private_key, recipient_public_key, 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) {
if (!response || !sender_private_key || !recipient_public_key) return NULL;
char* json_payload = NULL;
if (nostr_nip46_response_to_json(response, &json_payload) != NOSTR_SUCCESS) return NULL;
char encrypted[NOSTR_NIP46_MAX_PAYLOAD_LEN];
int rc = nostr_nip44_encrypt(sender_private_key, recipient_public_key, json_payload,
encrypted, sizeof(encrypted));
free(json_payload);
if (rc != NOSTR_SUCCESS) return NULL;
return create_nip46_event(NOSTR_NIP46_EVENT_KIND, encrypted,
sender_private_key, recipient_public_key, timestamp);
}
int nostr_nip46_decrypt_event(cJSON* event,
const unsigned char* recipient_private_key,
char* output,
size_t output_size) {
if (!event || !recipient_private_key || !output || output_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* content = cJSON_GetObjectItem(event, "content");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
cJSON* kind = cJSON_GetObjectItem(event, "kind");
if (!cJSON_IsString(content) || !cJSON_IsString(pubkey) || !cJSON_IsNumber(kind)) {
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
}
if ((int)cJSON_GetNumberValue(kind) != NOSTR_NIP46_EVENT_KIND) {
return NOSTR_ERROR_NIP46_INVALID_REQUEST;
}
const char* sender_hex = cJSON_GetStringValue(pubkey);
unsigned char sender_pubkey[32];
if (nostr_hex_to_bytes(sender_hex, sender_pubkey, 32) != 0) {
return NOSTR_ERROR_NIP46_DECRYPTION_FAILED;
}
int rc = nostr_nip44_decrypt(recipient_private_key, sender_pubkey,
cJSON_GetStringValue(content), output, output_size);
if (rc != NOSTR_SUCCESS) return NOSTR_ERROR_NIP46_DECRYPTION_FAILED;
return NOSTR_SUCCESS;
}
static int parse_query_pairs(const char* query,
int (*cb)(const char* key, const char* val, void* ctx),
void* ctx) {
if (!query || !cb) return NOSTR_ERROR_INVALID_INPUT;
char* work = nip46_strdup(query);
if (!work) return NOSTR_ERROR_MEMORY_FAILED;
char* saveptr = NULL;
char* token = strtok_r(work, "&", &saveptr);
while (token) {
char* eq = strchr(token, '=');
if (eq) {
*eq = '\0';
const char* key = token;
const char* val = eq + 1;
int rc = cb(key, val, ctx);
if (rc != NOSTR_SUCCESS) {
free(work);
return rc;
}
}
token = strtok_r(NULL, "&", &saveptr);
}
free(work);
return NOSTR_SUCCESS;
}
typedef struct {
nostr_nip46_bunker_url_t* out;
} bunker_parse_ctx_t;
static int bunker_pair_cb(const char* key, const char* val, void* ctx) {
bunker_parse_ctx_t* pctx = (bunker_parse_ctx_t*)ctx;
char decoded[512];
int rc = url_decode(val, decoded, sizeof(decoded));
if (rc != NOSTR_SUCCESS) return rc;
if (strcmp(key, "relay") == 0) {
if (pctx->out->relay_count >= NOSTR_NIP46_MAX_RELAYS) return NOSTR_SUCCESS;
safe_copy(pctx->out->relays[pctx->out->relay_count],
sizeof(pctx->out->relays[pctx->out->relay_count]), decoded);
pctx->out->relay_count++;
} else if (strcmp(key, "secret") == 0) {
safe_copy(pctx->out->secret, sizeof(pctx->out->secret), decoded);
}
return NOSTR_SUCCESS;
}
int nostr_nip46_parse_bunker_url(const char* url, nostr_nip46_bunker_url_t* out) {
if (!url || !out) return NOSTR_ERROR_INVALID_INPUT;
memset(out, 0, sizeof(*out));
const char* prefix = "bunker://";
size_t prefix_len = strlen(prefix);
if (strncmp(url, prefix, prefix_len) != 0) {
return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
}
const char* after = url + prefix_len;
const char* q = strchr(after, '?');
char pubkey[65];
if (!q) {
safe_copy(pubkey, sizeof(pubkey), after);
} else {
size_t l = (size_t)(q - after);
if (l >= sizeof(pubkey)) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
memcpy(pubkey, after, l);
pubkey[l] = '\0';
}
if (!is_hex_64(pubkey)) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
safe_copy(out->remote_signer_pubkey, sizeof(out->remote_signer_pubkey), pubkey);
if (q && *(q + 1)) {
bunker_parse_ctx_t ctx = { out };
int rc = parse_query_pairs(q + 1, bunker_pair_cb, &ctx);
if (rc != NOSTR_SUCCESS) return rc;
}
return NOSTR_SUCCESS;
}
typedef struct {
nostr_nip46_nostrconnect_url_t* out;
} nostrconnect_parse_ctx_t;
static int nostrconnect_pair_cb(const char* key, const char* val, void* ctx) {
nostrconnect_parse_ctx_t* pctx = (nostrconnect_parse_ctx_t*)ctx;
char decoded[1024];
int rc = url_decode(val, decoded, sizeof(decoded));
if (rc != NOSTR_SUCCESS) return rc;
if (strcmp(key, "relay") == 0) {
if (pctx->out->relay_count >= NOSTR_NIP46_MAX_RELAYS) return NOSTR_SUCCESS;
safe_copy(pctx->out->relays[pctx->out->relay_count],
sizeof(pctx->out->relays[pctx->out->relay_count]), decoded);
pctx->out->relay_count++;
} else if (strcmp(key, "secret") == 0) {
safe_copy(pctx->out->secret, sizeof(pctx->out->secret), decoded);
} else if (strcmp(key, "perms") == 0) {
safe_copy(pctx->out->perms, sizeof(pctx->out->perms), decoded);
} else if (strcmp(key, "name") == 0) {
safe_copy(pctx->out->name, sizeof(pctx->out->name), decoded);
} else if (strcmp(key, "url") == 0) {
safe_copy(pctx->out->url, sizeof(pctx->out->url), decoded);
} else if (strcmp(key, "image") == 0) {
safe_copy(pctx->out->image, sizeof(pctx->out->image), decoded);
}
return NOSTR_SUCCESS;
}
int nostr_nip46_parse_nostrconnect_url(const char* url, nostr_nip46_nostrconnect_url_t* out) {
if (!url || !out) return NOSTR_ERROR_INVALID_INPUT;
memset(out, 0, sizeof(*out));
const char* prefix = "nostrconnect://";
size_t prefix_len = strlen(prefix);
if (strncmp(url, prefix, prefix_len) != 0) {
return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
}
const char* after = url + prefix_len;
const char* q = strchr(after, '?');
char pubkey[65];
if (!q) {
safe_copy(pubkey, sizeof(pubkey), after);
} else {
size_t l = (size_t)(q - after);
if (l >= sizeof(pubkey)) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
memcpy(pubkey, after, l);
pubkey[l] = '\0';
}
if (!is_hex_64(pubkey)) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
safe_copy(out->client_pubkey, sizeof(out->client_pubkey), pubkey);
if (q && *(q + 1)) {
nostrconnect_parse_ctx_t ctx = { out };
int rc = parse_query_pairs(q + 1, nostrconnect_pair_cb, &ctx);
if (rc != NOSTR_SUCCESS) return rc;
}
if (out->relay_count <= 0 || out->secret[0] == '\0') {
return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
}
return NOSTR_SUCCESS;
}
int nostr_nip46_create_bunker_url(const nostr_nip46_bunker_url_t* in, char* output, size_t output_size) {
if (!in || !output || output_size == 0) return NOSTR_ERROR_INVALID_INPUT;
if (!is_hex_64(in->remote_signer_pubkey)) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
size_t used = (size_t)snprintf(output, output_size, "bunker://%s", in->remote_signer_pubkey);
if (used >= output_size) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
int first = 1;
for (int i = 0; i < in->relay_count; i++) {
char enc[768];
int rc = url_encode(in->relays[i], enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
int wrote = snprintf(output + used, output_size - used, "%crelay=%s", first ? '?' : '&', enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
used += (size_t)wrote;
first = 0;
}
if (in->secret[0]) {
char encs[512];
int rc = url_encode(in->secret, encs, sizeof(encs));
if (rc != NOSTR_SUCCESS) return rc;
int wrote = snprintf(output + used, output_size - used, "%csecret=%s", first ? '?' : '&', encs);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
}
return NOSTR_SUCCESS;
}
int nostr_nip46_create_nostrconnect_url(const nostr_nip46_nostrconnect_url_t* in,
char* output,
size_t output_size) {
if (!in || !output || output_size == 0) return NOSTR_ERROR_INVALID_INPUT;
if (!is_hex_64(in->client_pubkey)) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
if (in->relay_count <= 0 || in->secret[0] == '\0') return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
size_t used = (size_t)snprintf(output, output_size, "nostrconnect://%s", in->client_pubkey);
if (used >= output_size) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
int first = 1;
for (int i = 0; i < in->relay_count; i++) {
char enc[768];
int rc = url_encode(in->relays[i], enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
int wrote = snprintf(output + used, output_size - used, "%crelay=%s", first ? '?' : '&', enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
first = 0;
}
char enc_secret[512];
int rc = url_encode(in->secret, enc_secret, sizeof(enc_secret));
if (rc != NOSTR_SUCCESS) return rc;
int wrote = snprintf(output + used, output_size - used, "%csecret=%s", first ? '?' : '&', enc_secret);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
if (in->perms[0]) {
char enc[1024];
rc = url_encode(in->perms, enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
wrote = snprintf(output + used, output_size - used, "&perms=%s", enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
}
if (in->name[0]) {
char enc[512];
rc = url_encode(in->name, enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
wrote = snprintf(output + used, output_size - used, "&name=%s", enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
}
if (in->url[0]) {
char enc[768];
rc = url_encode(in->url, enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
wrote = snprintf(output + used, output_size - used, "&url=%s", enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
used += (size_t)wrote;
}
if (in->image[0]) {
char enc[768];
rc = url_encode(in->image, enc, sizeof(enc));
if (rc != NOSTR_SUCCESS) return rc;
wrote = snprintf(output + used, output_size - used, "&image=%s", enc);
if (wrote < 0 || (size_t)wrote >= output_size - used) return NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT;
}
return NOSTR_SUCCESS;
}
int nostr_nip46_client_session_init(nostr_nip46_client_session_t* session,
const unsigned char* client_private_key,
const char* bunker_url) {
if (!session || !client_private_key || !bunker_url) return NOSTR_ERROR_INVALID_INPUT;
memset(session, 0, sizeof(*session));
memcpy(session->client_private_key, client_private_key, 32);
unsigned char client_pub[32];
if (nostr_ec_public_key_from_private_key(client_private_key, client_pub) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
nostr_bytes_to_hex(client_pub, 32, session->client_pubkey_hex);
nostr_nip46_bunker_url_t parsed;
int rc = nostr_nip46_parse_bunker_url(bunker_url, &parsed);
if (rc != NOSTR_SUCCESS) return rc;
safe_copy(session->remote_signer_pubkey_hex, sizeof(session->remote_signer_pubkey_hex), parsed.remote_signer_pubkey);
if (nostr_hex_to_bytes(parsed.remote_signer_pubkey, session->remote_signer_pubkey, 32) != 0) {
return NOSTR_ERROR_NIP46_INVALID_BUNKER_URL;
}
session->relay_count = parsed.relay_count;
for (int i = 0; i < parsed.relay_count; i++) {
safe_copy(session->relays[i], sizeof(session->relays[i]), parsed.relays[i]);
}
session->connected = 0;
return NOSTR_SUCCESS;
}
void nostr_nip46_client_session_destroy(nostr_nip46_client_session_t* session) {
if (!session) return;
memset(session, 0, sizeof(*session));
}
static int client_make_request_event(nostr_nip46_client_session_t* session,
nostr_nip46_method_t method,
const char** params,
int param_count,
cJSON** request_event_out) {
if (!session || !request_event_out) return NOSTR_ERROR_INVALID_INPUT;
char req_id[NOSTR_NIP46_MAX_REQUEST_ID_LEN];
int rc = nostr_nip46_generate_request_id(req_id, sizeof(req_id));
if (rc != NOSTR_SUCCESS) return rc;
nostr_nip46_request_t req;
rc = nostr_nip46_create_request(req_id, method, params, param_count, &req);
if (rc != NOSTR_SUCCESS) return rc;
cJSON* evt = nostr_nip46_create_request_event(&req, session->client_private_key,
session->remote_signer_pubkey, 0);
nostr_nip46_free_request(&req);
if (!evt) return NOSTR_ERROR_NIP46_ENCRYPTION_FAILED;
*request_event_out = evt;
return NOSTR_SUCCESS;
}
int nostr_nip46_client_connect(nostr_nip46_client_session_t* session,
const char* optional_secret,
const char* optional_permissions,
cJSON** request_event_out) {
if (!session || !request_event_out) return NOSTR_ERROR_INVALID_INPUT;
const char* params[3];
int count = 1;
params[0] = session->remote_signer_pubkey_hex;
if (optional_secret && optional_secret[0]) {
params[count++] = optional_secret;
}
if (optional_permissions && optional_permissions[0]) {
params[count++] = optional_permissions;
}
return client_make_request_event(session, NOSTR_NIP46_METHOD_CONNECT, params, count, request_event_out);
}
int nostr_nip46_client_get_public_key(nostr_nip46_client_session_t* session,
cJSON** request_event_out) {
return client_make_request_event(session, NOSTR_NIP46_METHOD_GET_PUBLIC_KEY, NULL, 0, request_event_out);
}
int nostr_nip46_client_ping(nostr_nip46_client_session_t* session,
cJSON** request_event_out) {
return client_make_request_event(session, NOSTR_NIP46_METHOD_PING, NULL, 0, request_event_out);
}
int nostr_nip46_client_sign_event(nostr_nip46_client_session_t* session,
cJSON* unsigned_event,
cJSON** request_event_out) {
if (!session || !unsigned_event || !request_event_out) return NOSTR_ERROR_INVALID_INPUT;
char* ev = cJSON_PrintUnformatted(unsigned_event);
if (!ev) return NOSTR_ERROR_MEMORY_FAILED;
const char* params[1] = { ev };
int rc = client_make_request_event(session, NOSTR_NIP46_METHOD_SIGN_EVENT, params, 1, request_event_out);
free(ev);
return rc;
}
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) {
if (!session || !signer_private_key || !user_private_key) return NOSTR_ERROR_INVALID_INPUT;
memset(session, 0, sizeof(*session));
memcpy(session->signer_private_key, signer_private_key, 32);
memcpy(session->user_private_key, user_private_key, 32);
unsigned char signer_pub[32];
unsigned char user_pub[32];
if (nostr_ec_public_key_from_private_key(signer_private_key, signer_pub) != 0) return NOSTR_ERROR_CRYPTO_FAILED;
if (nostr_ec_public_key_from_private_key(user_private_key, user_pub) != 0) return NOSTR_ERROR_CRYPTO_FAILED;
nostr_bytes_to_hex(signer_pub, 32, session->signer_pubkey_hex);
nostr_bytes_to_hex(user_pub, 32, session->user_pubkey_hex);
if (relay_count > NOSTR_NIP46_MAX_RELAYS) relay_count = NOSTR_NIP46_MAX_RELAYS;
session->relay_count = relay_count;
for (int i = 0; i < relay_count; i++) {
safe_copy(session->relays[i], sizeof(session->relays[i]), relays[i]);
}
session->connected = 0;
return NOSTR_SUCCESS;
}
void nostr_nip46_signer_session_destroy(nostr_nip46_signer_session_t* session) {
if (!session) return;
memset(session, 0, sizeof(*session));
}
int nostr_nip46_signer_create_bunker_url(const nostr_nip46_signer_session_t* session,
const char* optional_secret,
char* output,
size_t output_size) {
if (!session || !output) return NOSTR_ERROR_INVALID_INPUT;
nostr_nip46_bunker_url_t b;
memset(&b, 0, sizeof(b));
safe_copy(b.remote_signer_pubkey, sizeof(b.remote_signer_pubkey), session->signer_pubkey_hex);
b.relay_count = session->relay_count;
for (int i = 0; i < session->relay_count; i++) {
safe_copy(b.relays[i], sizeof(b.relays[i]), session->relays[i]);
}
if (optional_secret) {
safe_copy(b.secret, sizeof(b.secret), optional_secret);
}
return nostr_nip46_create_bunker_url(&b, output, output_size);
}
static int signer_make_ok(const char* id, const char* result, nostr_nip46_response_t* out) {
return nostr_nip46_create_response(id, result, NULL, out);
}
static int signer_make_err(const char* id, const char* err, nostr_nip46_response_t* out) {
return nostr_nip46_create_response(id, "", err, out);
}
int nostr_nip46_signer_handle_request(nostr_nip46_signer_session_t* session,
const nostr_nip46_request_t* request,
nostr_nip46_response_t* response_out) {
if (!session || !request || !response_out) return NOSTR_ERROR_INVALID_INPUT;
switch (request->method) {
case NOSTR_NIP46_METHOD_CONNECT: {
if (request->param_count >= 1 && request->params[0] && request->params[0][0]) {
if (strcmp(request->params[0], session->signer_pubkey_hex) != 0) {
return signer_make_err(request->id, "remote signer pubkey mismatch", response_out);
}
}
session->connected = 1;
if (request->param_count >= 2 && request->params[1] && request->params[1][0]) {
return signer_make_ok(request->id, request->params[1], response_out);
}
return signer_make_ok(request->id, "ack", response_out);
}
case NOSTR_NIP46_METHOD_PING:
return signer_make_ok(request->id, "pong", response_out);
case NOSTR_NIP46_METHOD_GET_PUBLIC_KEY:
return signer_make_ok(request->id, session->user_pubkey_hex, response_out);
case NOSTR_NIP46_METHOD_SIGN_EVENT: {
if (request->param_count < 1 || !request->params[0]) {
return signer_make_err(request->id, "missing sign_event payload", response_out);
}
cJSON* in = cJSON_Parse(request->params[0]);
if (!in) return signer_make_err(request->id, "invalid sign_event JSON", response_out);
cJSON* kind = cJSON_GetObjectItem(in, "kind");
cJSON* content = cJSON_GetObjectItem(in, "content");
cJSON* tags = cJSON_GetObjectItem(in, "tags");
cJSON* created = cJSON_GetObjectItem(in, "created_at");
if (!cJSON_IsNumber(kind) || !cJSON_IsString(content)) {
cJSON_Delete(in);
return signer_make_err(request->id, "invalid sign_event fields", response_out);
}
time_t ts = 0;
if (created && cJSON_IsNumber(created)) {
ts = (time_t)cJSON_GetNumberValue(created);
}
cJSON* signed_evt = nostr_create_and_sign_event((int)cJSON_GetNumberValue(kind),
cJSON_GetStringValue(content),
cJSON_IsArray(tags) ? tags : NULL,
session->user_private_key,
ts);
cJSON_Delete(in);
if (!signed_evt) {
return signer_make_err(request->id, "failed to sign event", response_out);
}
char* signed_json = cJSON_PrintUnformatted(signed_evt);
cJSON_Delete(signed_evt);
if (!signed_json) {
return signer_make_err(request->id, "failed to encode signed event", response_out);
}
int rc = signer_make_ok(request->id, signed_json, response_out);
free(signed_json);
return rc;
}
case NOSTR_NIP46_METHOD_NIP04_ENCRYPT:
case NOSTR_NIP46_METHOD_NIP04_DECRYPT:
case NOSTR_NIP46_METHOD_NIP44_ENCRYPT:
case NOSTR_NIP46_METHOD_NIP44_DECRYPT: {
if (request->param_count < 2 || !request->params[0] || !request->params[1]) {
return signer_make_err(request->id, "missing crypto params", response_out);
}
unsigned char peer_pub[32];
if (nostr_hex_to_bytes(request->params[0], peer_pub, 32) != 0) {
return signer_make_err(request->id, "invalid peer pubkey", response_out);
}
char out_buf[NOSTR_NIP46_MAX_PAYLOAD_LEN];
int rc;
if (request->method == NOSTR_NIP46_METHOD_NIP04_ENCRYPT) {
rc = nostr_nip04_encrypt(session->user_private_key, peer_pub, request->params[1], out_buf, sizeof(out_buf));
} else if (request->method == NOSTR_NIP46_METHOD_NIP04_DECRYPT) {
rc = nostr_nip04_decrypt(session->user_private_key, peer_pub, request->params[1], out_buf, sizeof(out_buf));
} else if (request->method == NOSTR_NIP46_METHOD_NIP44_ENCRYPT) {
rc = nostr_nip44_encrypt(session->user_private_key, peer_pub, request->params[1], out_buf, sizeof(out_buf));
} else {
rc = nostr_nip44_decrypt(session->user_private_key, peer_pub, request->params[1], out_buf, sizeof(out_buf));
}
if (rc != NOSTR_SUCCESS) {
return signer_make_err(request->id, nostr_strerror(rc), response_out);
}
return signer_make_ok(request->id, out_buf, response_out);
}
default:
return signer_make_err(request->id, "unknown method", response_out);
}
}
+181
View File
@@ -0,0 +1,181 @@
/*
* 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 "../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_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_response_event(const nostr_nip46_response_t* response,
const unsigned char* sender_private_key,
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);
/* Client session */
int nostr_nip46_client_session_init(nostr_nip46_client_session_t* session,
const unsigned char* client_private_key,
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 */
+412
View File
@@ -0,0 +1,412 @@
/*
* 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;
}
/**
* 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) {
if (!rumor || !sender_private_key || !recipient_public_key) {
return NULL;
}
// Serialize the rumor to JSON
char* rumor_json = cJSON_PrintUnformatted(rumor);
if (!rumor_json) {
return NULL;
}
// Encrypt the rumor using NIP-44
char encrypted_content[4096]; // Should be large enough for most events
int encrypt_result = nostr_nip44_encrypt(sender_private_key, recipient_public_key,
rumor_json, encrypted_content, sizeof(encrypted_content));
free(rumor_json);
if (encrypt_result != NOSTR_SUCCESS) {
return NULL;
}
// Get sender's public key
unsigned char sender_public_key[32];
if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) != 0) {
return NULL;
}
char sender_pubkey_hex[65];
nostr_bytes_to_hex(sender_public_key, 32, sender_pubkey_hex);
// Create seal event (kind 13)
cJSON* seal = cJSON_CreateObject();
if (!seal) {
return NULL;
}
time_t seal_time = random_past_timestamp(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()); // Empty tags array
cJSON_AddStringToObject(seal, "content", encrypted_content);
// Calculate event ID
char event_id[65];
if (create_event_id(seal, event_id) != 0) {
cJSON_Delete(seal);
return NULL;
}
cJSON_AddStringToObject(seal, "id", event_id);
// Sign the seal
unsigned char event_hash[32];
if (nostr_hex_to_bytes(event_id, event_hash, 32) != 0) {
cJSON_Delete(seal);
return NULL;
}
unsigned char signature[64];
if (nostr_ec_sign(sender_private_key, event_hash, signature) != 0) {
cJSON_Delete(seal);
return NULL;
}
char sig_hex[129];
nostr_bytes_to_hex(signature, 64, sig_hex);
cJSON_AddStringToObject(seal, "sig", sig_hex);
return seal;
}
/**
* NIP-59: Create a gift wrap (kind 1059) wrapping a seal
*/
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_key_hex, 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
char encrypted_content[8192]; // Larger buffer for nested encryption
int encrypt_result = nostr_nip44_encrypt(random_private_key, recipient_public_key,
seal_json, encrypted_content, sizeof(encrypted_content));
free(seal_json);
if (encrypt_result != NOSTR_SUCCESS) {
memory_clear(random_private_key, 32);
return NULL;
}
// Create gift wrap event (kind 1059)
cJSON* gift_wrap = cJSON_CreateObject();
if (!gift_wrap) {
memory_clear(random_private_key, 32);
return NULL;
}
time_t wrap_time = random_past_timestamp(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);
// Calculate event ID
char event_id[65];
if (create_event_id(gift_wrap, event_id) != 0) {
memory_clear(random_private_key, 32);
cJSON_Delete(gift_wrap);
return NULL;
}
cJSON_AddStringToObject(gift_wrap, "id", event_id);
// Sign the gift wrap
unsigned char event_hash[32];
if (nostr_hex_to_bytes(event_id, event_hash, 32) != 0) {
memory_clear(random_private_key, 32);
cJSON_Delete(gift_wrap);
return NULL;
}
unsigned char signature[64];
if (nostr_ec_sign(random_private_key, event_hash, signature) != 0) {
memory_clear(random_private_key, 32);
cJSON_Delete(gift_wrap);
return NULL;
}
char sig_hex[129];
nostr_bytes_to_hex(signature, 64, sig_hex);
cJSON_AddStringToObject(gift_wrap, "sig", sig_hex);
// Clear the random private key from memory
memory_clear(random_private_key, 32);
return gift_wrap;
}
/**
* NIP-59: Unwrap a gift wrap to get the seal
*/
cJSON* nostr_nip59_unwrap_gift(cJSON* gift_wrap, const unsigned char* recipient_private_key) {
if (!gift_wrap || !recipient_private_key) {
return NULL;
}
// Get the encrypted content
cJSON* content_item = cJSON_GetObjectItem(gift_wrap, "content");
if (!content_item || !cJSON_IsString(content_item)) {
return NULL;
}
const char* encrypted_content = cJSON_GetStringValue(content_item);
// Get the sender's public key (gift wrap pubkey)
cJSON* pubkey_item = cJSON_GetObjectItem(gift_wrap, "pubkey");
if (!pubkey_item || !cJSON_IsString(pubkey_item)) {
return NULL;
}
const char* sender_pubkey_hex = cJSON_GetStringValue(pubkey_item);
// Convert sender pubkey hex to bytes
unsigned char sender_public_key[32];
if (nostr_hex_to_bytes(sender_pubkey_hex, sender_public_key, 32) != 0) {
return NULL;
}
// Decrypt the content using NIP-44
char decrypted_json[8192];
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
encrypted_content, decrypted_json, sizeof(decrypted_json));
if (decrypt_result != NOSTR_SUCCESS) {
return NULL;
}
// Parse the decrypted JSON as the seal event
cJSON* seal = cJSON_Parse(decrypted_json);
if (!seal) {
return NULL;
}
return seal;
}
/**
* NIP-59: Unseal a seal to get the rumor
*/
cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key,
const unsigned char* recipient_private_key) {
if (!seal || !sender_public_key || !recipient_private_key) {
return NULL;
}
// Get the encrypted content
cJSON* content_item = cJSON_GetObjectItem(seal, "content");
if (!content_item || !cJSON_IsString(content_item)) {
return NULL;
}
const char* encrypted_content = cJSON_GetStringValue(content_item);
// Decrypt the content using NIP-44
char decrypted_json[4096];
int decrypt_result = nostr_nip44_decrypt(recipient_private_key, sender_public_key,
encrypted_content, decrypted_json, sizeof(decrypted_json));
if (decrypt_result != NOSTR_SUCCESS) {
return NULL;
}
// Parse the decrypted JSON as the rumor event
cJSON* rumor = cJSON_Parse(decrypted_json);
if (!rumor) {
return NULL;
}
return rumor;
}
+76
View File
@@ -0,0 +1,76 @@
/*
* NIP-59: Gift Wrap
* https://github.com/nostr-protocol/nips/blob/master/59.md
*/
#ifndef NOSTR_NIP059_H
#define NOSTR_NIP059_H
#include <stddef.h>
#include <time.h>
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* NIP-59: Create a rumor (unsigned event)
*
* @param kind Event kind
* @param content Event content
* @param tags Event tags (cJSON array, can be NULL)
* @param pubkey_hex Sender's public key in hex format
* @param created_at Event timestamp (0 for current time)
* @return cJSON object representing the rumor, or NULL on error
*/
cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
const char* pubkey_hex, time_t created_at);
/**
* NIP-59: Create a seal (kind 13) wrapping a rumor
*
* @param rumor The rumor event to seal (cJSON object)
* @param sender_private_key 32-byte sender private key
* @param recipient_public_key 32-byte recipient public key (x-only)
* @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);
/**
* 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);
/**
* NIP-59: Unseal a seal to get the rumor
*
* @param seal The seal event (cJSON object)
* @param sender_public_key 32-byte sender public key (x-only)
* @param recipient_private_key 32-byte recipient private key
* @return cJSON object representing the rumor event, or NULL on error
*/
cJSON* nostr_nip59_unseal_rumor(cJSON* seal, const unsigned char* sender_public_key,
const unsigned char* recipient_private_key);
#ifdef __cplusplus
}
#endif
#endif // NOSTR_NIP059_H
+791
View File
@@ -0,0 +1,791 @@
/*
* 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(const unsigned char* private_key,
const char* plaintext,
char* output,
size_t output_size) {
if (!private_key || !plaintext || !output || output_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
unsigned char pubkey[32];
if (nostr_ec_public_key_from_private_key(private_key, pubkey) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return nostr_nip44_encrypt(private_key, pubkey, plaintext, output, output_size);
}
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(const nostr_nip60_wallet_data_t* wallet_data,
const unsigned char* private_key,
time_t timestamp) {
if (!wallet_data || !private_key || 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(private_key, plain, encrypted, sizeof(encrypted));
free(plain);
if (rc != NOSTR_SUCCESS) return NULL;
return nostr_create_and_sign_event(NOSTR_NIP60_WALLET_KIND, encrypted, NULL, private_key, timestamp);
}
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(const nostr_nip60_token_data_t* token_data,
const unsigned char* private_key,
time_t timestamp) {
if (!token_data || !private_key || !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(private_key, plain, encrypted, sizeof(encrypted));
free(plain);
if (rc != NOSTR_SUCCESS) return NULL;
return nostr_create_and_sign_event(NOSTR_NIP60_TOKEN_KIND, encrypted, NULL, private_key, timestamp);
}
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(const char* token_event_id,
const unsigned char* private_key,
time_t timestamp) {
if (!token_event_id || !private_key) 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(5, "NIP-60 token spent", tags, private_key, timestamp);
cJSON_Delete(tags);
return evt;
}
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) {
if (!remaining_proofs || !private_key) 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(&token, private_key, timestamp);
}
cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data,
const unsigned char* private_key,
time_t timestamp) {
if (!history_data || !private_key) 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(private_key, 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(NOSTR_NIP60_HISTORY_KIND, encrypted, tags, private_key, timestamp);
cJSON_Delete(tags);
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(const char* quote_id,
const char* mint_url,
time_t expiration,
const unsigned char* private_key,
time_t timestamp) {
if (!quote_id || !mint_url || !private_key || expiration <= 0) {
return NULL;
}
char encrypted[4096];
int rc = nip60_encrypt_self(private_key, 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(NOSTR_NIP60_QUOTE_KIND, encrypted, tags, private_key, timestamp);
cJSON_Delete(tags);
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;
}
+146
View File
@@ -0,0 +1,146 @@
/*
* 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 "../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);
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);
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_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_history_event(const nostr_nip60_history_data_t* history_data,
const unsigned char* private_key,
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);
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 */
+540
View File
@@ -0,0 +1,540 @@
/*
* 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(const nostr_nip61_nutzap_info_t* info,
const unsigned char* private_key,
time_t timestamp) {
if (!info || !private_key || 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(NOSTR_NIP61_NUTZAP_INFO_KIND, "", tags, private_key, timestamp);
cJSON_Delete(tags);
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(const nostr_nip61_nutzap_data_t* nutzap_data,
const unsigned char* sender_private_key,
time_t timestamp) {
if (!nutzap_data || !sender_private_key || !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(NOSTR_NIP61_NUTZAP_KIND, content, tags, sender_private_key, timestamp);
cJSON_Delete(tags);
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(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) {
if (!nutzap_event_id || !sender_pubkey || !created_token_event_id || !private_key) {
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(&hist, private_key, 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;
}
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;
}
+89
View File
@@ -0,0 +1,89 @@
/*
* 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 "../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);
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);
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);
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 */
+32
View File
@@ -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";
}
}
+43 -3
View File
@@ -62,6 +62,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,11 +112,11 @@
#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;
+66 -5
View File
@@ -2,10 +2,10 @@
#define NOSTR_CORE_H
// Version information (auto-updated by increment_and_push.sh)
#define VERSION "v0.4.3"
#define VERSION "v0.5.9"
#define VERSION_MAJOR 0
#define VERSION_MINOR 4
#define VERSION_PATCH 3
#define VERSION_MINOR 5
#define VERSION_PATCH 9
/*
* NOSTR Core Library - Complete API Reference
@@ -49,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)
@@ -132,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);
*
* ============================================================================
*/
@@ -150,13 +182,23 @@ extern "C" {
#include "nip006.h" // Key derivation from mnemonic
#include "nip011.h" // Relay information document
#include "nip013.h" // Proof of Work
#include "nip017.h" // Private Direct Messages
#include "nip019.h" // Bech32 encoding (nsec/npub)
#include "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
// 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,
@@ -213,6 +255,7 @@ nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* confi
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);
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
// Subscription management
@@ -260,11 +303,23 @@ cJSON* nostr_relay_pool_get_event(
int relay_count,
cJSON* filter,
int timeout_ms);
int nostr_relay_pool_publish(
// 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);
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(
@@ -283,6 +338,12 @@ int nostr_relay_pool_reset_relay_stats(
double nostr_relay_pool_get_relay_query_latency(
nostr_relay_pool_t* pool,
const char* relay_url);
const char* nostr_relay_pool_get_relay_last_publish_error(
nostr_relay_pool_t* pool,
const char* relay_url);
const char* nostr_relay_pool_get_relay_last_connection_error(
nostr_relay_pool_t* pool,
const char* relay_url);
double nostr_relay_pool_get_relay_ping_latency(
nostr_relay_pool_t* pool,
const char* relay_url);
+54
View File
@@ -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);
}
+35
View File
@@ -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 */
+68 -79
View File
@@ -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>
@@ -24,14 +25,6 @@
#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
// Transport layer abstraction
typedef struct {
int (*connect)(void* ctx, const char* host, int port);
@@ -131,7 +124,7 @@ static void init_openssl(void) {
nostr_ws_client_t* nostr_ws_connect(const char* url) {
if (!url) return NULL;
// Initialize OpenSSL
init_openssl();
@@ -453,6 +446,13 @@ static int tcp_connect(void* ctx, const char* host, int port) {
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));
return 0;
}
@@ -580,23 +580,30 @@ static int tls_recv(void* ctx, void* data, size_t len, int timeout_ms) {
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
// Always use select() to ensure proper blocking behavior
// If SSL has pending data, use zero timeout to return immediately
// Otherwise use the full timeout to block until data arrives
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 has buffered data, use zero timeout; otherwise use full timeout
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
}
}
@@ -781,32 +788,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 +860,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
@@ -898,11 +913,6 @@ 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) {
@@ -915,6 +925,21 @@ 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;
while (remaining > 0) {
size_t chunk = remaining > sizeof(discard) ? sizeof(discard) : (size_t)remaining;
int got = client->transport->recv(&client->transport_ctx, discard, chunk, timeout_ms);
if (got <= 0) {
return -1;
}
remaining -= (uint64_t)got;
}
return -1;
}
// Read payload
if (len > 0) {
@@ -933,14 +958,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 +1001,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
+148
View File
@@ -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.
+209
View File
@@ -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.
+350
View File
@@ -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`
+778
View File
@@ -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...
```
+46 -1
View File
@@ -1,2 +1,47 @@
[Thu Oct 2 14:40:34 2025] 🚀 Pool test started
[Tue Oct 7 05:51:04 2025] 🚀 Pool test started
[Tue Oct 7 05:51:07 2025] 🏊 Pool started with default relay
[Tue Oct 7 05:52:03 2025] 🔍 New subscription created (ID: 1)
Filter: {
"limit": 10
}
[Tue Oct 7 05:52:03 2025] 📨 EVENT from ws://localhost:7555
├── ID: 8433206a6e00...
├── Pubkey: 17323141f3a9...
├── Kind: 1
├── Created: 1759687410
└── Content: Test post at 2025-10-05 14:03:30
[Tue Oct 7 05:52:03 2025] 📨 EVENT from ws://localhost:7555
├── ID: ec98292f5700...
├── Pubkey: aa3b44608a9e...
├── Kind: 1
├── Created: 1759687283
└── Content: Test post at 2025-10-05 14:01:23
[Tue Oct 7 05:52:03 2025] 📨 EVENT from ws://localhost:7555
├── ID: c70d6c5c8745...
├── Pubkey: 2a0c81450868...
├── Kind: 1
├── Created: 1759687249
└── Content: Test post at 2025-10-05 14:00:49
[Tue Oct 7 05:52:03 2025] 📨 EVENT from ws://localhost:7555
├── ID: 15dbe2cfe923...
├── Pubkey: 7c2065299249...
├── Kind: 1
├── Created: 1759687219
└── Content: Test post at 2025-10-05 14:00:19
[Tue Oct 7 05:52:03 2025] 📋 EOSE received - 0 events collected
[Tue Oct 7 05:52:31 2025] 🔍 New subscription created (ID: 2)
Filter: {
"since": 1759830747,
"limit": 10
}
[Tue Oct 7 05:52:31 2025] 📋 EOSE received - 0 events collected
+93
View File
@@ -0,0 +1,93 @@
#define _DEFAULT_SOURCE
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// Test callback function
static int callback_count = 0;
static int success_count = 0;
void test_callback(const char* relay_url, const char* event_id,
int success, const char* message, void* user_data) {
callback_count++;
if (success) {
success_count++;
}
printf("📡 Callback %d: Relay %s, Event %s, Success: %s\n",
callback_count, relay_url, event_id, success ? "YES" : "NO");
if (message) {
printf(" Message: %s\n", message);
}
// Mark test as complete when we get the expected number of callbacks
int* expected_callbacks = (int*)user_data;
if (callback_count >= *expected_callbacks) {
printf("✅ All callbacks received!\n");
}
}
int main() {
printf("🧪 Testing Async Publish Functionality\n");
printf("=====================================\n");
// Create pool
nostr_relay_pool_t* pool = nostr_relay_pool_create(NULL);
if (!pool) {
printf("❌ Failed to create pool\n");
return 1;
}
// Create a test event
cJSON* event = cJSON_CreateObject();
cJSON_AddStringToObject(event, "id", "test_event_12345");
cJSON_AddNumberToObject(event, "kind", 1);
cJSON_AddStringToObject(event, "content", "Test async publish");
cJSON_AddNumberToObject(event, "created_at", time(NULL));
cJSON_AddStringToObject(event, "pubkey", "test_pubkey");
cJSON_AddStringToObject(event, "sig", "test_signature");
// Test with non-existent relays (should trigger connection failure callbacks)
const char* test_relays[] = {
"ws://nonexistent1.example.com",
"ws://nonexistent2.example.com"
};
int expected_callbacks = 2;
printf("🚀 Testing async publish with connection failure callbacks...\n");
// Call async publish
int sent_count = nostr_relay_pool_publish_async(
pool, test_relays, 2, event, test_callback, &expected_callbacks);
printf("📊 Sent to %d relays\n", sent_count);
// Wait a bit for callbacks (connection failures should be immediate)
printf("⏳ Waiting for callbacks...\n");
for (int i = 0; i < 10 && callback_count < expected_callbacks; i++) {
nostr_relay_pool_poll(pool, 100);
usleep(100000); // 100ms
}
printf("\n📈 Results:\n");
printf(" Callbacks received: %d/%d\n", callback_count, expected_callbacks);
printf(" Successful publishes: %d\n", success_count);
// Test backward compatibility with synchronous version
printf("\n🔄 Testing backward compatibility (sync version)...\n");
int sync_result = nostr_relay_pool_publish_async(pool, test_relays, 2, event, NULL, NULL);
printf(" Sync publish result: %d successful publishes\n", sync_result);
// Cleanup
cJSON_Delete(event);
nostr_relay_pool_destroy(pool);
printf("\n✅ Async publish test completed!\n");
printf(" - Async callbacks: %s\n", callback_count >= expected_callbacks ? "PASS" : "FAIL");
printf(" - Backward compatibility: %s\n", sync_result >= 0 ? "PASS" : "FAIL");
return (callback_count >= expected_callbacks && sync_result >= 0) ? 0 : 1;
}
+49
View File
@@ -0,0 +1,49 @@
#define _DEFAULT_SOURCE
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main() {
printf("🧪 Backward Compatibility Test\n");
printf("===============================\n");
// Create pool
nostr_relay_pool_t* pool = nostr_relay_pool_create(NULL);
if (!pool) {
printf("❌ Failed to create pool\n");
return 1;
}
// Create a test event
cJSON* event = cJSON_CreateObject();
cJSON_AddStringToObject(event, "id", "test_event_sync");
cJSON_AddNumberToObject(event, "kind", 1);
cJSON_AddStringToObject(event, "content", "Test sync publish");
cJSON_AddNumberToObject(event, "created_at", time(NULL));
cJSON_AddStringToObject(event, "pubkey", "test_pubkey");
cJSON_AddStringToObject(event, "sig", "test_signature");
// Test with non-existent relay (should return 0 successful publishes)
const char* test_relays[] = {"ws://nonexistent.example.com"};
printf("🚀 Testing synchronous publish (backward compatibility)...\n");
// Call synchronous publish (old API)
int result = nostr_relay_pool_publish_async(pool, test_relays, 1, event, NULL, NULL);
printf("📊 Synchronous publish result: %d successful publishes\n", result);
// Cleanup
cJSON_Delete(event);
nostr_relay_pool_destroy(pool);
printf("\n✅ Backward compatibility test completed!\n");
printf(" Expected: 0 successful publishes (connection failure)\n");
printf(" Actual: %d successful publishes\n", result);
printf(" Result: %s\n", result == 0 ? "PASS" : "FAIL");
return result == 0 ? 0 : 1;
}
+70
View File
@@ -0,0 +1,70 @@
/*
* Cashu Mint HTTP Client Test Suite
*/
#include <stdio.h>
#include <string.h>
#include "../nostr_core/nostr_core.h"
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static void test_invalid_inputs(void) {
printf("\n=== test_invalid_inputs ===\n");
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
int rc = cashu_mint_get_info(NULL, &info, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "get_info null url");
cashu_mint_quote_t mquote;
memset(&mquote, 0, sizeof(mquote));
rc = cashu_mint_request_mint_quote(NULL, 10, "sat", &mquote, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "mint quote null url");
rc = cashu_mint_request_mint_quote("https://mint.example.com", 10, NULL, &mquote, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "mint quote null unit");
rc = cashu_mint_check_mint_quote("https://mint.example.com", NULL, &mquote, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "check mint quote null id");
cashu_melt_quote_t melt;
memset(&melt, 0, sizeof(melt));
rc = cashu_mint_request_melt_quote("https://mint.example.com", NULL, "sat", &melt, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "melt quote null request");
rc = cashu_mint_check_melt_quote("https://mint.example.com", NULL, &melt, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "check melt quote null id");
cJSON* out = NULL;
rc = cashu_mint_swap("https://mint.example.com", NULL, &out, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "swap null request body");
rc = cashu_mint_mint_tokens("https://mint.example.com", NULL, &out, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "mint tokens null request body");
rc = cashu_mint_melt_tokens("https://mint.example.com", NULL, &out, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "melt tokens null request body");
rc = cashu_mint_check_proofs_state("https://mint.example.com", NULL, &out, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "checkstate null request body");
}
int main(void) {
printf("Cashu Mint HTTP Client Tests\n");
printf("============================\n");
test_invalid_inputs();
printf("\n=== Test Summary ===\n");
printf("Passed: %d/%d\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}
+3 -3
View File
@@ -671,8 +671,8 @@ int test_vector_7_10kb_payload(void) {
printf("Last 80 chars: \"...%.80s\"\n", encrypted + encrypted_len - 80);
printf("\n");
// Test decryption with our ciphertext
char* decrypted = malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
// Test decryption with our ciphertext - allocate larger buffer for safety
char* decrypted = malloc(NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1024); // 1MB + 1KB extra
if (!decrypted) {
printf("❌ MEMORY ALLOCATION FAILED for decrypted buffer\n");
free(large_plaintext);
@@ -680,7 +680,7 @@ int test_vector_7_10kb_payload(void) {
return 0;
}
printf("Testing decryption of 1MB ciphertext (Bob decrypts from Alice)...\n");
result = nostr_nip04_decrypt(sk2, pk1, encrypted, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE);
result = nostr_nip04_decrypt(sk2, pk1, encrypted, decrypted, NOSTR_NIP04_MAX_PLAINTEXT_SIZE + 1024);
if (result != NOSTR_SUCCESS) {
printf("❌ 1MB DECRYPTION FAILED: %s\n", nostr_strerror(result));
+341
View File
@@ -0,0 +1,341 @@
/*
* NIP-17 Private Direct Messages Test Program
*
* Tests the complete NIP-17 DM flow using synchronous relay operations:
* 1. Generate sender and recipient keypairs
* 2. Create a DM from sender to recipient
* 3. Publish the gift-wrapped DM to relay
* 4. Subscribe to receive the DM back
* 5. Decrypt and verify the received DM
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
// Enable debug output to see all relay messages
#define NOSTR_DEBUG_ENABLED
#include "../nostr_core/nostr_core.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
// Forward declarations for crypto functions
int nostr_secp256k1_get_random_bytes(unsigned char* buf, size_t len);
// Test configuration
#define RELAY_URL "wss://relay.laantungir.net"
#define TEST_TIMEOUT_MS 5000
// Progress callback for publishing
void publish_progress_callback(const char* relay_url, const char* status,
const char* message, int success_count,
int total_relays, int completed_relays, void* user_data) {
(void)user_data;
if (relay_url) {
printf("📡 PUBLISH [%s]: %s", relay_url, status);
if (message) {
printf(" - %s", message);
}
printf(" (%d/%d completed, %d successful)\n", completed_relays, total_relays, success_count);
} else {
printf("📡 PUBLISH COMPLETE: %d/%d successful\n", success_count, total_relays);
}
}
// Progress callback for querying/subscribing
void query_progress_callback(const char* relay_url, const char* status,
const char* event_id, int event_count,
int total_relays, int completed_relays, void* user_data) {
(void)user_data;
if (relay_url) {
printf("🔍 QUERY [%s]: %s", relay_url, status);
if (event_id) {
printf(" - Event: %.12s...", event_id);
}
if (event_count > 0) {
printf(" (%d events)", event_count);
}
printf(" (%d/%d completed)\n", completed_relays, total_relays);
} else {
printf("🔍 QUERY COMPLETE: %d events found\n", event_count);
}
}
/**
* Generate a random keypair for testing
*/
void generate_test_keypair(unsigned char* private_key, char* pubkey_hex) {
// Generate random private key
if (nostr_secp256k1_get_random_bytes(private_key, 32) != 1) {
fprintf(stderr, "Failed to generate random private key\n");
exit(1);
}
// Derive public key
unsigned char public_key[32];
if (nostr_ec_public_key_from_private_key(private_key, public_key) != 0) {
fprintf(stderr, "Failed to derive public key\n");
exit(1);
}
// Convert to hex
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
}
/**
* Main test function
*/
int main(int argc, char* argv[]) {
(void)argc; // Suppress unused parameter warning
(void)argv; // Suppress unused parameter warning
printf("🧪 NIP-17 Private Direct Messages Test (Synchronous)\n");
printf("=================================================\n\n");
// Initialize crypto
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize crypto\n");
return 1;
}
// Generate keypairs
unsigned char sender_privkey[32];
unsigned char recipient_privkey[32];
char sender_pubkey_hex[65];
char recipient_pubkey_hex[65];
printf("🔑 Generating keypairs...\n");
generate_test_keypair(sender_privkey, sender_pubkey_hex);
generate_test_keypair(recipient_privkey, recipient_pubkey_hex);
printf("📤 Sender pubkey: %s\n", sender_pubkey_hex);
printf("📥 Recipient pubkey: %s\n", recipient_pubkey_hex);
printf("\n");
// Create DM event with timestamp
printf("💬 Creating DM event...\n");
time_t now = time(NULL);
char test_message[256];
snprintf(test_message, sizeof(test_message),
"Hello from NIP-17! This is a private direct message sent at %ld", now);
const char* recipient_pubkeys[] = {recipient_pubkey_hex};
cJSON* dm_event = nostr_nip17_create_chat_event(
test_message,
recipient_pubkeys,
1,
"NIP-17 Test",
NULL, // no reply
RELAY_URL,
sender_pubkey_hex
);
if (!dm_event) {
fprintf(stderr, "Failed to create DM event\n");
return 1;
}
printf("📝 Created DM event (kind 14)\n");
// Send DM (create gift wraps)
printf("🎁 Creating gift wraps...\n");
cJSON* gift_wraps[10]; // Max 10 gift wraps
int gift_wrap_count = nostr_nip17_send_dm(
dm_event,
recipient_pubkeys,
1,
sender_privkey,
gift_wraps,
10
);
cJSON_Delete(dm_event); // Original DM event no longer needed
if (gift_wrap_count <= 0) {
fprintf(stderr, "Failed to create gift wraps\n");
return 1;
}
printf("✅ Created %d gift wrap(s)\n", gift_wrap_count);
// Print the gift wrap JSON
printf("\n📄 Gift wrap event JSON:\n");
printf("========================\n");
char* gift_wrap_json = cJSON_Print(gift_wraps[0]);
printf("%s\n", gift_wrap_json);
free(gift_wrap_json);
// PHASE 1: Publish the gift wrap to relay
printf("\n📤 PHASE 1: Publishing gift wrap to relay\n");
printf("==========================================\n");
const char* relay_urls[] = {RELAY_URL};
int success_count = 0;
publish_result_t* publish_results = synchronous_publish_event_with_progress(
relay_urls,
1, // single relay
gift_wraps[0], // Send the first gift wrap
&success_count,
10, // 10 second timeout
publish_progress_callback,
NULL, // no user data
0, // NIP-42 disabled
NULL // no private key for auth
);
if (!publish_results || success_count != 1) {
fprintf(stderr, "❌ Failed to publish gift wrap (success_count: %d)\n", success_count);
// Clean up gift wraps
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
if (publish_results) free(publish_results);
return 1;
}
printf("✅ Successfully published gift wrap!\n");
// Clean up publish results and gift wraps
free(publish_results);
for (int i = 0; i < gift_wrap_count; i++) {
cJSON_Delete(gift_wraps[i]);
}
// Small delay to let the relay process the event
printf("⏳ Waiting 2 seconds for relay to process...\n");
sleep(2);
// PHASE 2: Subscribe to receive the DM back
printf("\n📥 PHASE 2: Subscribing to receive DM back\n");
printf("===========================================\n");
// Create filter for gift wraps addressed to recipient
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1059)); // Gift wrap kind
cJSON_AddItemToObject(filter, "kinds", kinds);
// Filter for gift wraps with p tag matching recipient
cJSON* p_tags = cJSON_CreateArray();
cJSON_AddItemToArray(p_tags, cJSON_CreateString(recipient_pubkey_hex));
cJSON_AddItemToObject(filter, "#p", p_tags);
// Print the subscription filter JSON
printf("📄 Subscription filter JSON:\n");
printf("============================\n");
char* filter_json = cJSON_Print(filter);
printf("%s\n", filter_json);
free(filter_json);
int query_result_count = 0;
cJSON** query_results = synchronous_query_relays_with_progress(
relay_urls,
1, // single relay
filter,
RELAY_QUERY_ALL_RESULTS, // Get all matching events
&query_result_count,
10, // 10 second timeout per relay
query_progress_callback,
NULL, // no user data
0, // NIP-42 disabled
NULL // no private key for auth
);
cJSON_Delete(filter); // Clean up filter
if (!query_results || query_result_count == 0) {
fprintf(stderr, "❌ No DM events received back from relay\n");
if (query_results) free(query_results);
return 1;
}
printf("✅ Received %d event(s) from relay!\n", query_result_count);
// Process the received events
printf("\n🔍 PHASE 3: Processing received events\n");
printf("=====================================\n");
int dm_found = 0;
cJSON* received_dm = NULL;
for (int i = 0; i < query_result_count; i++) {
cJSON* event = query_results[i];
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* id_item = cJSON_GetObjectItem(event, "id");
if (kind_item && cJSON_IsNumber(kind_item) && cJSON_GetNumberValue(kind_item) == 1059) {
printf("🎁 Found gift wrap event: %.12s...\n",
id_item && cJSON_IsString(id_item) ? cJSON_GetStringValue(id_item) : "unknown");
// Try to decrypt this gift wrap
cJSON* decrypted_dm = nostr_nip17_receive_dm(event, recipient_privkey);
if (decrypted_dm) {
printf("✅ Successfully decrypted gift wrap!\n");
// Verify the decrypted DM
cJSON* content_item = cJSON_GetObjectItem(decrypted_dm, "content");
cJSON* dm_kind_item = cJSON_GetObjectItem(decrypted_dm, "kind");
cJSON* pubkey_item = cJSON_GetObjectItem(decrypted_dm, "pubkey");
if (content_item && dm_kind_item && pubkey_item) {
const char* decrypted_content = cJSON_GetStringValue(content_item);
int decrypted_kind = (int)cJSON_GetNumberValue(dm_kind_item);
const char* decrypted_pubkey = cJSON_GetStringValue(pubkey_item);
printf("📧 Decrypted DM:\n");
printf(" Kind: %d\n", decrypted_kind);
printf(" From: %s\n", decrypted_pubkey);
printf(" Content: %s\n", decrypted_content);
// Verify the DM content (check that it contains our message with timestamp)
int content_ok = strstr(decrypted_content, "Hello from NIP-17! This is a private direct message sent at ") != NULL;
if (decrypted_kind == 14 &&
content_ok &&
strlen(decrypted_content) >= 64 && // Should be at least as long as the prefix
strcmp(decrypted_pubkey, sender_pubkey_hex) == 0) {
printf("✅ DM verification successful!\n");
dm_found = 1;
received_dm = decrypted_dm; // Keep reference for cleanup
break; // Found our DM, no need to check more
} else {
printf("❌ DM verification failed\n");
cJSON_Delete(decrypted_dm);
}
} else {
printf("❌ Invalid decrypted DM structure\n");
cJSON_Delete(decrypted_dm);
}
} else {
printf("❌ Failed to decrypt gift wrap\n");
}
}
}
// Clean up query results
for (int i = 0; i < query_result_count; i++) {
cJSON_Delete(query_results[i]);
}
free(query_results);
if (!dm_found) {
fprintf(stderr, "❌ Could not find or decrypt the expected DM\n");
return 1;
}
printf("\n🎉 NIP-17 synchronous test completed successfully!\n");
// Cleanup
if (received_dm) {
cJSON_Delete(received_dm);
}
nostr_cleanup();
return 0;
}
+373
View File
@@ -0,0 +1,373 @@
/*
* NIP-21 URI Scheme Test Suite
* Tests nostr: URI parsing and construction functionality
* Following TESTS POLICY: Shows expected vs actual values
*/
#define _GNU_SOURCE // For strdup on Linux
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../nostr_core/nip021.h"
#include "../nostr_core/nostr_common.h"
#include "../nostr_core/utils.h"
// Ensure strdup is declared
#ifndef strdup
extern char *strdup(const char *s);
#endif
// Test counter for tracking progress
static int test_count = 0;
static int passed_tests = 0;
void print_test_header(const char* test_name) {
test_count++;
printf("\n=== TEST %d: %s ===\n", test_count, test_name);
}
void print_test_result(int passed, const char* test_name) {
if (passed) {
passed_tests++;
printf("✅ PASS: %s\n", test_name);
} else {
printf("❌ FAIL: %s\n", test_name);
}
}
// Test 1: Parse note URI
int test_parse_note_uri(void) {
print_test_header("Parse note: URI");
// First build a valid note URI, then parse it
unsigned char event_id[32];
nostr_hex_to_bytes("f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0", event_id, 32);
char built_uri[200];
int build_result = nostr_build_uri_note(event_id, built_uri, sizeof(built_uri));
if (build_result != NOSTR_SUCCESS) {
printf("Failed to build URI for testing: %d (%s)\n", build_result, nostr_strerror(build_result));
return 0;
}
printf("Input URI: %s\n", built_uri);
nostr_uri_result_t result;
int parse_result = nostr_parse_uri(built_uri, &result);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", parse_result, nostr_strerror(parse_result));
if (parse_result != NOSTR_SUCCESS) {
return 0;
}
printf("Expected type: NOSTR_URI_NOTE\n");
printf("Actual type: %d\n", result.type);
if (result.type != NOSTR_URI_NOTE) {
return 0;
}
printf("Expected event ID to be set\n");
printf("Event ID present: %s\n", result.data.event_id[0] ? "yes" : "no");
// Verify the parsed event ID matches the original
int event_id_match = (memcmp(result.data.event_id, event_id, 32) == 0);
printf("Event ID matches original: %s\n", event_id_match ? "yes" : "no");
return (result.type == NOSTR_URI_NOTE && event_id_match);
}
// Test 2: Parse nprofile URI
int test_parse_nprofile_uri(void) {
print_test_header("Parse nprofile: URI");
// First build a valid nprofile URI, then parse it
unsigned char pubkey[32];
nostr_hex_to_bytes("aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", pubkey, 32);
const char* relays[] = {"wss://relay.example.com"};
char built_uri[300];
int build_result = nostr_build_uri_nprofile(pubkey, relays, 1, built_uri, sizeof(built_uri));
if (build_result != NOSTR_SUCCESS) {
printf("Failed to build URI for testing: %d (%s)\n", build_result, nostr_strerror(build_result));
return 0;
}
printf("Input URI: %s\n", built_uri);
nostr_uri_result_t result;
int parse_result = nostr_parse_uri(built_uri, &result);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", parse_result, nostr_strerror(parse_result));
if (parse_result != NOSTR_SUCCESS) {
return 0;
}
printf("Expected type: NOSTR_URI_NPROFILE\n");
printf("Actual type: %d\n", result.type);
if (result.type != NOSTR_URI_NPROFILE) {
return 0;
}
// Verify the parsed pubkey matches the original
int pubkey_match = (memcmp(result.data.nprofile.pubkey, pubkey, 32) == 0);
printf("Pubkey matches original: %s\n", pubkey_match ? "yes" : "no");
// Verify relay count
printf("Expected relay count: 1\n");
printf("Actual relay count: %d\n", result.data.nprofile.relay_count);
return (result.type == NOSTR_URI_NPROFILE && pubkey_match && result.data.nprofile.relay_count == 1);
}
// Test 3: Parse nevent URI
int test_parse_nevent_uri(void) {
print_test_header("Parse nevent: URI");
// First build a valid nevent URI, then parse it
unsigned char event_id[32];
nostr_hex_to_bytes("f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0", event_id, 32);
const char* relays[] = {"wss://relay.example.com"};
char built_uri[400];
int build_result = nostr_build_uri_nevent(event_id, relays, 1, NULL, 1, 1234567890, built_uri, sizeof(built_uri));
if (build_result != NOSTR_SUCCESS) {
printf("Failed to build URI for testing: %d (%s)\n", build_result, nostr_strerror(build_result));
return 0;
}
printf("Input URI: %s\n", built_uri);
nostr_uri_result_t result;
int parse_result = nostr_parse_uri(built_uri, &result);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", parse_result, nostr_strerror(parse_result));
if (parse_result != NOSTR_SUCCESS) {
return 0;
}
printf("Expected type: NOSTR_URI_NEVENT\n");
printf("Actual type: %d\n", result.type);
if (result.type != NOSTR_URI_NEVENT) {
return 0;
}
// Verify the parsed event ID matches the original
int event_id_match = (memcmp(result.data.nevent.event_id, event_id, 32) == 0);
printf("Event ID matches original: %s\n", event_id_match ? "yes" : "no");
// Verify kind
printf("Expected kind: 1\n");
printf("Actual kind: %d\n", result.data.nevent.kind ? *result.data.nevent.kind : -1);
// Verify relay count
printf("Expected relay count: 1\n");
printf("Actual relay count: %d\n", result.data.nevent.relay_count);
return (result.type == NOSTR_URI_NEVENT && event_id_match && result.data.nevent.relay_count == 1);
}
// Test 4: Parse naddr URI
int test_parse_naddr_uri(void) {
print_test_header("Parse naddr: URI");
// First build a valid naddr URI, then parse it
const char* identifier = "draft";
unsigned char pubkey[32];
nostr_hex_to_bytes("aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", pubkey, 32);
const char* relays[] = {"wss://relay.example.com"};
char built_uri[400];
int build_result = nostr_build_uri_naddr(identifier, pubkey, 30023, relays, 1, built_uri, sizeof(built_uri));
if (build_result != NOSTR_SUCCESS) {
printf("Failed to build URI for testing: %d (%s)\n", build_result, nostr_strerror(build_result));
return 0;
}
printf("Input URI: %s\n", built_uri);
nostr_uri_result_t result;
int parse_result = nostr_parse_uri(built_uri, &result);
printf("Expected: NOSTR_SUCCESS (0)\n");
printf("Actual: %d (%s)\n", parse_result, nostr_strerror(parse_result));
if (parse_result != NOSTR_SUCCESS) {
return 0;
}
printf("Expected type: NOSTR_URI_NADDR\n");
printf("Actual type: %d\n", result.type);
if (result.type != NOSTR_URI_NADDR) {
return 0;
}
// Verify the parsed identifier matches the original
int identifier_match = (strcmp(result.data.naddr.identifier, identifier) == 0);
printf("Identifier matches original: %s\n", identifier_match ? "yes" : "no");
// Verify kind
printf("Expected kind: 30023\n");
printf("Actual kind: %d\n", result.data.naddr.kind);
// Verify relay count
printf("Expected relay count: 1\n");
printf("Actual relay count: %d\n", result.data.naddr.relay_count);
return (result.type == NOSTR_URI_NADDR && identifier_match && result.data.naddr.relay_count == 1);
}
// Test 5: Invalid URI (wrong prefix)
int test_invalid_uri_prefix(void) {
print_test_header("Invalid URI - Wrong Prefix");
const char* uri = "bitcoin:note1example";
printf("Input URI: %s\n", uri);
nostr_uri_result_t result;
int parse_result = nostr_parse_uri(uri, &result);
printf("Expected: NOSTR_ERROR_INVALID_INPUT (-1)\n");
printf("Actual: %d (%s)\n", parse_result, nostr_strerror(parse_result));
return (parse_result == NOSTR_ERROR_INVALID_INPUT);
}
// Test 6: Invalid URI (missing colon)
int test_invalid_uri_no_colon(void) {
print_test_header("Invalid URI - No Colon");
const char* uri = "nostrnote1example";
printf("Input URI: %s\n", uri);
nostr_uri_result_t result;
int parse_result = nostr_parse_uri(uri, &result);
printf("Expected: NOSTR_ERROR_INVALID_INPUT (-1)\n");
printf("Actual: %d (%s)\n", parse_result, nostr_strerror(parse_result));
return (parse_result == NOSTR_ERROR_INVALID_INPUT);
}
// Test 7: Build note URI
int test_build_note_uri(void) {
print_test_header("Build note: URI");
unsigned char event_id[32];
nostr_hex_to_bytes("f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0", event_id, 32);
printf("Input event ID: f1e582c90f071c0110cc5bcac2dcc6d8c32250e3cc26fcbe93470d918f2ffaf0\n");
char uri[200];
int result = nostr_build_uri_note(event_id, uri, sizeof(uri));
printf("Build result: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_SUCCESS) {
return 0;
}
printf("Built URI: %s\n", uri);
int success = (strncmp(uri, "nostr:note1", 11) == 0);
printf("Expected: URI starts with 'nostr:note1'\n");
printf("Actual: %s\n", success ? "yes" : "no");
return success;
}
// Test 8: Build nprofile URI
int test_build_nprofile_uri(void) {
print_test_header("Build nprofile: URI");
unsigned char pubkey[32];
nostr_hex_to_bytes("aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4", pubkey, 32);
const char* relays[] = {"wss://relay.example.com", "wss://relay2.example.com"};
printf("Input pubkey: aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4\n");
char uri[300];
int result = nostr_build_uri_nprofile(pubkey, relays, 2, uri, sizeof(uri));
printf("Build result: %d (%s)\n", result, nostr_strerror(result));
if (result != NOSTR_SUCCESS) {
return 0;
}
printf("Built URI: %s\n", uri);
int success = (strncmp(uri, "nostr:nprofile1", 14) == 0);
printf("Expected: URI starts with 'nostr:nprofile1'\n");
printf("Actual: %s\n", success ? "yes" : "no");
return success;
}
int main(void) {
printf("=== NIP-21 URI Scheme Test Suite ===\n");
printf("Following TESTS POLICY: Shows expected vs actual values\n");
// Initialize crypto library
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize nostr library\n");
return 1;
}
int all_passed = 1;
int test_result;
// Valid URI parsing tests
test_result = test_parse_note_uri();
print_test_result(test_result, "Parse note: URI");
if (!test_result) all_passed = 0;
test_result = test_parse_nprofile_uri();
print_test_result(test_result, "Parse nprofile: URI");
if (!test_result) all_passed = 0;
test_result = test_parse_nevent_uri();
print_test_result(test_result, "Parse nevent: URI");
if (!test_result) all_passed = 0;
test_result = test_parse_naddr_uri();
print_test_result(test_result, "Parse naddr: URI");
if (!test_result) all_passed = 0;
// Invalid URI tests
test_result = test_invalid_uri_prefix();
print_test_result(test_result, "Invalid URI - Wrong Prefix");
if (!test_result) all_passed = 0;
test_result = test_invalid_uri_no_colon();
print_test_result(test_result, "Invalid URI - No Colon");
if (!test_result) all_passed = 0;
// URI building tests
test_result = test_build_note_uri();
print_test_result(test_result, "Build note: URI");
if (!test_result) all_passed = 0;
test_result = test_build_nprofile_uri();
print_test_result(test_result, "Build nprofile: URI");
if (!test_result) all_passed = 0;
// Summary
printf("\n=== TEST SUMMARY ===\n");
printf("Total tests: %d\n", test_count);
printf("Passed: %d\n", passed_tests);
printf("Failed: %d\n", test_count - passed_tests);
if (all_passed) {
printf("🎉 ALL TESTS PASSED! NIP-21 URI scheme implementation is working correctly.\n");
} else {
printf("❌ SOME TESTS FAILED. Please review the output above.\n");
}
nostr_cleanup();
return all_passed ? 0 : 1;
}
+309
View File
@@ -0,0 +1,309 @@
#define _DEFAULT_SOURCE
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
#include "../nostr_websocket/nostr_websocket_tls.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static int g_callback_count = 0;
static int g_publish_ok = 0;
static int g_publish_fail = 0;
static int g_auth_required_seen = 0;
static const char* relay_status_str(nostr_pool_relay_status_t status) {
switch (status) {
case NOSTR_POOL_RELAY_DISCONNECTED: return "DISCONNECTED";
case NOSTR_POOL_RELAY_CONNECTING: return "CONNECTING";
case NOSTR_POOL_RELAY_CONNECTED: return "CONNECTED";
case NOSTR_POOL_RELAY_ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
static double now_ms(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
return (double)time(NULL) * 1000.0;
}
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0;
}
static void publish_callback(const char* relay_url,
const char* event_id,
int success,
const char* message,
void* user_data) {
(void)user_data;
g_callback_count++;
if (success) {
g_publish_ok++;
} else {
g_publish_fail++;
if (message && strstr(message, "auth-required") != NULL) {
g_auth_required_seen++;
}
}
printf("[POOL CALLBACK %d] relay=%s event_id=%s success=%d", g_callback_count,
relay_url ? relay_url : "(null)", event_id ? event_id : "(null)", success);
if (message) {
printf(" message=\"%s\"", message);
}
printf("\n");
}
static cJSON* create_kind4_event(const unsigned char* private_key, int sequence) {
if (!private_key) {
return NULL;
}
char content[256];
snprintf(content, sizeof(content), "pool-auth-test message #%d at %ld", sequence, (long)time(NULL));
cJSON* tags = cJSON_CreateArray();
if (!tags) {
return NULL;
}
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("0000000000000000000000000000000000000000000000000000000000000000"));
cJSON_AddItemToArray(tags, p_tag);
cJSON* event = nostr_create_and_sign_event(4, content, tags, private_key, 0);
cJSON_Delete(tags);
return event;
}
static void run_relay_auth_probe(const char* relay_url) {
printf("\n=== Relay AUTH Probe (raw responses) ===\n");
nostr_ws_client_t* probe = nostr_ws_connect(relay_url);
if (!probe) {
printf("[AUTH PROBE] connect failed for %s\n", relay_url);
return;
}
int auth_seen = 0;
int ok_seen = 0;
int notice_seen = 0;
double start = now_ms();
while ((now_ms() - start) < 2500.0) {
char buffer[8192];
int len = nostr_ws_receive(probe, buffer, sizeof(buffer) - 1, 150);
if (len <= 0) {
continue;
}
buffer[len] = '\0';
printf("[AUTH PROBE RAW] %s\n", buffer);
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type) {
printf("[AUTH PROBE PARSED] type=%s\n", msg_type);
}
if (msg_type && strcmp(msg_type, "AUTH") == 0 && cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
if (cJSON_IsString(challenge_json)) {
printf("[AUTH PROBE] challenge=%s\n", cJSON_GetStringValue(challenge_json));
}
auth_seen++;
} else if (msg_type && strcmp(msg_type, "OK") == 0) {
ok_seen++;
} else if (msg_type && strcmp(msg_type, "NOTICE") == 0) {
notice_seen++;
}
}
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
}
printf("[AUTH PROBE] summary: AUTH=%d OK=%d NOTICE=%d\n", auth_seen, ok_seen, notice_seen);
nostr_ws_close(probe);
}
int main(void) {
printf("=== NIP-42 Relay Pool Publish Test (kind-4 over 5s) ===\n");
if (nostr_init() != NOSTR_SUCCESS) {
printf("FAILED: nostr_init() failed\n");
return 1;
}
const char* relay_url = "ws://127.0.0.1:7777";
const char* private_key_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
unsigned char private_key[32];
memset(private_key, 0, sizeof(private_key));
if (nostr_hex_to_bytes(private_key_hex, private_key, sizeof(private_key)) != NOSTR_SUCCESS) {
printf("FAILED: unable to decode private key hex\n");
nostr_cleanup();
return 1;
}
unsigned char public_key[32];
char pubkey_hex[65];
if (nostr_ec_public_key_from_private_key(private_key, public_key) != NOSTR_SUCCESS) {
printf("FAILED: unable to derive public key\n");
memset(private_key, 0, sizeof(private_key));
nostr_cleanup();
return 1;
}
nostr_bytes_to_hex(public_key, 32, pubkey_hex);
printf("Using pubkey: %s\n", pubkey_hex);
nostr_relay_pool_t* pool = nostr_relay_pool_create(NULL);
if (!pool) {
printf("FAILED: unable to create relay pool\n");
nostr_cleanup();
return 1;
}
if (nostr_relay_pool_set_auth(pool, private_key, 1) != NOSTR_SUCCESS) {
printf("FAILED: unable to configure relay pool authentication\n");
nostr_relay_pool_destroy(pool);
nostr_cleanup();
return 1;
}
if (nostr_relay_pool_add_relay(pool, relay_url) != NOSTR_SUCCESS) {
printf("FAILED: unable to add relay %s\n", relay_url);
nostr_relay_pool_destroy(pool);
nostr_cleanup();
return 1;
}
run_relay_auth_probe(relay_url);
const char* relays[] = { relay_url };
int sent_attempts = 0;
const double test_duration_ms = 5000.0;
const double publish_interval_ms = 650.0;
double start = now_ms();
double next_publish = start;
int sequence = 1;
nostr_pool_relay_status_t last_status = NOSTR_POOL_RELAY_DISCONNECTED;
char last_error_snapshot[512] = {0};
while ((now_ms() - start) < test_duration_ms) {
nostr_relay_pool_poll(pool, 100);
nostr_pool_relay_status_t current_status = nostr_relay_pool_get_relay_status(pool, relay_url);
if (current_status != last_status) {
printf("[POOL STATUS] %s -> %s\n", relay_status_str(last_status), relay_status_str(current_status));
last_status = current_status;
}
const char* last_err_live = nostr_relay_pool_get_relay_last_publish_error(pool, relay_url);
if (last_err_live && strcmp(last_err_live, last_error_snapshot) != 0) {
strncpy(last_error_snapshot, last_err_live, sizeof(last_error_snapshot) - 1);
last_error_snapshot[sizeof(last_error_snapshot) - 1] = '\0';
printf("[POOL ERROR] %s\n", last_error_snapshot);
}
double t = now_ms();
if (t >= next_publish) {
cJSON* event = create_kind4_event(private_key, sequence++);
if (!event) {
printf("WARN: failed to create signed event, skipping publish\n");
next_publish += publish_interval_ms;
usleep(100000);
continue;
}
int sent = nostr_relay_pool_publish_async(pool, relays, 1, event, publish_callback, NULL);
if (sent > 0) {
sent_attempts++;
}
printf("[PUBLISH] attempt=%d sent=%d\n", sequence - 1, sent);
cJSON_Delete(event);
next_publish += publish_interval_ms;
}
usleep(100000);
}
// Drain callbacks/messages a bit after last publish
double drain_start = now_ms();
while ((now_ms() - drain_start) < 1500.0) {
nostr_relay_pool_poll(pool, 100);
usleep(100000);
}
const char* last_err = nostr_relay_pool_get_relay_last_publish_error(pool, relay_url);
cJSON* auth_filter = cJSON_CreateObject();
cJSON* auth_kinds = cJSON_CreateArray();
cJSON* auth_authors = cJSON_CreateArray();
cJSON_AddItemToArray(auth_kinds, cJSON_CreateNumber(22242));
cJSON_AddItemToArray(auth_authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(auth_filter, "kinds", auth_kinds);
cJSON_AddItemToObject(auth_filter, "authors", auth_authors);
cJSON_AddItemToObject(auth_filter, "limit", cJSON_CreateNumber(20));
int auth_event_count = 0;
cJSON** auth_events = nostr_relay_pool_query_sync(pool, relays, 1, auth_filter, &auth_event_count, 2500);
cJSON_Delete(auth_filter);
printf("\n=== AUTH Events Seen On Relay (kind 22242) ===\n");
printf("count=%d\n", auth_event_count);
for (int i = 0; i < auth_event_count; i++) {
cJSON* id = cJSON_GetObjectItem(auth_events[i], "id");
cJSON* created_at = cJSON_GetObjectItem(auth_events[i], "created_at");
printf("[AUTH EVENT %d] id=%s created_at=%lld\n",
i + 1,
(id && cJSON_IsString(id)) ? cJSON_GetStringValue(id) : "(no-id)",
(long long)((created_at && cJSON_IsNumber(created_at)) ? cJSON_GetNumberValue(created_at) : 0));
}
printf("\n=== Summary ===\n");
printf("Relay: %s\n", relay_url);
printf("Publish attempts sent: %d\n", sent_attempts);
printf("Callbacks: %d\n", g_callback_count);
printf("Accepted: %d\n", g_publish_ok);
printf("Rejected/Failed: %d\n", g_publish_fail);
printf("auth-required seen in callbacks: %d\n", g_auth_required_seen);
printf("Last relay publish error: %s\n", last_err ? last_err : "(none)");
if (auth_events) {
for (int i = 0; i < auth_event_count; i++) {
if (auth_events[i]) {
cJSON_Delete(auth_events[i]);
}
}
free(auth_events);
}
nostr_relay_pool_destroy(pool);
memset(private_key, 0, sizeof(private_key));
memset(public_key, 0, sizeof(public_key));
nostr_cleanup();
// Test is considered successful if the loop executed and we attempted publishes.
// Auth behavior is diagnosed by callback details and summary output.
if (sent_attempts <= 0) {
printf("RESULT: FAIL (no publish attempts were sent)\n");
return 1;
}
printf("RESULT: PASS (publish attempts sent; inspect auth behavior in logs above)\n");
return 0;
}
+183 -124
View File
@@ -20,32 +20,7 @@ typedef struct {
const char* expected_encrypted; // Optional - for known test vectors
} nip44_test_vector_t;
// Known decryption-only test vectors from nostr-tools (for cross-compatibility testing)
// Note: NIP-44 encryption is non-deterministic - ciphertext varies each time
// These vectors test our ability to decrypt known good ciphertext from reference implementations
static nip44_test_vector_t decryption_test_vectors[] = {
{
"Decryption test: single char 'a'",
"0000000000000000000000000000000000000000000000000000000000000001", // sec1
"0000000000000000000000000000000000000000000000000000000000000002", // sec2
"a",
"AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"
},
{
"Decryption test: emoji",
"0000000000000000000000000000000000000000000000000000000000000002", // sec1
"0000000000000000000000000000000000000000000000000000000000000001", // sec2
"🍕🫃",
"AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPSKSK6is9ngkX2+cSq85Th16oRTISAOfhStnixqZziKMDvB0QQzgFZdjLTPicCJaV8nDITO+QfaQ61+KbWQIOO2Yj"
},
{
"Decryption test: wide unicode",
"5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a", // sec1
"4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d", // sec2
"表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀",
"ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7s7JqlCMJBAIIjfkpHReBPXeoMCyuClwgbT419jUWU1PwaNl4FEQYKCDKVJz+97Mp3K+Q2YGa77B6gpxB/lr1QgoqpDf7wDVrDmOqGoiPjWDqy8KzLueKDcm9BVP8xeTJIxs="
}
};
// Additional test vectors for edge cases (converted to round-trip tests with new 32-bit padding)
// Round-trip test vectors with proper key pairs
static nip44_test_vector_t test_vectors[] = {
@@ -69,6 +44,13 @@ static nip44_test_vector_t test_vectors[] = {
"4444444444444444444444444444444444444444444444444444444444444444",
"",
NULL
},
{
"64KB payload test",
"91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe", // Same keys as basic test
"96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220",
NULL, // Will be generated dynamically
NULL
}
};
@@ -86,76 +68,144 @@ static int hex_to_bytes(const char* hex, unsigned char* bytes, size_t len) {
static int test_nip44_round_trip(const nip44_test_vector_t* tv) {
printf("Test: %s\n", tv->name);
// Parse keys - both private keys
unsigned char sender_private_key[32];
unsigned char recipient_private_key[32];
if (hex_to_bytes(tv->sender_private_key_hex, sender_private_key, 32) != 0) {
printf(" FAIL: Failed to parse sender private key\n");
return -1;
}
if (hex_to_bytes(tv->recipient_private_key_hex, recipient_private_key, 32) != 0) {
printf(" FAIL: Failed to parse recipient private key\n");
return -1;
}
// Generate the public keys from the private keys
unsigned char sender_public_key[32];
unsigned char recipient_public_key[32];
if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) != 0) {
printf(" FAIL: Failed to derive sender public key\n");
return -1;
}
if (nostr_ec_public_key_from_private_key(recipient_private_key, recipient_public_key) != 0) {
printf(" FAIL: Failed to derive recipient public key\n");
return -1;
}
// Test encryption
char encrypted[8192];
int encrypt_result = nostr_nip44_encrypt(
sender_private_key,
recipient_public_key,
tv->plaintext,
encrypted,
sizeof(encrypted)
);
if (encrypt_result != NOSTR_SUCCESS) {
printf(" FAIL: Encryption - Expected: %d, Actual: %d\n", NOSTR_SUCCESS, encrypt_result);
// Special handling for large payload tests
char* test_plaintext;
if (strcmp(tv->name, "64KB payload test") == 0) {
// Generate exactly 64KB (65,535 bytes) of predictable content - max NIP-44 size
const size_t payload_size = 65535;
test_plaintext = malloc(payload_size + 1);
if (!test_plaintext) {
printf(" FAIL: Memory allocation failed for 64KB test payload\n");
return -1;
}
// Fill with a predictable pattern: "ABCDEFGH01234567" repeated
const char* pattern = "ABCDEFGH01234567"; // 16 bytes
const size_t pattern_len = 16;
for (size_t i = 0; i < payload_size; i += pattern_len) {
size_t copy_len = (i + pattern_len <= payload_size) ? pattern_len : payload_size - i;
memcpy(test_plaintext + i, pattern, copy_len);
}
test_plaintext[payload_size] = '\0';
printf(" Generated 64KB test payload (%zu bytes)\n", payload_size);
printf(" Pattern: \"%s\" repeated\n", pattern);
printf(" First 64 chars: \"%.64s...\"\n", test_plaintext);
printf(" Last 64 chars: \"...%.64s\"\n", test_plaintext + payload_size - 64);
} else {
test_plaintext = (char*)tv->plaintext;
}
// Debug: Check plaintext length
size_t plaintext_len = strlen(test_plaintext);
printf(" Plaintext length: %zu bytes\n", plaintext_len);
printf(" Output buffer size: %zu bytes\n", (size_t)10485760);
// Test encryption - use larger buffer for 1MB+ payloads (10MB for NIP-44 overhead)
char* encrypted = malloc(10485760); // 10MB buffer for large payloads
if (!encrypted) {
printf(" FAIL: Memory allocation failed for encrypted buffer\n");
if (strcmp(tv->name, "0.5MB payload test") == 0) free(test_plaintext);
return -1;
}
// For large payloads, use _with_nonce to avoid random generation issues
unsigned char fixed_nonce[32] = {0};
int encrypt_result = nostr_nip44_encrypt_with_nonce(
sender_private_key,
recipient_public_key,
test_plaintext,
fixed_nonce,
encrypted,
10485760
);
if (encrypt_result != NOSTR_SUCCESS) {
printf(" FAIL: Encryption - Expected: %d, Actual: %d\n", NOSTR_SUCCESS, encrypt_result);
if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext);
free(encrypted);
return -1;
}
// Test decryption - use recipient private key + sender public key
char decrypted[8192];
char* decrypted = malloc(65536 + 1); // 64KB + 1 for null terminator
if (!decrypted) {
printf(" FAIL: Memory allocation failed for decrypted buffer\n");
if (strcmp(tv->name, "64KB payload test") == 0) free(test_plaintext);
free(encrypted);
return -1;
}
int decrypt_result = nostr_nip44_decrypt(
recipient_private_key,
sender_public_key,
encrypted,
decrypted,
sizeof(decrypted)
65536 + 1
);
if (decrypt_result != NOSTR_SUCCESS) {
printf(" FAIL: Decryption - Expected: %d, Actual: %d\n", NOSTR_SUCCESS, decrypt_result);
if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext);
free(encrypted);
free(decrypted);
return -1;
}
// Verify round-trip
if (strcmp(tv->plaintext, decrypted) != 0) {
if (strcmp(test_plaintext, decrypted) != 0) {
printf(" FAIL: Round-trip mismatch\n");
printf(" Expected: \"%s\"\n", tv->plaintext);
printf(" Expected: \"%s\"\n", test_plaintext);
printf(" Actual: \"%s\"\n", decrypted);
if (strcmp(tv->name, "1MB payload test") == 0) free(test_plaintext);
free(encrypted);
free(decrypted);
return -1;
}
printf(" PASS: Expected: \"%s\", Actual: \"%s\"\n", tv->plaintext, decrypted);
printf(" Encrypted output: %s\n", encrypted);
if (strcmp(tv->name, "64KB payload test") == 0) {
printf(" ✅ 64KB payload round-trip: PASS\n");
printf(" ✅ Content verification: All %zu bytes match perfectly!\n", strlen(test_plaintext));
printf(" Encrypted length: %zu bytes\n", strlen(encrypted));
printf(" 🎉 64KB NIP-44 STRESS TEST COMPLETED SUCCESSFULLY! 🎉\n");
} else {
printf(" PASS: Expected: \"%s\", Actual: \"%s\"\n", test_plaintext, decrypted);
printf(" Encrypted output: %s\n", encrypted);
}
if (strcmp(tv->name, "64KB payload test") == 0) free(test_plaintext);
free(encrypted);
free(decrypted);
return 0;
}
@@ -215,59 +265,6 @@ static int test_nip44_error_conditions() {
return 0;
}
static int test_nip44_decryption_vector(const nip44_test_vector_t* tv) {
printf("Test: %s\n", tv->name);
// Parse keys
unsigned char sender_private_key[32];
unsigned char recipient_private_key[32];
if (hex_to_bytes(tv->sender_private_key_hex, sender_private_key, 32) != 0) {
printf(" FAIL: Failed to parse sender private key\n");
return -1;
}
if (hex_to_bytes(tv->recipient_private_key_hex, recipient_private_key, 32) != 0) {
printf(" FAIL: Failed to parse recipient private key\n");
return -1;
}
// Generate the public keys from the private keys
unsigned char sender_public_key[32];
if (nostr_ec_public_key_from_private_key(sender_private_key, sender_public_key) != 0) {
printf(" FAIL: Failed to derive sender public key\n");
return -1;
}
// Test decryption of known vector
char decrypted[8192];
int decrypt_result = nostr_nip44_decrypt(
recipient_private_key,
sender_public_key,
tv->expected_encrypted,
decrypted,
sizeof(decrypted)
);
if (decrypt_result != NOSTR_SUCCESS) {
printf(" FAIL: Decryption - Expected: %d, Actual: %d\n", NOSTR_SUCCESS, decrypt_result);
printf(" Input payload: %s\n", tv->expected_encrypted);
return -1;
}
// Verify decrypted plaintext matches expected
if (strcmp(tv->plaintext, decrypted) != 0) {
printf(" FAIL: Plaintext mismatch\n");
printf(" Expected: \"%s\"\n", tv->plaintext);
printf(" Actual: \"%s\"\n", decrypted);
return -1;
}
printf(" PASS: Expected: \"%s\", Actual: \"%s\"\n", tv->plaintext, decrypted);
return 0;
}
static int test_nip44_encryption_variability() {
printf("Test: NIP-44 encryption variability (non-deterministic)\n");
@@ -287,11 +284,20 @@ static int test_nip44_encryption_variability() {
}
// Encrypt the same message multiple times
char encrypted1[8192], encrypted2[8192], encrypted3[8192];
char* encrypted1 = malloc(2097152); // 2MB buffer
char* encrypted2 = malloc(2097152);
char* encrypted3 = malloc(2097152);
if (!encrypted1 || !encrypted2 || !encrypted3) {
printf(" FAIL: Memory allocation failed for encrypted buffers\n");
free(encrypted1);
free(encrypted2);
free(encrypted3);
return -1;
}
int result1 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted1, sizeof(encrypted1));
int result2 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted2, sizeof(encrypted2));
int result3 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted3, sizeof(encrypted3));
int result1 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted1, 2097152);
int result2 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted2, 2097152);
int result3 = nostr_nip44_encrypt(sender_key, recipient_pubkey, test_message, encrypted3, 2097152);
if (result1 != NOSTR_SUCCESS || result2 != NOSTR_SUCCESS || result3 != NOSTR_SUCCESS) {
printf(" FAIL: Encryption failed - Results: %d, %d, %d\n", result1, result2, result3);
@@ -304,6 +310,9 @@ static int test_nip44_encryption_variability() {
printf(" Encryption 1: %.50s...\n", encrypted1);
printf(" Encryption 2: %.50s...\n", encrypted2);
printf(" Encryption 3: %.50s...\n", encrypted3);
free(encrypted1);
free(encrypted2);
free(encrypted3);
return -1;
}
@@ -314,11 +323,23 @@ static int test_nip44_encryption_variability() {
return -1;
}
char decrypted1[8192], decrypted2[8192], decrypted3[8192];
int decrypt1 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted1, decrypted1, sizeof(decrypted1));
int decrypt2 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted2, decrypted2, sizeof(decrypted2));
int decrypt3 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted3, decrypted3, sizeof(decrypted3));
char* decrypted1 = malloc(1048576 + 1);
char* decrypted2 = malloc(1048576 + 1);
char* decrypted3 = malloc(1048576 + 1);
if (!decrypted1 || !decrypted2 || !decrypted3) {
printf(" FAIL: Memory allocation failed for decrypted buffers\n");
free(encrypted1);
free(encrypted2);
free(encrypted3);
free(decrypted1);
free(decrypted2);
free(decrypted3);
return -1;
}
int decrypt1 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted1, decrypted1, 1048576 + 1);
int decrypt2 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted2, decrypted2, 1048576 + 1);
int decrypt3 = nostr_nip44_decrypt(recipient_key, sender_pubkey, encrypted3, decrypted3, 1048576 + 1);
if (decrypt1 != NOSTR_SUCCESS || decrypt2 != NOSTR_SUCCESS || decrypt3 != NOSTR_SUCCESS) {
printf(" FAIL: Decryption failed - Results: %d, %d, %d\n", decrypt1, decrypt2, decrypt3);
@@ -331,12 +352,25 @@ static int test_nip44_encryption_variability() {
printf(" Decrypted1: \"%s\"\n", decrypted1);
printf(" Decrypted2: \"%s\"\n", decrypted2);
printf(" Decrypted3: \"%s\"\n", decrypted3);
free(encrypted1);
free(encrypted2);
free(encrypted3);
free(decrypted1);
free(decrypted2);
free(decrypted3);
return -1;
}
printf(" PASS: All encryptions different, all decrypt to: \"%s\"\n", test_message);
printf(" Sample ciphertext lengths: %zu, %zu, %zu bytes\n", strlen(encrypted1), strlen(encrypted2), strlen(encrypted3));
free(encrypted1);
free(encrypted2);
free(encrypted3);
free(decrypted1);
free(decrypted2);
free(decrypted3);
return 0;
}
@@ -365,12 +399,37 @@ int main() {
printf("\n");
}
// Test decryption vectors (cross-compatibility)
size_t num_decryption_vectors = sizeof(decryption_test_vectors) / sizeof(decryption_test_vectors[0]);
for (size_t i = 0; i < num_decryption_vectors; i++) {
// Additional edge case tests (converted to round-trip tests with new 32-bit padding)
// These test the same plaintexts as the old decryption vectors but with our new format
static nip44_test_vector_t edge_case_test_vectors[] = {
{
"Edge case: single char 'a'",
"0000000000000000000000000000000000000000000000000000000000000001", // sec1
"0000000000000000000000000000000000000000000000000000000000000002", // sec2
"a",
NULL
},
{
"Edge case: emoji",
"0000000000000000000000000000000000000000000000000000000000000002", // sec1
"0000000000000000000000000000000000000000000000000000000000000001", // sec2
"🍕🫃",
NULL
},
{
"Edge case: wide unicode",
"5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a", // sec1
"4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d", // sec2
"表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀",
NULL
}
};
size_t num_edge_case_vectors = sizeof(edge_case_test_vectors) / sizeof(edge_case_test_vectors[0]);
for (size_t i = 0; i < num_edge_case_vectors; i++) {
total_tests++;
printf("Test #%d\n", total_tests);
if (test_nip44_decryption_vector(&decryption_test_vectors[i]) == 0) {
if (test_nip44_round_trip(&edge_case_test_vectors[i]) == 0) {
passed_tests++;
}
printf("\n");
+247
View File
@@ -0,0 +1,247 @@
/*
* NIP-46 Remote Signing Test Suite
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../nostr_core/nostr_core.h"
static int tests_run = 0;
static int tests_passed = 0;
static void expect_int(const char* name, int expected, int actual) {
tests_run++;
if (expected == actual) {
tests_passed++;
printf("✅ %s (expected=%d actual=%d)\n", name, expected, actual);
} else {
printf("❌ %s (expected=%d actual=%d)\n", name, expected, actual);
}
}
static void expect_true(const char* name, int cond) {
tests_run++;
if (cond) {
tests_passed++;
printf("✅ %s\n", name);
} else {
printf("❌ %s\n", name);
}
}
static int hex_to_bytes32(const char* hex, unsigned char out[32]) {
return nostr_hex_to_bytes(hex, out, 32) == 0 ? 0 : -1;
}
static void test_url_parsing_and_generation(void) {
printf("\n=== test_url_parsing_and_generation ===\n");
const char* bunker = "bunker://fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52?relay=wss%3A%2F%2Frelay1.example.com&relay=wss%3A%2F%2Frelay2.example.com&secret=s3cr3t";
nostr_nip46_bunker_url_t bu;
int rc = nostr_nip46_parse_bunker_url(bunker, &bu);
expect_int("parse bunker url", NOSTR_SUCCESS, rc);
expect_true("bunker pubkey parsed", strcmp(bu.remote_signer_pubkey, "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52") == 0);
expect_int("bunker relay count", 2, bu.relay_count);
expect_true("bunker relay[0] decoded", strcmp(bu.relays[0], "wss://relay1.example.com") == 0);
expect_true("bunker secret parsed", strcmp(bu.secret, "s3cr3t") == 0);
char bunker_roundtrip[2048];
rc = nostr_nip46_create_bunker_url(&bu, bunker_roundtrip, sizeof(bunker_roundtrip));
expect_int("create bunker url", NOSTR_SUCCESS, rc);
expect_true("bunker roundtrip has scheme", strstr(bunker_roundtrip, "bunker://") == bunker_roundtrip);
const char* nc = "nostrconnect://83f3b2ae6aa368e8275397b9c26cf550101d63ebaab900d19dd4a4429f5ad8f5?relay=wss%3A%2F%2Frelay1.example.com&secret=0s8j2djs&perms=nip44_encrypt%2Csign_event%3A1&name=My+Client&url=https%3A%2F%2Fclient.example.com";
nostr_nip46_nostrconnect_url_t nu;
rc = nostr_nip46_parse_nostrconnect_url(nc, &nu);
expect_int("parse nostrconnect url", NOSTR_SUCCESS, rc);
expect_int("nostrconnect relay count", 1, nu.relay_count);
expect_true("nostrconnect secret parsed", strcmp(nu.secret, "0s8j2djs") == 0);
expect_true("nostrconnect name decoded (+ to space)", strcmp(nu.name, "My Client") == 0);
char nc_roundtrip[2048];
rc = nostr_nip46_create_nostrconnect_url(&nu, nc_roundtrip, sizeof(nc_roundtrip));
expect_int("create nostrconnect url", NOSTR_SUCCESS, rc);
expect_true("nostrconnect roundtrip has scheme", strstr(nc_roundtrip, "nostrconnect://") == nc_roundtrip);
}
static void test_request_response_roundtrip(void) {
printf("\n=== test_request_response_roundtrip ===\n");
char id[65];
int rc = nostr_nip46_generate_request_id(id, sizeof(id));
expect_int("generate request id", NOSTR_SUCCESS, rc);
expect_true("request id hex length", strlen(id) == 32);
const char* params[] = {"abc", "def"};
nostr_nip46_request_t req;
rc = nostr_nip46_create_request(id, NOSTR_NIP46_METHOD_PING, params, 2, &req);
expect_int("create request", NOSTR_SUCCESS, rc);
char* req_json = NULL;
rc = nostr_nip46_request_to_json(&req, &req_json);
expect_int("request to json", NOSTR_SUCCESS, rc);
nostr_nip46_request_t parsed_req;
rc = nostr_nip46_parse_request(req_json, &parsed_req);
expect_int("parse request", NOSTR_SUCCESS, rc);
expect_true("parsed request id matches", strcmp(parsed_req.id, id) == 0);
expect_true("parsed request method string", strcmp(parsed_req.method_str, "ping") == 0);
expect_int("parsed request param count", 2, parsed_req.param_count);
free(req_json);
nostr_nip46_free_request(&req);
nostr_nip46_free_request(&parsed_req);
nostr_nip46_response_t resp;
rc = nostr_nip46_create_response(id, "pong", NULL, &resp);
expect_int("create response", NOSTR_SUCCESS, rc);
char* resp_json = NULL;
rc = nostr_nip46_response_to_json(&resp, &resp_json);
expect_int("response to json", NOSTR_SUCCESS, rc);
nostr_nip46_response_t parsed_resp;
rc = nostr_nip46_parse_response(resp_json, &parsed_resp);
expect_int("parse response", NOSTR_SUCCESS, rc);
expect_true("parsed response id matches", strcmp(parsed_resp.id, id) == 0);
expect_true("parsed response result matches", strcmp(parsed_resp.result, "pong") == 0);
free(resp_json);
nostr_nip46_free_response(&resp);
nostr_nip46_free_response(&parsed_resp);
}
static void test_event_encryption_flow(void) {
printf("\n=== test_event_encryption_flow ===\n");
const char* client_sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
const char* signer_sk_hex = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220";
unsigned char client_sk[32], signer_sk[32];
unsigned char signer_pk[32];
int rc = hex_to_bytes32(client_sk_hex, client_sk);
expect_int("client sk parse", 0, rc);
rc = hex_to_bytes32(signer_sk_hex, signer_sk);
expect_int("signer sk parse", 0, rc);
rc = nostr_ec_public_key_from_private_key(signer_sk, signer_pk);
expect_int("derive signer public key", 0, rc);
const char* params[] = {"hello"};
nostr_nip46_request_t req;
rc = nostr_nip46_create_request("abc123", NOSTR_NIP46_METHOD_PING, params, 1, &req);
expect_int("create ping request", NOSTR_SUCCESS, rc);
cJSON* evt = nostr_nip46_create_request_event(&req, client_sk, signer_pk, 0);
expect_true("create encrypted request event", evt != NULL);
char decrypted[65536];
rc = nostr_nip46_decrypt_event(evt, signer_sk, decrypted, sizeof(decrypted));
expect_int("decrypt request event", NOSTR_SUCCESS, rc);
expect_true("decrypted payload has method ping", strstr(decrypted, "\"method\":\"ping\"") != NULL);
cJSON_Delete(evt);
nostr_nip46_free_request(&req);
}
static void test_signer_handle_request(void) {
printf("\n=== test_signer_handle_request ===\n");
const char* signer_sk_hex = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220";
const char* user_sk_hex = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
unsigned char signer_sk[32], user_sk[32];
int rc = hex_to_bytes32(signer_sk_hex, signer_sk);
expect_int("parse signer sk", 0, rc);
rc = hex_to_bytes32(user_sk_hex, user_sk);
expect_int("parse user sk", 0, rc);
const char* relays[] = {"wss://relay.example.com"};
nostr_nip46_signer_session_t ss;
rc = nostr_nip46_signer_session_init(&ss, signer_sk, user_sk, relays, 1);
expect_int("signer session init", NOSTR_SUCCESS, rc);
const char* connect_params[] = { ss.signer_pubkey_hex, "secret123" };
nostr_nip46_request_t connect_req;
rc = nostr_nip46_create_request("req-connect", NOSTR_NIP46_METHOD_CONNECT, connect_params, 2, &connect_req);
expect_int("build connect request", NOSTR_SUCCESS, rc);
nostr_nip46_response_t connect_resp;
rc = nostr_nip46_signer_handle_request(&ss, &connect_req, &connect_resp);
expect_int("handle connect request", NOSTR_SUCCESS, rc);
expect_true("connect returns provided secret", connect_resp.result && strcmp(connect_resp.result, "secret123") == 0);
nostr_nip46_free_request(&connect_req);
nostr_nip46_free_response(&connect_resp);
nostr_nip46_request_t ping_req;
rc = nostr_nip46_create_request("req-ping", NOSTR_NIP46_METHOD_PING, NULL, 0, &ping_req);
expect_int("build ping request", NOSTR_SUCCESS, rc);
nostr_nip46_response_t ping_resp;
rc = nostr_nip46_signer_handle_request(&ss, &ping_req, &ping_resp);
expect_int("handle ping request", NOSTR_SUCCESS, rc);
expect_true("ping response is pong", ping_resp.result && strcmp(ping_resp.result, "pong") == 0);
nostr_nip46_free_request(&ping_req);
nostr_nip46_free_response(&ping_resp);
nostr_nip46_request_t gpk_req;
rc = nostr_nip46_create_request("req-gpk", NOSTR_NIP46_METHOD_GET_PUBLIC_KEY, NULL, 0, &gpk_req);
expect_int("build get_public_key request", NOSTR_SUCCESS, rc);
nostr_nip46_response_t gpk_resp;
rc = nostr_nip46_signer_handle_request(&ss, &gpk_req, &gpk_resp);
expect_int("handle get_public_key request", NOSTR_SUCCESS, rc);
expect_true("get_public_key returns hex", gpk_resp.result && strlen(gpk_resp.result) == 64);
nostr_nip46_free_request(&gpk_req);
nostr_nip46_free_response(&gpk_resp);
cJSON* tags = cJSON_CreateArray();
cJSON* unsigned_event = cJSON_CreateObject();
cJSON_AddNumberToObject(unsigned_event, "kind", 1);
cJSON_AddStringToObject(unsigned_event, "content", "hello signer");
cJSON_AddItemToObject(unsigned_event, "tags", tags);
cJSON_AddNumberToObject(unsigned_event, "created_at", (double)time(NULL));
char* unsigned_event_json = cJSON_PrintUnformatted(unsigned_event);
cJSON_Delete(unsigned_event);
const char* sign_params[] = { unsigned_event_json };
nostr_nip46_request_t sign_req;
rc = nostr_nip46_create_request("req-sign", NOSTR_NIP46_METHOD_SIGN_EVENT, sign_params, 1, &sign_req);
expect_int("build sign_event request", NOSTR_SUCCESS, rc);
nostr_nip46_response_t sign_resp;
rc = nostr_nip46_signer_handle_request(&ss, &sign_req, &sign_resp);
expect_int("handle sign_event request", NOSTR_SUCCESS, rc);
expect_true("sign_event response contains id field", sign_resp.result && strstr(sign_resp.result, "\"id\"") != NULL);
expect_true("sign_event response contains sig field", sign_resp.result && strstr(sign_resp.result, "\"sig\"") != NULL);
nostr_nip46_free_request(&sign_req);
nostr_nip46_free_response(&sign_resp);
free(unsigned_event_json);
nostr_nip46_signer_session_destroy(&ss);
}
int main(void) {
printf("🧪 NIP-46 Test Suite\n");
printf("===================\n");
int init_rc = nostr_init();
if (init_rc != NOSTR_SUCCESS) {
printf("❌ Failed to initialize nostr library: %s\n", nostr_strerror(init_rc));
return 1;
}
test_url_parsing_and_generation();
test_request_response_roundtrip();
test_event_encryption_flow();
test_signer_handle_request();
nostr_cleanup();
printf("\n=== RESULT ===\n");
printf("Passed %d / %d tests\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}
+402
View File
@@ -0,0 +1,402 @@
/*
* NIP-60 Live Cashu Receive Integration Test
*
* This test performs a live mint against testnut mints, constructs a cashuA token
* string from the returned signature, and verifies our receive/decode path.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../nostr_core/nostr_core.h"
/* private secp256k1 wrappers from utils.c */
typedef struct {
unsigned char data[64];
} nostr_secp256k1_pubkey;
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey* pubkey, const unsigned char* input, size_t inputlen);
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static int secret_to_Y_hex(const char* secret, char out_Y_hex[67]) {
if (!secret || !out_Y_hex) return NOSTR_ERROR_INVALID_INPUT;
unsigned char digest[32] = {0};
if (nostr_sha256((const unsigned char*)secret, strlen(secret), digest) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
unsigned char candidate[33] = {0};
for (int counter = 0; counter < 65536; counter++) {
unsigned char h[32] = {0};
unsigned char data[37] = {0};
data[0] = 0x02;
memcpy(data + 1, digest, 32);
data[33] = (unsigned char)(counter & 0xff);
data[34] = (unsigned char)((counter >> 8) & 0xff);
data[35] = (unsigned char)((counter >> 16) & 0xff);
data[36] = (unsigned char)((counter >> 24) & 0xff);
if (nostr_sha256(data, sizeof(data), h) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
candidate[0] = 0x02;
memcpy(candidate + 1, h, 32);
nostr_secp256k1_pubkey pub;
if (nostr_secp256k1_ec_pubkey_parse(&pub, candidate, sizeof(candidate))) {
nostr_bytes_to_hex(candidate, sizeof(candidate), out_Y_hex);
return NOSTR_SUCCESS;
}
}
return NOSTR_ERROR_CRYPTO_FAILED;
}
static int base64_to_base64url_no_pad(const unsigned char* in, size_t in_len, char* out, size_t out_size) {
if (!in || !out || out_size == 0) return NOSTR_ERROR_INVALID_INPUT;
size_t needed = ((in_len + 2) / 3) * 4 + 1;
char* b64 = (char*)malloc(needed);
if (!b64) return NOSTR_ERROR_MEMORY_FAILED;
size_t n = base64_encode(in, in_len, b64, needed);
if (n == 0) {
free(b64);
return NOSTR_ERROR_INVALID_INPUT;
}
size_t j = 0;
for (size_t i = 0; i < n && b64[i] != '\0'; i++) {
char c = b64[i];
if (c == '=') continue;
if (c == '+') c = '-';
else if (c == '/') c = '_';
if (j + 1 >= out_size) {
free(b64);
return NOSTR_ERROR_INVALID_INPUT;
}
out[j++] = c;
}
out[j] = '\0';
free(b64);
return NOSTR_SUCCESS;
}
static int build_cashuA_token_string(const char* mint_url,
const char* keyset_id,
uint64_t amount,
const char* secret,
const char* C_hex,
char* out_token,
size_t out_token_size) {
if (!mint_url || !keyset_id || !secret || !C_hex || !out_token || out_token_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* root = cJSON_CreateObject();
cJSON* token_arr = cJSON_CreateArray();
cJSON* entry = cJSON_CreateObject();
cJSON* proofs = cJSON_CreateArray();
cJSON* proof = cJSON_CreateObject();
if (!root || !token_arr || !entry || !proofs || !proof) {
cJSON_Delete(root);
cJSON_Delete(token_arr);
cJSON_Delete(entry);
cJSON_Delete(proofs);
cJSON_Delete(proof);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(entry, "mint", mint_url);
cJSON_AddStringToObject(proof, "id", keyset_id);
cJSON_AddNumberToObject(proof, "amount", (double)amount);
cJSON_AddStringToObject(proof, "secret", secret);
cJSON_AddStringToObject(proof, "C", C_hex);
cJSON_AddItemToArray(proofs, proof);
cJSON_AddItemToObject(entry, "proofs", proofs);
cJSON_AddItemToArray(token_arr, entry);
cJSON_AddItemToObject(root, "token", token_arr);
char* json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!json) return NOSTR_ERROR_MEMORY_FAILED;
char b64url[8192];
int rc = base64_to_base64url_no_pad((const unsigned char*)json, strlen(json), b64url, sizeof(b64url));
free(json);
if (rc != NOSTR_SUCCESS) return rc;
int written = snprintf(out_token, out_token_size, "cashuA%s", b64url);
if (written < 0 || (size_t)written >= out_token_size) {
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}
static void test_live_mint_and_receive(const char* mint_url) {
printf("\n=== test_live_mint_and_receive: %s ===\n", mint_url);
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
int rc = cashu_mint_get_info(mint_url, &info, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "get mint info");
if (rc != NOSTR_SUCCESS) return;
TEST_ASSERT(info.pubkey != NULL && strlen(info.pubkey) == 66, "mint pubkey available");
if (!info.pubkey || strlen(info.pubkey) != 66) {
cashu_mint_free_info(&info);
return;
}
cashu_keyset_t active_keyset;
memset(&active_keyset, 0, sizeof(active_keyset));
rc = cashu_mint_get_active_keyset(&info, "sat", &active_keyset);
TEST_ASSERT(rc == NOSTR_SUCCESS, "select active sat keyset");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_keyset_keys_t keyset_keys;
memset(&keyset_keys, 0, sizeof(keyset_keys));
rc = cashu_mint_get_keys(mint_url, active_keyset.id, &keyset_keys, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "fetch per-amount keyset keys");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
const char* amount_pubkey = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, 1, &amount_pubkey);
TEST_ASSERT(rc == NOSTR_SUCCESS && amount_pubkey != NULL, "find pubkey for amount=1");
if (rc != NOSTR_SUCCESS || !amount_pubkey) {
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
cashu_mint_quote_t quote;
memset(&quote, 0, sizeof(quote));
rc = cashu_mint_request_mint_quote(mint_url, 1, "sat", &quote, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "request mint quote");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_mint_quote_t quote_check;
memset(&quote_check, 0, sizeof(quote_check));
rc = cashu_mint_check_mint_quote(mint_url, quote.quote_id, &quote_check, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "check mint quote");
TEST_ASSERT(quote_check.paid == 1, "mint quote paid");
if (rc != NOSTR_SUCCESS || quote_check.paid != 1) {
cashu_mint_free_info(&info);
return;
}
char secret[160];
snprintf(secret, sizeof(secret), "nostr-live-receive-test-%ld-%s", (long)time(NULL), mint_url);
char B_hex[67];
unsigned char r[32];
rc = cashu_blind_message(secret, info.pubkey, B_hex, r);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blind message");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_blinded_output_t output;
memset(&output, 0, sizeof(output));
size_t keyset_len = strlen(active_keyset.id);
TEST_ASSERT(keyset_len < sizeof(output.id), "keyset id fits library struct");
if (keyset_len >= sizeof(output.id)) {
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
memcpy(output.id, active_keyset.id, keyset_len + 1);
output.amount = 1;
snprintf(output.B_, sizeof(output.B_), "%s", B_hex);
cashu_mint_tokens_request_t mint_req;
memset(&mint_req, 0, sizeof(mint_req));
snprintf(mint_req.quote_id, sizeof(mint_req.quote_id), "%s", quote.quote_id);
snprintf(mint_req.unit, sizeof(mint_req.unit), "sat");
mint_req.outputs = &output;
mint_req.output_count = 1;
cJSON* mint_body = NULL;
rc = cashu_build_mint_tokens_request(&mint_req, &mint_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && mint_body != NULL, "build mint tokens request");
if (rc != NOSTR_SUCCESS || !mint_body) {
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
cJSON* mint_resp_json = NULL;
rc = cashu_mint_mint_tokens(mint_url, mint_body, &mint_resp_json, 15);
cJSON_Delete(mint_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && mint_resp_json != NULL, "mint tokens call");
if (rc != NOSTR_SUCCESS || !mint_resp_json) {
cJSON_Delete(mint_resp_json);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
cashu_mint_tokens_response_t mint_resp;
memset(&mint_resp, 0, sizeof(mint_resp));
rc = cashu_parse_mint_tokens_response(mint_resp_json, &mint_resp);
cJSON_Delete(mint_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse mint tokens response");
TEST_ASSERT(mint_resp.signature_count >= 1, "mint returned signatures");
if (rc != NOSTR_SUCCESS || mint_resp.signature_count < 1) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
char C_hex[67];
rc = cashu_unblind_signature(mint_resp.signatures[0].C_, amount_pubkey, r, C_hex);
TEST_ASSERT(rc == NOSTR_SUCCESS, "unblind signature with per-amount key");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
nostr_cashu_proof_t send_proof;
memset(&send_proof, 0, sizeof(send_proof));
snprintf(send_proof.id, sizeof(send_proof.id), "%s", active_keyset.id);
send_proof.amount = 1;
send_proof.secret = secret;
send_proof.C = C_hex;
cashu_decoded_token_t token_for_encode;
memset(&token_for_encode, 0, sizeof(token_for_encode));
token_for_encode.mint_url = (char*)mint_url;
token_for_encode.proofs = &send_proof;
token_for_encode.proof_count = 1;
char* token_a = NULL;
rc = cashu_encode_token(&token_for_encode, CASHU_TOKEN_FORMAT_A, &token_a);
TEST_ASSERT(rc == NOSTR_SUCCESS && token_a != NULL, "encode cashuA token");
if (rc != NOSTR_SUCCESS || !token_a) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
char* token_b = NULL;
rc = cashu_encode_token(&token_for_encode, CASHU_TOKEN_FORMAT_B, &token_b);
TEST_ASSERT(rc == NOSTR_SUCCESS && token_b != NULL, "encode cashuB token");
if (rc != NOSTR_SUCCESS || !token_b) {
free(token_a);
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
return;
}
cashu_decoded_token_t decoded;
memset(&decoded, 0, sizeof(decoded));
rc = cashu_decode_token(token_a, &decoded);
TEST_ASSERT(rc == NOSTR_SUCCESS, "decode cashuA token");
TEST_ASSERT(decoded.mint_url != NULL && strcmp(decoded.mint_url, mint_url) == 0, "cashuA decoded mint url matches");
TEST_ASSERT(decoded.proof_count == 1, "cashuA decoded proof count");
TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].amount == 1, "cashuA decoded proof amount");
TEST_ASSERT(decoded.proofs != NULL && strcmp(decoded.proofs[0].id, active_keyset.id) == 0, "cashuA decoded keyset id");
TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].secret != NULL && strcmp(decoded.proofs[0].secret, secret) == 0,
"cashuA decoded secret matches");
cashu_decoded_token_t decoded_b;
memset(&decoded_b, 0, sizeof(decoded_b));
rc = cashu_decode_token(token_b, &decoded_b);
TEST_ASSERT(rc == NOSTR_SUCCESS, "decode cashuB token");
TEST_ASSERT(decoded_b.mint_url != NULL && strcmp(decoded_b.mint_url, mint_url) == 0, "cashuB decoded mint url matches");
TEST_ASSERT(decoded_b.proof_count == 1, "cashuB decoded proof count");
TEST_ASSERT(decoded_b.proofs != NULL && decoded_b.proofs[0].amount == 1, "cashuB decoded proof amount");
TEST_ASSERT(decoded_b.proofs != NULL && strcmp(decoded_b.proofs[0].id, active_keyset.id) == 0, "cashuB decoded keyset id");
TEST_ASSERT(decoded_b.proofs != NULL && decoded_b.proofs[0].secret != NULL && strcmp(decoded_b.proofs[0].secret, secret) == 0,
"cashuB decoded secret matches");
char Y_hex[67];
rc = secret_to_Y_hex(secret, Y_hex);
TEST_ASSERT(rc == NOSTR_SUCCESS, "derive Y from secret for checkstate");
if (rc == NOSTR_SUCCESS) {
const char* Ys[1] = {Y_hex};
cJSON* check_req = NULL;
rc = cashu_build_checkstate_request(Ys, 1, &check_req);
TEST_ASSERT(rc == NOSTR_SUCCESS && check_req != NULL, "build checkstate request");
if (rc == NOSTR_SUCCESS && check_req != NULL) {
cJSON* check_resp_json = NULL;
rc = cashu_mint_check_proofs_state(mint_url, check_req, &check_resp_json, 15);
cJSON_Delete(check_req);
TEST_ASSERT(rc == NOSTR_SUCCESS && check_resp_json != NULL, "checkstate call");
if (rc == NOSTR_SUCCESS && check_resp_json != NULL) {
cashu_checkstate_response_t state;
memset(&state, 0, sizeof(state));
rc = cashu_parse_checkstate_response(check_resp_json, &state);
cJSON_Delete(check_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse checkstate response");
TEST_ASSERT(state.state_count >= 1, "checkstate returned at least one state");
TEST_ASSERT(state.state_count >= 1 && strcmp(state.states[0].state, "UNSPENT") == 0,
"mint reports proof as UNSPENT");
cashu_mint_free_checkstate_response(&state);
}
}
}
cashu_free_decoded_token(&decoded_b);
cashu_free_decoded_token(&decoded);
free(token_b);
free(token_a);
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
}
int main(void) {
printf("NIP-60 Live Cashu Receive Integration Test\n");
printf("========================================\n");
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize NOSTR library\n");
return 1;
}
test_live_mint_and_receive("https://nofee.testnut.cashu.space");
printf("\n=== Test Summary ===\n");
printf("Passed: %d/%d\n", tests_passed, tests_run);
nostr_cleanup();
return (tests_passed == tests_run) ? 0 : 1;
}
+185
View File
@@ -0,0 +1,185 @@
/*
* NIP-60 Cashu Wallet Test Suite
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "../nostr_core/nostr_core.h"
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static int hex_to_bytes32(const char* hex, unsigned char out[32]) {
return nostr_hex_to_bytes(hex, out, 32) == 0 ? 0 : -1;
}
static void test_wallet_roundtrip(void) {
printf("\n=== test_wallet_roundtrip ===\n");
const char* sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
char* mints[] = {
"https://mint1.example.com",
"https://mint2.example.com"
};
nostr_nip60_wallet_data_t in;
memset(&in, 0, sizeof(in));
strcpy(in.privkey, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
in.mint_urls = mints;
in.mint_count = 2;
cJSON* evt = nostr_nip60_create_wallet_event(&in, sk, 0);
TEST_ASSERT(evt != NULL, "create wallet event");
nostr_nip60_wallet_data_t out;
memset(&out, 0, sizeof(out));
int rc = nostr_nip60_parse_wallet_event(evt, sk, &out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse wallet event");
TEST_ASSERT(strcmp(out.privkey, in.privkey) == 0, "wallet privkey preserved");
TEST_ASSERT(out.mint_count == 2, "wallet mint count");
TEST_ASSERT(strcmp(out.mint_urls[0], mints[0]) == 0, "wallet mint 0");
TEST_ASSERT(strcmp(out.mint_urls[1], mints[1]) == 0, "wallet mint 1");
nostr_nip60_free_wallet_data(&out);
cJSON_Delete(evt);
}
static void test_token_roundtrip_and_sum(void) {
printf("\n=== test_token_roundtrip_and_sum ===\n");
const char* sk_hex = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
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 = 8;
proofs[1].secret = "secret-8";
proofs[1].C = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
char* del_ids[] = {"event-id-1"};
nostr_nip60_token_data_t token;
memset(&token, 0, sizeof(token));
token.mint_url = "https://mint.example.com";
token.proofs = proofs;
token.proof_count = 2;
token.deleted_token_ids = del_ids;
token.deleted_count = 1;
TEST_ASSERT(nostr_nip60_sum_proofs(proofs, 2) == 9, "proof sum");
cJSON* evt = nostr_nip60_create_token_event(&token, sk, 0);
TEST_ASSERT(evt != NULL, "create token event");
nostr_nip60_token_data_t out;
memset(&out, 0, sizeof(out));
int rc = nostr_nip60_parse_token_event(evt, sk, &out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse token event");
TEST_ASSERT(strcmp(out.mint_url, token.mint_url) == 0, "token mint");
TEST_ASSERT(out.proof_count == 2, "token proof count");
TEST_ASSERT(out.deleted_count == 1, "token del count");
TEST_ASSERT(nostr_nip60_sum_proofs(out.proofs, out.proof_count) == 9, "parsed proof sum");
cJSON* del_evt = nostr_nip60_create_token_deletion("event-id-1", sk, 0);
TEST_ASSERT(del_evt != NULL, "create token deletion event");
nostr_nip60_free_token_data(&out);
cJSON_Delete(del_evt);
cJSON_Delete(evt);
}
static void test_history_quote_and_filters(void) {
printf("\n=== test_history_quote_and_filters ===\n");
const char* sk_hex = "1111111111111111111111111111111111111111111111111111111111111111";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
nostr_nip60_history_ref_t refs[2];
memset(refs, 0, sizeof(refs));
strcpy(refs[0].event_id, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
strcpy(refs[0].relay_hint, "wss://relay.example.com");
refs[0].marker = NOSTR_NIP60_REF_DESTROYED;
strcpy(refs[1].event_id, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
refs[1].marker = NOSTR_NIP60_REF_CREATED;
nostr_nip60_history_data_t hist;
memset(&hist, 0, sizeof(hist));
hist.direction = NOSTR_NIP60_DIRECTION_OUT;
hist.amount = 4;
hist.refs = refs;
hist.ref_count = 2;
cJSON* hist_evt = nostr_nip60_create_history_event(&hist, sk, 0);
TEST_ASSERT(hist_evt != NULL, "create history event");
nostr_nip60_history_data_t parsed;
memset(&parsed, 0, sizeof(parsed));
int rc = nostr_nip60_parse_history_event(hist_evt, sk, &parsed);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse history event");
TEST_ASSERT(parsed.direction == NOSTR_NIP60_DIRECTION_OUT, "history direction");
TEST_ASSERT(parsed.amount == 4, "history amount");
TEST_ASSERT(parsed.ref_count == 2, "history refs");
time_t exp = 2000000000;
cJSON* quote_evt = nostr_nip60_create_quote_event("quote-123", "https://mint.example.com", exp, sk, 0);
TEST_ASSERT(quote_evt != NULL, "create quote event");
char quote_id[128];
char mint[256];
time_t exp_out = 0;
rc = nostr_nip60_parse_quote_event(quote_evt, sk, quote_id, sizeof(quote_id), mint, sizeof(mint), &exp_out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse quote event");
TEST_ASSERT(strcmp(quote_id, "quote-123") == 0, "quote id");
TEST_ASSERT(strcmp(mint, "https://mint.example.com") == 0, "quote mint");
TEST_ASSERT(exp_out == exp, "quote expiration");
cJSON* wf = nostr_nip60_create_wallet_filter("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
cJSON* hf = nostr_nip60_create_history_filter("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1700000000);
TEST_ASSERT(wf != NULL, "wallet filter");
TEST_ASSERT(hf != NULL, "history filter");
cJSON_Delete(wf);
cJSON_Delete(hf);
cJSON_Delete(quote_evt);
nostr_nip60_free_history_data(&parsed);
cJSON_Delete(hist_evt);
}
int main(void) {
printf("NIP-60 Cashu Wallet Tests\n");
printf("==========================\n");
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize NOSTR library\n");
return 1;
}
test_wallet_roundtrip();
test_token_roundtrip_and_sum();
test_history_quote_and_filters();
printf("\n=== Test Summary ===\n");
printf("Passed: %d/%d\n", tests_passed, tests_run);
nostr_cleanup();
return (tests_passed == tests_run) ? 0 : 1;
}
+199
View File
@@ -0,0 +1,199 @@
/*
* NIP-61 Nutzaps Test Suite
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "../nostr_core/nostr_core.h"
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static int hex_to_bytes32(const char* hex, unsigned char out[32]) {
return nostr_hex_to_bytes(hex, out, 32) == 0 ? 0 : -1;
}
static void test_nutzap_info_roundtrip(void) {
printf("\n=== test_nutzap_info_roundtrip ===\n");
const char* sk_hex = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
char* relays[] = {"wss://relay1.example.com", "wss://relay2.example.com"};
char* mint1_units[] = {"usd", "sat"};
char* mint2_units[] = {"sat"};
nostr_nip61_mint_entry_t mints[2];
memset(mints, 0, sizeof(mints));
mints[0].url = "https://mint1.example.com";
mints[0].units = mint1_units;
mints[0].unit_count = 2;
mints[1].url = "https://mint2.example.com";
mints[1].units = mint2_units;
mints[1].unit_count = 1;
nostr_nip61_nutzap_info_t info;
memset(&info, 0, sizeof(info));
info.relay_urls = relays;
info.relay_count = 2;
info.mints = mints;
info.mint_count = 2;
strcpy(info.pubkey, "02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed");
cJSON* evt = nostr_nip61_create_nutzap_info_event(&info, sk, 0);
TEST_ASSERT(evt != NULL, "create nutzap info event");
nostr_nip61_nutzap_info_t out;
memset(&out, 0, sizeof(out));
int rc = nostr_nip61_parse_nutzap_info_event(evt, &out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse nutzap info event");
TEST_ASSERT(out.relay_count == 2, "relay count");
TEST_ASSERT(out.mint_count == 2, "mint count");
TEST_ASSERT(strcmp(out.pubkey, info.pubkey) == 0, "pubkey value");
nostr_nip61_free_nutzap_info(&out);
cJSON_Delete(evt);
}
static void test_nutzap_event_and_filters(void) {
printf("\n=== test_nutzap_event_and_filters ===\n");
const char* sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
nostr_cashu_proof_t proofs[1];
memset(proofs, 0, sizeof(proofs));
strcpy(proofs[0].id, "005c2502034d4f12");
proofs[0].amount = 1;
proofs[0].secret = "[\"P2PK\",{\"nonce\":\"n1\",\"data\":\"02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed\"}]";
proofs[0].C = "02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f";
nostr_nip61_nutzap_data_t in;
memset(&in, 0, sizeof(in));
in.content = "Thanks for this great idea.";
in.proofs = proofs;
in.proof_count = 1;
in.mint_url = "https://mint1.example.com";
strcpy(in.recipient_pubkey, "e9fbced3a42dcf551486650cc752ab354347dd413b307484e4fd1818ab53f991");
strcpy(in.nutzapped_event_id, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
strcpy(in.nutzapped_relay_hint, "wss://relay.example.com");
in.nutzapped_kind = 1;
cJSON* evt = nostr_nip61_create_nutzap_event(&in, sk, 0);
TEST_ASSERT(evt != NULL, "create nutzap event");
nostr_nip61_nutzap_data_t out;
memset(&out, 0, sizeof(out));
int rc = nostr_nip61_parse_nutzap_event(evt, &out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse nutzap event");
TEST_ASSERT(out.proof_count == 1, "proof count");
TEST_ASSERT(strcmp(out.mint_url, in.mint_url) == 0, "mint url");
TEST_ASSERT(strcmp(out.recipient_pubkey, in.recipient_pubkey) == 0, "recipient pubkey");
cJSON* info_filter = nostr_nip61_create_nutzap_info_filter(in.recipient_pubkey);
TEST_ASSERT(info_filter != NULL, "create nutzap info filter");
const char* mints[] = {"https://mint1.example.com", "https://mint2.example.com"};
cJSON* nz_filter = nostr_nip61_create_nutzap_filter(in.recipient_pubkey, mints, 2, 1700000000);
TEST_ASSERT(nz_filter != NULL, "create nutzap filter");
cJSON* redeem_evt = nostr_nip61_create_redemption_event(
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"wss://sender-relay.example.com",
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"wss://my-relay.example.com",
1,
sk,
0
);
TEST_ASSERT(redeem_evt != NULL, "create redemption event");
nostr_nip61_free_nutzap_data(&out);
cJSON_Delete(redeem_evt);
cJSON_Delete(nz_filter);
cJSON_Delete(info_filter);
cJSON_Delete(evt);
}
static void test_nutzap_verify(void) {
printf("\n=== test_nutzap_verify ===\n");
const char* sk_hex = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
char* relays[] = {"wss://relay.example.com"};
char* mint_units[] = {"sat"};
nostr_nip61_mint_entry_t mint;
memset(&mint, 0, sizeof(mint));
mint.url = "https://mint-verify.example.com";
mint.units = mint_units;
mint.unit_count = 1;
nostr_nip61_nutzap_info_t info;
memset(&info, 0, sizeof(info));
info.relay_urls = relays;
info.relay_count = 1;
info.mints = &mint;
info.mint_count = 1;
strcpy(info.pubkey, "02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed");
cJSON* info_evt = nostr_nip61_create_nutzap_info_event(&info, sk, 0);
TEST_ASSERT(info_evt != NULL, "create info event for verify");
nostr_cashu_proof_t proof;
memset(&proof, 0, sizeof(proof));
strcpy(proof.id, "005c2502034d4f12");
proof.amount = 1;
proof.secret = "[\"P2PK\",{\"nonce\":\"n1\",\"data\":\"02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed\"}]";
proof.C = "02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f";
nostr_nip61_nutzap_data_t nz;
memset(&nz, 0, sizeof(nz));
nz.content = "zap";
nz.proofs = &proof;
nz.proof_count = 1;
nz.mint_url = "https://mint-verify.example.com";
strcpy(nz.recipient_pubkey, "e9fbced3a42dcf551486650cc752ab354347dd413b307484e4fd1818ab53f991");
cJSON* nz_evt = nostr_nip61_create_nutzap_event(&nz, sk, 0);
TEST_ASSERT(nz_evt != NULL, "create nutzap event for verify");
int rc = nostr_nip61_verify_nutzap(nz_evt, info_evt);
TEST_ASSERT(rc == NOSTR_SUCCESS, "verify nutzap against info");
cJSON_Delete(nz_evt);
cJSON_Delete(info_evt);
}
int main(void) {
printf("NIP-61 Nutzaps Tests\n");
printf("====================\n");
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize NOSTR library\n");
return 1;
}
test_nutzap_info_roundtrip();
test_nutzap_event_and_filters();
test_nutzap_verify();
printf("\n=== Test Summary ===\n");
printf("Passed: %d/%d\n", tests_passed, tests_run);
nostr_cleanup();
return (tests_passed == tests_run) ? 0 : 1;
}
+141
View File
@@ -0,0 +1,141 @@
#define _POSIX_C_SOURCE 200809L
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
const char** relays;
int relay_count;
const char* pubkey;
int timeout_ms;
} test_config_t;
static cJSON* build_kind10002_filter(const char* pubkey_hex) {
if (!pubkey_hex || pubkey_hex[0] == '\0') {
return NULL;
}
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
if (!filter || !kinds || !authors) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
return NULL;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10002));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(16));
return filter;
}
static void free_events(cJSON** events, int event_count) {
if (!events) return;
for (int i = 0; i < event_count; i++) {
if (events[i]) cJSON_Delete(events[i]);
}
free(events);
}
static void print_event_summary(const char* label, cJSON** events, int event_count) {
printf("%s event_count=%d\n", label, event_count);
for (int i = 0; i < event_count; i++) {
cJSON* id = cJSON_GetObjectItemCaseSensitive(events[i], "id");
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(events[i], "created_at");
const char* id_str = (id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "(no-id)";
long long ts = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0;
printf(" [%d] id=%.16s... created_at=%lld\n", i, id_str, ts);
}
}
static int run_query(nostr_relay_pool_t* pool, const test_config_t* cfg, const char* label) {
int event_count = 0;
cJSON* filter = build_kind10002_filter(cfg->pubkey);
if (!filter) {
fprintf(stderr, "[%s] failed to build filter\n", label);
return -1;
}
cJSON** events = nostr_relay_pool_query_sync(
pool,
cfg->relays,
cfg->relay_count,
filter,
&event_count,
cfg->timeout_ms
);
cJSON_Delete(filter);
print_event_summary(label, events, event_count);
free_events(events, event_count);
return event_count;
}
int main(int argc, char** argv) {
const char* pubkey = "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8";
if (argc > 1 && argv[1] && argv[1][0] != '\0') {
pubkey = argv[1];
}
const char* relays[] = {
"wss://relay.damus.io",
"wss://relay.primal.net"
};
test_config_t cfg = {
.relays = relays,
.relay_count = 2,
.pubkey = pubkey,
.timeout_ms = 5000
};
printf("=== repeated_sync_query_test ===\n");
printf("pubkey=%s\n", cfg.pubkey);
printf("relay[0]=%s\n", cfg.relays[0]);
printf("relay[1]=%s\n", cfg.relays[1]);
printf("timeout_ms=%d\n", cfg.timeout_ms);
printf("Set NOSTR_POOL_QUERY_SYNC_DEBUG=1 for internal query-sync tracing.\n\n");
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "nostr_init failed\n");
return 1;
}
nostr_relay_pool_t* pool = nostr_relay_pool_create(NULL);
if (!pool) {
fprintf(stderr, "nostr_relay_pool_create failed\n");
nostr_cleanup();
return 1;
}
int first_count = run_query(pool, &cfg, "first_query");
int second_count = run_query(pool, &cfg, "second_query");
int exit_code = 0;
if (first_count <= 0) {
fprintf(stderr, "FAIL: first query returned no events\n");
exit_code = 2;
} else if (second_count <= 0) {
fprintf(stderr, "FAIL: second query returned no events (regression candidate)\n");
exit_code = 3;
} else {
printf("PASS: both queries returned events\n");
}
nostr_relay_pool_destroy(pool);
nostr_cleanup();
return exit_code;
}
+73
View File
@@ -0,0 +1,73 @@
#define _DEFAULT_SOURCE
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// Test callback function
static int callback_count = 0;
void test_callback(const char* relay_url, const char* event_id,
int success, const char* message, void* user_data) {
(void)event_id; // Suppress unused parameter warning
(void)user_data; // Suppress unused parameter warning
callback_count++;
printf("📡 Callback %d: Relay %s, Success: %s\n",
callback_count, relay_url, success ? "YES" : "NO");
if (message) {
printf(" Message: %s\n", message);
}
}
int main() {
printf("🧪 Simple Async Publish Test\n");
printf("============================\n");
// Create pool
nostr_relay_pool_t* pool = nostr_relay_pool_create(NULL);
if (!pool) {
printf("❌ Failed to create pool\n");
return 1;
}
// Create a test event
cJSON* event = cJSON_CreateObject();
cJSON_AddStringToObject(event, "id", "test_event_simple");
cJSON_AddNumberToObject(event, "kind", 1);
cJSON_AddStringToObject(event, "content", "Test async publish");
cJSON_AddNumberToObject(event, "created_at", time(NULL));
cJSON_AddStringToObject(event, "pubkey", "test_pubkey");
cJSON_AddStringToObject(event, "sig", "test_signature");
// Test with non-existent relay (should trigger connection failure callback)
const char* test_relays[] = {"ws://nonexistent.example.com"};
printf("🚀 Testing async publish...\n");
// Call async publish
int sent_count = nostr_relay_pool_publish_async(
pool, test_relays, 1, event, test_callback, NULL);
printf("📊 Sent to %d relays\n", sent_count);
// Wait a bit for callback
printf("⏳ Waiting for callback...\n");
for (int i = 0; i < 5 && callback_count == 0; i++) {
nostr_relay_pool_poll(pool, 100);
usleep(100000); // 100ms
}
printf("\n📈 Results:\n");
printf(" Callbacks received: %d\n", callback_count);
// Cleanup
cJSON_Delete(event);
nostr_relay_pool_destroy(pool);
printf("\n✅ Simple async test completed!\n");
return callback_count > 0 ? 0 : 1;
}
+188
View File
@@ -0,0 +1,188 @@
/*
* Simple WebSocket Debug Tool for NIP-17 Testing
*
* Connects to a relay and sends a subscription request to see what responses we get.
*/
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "../nostr_core/nostr_core.h"
#include "../nostr_websocket/nostr_websocket_tls.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <relay_url> [event_id]\n", argv[0]);
fprintf(stderr, "Example: websocket_debug wss://relay.laantungir.net 06cdf2cdd095ddb1ebe15d5b3c736b27a34de2683e847b871fe37d86ac998772\n");
return 1;
}
const char* relay_url = argv[1];
const char* event_id = (argc >= 3) ? argv[2] : NULL;
printf("🔍 WebSocket Debug Tool\n");
printf("=======================\n");
printf("Relay: %s\n", relay_url);
if (event_id) {
printf("Looking for event: %s\n", event_id);
}
printf("\n");
// Initialize crypto
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "Failed to initialize crypto\n");
return 1;
}
// Connect to relay
printf("🔌 Connecting to relay...\n");
nostr_ws_client_t* client = nostr_ws_connect(relay_url);
if (!client) {
fprintf(stderr, "Failed to connect to relay - nostr_ws_connect returned NULL\n");
return 1;
}
// Check initial state
nostr_ws_state_t initial_state = nostr_ws_get_state(client);
printf("Initial connection state: %d\n", (int)initial_state);
// Wait for connection
time_t start_time = time(NULL);
while (time(NULL) - start_time < 10) { // 10 second timeout
nostr_ws_state_t state = nostr_ws_get_state(client);
if (state == NOSTR_WS_CONNECTED) {
printf("✅ Connected!\n");
break;
} else if (state == NOSTR_WS_ERROR) {
fprintf(stderr, "❌ Connection failed\n");
nostr_ws_close(client);
return 1;
}
usleep(100000); // 100ms
}
if (nostr_ws_get_state(client) != NOSTR_WS_CONNECTED) {
fprintf(stderr, "❌ Connection timeout\n");
nostr_ws_close(client);
return 1;
}
// Send subscription request
printf("📡 Sending subscription request...\n");
// Create filter for kind 1059 events
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1059));
cJSON_AddItemToObject(filter, "kinds", kinds);
// If we have a specific event ID, add it to the filter
if (event_id) {
cJSON* ids = cJSON_CreateArray();
cJSON_AddItemToArray(ids, cJSON_CreateString(event_id));
cJSON_AddItemToObject(filter, "ids", ids);
}
char* filter_json = cJSON_PrintUnformatted(filter);
printf("Filter: %s\n", filter_json);
// Send REQ message
char subscription_id[32];
snprintf(subscription_id, sizeof(subscription_id), "debug_%ld", time(NULL));
if (nostr_relay_send_req(client, subscription_id, filter) < 0) {
fprintf(stderr, "Failed to send subscription request\n");
cJSON_Delete(filter);
free(filter_json);
nostr_ws_close(client);
return 1;
}
cJSON_Delete(filter);
free(filter_json);
printf("✅ Subscription sent (ID: %s)\n", subscription_id);
printf("⏳ Listening for responses (30 seconds)...\n");
printf("Press Ctrl+C to stop\n\n");
// Listen for responses
start_time = time(NULL);
int message_count = 0;
while (time(NULL) - start_time < 30) { // 30 second timeout
char buffer[16384];
int len = nostr_ws_receive(client, buffer, sizeof(buffer) - 1, 1000); // 1 second timeout
if (len > 0) {
buffer[len] = '\0';
message_count++;
printf("📨 Message %d:\n", message_count);
printf("%s\n", buffer);
// Parse the message
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
printf(" → EVENT received\n");
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* event = cJSON_GetArrayItem(parsed, 2);
if (event) {
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* id_item = cJSON_GetObjectItem(event, "id");
if (kind_item && cJSON_IsNumber(kind_item)) {
printf(" → Kind: %d\n", (int)cJSON_GetNumberValue(kind_item));
}
if (id_item && cJSON_IsString(id_item)) {
printf(" → ID: %.12s...\n", cJSON_GetStringValue(id_item));
}
}
}
} else if (msg_type && strcmp(msg_type, "EOSE") == 0) {
printf(" → EOSE (End of Stored Events)\n");
} else if (msg_type && strcmp(msg_type, "NOTICE") == 0) {
printf(" → NOTICE from relay\n");
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
cJSON* notice_msg = cJSON_GetArrayItem(parsed, 1);
if (notice_msg && cJSON_IsString(notice_msg)) {
printf(" → Message: %s\n", cJSON_GetStringValue(notice_msg));
}
}
} else if (msg_type) {
printf(" → %s\n", msg_type);
}
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
}
printf("\n");
} else if (len < 0) {
printf("❌ Receive error\n");
break;
}
// Small delay to prevent busy waiting
usleep(10000); // 10ms
}
printf("📊 Total messages received: %d\n", message_count);
// Send CLOSE message
printf("🔌 Closing subscription...\n");
nostr_relay_send_close(client, subscription_id);
// Close connection
nostr_ws_close(client);
nostr_cleanup();
printf("✅ Done\n");
return 0;
}