Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c6bcba650 | ||
|
|
7aea4b07df | ||
|
|
84257b2f58 | ||
|
|
af4f7cace9 | ||
|
|
363b7e9adf | ||
|
|
3186c2ead6 | ||
|
|
f07433578a | ||
|
|
d264d9f5ed | ||
|
|
98094cac86 | ||
|
|
79d98587d3 | ||
|
|
81814e8518 | ||
|
|
784cfc09c3 | ||
|
|
c87f8db6d8 | ||
|
|
c871c40219 | ||
|
|
2b3113dafc | ||
|
|
5c51ab1f9f | ||
|
|
df50ca1273 | ||
|
|
2ce2ecbbfb | ||
|
|
57ecbf0c11 | ||
|
|
e87d7364ff | ||
|
|
0b2cca6549 | ||
|
|
fca1a8077d | ||
|
|
2c128a6295 | ||
|
|
24903b8c90 | ||
|
|
c6896eebb4 | ||
|
|
4e06d8f723 |
@@ -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
@@ -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.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A C library for NOSTR protocol implementation. Work in progress.
|
||||
|
||||
[](VERSION)
|
||||
[](VERSION)
|
||||
[](#license)
|
||||
[](#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
|
||||
|
||||
@@ -142,9 +142,13 @@ if [ "$HELP" = true ]; then
|
||||
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"
|
||||
@@ -175,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
|
||||
@@ -193,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 017 019 042 044 059"
|
||||
NEEDED_NIPS="001 004 005 006 011 013 017 019 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
|
||||
@@ -212,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 042 044 046 059 060 061"
|
||||
elif [ -n "$DETECTED" ]; then
|
||||
NEEDED_NIPS="$DETECTED"
|
||||
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
|
||||
@@ -230,7 +234,7 @@ fi
|
||||
|
||||
# If building tests or examples, include all NIPs to ensure compatibility
|
||||
if ([ "$BUILD_TESTS" = true ] || [ "$BUILD_EXAMPLES" = true ]) && [ -z "$FORCE_NIPS" ]; then
|
||||
NEEDED_NIPS="001 004 005 006 011 013 017 019 042 044 059"
|
||||
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
|
||||
|
||||
@@ -500,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"
|
||||
|
||||
@@ -518,15 +523,23 @@ for nip in $NEEDED_NIPS; do
|
||||
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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+204
-7
@@ -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);
|
||||
@@ -489,14 +646,14 @@ int main() {
|
||||
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;
|
||||
|
||||
|
||||
@@ -0,0 +1,821 @@
|
||||
/*
|
||||
* Cashu Mint HTTP Client Implementation
|
||||
*/
|
||||
|
||||
#include "cashu_mint.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
typedef struct {
|
||||
char* data;
|
||||
size_t size;
|
||||
size_t capacity;
|
||||
} cashu_http_response_t;
|
||||
|
||||
static char* cashu_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 size_t cashu_write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||
cashu_http_response_t* response = (cashu_http_response_t*)userp;
|
||||
size_t total_size = size * nmemb;
|
||||
|
||||
if (response->size + total_size + 1 > response->capacity) {
|
||||
size_t new_capacity = response->capacity ? response->capacity * 2 : 1024;
|
||||
while (new_capacity < response->size + total_size + 1) {
|
||||
new_capacity *= 2;
|
||||
}
|
||||
|
||||
char* new_data = (char*)realloc(response->data, new_capacity);
|
||||
if (!new_data) return 0;
|
||||
|
||||
response->data = new_data;
|
||||
response->capacity = new_capacity;
|
||||
}
|
||||
|
||||
memcpy(response->data + response->size, contents, total_size);
|
||||
response->size += total_size;
|
||||
response->data[response->size] = '\0';
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
static int cashu_http_json_request(const char* method,
|
||||
const char* url,
|
||||
const char* body,
|
||||
int timeout_seconds,
|
||||
char** response_out,
|
||||
long* status_out) {
|
||||
if (!method || !url || !response_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*response_out = NULL;
|
||||
if (status_out) *status_out = 0;
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
return NOSTR_ERROR_NETWORK_FAILED;
|
||||
}
|
||||
|
||||
cashu_http_response_t response = {0};
|
||||
response.capacity = 1024;
|
||||
response.data = (char*)malloc(response.capacity);
|
||||
if (!response.data) {
|
||||
curl_easy_cleanup(curl);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
|
||||
struct curl_slist* headers = NULL;
|
||||
headers = curl_slist_append(headers, "Accept: application/json");
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)cashu_write_callback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)(timeout_seconds > 0 ? timeout_seconds : 10));
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "nostr-core/1.0");
|
||||
|
||||
if (strcmp(method, "POST") == 0) {
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body ? body : "{}");
|
||||
}
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long response_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (status_out) *status_out = response_code;
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
free(response.data);
|
||||
return NOSTR_ERROR_CASHU_HTTP_FAILED;
|
||||
}
|
||||
|
||||
*response_out = response.data;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int cashu_parse_quote_common(cJSON* json,
|
||||
cashu_mint_quote_t* mint_quote_out,
|
||||
cashu_melt_quote_t* melt_quote_out) {
|
||||
if (!json) return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
|
||||
|
||||
cJSON* quote_id = cJSON_GetObjectItem(json, "quote");
|
||||
cJSON* request = cJSON_GetObjectItem(json, "request");
|
||||
cJSON* paid = cJSON_GetObjectItem(json, "paid");
|
||||
cJSON* amount = cJSON_GetObjectItem(json, "amount");
|
||||
cJSON* expiry = cJSON_GetObjectItem(json, "expiry");
|
||||
|
||||
if (mint_quote_out) {
|
||||
memset(mint_quote_out, 0, sizeof(*mint_quote_out));
|
||||
if (quote_id && cJSON_IsString(quote_id)) {
|
||||
strncpy(mint_quote_out->quote_id, cJSON_GetStringValue(quote_id), sizeof(mint_quote_out->quote_id) - 1);
|
||||
}
|
||||
if (request && cJSON_IsString(request)) {
|
||||
strncpy(mint_quote_out->payment_request, cJSON_GetStringValue(request), sizeof(mint_quote_out->payment_request) - 1);
|
||||
}
|
||||
if (paid && cJSON_IsBool(paid)) mint_quote_out->paid = cJSON_IsTrue(paid) ? 1 : 0;
|
||||
if (amount && cJSON_IsNumber(amount)) mint_quote_out->amount = (uint64_t)cJSON_GetNumberValue(amount);
|
||||
if (expiry && cJSON_IsNumber(expiry)) mint_quote_out->expiry = (time_t)cJSON_GetNumberValue(expiry);
|
||||
}
|
||||
|
||||
if (melt_quote_out) {
|
||||
memset(melt_quote_out, 0, sizeof(*melt_quote_out));
|
||||
if (quote_id && cJSON_IsString(quote_id)) {
|
||||
strncpy(melt_quote_out->quote_id, cJSON_GetStringValue(quote_id), sizeof(melt_quote_out->quote_id) - 1);
|
||||
}
|
||||
if (paid && cJSON_IsBool(paid)) melt_quote_out->paid = cJSON_IsTrue(paid) ? 1 : 0;
|
||||
if (amount && cJSON_IsNumber(amount)) melt_quote_out->amount = (uint64_t)cJSON_GetNumberValue(amount);
|
||||
if (expiry && cJSON_IsNumber(expiry)) melt_quote_out->expiry = (time_t)cJSON_GetNumberValue(expiry);
|
||||
|
||||
cJSON* fee_reserve = cJSON_GetObjectItem(json, "fee_reserve");
|
||||
if (fee_reserve && cJSON_IsNumber(fee_reserve)) {
|
||||
melt_quote_out->fee_reserve = (uint64_t)cJSON_GetNumberValue(fee_reserve);
|
||||
}
|
||||
|
||||
cJSON* preimage = cJSON_GetObjectItem(json, "payment_preimage");
|
||||
if (preimage && cJSON_IsString(preimage)) {
|
||||
melt_quote_out->payment_preimage = cashu_strdup(cJSON_GetStringValue(preimage));
|
||||
}
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int cashu_call_raw(const char* mint_url,
|
||||
const char* endpoint,
|
||||
const char* method,
|
||||
const char* body,
|
||||
cJSON** response_out,
|
||||
int timeout_seconds) {
|
||||
if (!mint_url || !endpoint || !method || !response_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
*response_out = NULL;
|
||||
|
||||
char url[1024];
|
||||
snprintf(url, sizeof(url), "%s%s", mint_url, endpoint);
|
||||
|
||||
char* response = NULL;
|
||||
long status = 0;
|
||||
int rc = cashu_http_json_request(method, url, body, timeout_seconds, &response, &status);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
cJSON* json = cJSON_Parse(response ? response : "{}");
|
||||
free(response);
|
||||
|
||||
if (!json) return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
|
||||
|
||||
if (status < 200 || status >= 300) {
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_ERROR_CASHU_MINT_ERROR;
|
||||
}
|
||||
|
||||
*response_out = json;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int cashu_mint_get_info(const char* mint_url,
|
||||
cashu_mint_info_t* info_out,
|
||||
int timeout_seconds) {
|
||||
if (!mint_url || !info_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
memset(info_out, 0, sizeof(*info_out));
|
||||
|
||||
cJSON* json = NULL;
|
||||
int rc = cashu_call_raw(mint_url, "/v1/info", "GET", NULL, &json, timeout_seconds);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
cJSON* name = cJSON_GetObjectItem(json, "name");
|
||||
cJSON* pubkey = cJSON_GetObjectItem(json, "pubkey");
|
||||
cJSON* version = cJSON_GetObjectItem(json, "version");
|
||||
|
||||
if (name && cJSON_IsString(name)) info_out->name = cashu_strdup(cJSON_GetStringValue(name));
|
||||
if (pubkey && cJSON_IsString(pubkey)) info_out->pubkey = cashu_strdup(cJSON_GetStringValue(pubkey));
|
||||
if (version && cJSON_IsString(version)) info_out->version = cashu_strdup(cJSON_GetStringValue(version));
|
||||
|
||||
cJSON* keysets = cJSON_GetObjectItem(json, "keysets");
|
||||
if (keysets && cJSON_IsArray(keysets)) {
|
||||
int n = cJSON_GetArraySize(keysets);
|
||||
if (n > 0) {
|
||||
info_out->keysets = (cashu_keyset_t*)calloc((size_t)n, sizeof(cashu_keyset_t));
|
||||
if (!info_out->keysets) {
|
||||
cJSON_Delete(json);
|
||||
cashu_mint_free_info(info_out);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* ks = cJSON_GetArrayItem(keysets, i);
|
||||
if (!ks || !cJSON_IsObject(ks)) continue;
|
||||
|
||||
cJSON* id = cJSON_GetObjectItem(ks, "id");
|
||||
cJSON* unit = cJSON_GetObjectItem(ks, "unit");
|
||||
cJSON* active = cJSON_GetObjectItem(ks, "active");
|
||||
|
||||
if (id && cJSON_IsString(id)) {
|
||||
strncpy(info_out->keysets[info_out->keyset_count].id,
|
||||
cJSON_GetStringValue(id), sizeof(info_out->keysets[info_out->keyset_count].id) - 1);
|
||||
}
|
||||
if (unit && cJSON_IsString(unit)) {
|
||||
strncpy(info_out->keysets[info_out->keyset_count].unit,
|
||||
cJSON_GetStringValue(unit), sizeof(info_out->keysets[info_out->keyset_count].unit) - 1);
|
||||
}
|
||||
if (active && cJSON_IsBool(active)) {
|
||||
info_out->keysets[info_out->keyset_count].active = cJSON_IsTrue(active) ? 1 : 0;
|
||||
}
|
||||
info_out->keyset_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void cashu_mint_free_info(cashu_mint_info_t* info) {
|
||||
if (!info) return;
|
||||
free(info->name);
|
||||
free(info->pubkey);
|
||||
free(info->version);
|
||||
free(info->keysets);
|
||||
memset(info, 0, sizeof(*info));
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!mint_url || !unit || !quote_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
cJSON* req = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(req, "amount", (double)amount);
|
||||
cJSON_AddStringToObject(req, "unit", unit);
|
||||
char* body = cJSON_PrintUnformatted(req);
|
||||
cJSON_Delete(req);
|
||||
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
cJSON* json = NULL;
|
||||
int rc = cashu_call_raw(mint_url, "/v1/mint/quote/bolt11", "POST", body, &json, timeout_seconds);
|
||||
free(body);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
rc = cashu_parse_quote_common(json, quote_out, NULL);
|
||||
cJSON_Delete(json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cashu_mint_check_mint_quote(const char* mint_url,
|
||||
const char* quote_id,
|
||||
cashu_mint_quote_t* quote_out,
|
||||
int timeout_seconds) {
|
||||
if (!mint_url || !quote_id || !quote_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
char endpoint[512];
|
||||
snprintf(endpoint, sizeof(endpoint), "/v1/mint/quote/bolt11/%s", quote_id);
|
||||
|
||||
cJSON* json = NULL;
|
||||
int rc = cashu_call_raw(mint_url, endpoint, "GET", NULL, &json, timeout_seconds);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
rc = cashu_parse_quote_common(json, quote_out, NULL);
|
||||
cJSON_Delete(json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!mint_url || !payment_request || !unit || !quote_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
cJSON* req = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(req, "request", payment_request);
|
||||
cJSON_AddStringToObject(req, "unit", unit);
|
||||
char* body = cJSON_PrintUnformatted(req);
|
||||
cJSON_Delete(req);
|
||||
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
cJSON* json = NULL;
|
||||
int rc = cashu_call_raw(mint_url, "/v1/melt/quote/bolt11", "POST", body, &json, timeout_seconds);
|
||||
free(body);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
rc = cashu_parse_quote_common(json, NULL, quote_out);
|
||||
cJSON_Delete(json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cashu_mint_check_melt_quote(const char* mint_url,
|
||||
const char* quote_id,
|
||||
cashu_melt_quote_t* quote_out,
|
||||
int timeout_seconds) {
|
||||
if (!mint_url || !quote_id || !quote_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
char endpoint[512];
|
||||
snprintf(endpoint, sizeof(endpoint), "/v1/melt/quote/bolt11/%s", quote_id);
|
||||
|
||||
cJSON* json = NULL;
|
||||
int rc = cashu_call_raw(mint_url, endpoint, "GET", NULL, &json, timeout_seconds);
|
||||
if (rc != NOSTR_SUCCESS) return rc;
|
||||
|
||||
rc = cashu_parse_quote_common(json, NULL, quote_out);
|
||||
cJSON_Delete(json);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cashu_mint_swap(const char* mint_url,
|
||||
cJSON* request_body,
|
||||
cJSON** response_out,
|
||||
int timeout_seconds) {
|
||||
if (!request_body) return NOSTR_ERROR_INVALID_INPUT;
|
||||
char* body = cJSON_PrintUnformatted(request_body);
|
||||
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
int rc = cashu_call_raw(mint_url, "/v1/swap", "POST", body, response_out, timeout_seconds);
|
||||
free(body);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cashu_mint_mint_tokens(const char* mint_url,
|
||||
cJSON* request_body,
|
||||
cJSON** response_out,
|
||||
int timeout_seconds) {
|
||||
if (!request_body) return NOSTR_ERROR_INVALID_INPUT;
|
||||
char* body = cJSON_PrintUnformatted(request_body);
|
||||
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
int rc = cashu_call_raw(mint_url, "/v1/mint/bolt11", "POST", body, response_out, timeout_seconds);
|
||||
free(body);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cashu_mint_melt_tokens(const char* mint_url,
|
||||
cJSON* request_body,
|
||||
cJSON** response_out,
|
||||
int timeout_seconds) {
|
||||
if (!request_body) return NOSTR_ERROR_INVALID_INPUT;
|
||||
char* body = cJSON_PrintUnformatted(request_body);
|
||||
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
int rc = cashu_call_raw(mint_url, "/v1/melt/bolt11", "POST", body, response_out, timeout_seconds);
|
||||
free(body);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cashu_mint_check_proofs_state(const char* mint_url,
|
||||
cJSON* request_body,
|
||||
cJSON** response_out,
|
||||
int timeout_seconds) {
|
||||
if (!request_body) return NOSTR_ERROR_INVALID_INPUT;
|
||||
char* body = cJSON_PrintUnformatted(request_body);
|
||||
if (!body) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
int rc = cashu_call_raw(mint_url, "/v1/checkstate", "POST", body, response_out, timeout_seconds);
|
||||
free(body);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cashu_mint_get_active_keyset(const cashu_mint_info_t* info,
|
||||
const char* optional_unit,
|
||||
cashu_keyset_t* keyset_out) {
|
||||
if (!info || !keyset_out || !info->keysets || info->keyset_count <= 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
for (int i = 0; i < info->keyset_count; i++) {
|
||||
const cashu_keyset_t* ks = &info->keysets[i];
|
||||
if (!ks->active) continue;
|
||||
if (optional_unit && optional_unit[0] != '\0' && strcmp(ks->unit, optional_unit) != 0) continue;
|
||||
memset(keyset_out, 0, sizeof(*keyset_out));
|
||||
memcpy(keyset_out, ks, sizeof(*keyset_out));
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
if (optional_unit && optional_unit[0] != '\0') {
|
||||
for (int i = 0; i < info->keyset_count; i++) {
|
||||
const cashu_keyset_t* ks = &info->keysets[i];
|
||||
if (strcmp(ks->unit, optional_unit) != 0) continue;
|
||||
memset(keyset_out, 0, sizeof(*keyset_out));
|
||||
memcpy(keyset_out, ks, sizeof(*keyset_out));
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
return NOSTR_ERROR_CASHU_INVALID_KEYSET;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!proofs || proof_count <= 0 || !selected_indices_out || selected_indices_cap <= 0 || target_amount == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int* used = (int*)calloc((size_t)proof_count, sizeof(int));
|
||||
if (!used) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
int selected_count = 0;
|
||||
uint64_t total = 0;
|
||||
|
||||
while (total < target_amount) {
|
||||
int best_idx = -1;
|
||||
uint64_t best_amt = 0;
|
||||
|
||||
for (int i = 0; i < proof_count; i++) {
|
||||
if (used[i]) continue;
|
||||
if (proofs[i].amount > best_amt) {
|
||||
best_amt = proofs[i].amount;
|
||||
best_idx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_idx < 0 || best_amt == 0) {
|
||||
free(used);
|
||||
return NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
if (selected_count >= selected_indices_cap) {
|
||||
free(used);
|
||||
return NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
used[best_idx] = 1;
|
||||
selected_indices_out[selected_count++] = best_idx;
|
||||
total += proofs[best_idx].amount;
|
||||
}
|
||||
|
||||
free(used);
|
||||
|
||||
if (selected_total_out) {
|
||||
*selected_total_out = total;
|
||||
}
|
||||
|
||||
return selected_count;
|
||||
}
|
||||
|
||||
int cashu_mint_plan_split_amounts(uint64_t amount,
|
||||
uint64_t* amounts_out,
|
||||
int max_amounts,
|
||||
int* amount_count_out) {
|
||||
if (!amounts_out || max_amounts <= 0 || !amount_count_out || amount == 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
|
||||
while (amount > 0) {
|
||||
uint64_t part = 1;
|
||||
while ((part << 1) > part && (part << 1) <= amount) {
|
||||
part <<= 1;
|
||||
}
|
||||
|
||||
if (count >= max_amounts) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
amounts_out[count++] = part;
|
||||
amount -= part;
|
||||
}
|
||||
|
||||
*amount_count_out = count;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int cashu_build_outputs_json(const cashu_blinded_output_t* outputs,
|
||||
int output_count,
|
||||
cJSON** outputs_out) {
|
||||
if (!outputs || output_count <= 0 || !outputs_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
cJSON* arr = cJSON_CreateArray();
|
||||
if (!arr) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
for (int i = 0; i < output_count; i++) {
|
||||
cJSON* o = cJSON_CreateObject();
|
||||
if (!o) {
|
||||
cJSON_Delete(arr);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
cJSON_AddStringToObject(o, "id", outputs[i].id);
|
||||
cJSON_AddNumberToObject(o, "amount", (double)outputs[i].amount);
|
||||
cJSON_AddStringToObject(o, "B_", outputs[i].B_);
|
||||
cJSON_AddItemToArray(arr, o);
|
||||
}
|
||||
|
||||
*outputs_out = arr;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
static int cashu_parse_signatures_array(cJSON* signatures,
|
||||
cashu_blinded_signature_t** sigs_out,
|
||||
int* sig_count_out) {
|
||||
if (!sigs_out || !sig_count_out) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
*sigs_out = NULL;
|
||||
*sig_count_out = 0;
|
||||
|
||||
if (!signatures || !cJSON_IsArray(signatures)) {
|
||||
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(signatures);
|
||||
if (n <= 0) return NOSTR_SUCCESS;
|
||||
|
||||
cashu_blinded_signature_t* sigs =
|
||||
(cashu_blinded_signature_t*)calloc((size_t)n, sizeof(cashu_blinded_signature_t));
|
||||
if (!sigs) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* item = cJSON_GetArrayItem(signatures, i);
|
||||
if (!item || !cJSON_IsObject(item)) {
|
||||
free(sigs);
|
||||
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
|
||||
}
|
||||
|
||||
cJSON* id = cJSON_GetObjectItemCaseSensitive(item, "id");
|
||||
cJSON* amount = cJSON_GetObjectItemCaseSensitive(item, "amount");
|
||||
cJSON* C_ = cJSON_GetObjectItemCaseSensitive(item, "C_");
|
||||
|
||||
if (!id || !cJSON_IsString(id) || !id->valuestring ||
|
||||
!amount || !cJSON_IsNumber(amount) ||
|
||||
!C_ || !cJSON_IsString(C_) || !C_->valuestring) {
|
||||
free(sigs);
|
||||
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
|
||||
}
|
||||
|
||||
snprintf(sigs[i].id, sizeof(sigs[i].id), "%s", id->valuestring);
|
||||
sigs[i].amount = (uint64_t)cJSON_GetNumberValue(amount);
|
||||
snprintf(sigs[i].C_, sizeof(sigs[i].C_), "%s", C_->valuestring);
|
||||
}
|
||||
|
||||
*sigs_out = sigs;
|
||||
*sig_count_out = n;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int cashu_build_mint_tokens_request(const cashu_mint_tokens_request_t* req,
|
||||
cJSON** request_body_out) {
|
||||
if (!req || !request_body_out || req->quote_id[0] == '\0' || req->output_count <= 0 || !req->outputs) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (!root) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
cJSON_AddStringToObject(root, "quote", req->quote_id);
|
||||
if (req->unit[0] != '\0') {
|
||||
cJSON_AddStringToObject(root, "unit", req->unit);
|
||||
}
|
||||
|
||||
cJSON* outputs = NULL;
|
||||
int rc = cashu_build_outputs_json(req->outputs, req->output_count, &outputs);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(root);
|
||||
return rc;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "outputs", outputs);
|
||||
*request_body_out = root;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int cashu_parse_mint_tokens_response(cJSON* response_body,
|
||||
cashu_mint_tokens_response_t* response_out) {
|
||||
if (!response_body || !response_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(response_out, 0, sizeof(*response_out));
|
||||
|
||||
cJSON* signatures = cJSON_GetObjectItemCaseSensitive(response_body, "signatures");
|
||||
return cashu_parse_signatures_array(signatures,
|
||||
&response_out->signatures,
|
||||
&response_out->signature_count);
|
||||
}
|
||||
|
||||
void cashu_mint_free_mint_tokens_response(cashu_mint_tokens_response_t* response) {
|
||||
if (!response) return;
|
||||
free(response->signatures);
|
||||
memset(response, 0, sizeof(*response));
|
||||
}
|
||||
|
||||
int cashu_build_swap_request(const cashu_swap_request_t* req,
|
||||
cJSON** request_body_out) {
|
||||
if (!req || !request_body_out || !req->inputs || req->input_count <= 0 || !req->outputs || req->output_count <= 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (!root) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
cJSON* inputs = nostr_nip60_proofs_to_json(req->inputs, req->input_count);
|
||||
if (!inputs) {
|
||||
cJSON_Delete(root);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
cJSON_AddItemToObject(root, "inputs", inputs);
|
||||
|
||||
cJSON* outputs = NULL;
|
||||
int rc = cashu_build_outputs_json(req->outputs, req->output_count, &outputs);
|
||||
if (rc != NOSTR_SUCCESS) {
|
||||
cJSON_Delete(root);
|
||||
return rc;
|
||||
}
|
||||
cJSON_AddItemToObject(root, "outputs", outputs);
|
||||
|
||||
*request_body_out = root;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int cashu_parse_swap_response(cJSON* response_body,
|
||||
cashu_swap_response_t* response_out) {
|
||||
if (!response_body || !response_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(response_out, 0, sizeof(*response_out));
|
||||
|
||||
cJSON* signatures = cJSON_GetObjectItemCaseSensitive(response_body, "signatures");
|
||||
return cashu_parse_signatures_array(signatures,
|
||||
&response_out->signatures,
|
||||
&response_out->signature_count);
|
||||
}
|
||||
|
||||
void cashu_mint_free_swap_response(cashu_swap_response_t* response) {
|
||||
if (!response) return;
|
||||
free(response->signatures);
|
||||
memset(response, 0, sizeof(*response));
|
||||
}
|
||||
|
||||
int cashu_build_melt_tokens_request(const cashu_melt_tokens_request_t* req,
|
||||
cJSON** request_body_out) {
|
||||
if (!req || !request_body_out || req->quote_id[0] == '\0' || !req->inputs || req->input_count <= 0) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (!root) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
cJSON_AddStringToObject(root, "quote", req->quote_id);
|
||||
|
||||
cJSON* inputs = nostr_nip60_proofs_to_json(req->inputs, req->input_count);
|
||||
if (!inputs) {
|
||||
cJSON_Delete(root);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "inputs", inputs);
|
||||
*request_body_out = root;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int cashu_parse_melt_tokens_response(cJSON* response_body,
|
||||
cashu_melt_tokens_response_t* response_out) {
|
||||
if (!response_body || !response_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(response_out, 0, sizeof(*response_out));
|
||||
|
||||
cJSON* paid = cJSON_GetObjectItemCaseSensitive(response_body, "paid");
|
||||
cJSON* preimage = cJSON_GetObjectItemCaseSensitive(response_body, "payment_preimage");
|
||||
cJSON* change = cJSON_GetObjectItemCaseSensitive(response_body, "change");
|
||||
|
||||
response_out->paid = (paid && cJSON_IsBool(paid) && cJSON_IsTrue(paid)) ? 1 : 0;
|
||||
|
||||
if (preimage && cJSON_IsString(preimage) && preimage->valuestring) {
|
||||
response_out->payment_preimage = cashu_strdup(preimage->valuestring);
|
||||
if (!response_out->payment_preimage) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
if (change) {
|
||||
response_out->change = cJSON_Duplicate(change, 1);
|
||||
if (!response_out->change) {
|
||||
free(response_out->payment_preimage);
|
||||
memset(response_out, 0, sizeof(*response_out));
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void cashu_mint_free_melt_tokens_response(cashu_melt_tokens_response_t* response) {
|
||||
if (!response) return;
|
||||
free(response->payment_preimage);
|
||||
cJSON_Delete(response->change);
|
||||
memset(response, 0, sizeof(*response));
|
||||
}
|
||||
|
||||
int cashu_build_checkstate_request(const char** Ys,
|
||||
int y_count,
|
||||
cJSON** request_body_out) {
|
||||
if (!Ys || y_count <= 0 || !request_body_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
cJSON* ys = cJSON_CreateArray();
|
||||
if (!root || !ys) {
|
||||
cJSON_Delete(root);
|
||||
cJSON_Delete(ys);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
for (int i = 0; i < y_count; i++) {
|
||||
if (!Ys[i] || Ys[i][0] == '\0') {
|
||||
cJSON_Delete(root);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
cJSON_AddItemToArray(ys, cJSON_CreateString(Ys[i]));
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "Ys", ys);
|
||||
*request_body_out = root;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int cashu_parse_checkstate_response(cJSON* response_body,
|
||||
cashu_checkstate_response_t* response_out) {
|
||||
if (!response_body || !response_out) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(response_out, 0, sizeof(*response_out));
|
||||
|
||||
cJSON* states = cJSON_GetObjectItemCaseSensitive(response_body, "states");
|
||||
if (!states || !cJSON_IsArray(states)) {
|
||||
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
|
||||
}
|
||||
|
||||
int n = cJSON_GetArraySize(states);
|
||||
if (n <= 0) {
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
response_out->states = (cashu_proof_state_t*)calloc((size_t)n, sizeof(cashu_proof_state_t));
|
||||
if (!response_out->states) {
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON* item = cJSON_GetArrayItem(states, i);
|
||||
if (!item || !cJSON_IsObject(item)) {
|
||||
cashu_mint_free_checkstate_response(response_out);
|
||||
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
|
||||
}
|
||||
|
||||
cJSON* Y = cJSON_GetObjectItemCaseSensitive(item, "Y");
|
||||
cJSON* state = cJSON_GetObjectItemCaseSensitive(item, "state");
|
||||
cJSON* witness = cJSON_GetObjectItemCaseSensitive(item, "witness");
|
||||
|
||||
if (Y && cJSON_IsString(Y) && Y->valuestring) {
|
||||
snprintf(response_out->states[i].Y, sizeof(response_out->states[i].Y), "%s", Y->valuestring);
|
||||
}
|
||||
if (state && cJSON_IsString(state) && state->valuestring) {
|
||||
snprintf(response_out->states[i].state, sizeof(response_out->states[i].state), "%s", state->valuestring);
|
||||
}
|
||||
if (witness && cJSON_IsString(witness) && witness->valuestring) {
|
||||
snprintf(response_out->states[i].witness, sizeof(response_out->states[i].witness), "%s", witness->valuestring);
|
||||
}
|
||||
|
||||
if (response_out->states[i].state[0] == '\0') {
|
||||
cashu_mint_free_checkstate_response(response_out);
|
||||
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
response_out->state_count = n;
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
void cashu_mint_free_checkstate_response(cashu_checkstate_response_t* response) {
|
||||
if (!response) return;
|
||||
free(response->states);
|
||||
memset(response, 0, sizeof(*response));
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* 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[17];
|
||||
char unit[16];
|
||||
int active;
|
||||
} cashu_keyset_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;
|
||||
|
||||
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_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);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_CASHU_MINT_H */
|
||||
+536
-106
@@ -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;
|
||||
@@ -93,6 +136,12 @@ typedef struct relay_connection {
|
||||
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;
|
||||
@@ -147,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
|
||||
@@ -217,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) {
|
||||
@@ -470,6 +644,38 @@ 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;
|
||||
@@ -508,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);
|
||||
@@ -559,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]) {
|
||||
@@ -570,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);
|
||||
}
|
||||
|
||||
@@ -776,9 +1007,6 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
|
||||
}
|
||||
|
||||
relay->stats.last_event_time = time(NULL);
|
||||
|
||||
// Debug: Print all message types received
|
||||
printf("📨 DEBUG: Received %s message from %s\n", msg_type, relay->url);
|
||||
|
||||
if (strcmp(msg_type, "EVENT") == 0) {
|
||||
// Handle EVENT message: ["EVENT", subscription_id, event]
|
||||
@@ -905,26 +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++;
|
||||
// Store error message if available
|
||||
if (cJSON_GetArraySize(parsed) >= 4) {
|
||||
cJSON* error_msg = cJSON_GetArrayItem(parsed, 3);
|
||||
if (cJSON_IsString(error_msg)) {
|
||||
const char* msg = cJSON_GetStringValue(error_msg);
|
||||
if (msg) {
|
||||
strncpy(relay->last_publish_error, msg,
|
||||
sizeof(relay->last_publish_error) - 1);
|
||||
relay->last_publish_error[sizeof(relay->last_publish_error) - 1] = '\0';
|
||||
relay->last_publish_error_time = time(NULL);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -954,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];
|
||||
@@ -979,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
|
||||
@@ -988,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';
|
||||
|
||||
@@ -1009,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++) {
|
||||
@@ -1116,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++) {
|
||||
@@ -1137,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1558,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;
|
||||
}
|
||||
|
||||
@@ -1568,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);
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
// NIP-04 constants
|
||||
// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 65535
|
||||
// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 1048576 // 1MB
|
||||
// NIP-04 Constants
|
||||
// #define NOSTR_NIP04_MAX_PLAINTEXT_SIZE 16777216 // 16MB
|
||||
// #define NOSTR_NIP04_MAX_ENCRYPTED_SIZE 22369621 // ~21.3MB (accounts for base64 overhead + IV)
|
||||
|
||||
+28
-23
@@ -6,6 +6,7 @@
|
||||
#include "nip001.h"
|
||||
#include "utils.h"
|
||||
#include "../cjson/cJSON.h"
|
||||
#include "nostr_log.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -175,12 +176,13 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
||||
if (attempts % timestamp_update_interval == 0) {
|
||||
current_timestamp = time(NULL);
|
||||
#ifdef ENABLE_DEBUG_LOGGING
|
||||
FILE* f = fopen("debug.log", "a");
|
||||
if (f) {
|
||||
fprintf(f, "PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d\n",
|
||||
attempts, best_difficulty_this_round, best_difficulty_overall, target_difficulty);
|
||||
fclose(f);
|
||||
}
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_DEBUG,
|
||||
"nip013",
|
||||
"PoW mining: %d attempts, best this round: %d, overall best: %d, goal: %d",
|
||||
attempts,
|
||||
best_difficulty_this_round,
|
||||
best_difficulty_overall,
|
||||
target_difficulty);
|
||||
#endif
|
||||
// Reset best difficulty for the new round
|
||||
best_difficulty_this_round = 0;
|
||||
@@ -239,18 +241,22 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
||||
// Check if we've reached the target
|
||||
if (current_difficulty >= target_difficulty) {
|
||||
#ifdef ENABLE_DEBUG_LOGGING
|
||||
FILE* f = fopen("debug.log", "a");
|
||||
if (f) {
|
||||
fprintf(f, "PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts\n",
|
||||
current_difficulty, target_difficulty, (unsigned long long)nonce, attempts + 1);
|
||||
|
||||
// Print the final event JSON
|
||||
char* event_json = cJSON_Print(test_event);
|
||||
if (event_json) {
|
||||
fprintf(f, "Final event: %s\n", event_json);
|
||||
free(event_json);
|
||||
}
|
||||
fclose(f);
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_INFO,
|
||||
"nip013",
|
||||
"PoW SUCCESS: Found difficulty %d (target %d) at nonce %llu after %d attempts",
|
||||
current_difficulty,
|
||||
target_difficulty,
|
||||
(unsigned long long)nonce,
|
||||
attempts + 1);
|
||||
|
||||
// Print the final event JSON
|
||||
char* event_json = cJSON_Print(test_event);
|
||||
if (event_json) {
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_DEBUG,
|
||||
"nip013",
|
||||
"Final event: %s",
|
||||
event_json);
|
||||
free(event_json);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -267,11 +273,10 @@ int nostr_add_proof_of_work(cJSON* event, const unsigned char* private_key,
|
||||
|
||||
#ifdef ENABLE_DEBUG_LOGGING
|
||||
// Debug logging - failure
|
||||
FILE* f = fopen("debug.log", "a");
|
||||
if (f) {
|
||||
fprintf(f, "PoW FAILED: Mining failed after %d attempts\n", max_attempts);
|
||||
fclose(f);
|
||||
}
|
||||
nostr_log_emitf(NOSTR_LOG_LEVEL_WARN,
|
||||
"nip013",
|
||||
"PoW FAILED: Mining failed after %d attempts",
|
||||
max_attempts);
|
||||
#endif
|
||||
|
||||
// If we reach here, we've exceeded max attempts
|
||||
|
||||
+10
-9
@@ -258,11 +258,12 @@ cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
|
||||
* 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) {
|
||||
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;
|
||||
@@ -278,13 +279,13 @@ int nostr_nip17_send_dm(cJSON* dm_event,
|
||||
}
|
||||
|
||||
// Create seal for this recipient
|
||||
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key);
|
||||
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]);
|
||||
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) {
|
||||
@@ -303,10 +304,10 @@ int nostr_nip17_send_dm(cJSON* dm_event,
|
||||
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);
|
||||
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);
|
||||
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) {
|
||||
|
||||
+3
-1
@@ -97,6 +97,7 @@ cJSON* nostr_nip17_create_relay_list_event(const char** relay_urls,
|
||||
* @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,
|
||||
@@ -104,7 +105,8 @@ int nostr_nip17_send_dm(cJSON* dm_event,
|
||||
int num_recipients,
|
||||
const unsigned char* sender_private_key,
|
||||
cJSON** gift_wraps_out,
|
||||
int max_gift_wraps);
|
||||
int max_gift_wraps,
|
||||
long max_delay_sec);
|
||||
|
||||
/**
|
||||
* NIP-17: Receive and decrypt a direct message
|
||||
|
||||
@@ -0,0 +1,855 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-021: nostr: URI scheme
|
||||
*/
|
||||
|
||||
#include "nip021.h"
|
||||
#include "nip019.h" // For existing bech32 functions
|
||||
#include "utils.h"
|
||||
#include "nostr_common.h" // For error codes
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include "../cjson/cJSON.h"
|
||||
|
||||
// Forward declarations for internal parsing functions
|
||||
static int parse_nprofile_data(const uint8_t* data, size_t data_len, nostr_nprofile_t* nprofile);
|
||||
static int parse_nevent_data(const uint8_t* data, size_t data_len, nostr_nevent_t* nevent);
|
||||
static int parse_naddr_data(const uint8_t* data, size_t data_len, nostr_naddr_t* naddr);
|
||||
|
||||
// Bech32 constants and functions (copied from nip019.c for internal use)
|
||||
static const char bech32_charset[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
static const int8_t bech32_charset_rev[128] = {
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
|
||||
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
|
||||
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
|
||||
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
|
||||
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
|
||||
};
|
||||
|
||||
static uint32_t bech32_polymod_step(uint32_t pre) {
|
||||
uint8_t b = pre >> 25;
|
||||
return ((pre & 0x1FFFFFF) << 5) ^
|
||||
(-((b >> 0) & 1) & 0x3b6a57b2UL) ^
|
||||
(-((b >> 1) & 1) & 0x26508e6dUL) ^
|
||||
(-((b >> 2) & 1) & 0x1ea119faUL) ^
|
||||
(-((b >> 3) & 1) & 0x3d4233ddUL) ^
|
||||
(-((b >> 4) & 1) & 0x2a1462b3UL);
|
||||
}
|
||||
|
||||
static int convert_bits(uint8_t *out, size_t *outlen, int outbits, const uint8_t *in, size_t inlen, int inbits, int pad) {
|
||||
uint32_t val = 0;
|
||||
int bits = 0;
|
||||
uint32_t maxv = (((uint32_t)1) << outbits) - 1;
|
||||
*outlen = 0;
|
||||
while (inlen--) {
|
||||
val = (val << inbits) | *(in++);
|
||||
bits += inbits;
|
||||
while (bits >= outbits) {
|
||||
bits -= outbits;
|
||||
out[(*outlen)++] = (val >> bits) & maxv;
|
||||
}
|
||||
}
|
||||
if (pad) {
|
||||
if (bits) {
|
||||
out[(*outlen)++] = (val << (outbits - bits)) & maxv;
|
||||
}
|
||||
} else if (((val << (outbits - bits)) & maxv) || bits >= inbits) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len) {
|
||||
uint32_t chk = 1;
|
||||
size_t i, hrp_len = strlen(hrp);
|
||||
|
||||
for (i = 0; i < hrp_len; ++i) {
|
||||
int ch = hrp[i];
|
||||
if (ch < 33 || ch > 126) return 0;
|
||||
if (ch >= 'A' && ch <= 'Z') return 0;
|
||||
chk = bech32_polymod_step(chk) ^ (ch >> 5);
|
||||
}
|
||||
|
||||
chk = bech32_polymod_step(chk);
|
||||
for (i = 0; i < hrp_len; ++i) {
|
||||
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
|
||||
*(output++) = hrp[i];
|
||||
}
|
||||
|
||||
*(output++) = '1';
|
||||
for (i = 0; i < data_len; ++i) {
|
||||
if (*data >> 5) return 0;
|
||||
chk = bech32_polymod_step(chk) ^ (*data);
|
||||
*(output++) = bech32_charset[*(data++)];
|
||||
}
|
||||
|
||||
for (i = 0; i < 6; ++i) {
|
||||
chk = bech32_polymod_step(chk);
|
||||
}
|
||||
|
||||
chk ^= 1;
|
||||
for (i = 0; i < 6; ++i) {
|
||||
*(output++) = bech32_charset[(chk >> ((5 - i) * 5)) & 0x1f];
|
||||
}
|
||||
|
||||
*output = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int bech32_decode(const char* input, const char* hrp, unsigned char* data, size_t* data_len) {
|
||||
if (!input || !hrp || !data || !data_len) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t input_len = strlen(input);
|
||||
size_t hrp_len = strlen(hrp);
|
||||
|
||||
if (input_len < hrp_len + 7) return 0;
|
||||
if (strncmp(input, hrp, hrp_len) != 0) return 0;
|
||||
if (input[hrp_len] != '1') return 0;
|
||||
|
||||
const char* data_part = input + hrp_len + 1;
|
||||
size_t data_part_len = input_len - hrp_len - 1;
|
||||
|
||||
uint8_t values[256];
|
||||
for (size_t i = 0; i < data_part_len; i++) {
|
||||
unsigned char c = (unsigned char)data_part[i];
|
||||
if (c >= 128) return 0;
|
||||
int8_t val = bech32_charset_rev[c];
|
||||
if (val == -1) return 0;
|
||||
values[i] = (uint8_t)val;
|
||||
}
|
||||
|
||||
if (data_part_len < 6) return 0;
|
||||
|
||||
uint32_t chk = 1;
|
||||
for (size_t i = 0; i < hrp_len; i++) {
|
||||
chk = bech32_polymod_step(chk) ^ (hrp[i] >> 5);
|
||||
}
|
||||
chk = bech32_polymod_step(chk);
|
||||
for (size_t i = 0; i < hrp_len; i++) {
|
||||
chk = bech32_polymod_step(chk) ^ (hrp[i] & 0x1f);
|
||||
}
|
||||
for (size_t i = 0; i < data_part_len; i++) {
|
||||
chk = bech32_polymod_step(chk) ^ values[i];
|
||||
}
|
||||
|
||||
if (chk != 1) return 0;
|
||||
|
||||
size_t payload_len = data_part_len - 6;
|
||||
size_t decoded_len;
|
||||
if (!convert_bits(data, &decoded_len, 8, values, payload_len, 5, 0)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*data_len = decoded_len;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TLV (Type-Length-Value) constants for structured data
|
||||
#define TLV_SPECIAL 0
|
||||
#define TLV_RELAY 1
|
||||
#define TLV_AUTHOR 2
|
||||
#define TLV_KIND 3
|
||||
#define TLV_CREATED_AT 4
|
||||
#define TLV_IDENTIFIER 5
|
||||
|
||||
// Forward declarations for internal functions
|
||||
static int tlv_encode(const uint8_t* data, size_t data_len, uint8_t type, uint8_t** output, size_t* output_len);
|
||||
static int encode_structured_bech32(const char* hrp, const uint8_t* data, size_t data_len, char* output, size_t output_size);
|
||||
static int decode_structured_bech32(const char* input, const char* expected_hrp, uint8_t** data, size_t* data_len);
|
||||
|
||||
// Utility function to duplicate string array (removed - not used)
|
||||
|
||||
// Free string array
|
||||
static void free_string_array(char** array, int count) {
|
||||
if (!array) return;
|
||||
for (int i = 0; i < count; i++) {
|
||||
free(array[i]);
|
||||
}
|
||||
free(array);
|
||||
}
|
||||
|
||||
// TLV encoding: Type (1 byte) + Length (1 byte) + Value
|
||||
static int tlv_encode(const uint8_t* data, size_t data_len, uint8_t type, uint8_t** output, size_t* output_len) {
|
||||
if (data_len > 255) return 0; // Length must fit in 1 byte
|
||||
|
||||
*output_len = 2 + data_len;
|
||||
*output = malloc(*output_len);
|
||||
if (!*output) return 0;
|
||||
|
||||
(*output)[0] = type;
|
||||
(*output)[1] = (uint8_t)data_len;
|
||||
memcpy(*output + 2, data, data_len);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// TLV decoding (removed - not used)
|
||||
|
||||
// Encode structured data to bech32
|
||||
static int encode_structured_bech32(const char* hrp, const uint8_t* data, size_t data_len, char* output, size_t output_size) {
|
||||
// For simple cases like note (32 bytes), use the existing key encoding
|
||||
if (strcmp(hrp, "note") == 0 && data_len == 32) {
|
||||
return nostr_key_to_bech32(data, "note", output);
|
||||
}
|
||||
|
||||
uint8_t conv[256];
|
||||
size_t conv_len;
|
||||
|
||||
if (!convert_bits(conv, &conv_len, 5, data, data_len, 8, 1)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (!bech32_encode(output, hrp, conv, conv_len)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
if (strlen(output) >= output_size) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Decode structured bech32 data
|
||||
static int decode_structured_bech32(const char* input, const char* expected_hrp, uint8_t** data, size_t* data_len) {
|
||||
// bech32_decode already converts from 5-bit to 8-bit internally
|
||||
*data = malloc(256); // Max size
|
||||
if (!*data) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
|
||||
if (!bech32_decode(input, expected_hrp, *data, data_len)) {
|
||||
free(*data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Detect URI type from string
|
||||
nostr_uri_type_t nostr_detect_uri_type(const char* uri) {
|
||||
if (!uri) return NOSTR_URI_INVALID;
|
||||
|
||||
// Check for nostr: prefix
|
||||
if (strncmp(uri, "nostr:", 6) != 0) {
|
||||
return NOSTR_URI_INVALID;
|
||||
}
|
||||
|
||||
const char* bech32_part = uri + 6;
|
||||
|
||||
// Check prefixes
|
||||
if (strncmp(bech32_part, "npub1", 5) == 0) return NOSTR_URI_NPUB;
|
||||
if (strncmp(bech32_part, "nsec1", 5) == 0) return NOSTR_URI_NSEC;
|
||||
if (strncmp(bech32_part, "note1", 5) == 0) return NOSTR_URI_NOTE;
|
||||
if (strncmp(bech32_part, "nprofile1", 9) == 0) return NOSTR_URI_NPROFILE;
|
||||
if (strncmp(bech32_part, "nevent1", 7) == 0) return NOSTR_URI_NEVENT;
|
||||
if (strncmp(bech32_part, "naddr1", 6) == 0) return NOSTR_URI_NADDR;
|
||||
|
||||
return NOSTR_URI_INVALID;
|
||||
}
|
||||
|
||||
// Free URI result resources
|
||||
void nostr_uri_result_free(nostr_uri_result_t* result) {
|
||||
if (!result) return;
|
||||
|
||||
switch (result->type) {
|
||||
case NOSTR_URI_NPROFILE:
|
||||
free_string_array(result->data.nprofile.relays, result->data.nprofile.relay_count);
|
||||
break;
|
||||
case NOSTR_URI_NEVENT:
|
||||
free_string_array(result->data.nevent.relays, result->data.nevent.relay_count);
|
||||
free(result->data.nevent.author);
|
||||
free(result->data.nevent.kind);
|
||||
free(result->data.nevent.created_at);
|
||||
break;
|
||||
case NOSTR_URI_NADDR:
|
||||
free(result->data.naddr.identifier);
|
||||
free_string_array(result->data.naddr.relays, result->data.naddr.relay_count);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Main URI parsing function
|
||||
int nostr_parse_uri(const char* uri, nostr_uri_result_t* result) {
|
||||
if (!uri || !result) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
memset(result, 0, sizeof(nostr_uri_result_t));
|
||||
|
||||
nostr_uri_type_t type = nostr_detect_uri_type(uri);
|
||||
if (type == NOSTR_URI_INVALID) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
const char* bech32_part = uri + 6; // Skip "nostr:"
|
||||
result->type = type;
|
||||
|
||||
int ret;
|
||||
switch (type) {
|
||||
case NOSTR_URI_NPUB: {
|
||||
ret = nostr_decode_npub(bech32_part, result->data.pubkey);
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NSEC: {
|
||||
ret = nostr_decode_nsec(bech32_part, result->data.privkey);
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NOTE: {
|
||||
// Note is similar to npub but with "note" prefix
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
ret = decode_structured_bech32(bech32_part, "note", &decoded, &decoded_len);
|
||||
if (ret == NOSTR_SUCCESS) {
|
||||
if (decoded_len == 32) {
|
||||
memcpy(result->data.event_id, decoded, 32);
|
||||
} else {
|
||||
ret = NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
free(decoded);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NPROFILE: {
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
ret = decode_structured_bech32(bech32_part, "nprofile", &decoded, &decoded_len);
|
||||
if (ret == NOSTR_SUCCESS) {
|
||||
ret = parse_nprofile_data(decoded, decoded_len, &result->data.nprofile);
|
||||
free(decoded);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NEVENT: {
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
ret = decode_structured_bech32(bech32_part, "nevent", &decoded, &decoded_len);
|
||||
if (ret == NOSTR_SUCCESS) {
|
||||
ret = parse_nevent_data(decoded, decoded_len, &result->data.nevent);
|
||||
free(decoded);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NOSTR_URI_NADDR: {
|
||||
uint8_t* decoded;
|
||||
size_t decoded_len;
|
||||
ret = decode_structured_bech32(bech32_part, "naddr", &decoded, &decoded_len);
|
||||
if (ret == NOSTR_SUCCESS) {
|
||||
ret = parse_naddr_data(decoded, decoded_len, &result->data.naddr);
|
||||
free(decoded);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ret = NOSTR_ERROR_INVALID_INPUT;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ret != NOSTR_SUCCESS) {
|
||||
nostr_uri_result_free(result);
|
||||
memset(result, 0, sizeof(nostr_uri_result_t));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Parse nprofile structured data
|
||||
static int parse_nprofile_data(const uint8_t* data, size_t data_len, nostr_nprofile_t* nprofile) {
|
||||
size_t offset = 0;
|
||||
|
||||
while (offset < data_len) {
|
||||
if (offset + 2 > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
uint8_t type = data[offset];
|
||||
uint8_t length = data[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
switch (type) {
|
||||
case TLV_SPECIAL: // pubkey
|
||||
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
|
||||
memcpy(nprofile->pubkey, data + offset, 32);
|
||||
break;
|
||||
case TLV_RELAY: // relay URL
|
||||
{
|
||||
char* relay = malloc(length + 1);
|
||||
if (!relay) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(relay, data + offset, length);
|
||||
relay[length] = '\0';
|
||||
|
||||
char** new_relays = realloc(nprofile->relays, (nprofile->relay_count + 1) * sizeof(char*));
|
||||
if (!new_relays) {
|
||||
free(relay);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
nprofile->relays = new_relays;
|
||||
nprofile->relays[nprofile->relay_count++] = relay;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Ignore unknown types
|
||||
break;
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Parse nevent structured data
|
||||
static int parse_nevent_data(const uint8_t* data, size_t data_len, nostr_nevent_t* nevent) {
|
||||
size_t offset = 0;
|
||||
|
||||
while (offset < data_len) {
|
||||
if (offset + 2 > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
uint8_t type = data[offset];
|
||||
uint8_t length = data[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
switch (type) {
|
||||
case TLV_SPECIAL: // event ID
|
||||
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
|
||||
memcpy(nevent->event_id, data + offset, 32);
|
||||
break;
|
||||
case TLV_RELAY: // relay URL
|
||||
{
|
||||
char* relay = malloc(length + 1);
|
||||
if (!relay) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(relay, data + offset, length);
|
||||
relay[length] = '\0';
|
||||
|
||||
char** new_relays = realloc(nevent->relays, (nevent->relay_count + 1) * sizeof(char*));
|
||||
if (!new_relays) {
|
||||
free(relay);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
nevent->relays = new_relays;
|
||||
nevent->relays[nevent->relay_count++] = relay;
|
||||
}
|
||||
break;
|
||||
case TLV_AUTHOR: // author pubkey
|
||||
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
|
||||
nevent->author = malloc(32);
|
||||
if (!nevent->author) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(nevent->author, data + offset, 32);
|
||||
break;
|
||||
case TLV_KIND: // kind
|
||||
if (length != 4) return NOSTR_ERROR_INVALID_INPUT;
|
||||
nevent->kind = malloc(sizeof(int));
|
||||
if (!nevent->kind) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
*nevent->kind = (data[offset] << 24) | (data[offset+1] << 16) | (data[offset+2] << 8) | data[offset+3];
|
||||
break;
|
||||
case TLV_CREATED_AT: // created_at
|
||||
if (length != 8) return NOSTR_ERROR_INVALID_INPUT;
|
||||
nevent->created_at = malloc(sizeof(time_t));
|
||||
if (!nevent->created_at) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
*nevent->created_at = ((time_t)data[offset] << 56) | ((time_t)data[offset+1] << 48) |
|
||||
((time_t)data[offset+2] << 40) | ((time_t)data[offset+3] << 32) |
|
||||
((time_t)data[offset+4] << 24) | ((time_t)data[offset+5] << 16) |
|
||||
((time_t)data[offset+6] << 8) | (time_t)data[offset+7];
|
||||
break;
|
||||
default:
|
||||
// Ignore unknown types
|
||||
break;
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Parse naddr structured data
|
||||
static int parse_naddr_data(const uint8_t* data, size_t data_len, nostr_naddr_t* naddr) {
|
||||
size_t offset = 0;
|
||||
|
||||
while (offset < data_len) {
|
||||
if (offset + 2 > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
uint8_t type = data[offset];
|
||||
uint8_t length = data[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
if (offset + length > data_len) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
switch (type) {
|
||||
case TLV_IDENTIFIER: // identifier
|
||||
naddr->identifier = malloc(length + 1);
|
||||
if (!naddr->identifier) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(naddr->identifier, data + offset, length);
|
||||
naddr->identifier[length] = '\0';
|
||||
break;
|
||||
case TLV_SPECIAL: // pubkey
|
||||
if (length != 32) return NOSTR_ERROR_INVALID_INPUT;
|
||||
memcpy(naddr->pubkey, data + offset, 32);
|
||||
break;
|
||||
case TLV_KIND: // kind
|
||||
if (length != 4) return NOSTR_ERROR_INVALID_INPUT;
|
||||
naddr->kind = (data[offset] << 24) | (data[offset+1] << 16) | (data[offset+2] << 8) | data[offset+3];
|
||||
break;
|
||||
case TLV_RELAY: // relay URL
|
||||
{
|
||||
char* relay = malloc(length + 1);
|
||||
if (!relay) return NOSTR_ERROR_MEMORY_FAILED;
|
||||
memcpy(relay, data + offset, length);
|
||||
relay[length] = '\0';
|
||||
|
||||
char** new_relays = realloc(naddr->relays, (naddr->relay_count + 1) * sizeof(char*));
|
||||
if (!new_relays) {
|
||||
free(relay);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
naddr->relays = new_relays;
|
||||
naddr->relays[naddr->relay_count++] = relay;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Ignore unknown types
|
||||
break;
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
// URI construction functions
|
||||
|
||||
int nostr_build_uri_npub(const unsigned char* pubkey, char* output, size_t output_size) {
|
||||
if (!pubkey || !output || output_size < 70) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char bech32[100];
|
||||
int ret = nostr_key_to_bech32(pubkey, "npub", bech32);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
size_t len = strlen(bech32);
|
||||
if (len + 7 >= output_size) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
strcpy(output, "nostr:");
|
||||
strcpy(output + 6, bech32);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_build_uri_nsec(const unsigned char* privkey, char* output, size_t output_size) {
|
||||
if (!privkey || !output || output_size < 70) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char bech32[100];
|
||||
int ret = nostr_key_to_bech32(privkey, "nsec", bech32);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
size_t len = strlen(bech32);
|
||||
if (len + 7 >= output_size) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
strcpy(output, "nostr:");
|
||||
strcpy(output + 6, bech32);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
// Helper to build URI with prefix
|
||||
static int build_uri_with_prefix(const char* bech32, char* output, size_t output_size) {
|
||||
size_t len = strlen(bech32);
|
||||
if (len + 7 >= output_size) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
strcpy(output, "nostr:");
|
||||
strcpy(output + 6, bech32);
|
||||
return NOSTR_SUCCESS;
|
||||
}
|
||||
|
||||
int nostr_build_uri_note(const unsigned char* event_id, char* output, size_t output_size) {
|
||||
if (!event_id || !output || output_size < 70) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
char bech32[100];
|
||||
int ret = encode_structured_bech32("note", event_id, 32, bech32, sizeof(bech32));
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
return build_uri_with_prefix(bech32, output, output_size);
|
||||
}
|
||||
|
||||
int nostr_build_uri_nprofile(const unsigned char* pubkey, const char** relays, int relay_count,
|
||||
char* output, size_t output_size) {
|
||||
if (!pubkey || !output) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
// Build TLV data
|
||||
uint8_t* data = NULL;
|
||||
size_t data_len = 0;
|
||||
|
||||
// Add pubkey (special)
|
||||
uint8_t* pubkey_tlv;
|
||||
size_t pubkey_tlv_len;
|
||||
if (!tlv_encode(pubkey, 32, TLV_SPECIAL, &pubkey_tlv, &pubkey_tlv_len)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + pubkey_tlv_len);
|
||||
if (!data) {
|
||||
free(pubkey_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, pubkey_tlv, pubkey_tlv_len);
|
||||
data_len += pubkey_tlv_len;
|
||||
free(pubkey_tlv);
|
||||
|
||||
// Add relays
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
size_t relay_len = strlen(relays[i]);
|
||||
uint8_t* relay_tlv;
|
||||
size_t relay_tlv_len;
|
||||
if (!tlv_encode((uint8_t*)relays[i], relay_len, TLV_RELAY, &relay_tlv, &relay_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + relay_tlv_len);
|
||||
if (!data) {
|
||||
free(relay_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, relay_tlv, relay_tlv_len);
|
||||
data_len += relay_tlv_len;
|
||||
free(relay_tlv);
|
||||
}
|
||||
|
||||
// Encode to bech32
|
||||
char bech32[500];
|
||||
int ret = encode_structured_bech32("nprofile", data, data_len, bech32, sizeof(bech32));
|
||||
free(data);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
return build_uri_with_prefix(bech32, output, output_size);
|
||||
}
|
||||
|
||||
int nostr_build_uri_nevent(const unsigned char* event_id, const char** relays, int relay_count,
|
||||
const unsigned char* author, int kind, time_t created_at,
|
||||
char* output, size_t output_size) {
|
||||
if (!event_id || !output) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
// Build TLV data
|
||||
uint8_t* data = NULL;
|
||||
size_t data_len = 0;
|
||||
|
||||
// Add event_id (special)
|
||||
uint8_t* event_tlv;
|
||||
size_t event_tlv_len;
|
||||
if (!tlv_encode(event_id, 32, TLV_SPECIAL, &event_tlv, &event_tlv_len)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + event_tlv_len);
|
||||
if (!data) {
|
||||
free(event_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, event_tlv, event_tlv_len);
|
||||
data_len += event_tlv_len;
|
||||
free(event_tlv);
|
||||
|
||||
// Add relays
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
size_t relay_len = strlen(relays[i]);
|
||||
uint8_t* relay_tlv;
|
||||
size_t relay_tlv_len;
|
||||
if (!tlv_encode((uint8_t*)relays[i], relay_len, TLV_RELAY, &relay_tlv, &relay_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + relay_tlv_len);
|
||||
if (!data) {
|
||||
free(relay_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, relay_tlv, relay_tlv_len);
|
||||
data_len += relay_tlv_len;
|
||||
free(relay_tlv);
|
||||
}
|
||||
|
||||
// Add author if provided
|
||||
if (author) {
|
||||
uint8_t* author_tlv;
|
||||
size_t author_tlv_len;
|
||||
if (!tlv_encode(author, 32, TLV_AUTHOR, &author_tlv, &author_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + author_tlv_len);
|
||||
if (!data) {
|
||||
free(author_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, author_tlv, author_tlv_len);
|
||||
data_len += author_tlv_len;
|
||||
free(author_tlv);
|
||||
}
|
||||
|
||||
// Add kind if provided
|
||||
if (kind >= 0) {
|
||||
uint8_t kind_bytes[4];
|
||||
kind_bytes[0] = (kind >> 24) & 0xFF;
|
||||
kind_bytes[1] = (kind >> 16) & 0xFF;
|
||||
kind_bytes[2] = (kind >> 8) & 0xFF;
|
||||
kind_bytes[3] = kind & 0xFF;
|
||||
|
||||
uint8_t* kind_tlv;
|
||||
size_t kind_tlv_len;
|
||||
if (!tlv_encode(kind_bytes, 4, TLV_KIND, &kind_tlv, &kind_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + kind_tlv_len);
|
||||
if (!data) {
|
||||
free(kind_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, kind_tlv, kind_tlv_len);
|
||||
data_len += kind_tlv_len;
|
||||
free(kind_tlv);
|
||||
}
|
||||
|
||||
// Add created_at if provided
|
||||
if (created_at > 0) {
|
||||
uint8_t time_bytes[8];
|
||||
time_bytes[0] = (created_at >> 56) & 0xFF;
|
||||
time_bytes[1] = (created_at >> 48) & 0xFF;
|
||||
time_bytes[2] = (created_at >> 40) & 0xFF;
|
||||
time_bytes[3] = (created_at >> 32) & 0xFF;
|
||||
time_bytes[4] = (created_at >> 24) & 0xFF;
|
||||
time_bytes[5] = (created_at >> 16) & 0xFF;
|
||||
time_bytes[6] = (created_at >> 8) & 0xFF;
|
||||
time_bytes[7] = created_at & 0xFF;
|
||||
|
||||
uint8_t* time_tlv;
|
||||
size_t time_tlv_len;
|
||||
if (!tlv_encode(time_bytes, 8, TLV_CREATED_AT, &time_tlv, &time_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + time_tlv_len);
|
||||
if (!data) {
|
||||
free(time_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, time_tlv, time_tlv_len);
|
||||
data_len += time_tlv_len;
|
||||
free(time_tlv);
|
||||
}
|
||||
|
||||
// Encode to bech32
|
||||
char bech32[1000];
|
||||
int ret = encode_structured_bech32("nevent", data, data_len, bech32, sizeof(bech32));
|
||||
free(data);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
return build_uri_with_prefix(bech32, output, output_size);
|
||||
}
|
||||
|
||||
int nostr_build_uri_naddr(const char* identifier, const unsigned char* pubkey, int kind,
|
||||
const char** relays, int relay_count, char* output, size_t output_size) {
|
||||
if (!identifier || !pubkey || !output) return NOSTR_ERROR_INVALID_INPUT;
|
||||
|
||||
// Build TLV data
|
||||
uint8_t* data = NULL;
|
||||
size_t data_len = 0;
|
||||
|
||||
// Add identifier
|
||||
size_t id_len = strlen(identifier);
|
||||
uint8_t* id_tlv;
|
||||
size_t id_tlv_len;
|
||||
if (!tlv_encode((uint8_t*)identifier, id_len, TLV_IDENTIFIER, &id_tlv, &id_tlv_len)) {
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + id_tlv_len);
|
||||
if (!data) {
|
||||
free(id_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, id_tlv, id_tlv_len);
|
||||
data_len += id_tlv_len;
|
||||
free(id_tlv);
|
||||
|
||||
// Add pubkey (special)
|
||||
uint8_t* pubkey_tlv;
|
||||
size_t pubkey_tlv_len;
|
||||
if (!tlv_encode(pubkey, 32, TLV_SPECIAL, &pubkey_tlv, &pubkey_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + pubkey_tlv_len);
|
||||
if (!data) {
|
||||
free(pubkey_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, pubkey_tlv, pubkey_tlv_len);
|
||||
data_len += pubkey_tlv_len;
|
||||
free(pubkey_tlv);
|
||||
|
||||
// Add kind
|
||||
uint8_t kind_bytes[4];
|
||||
kind_bytes[0] = (kind >> 24) & 0xFF;
|
||||
kind_bytes[1] = (kind >> 16) & 0xFF;
|
||||
kind_bytes[2] = (kind >> 8) & 0xFF;
|
||||
kind_bytes[3] = kind & 0xFF;
|
||||
|
||||
uint8_t* kind_tlv;
|
||||
size_t kind_tlv_len;
|
||||
if (!tlv_encode(kind_bytes, 4, TLV_KIND, &kind_tlv, &kind_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + kind_tlv_len);
|
||||
if (!data) {
|
||||
free(kind_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, kind_tlv, kind_tlv_len);
|
||||
data_len += kind_tlv_len;
|
||||
free(kind_tlv);
|
||||
|
||||
// Add relays
|
||||
for (int i = 0; i < relay_count; i++) {
|
||||
size_t relay_len = strlen(relays[i]);
|
||||
uint8_t* relay_tlv;
|
||||
size_t relay_tlv_len;
|
||||
if (!tlv_encode((uint8_t*)relays[i], relay_len, TLV_RELAY, &relay_tlv, &relay_tlv_len)) {
|
||||
free(data);
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
data = realloc(data, data_len + relay_tlv_len);
|
||||
if (!data) {
|
||||
free(relay_tlv);
|
||||
return NOSTR_ERROR_MEMORY_FAILED;
|
||||
}
|
||||
memcpy(data + data_len, relay_tlv, relay_tlv_len);
|
||||
data_len += relay_tlv_len;
|
||||
free(relay_tlv);
|
||||
}
|
||||
|
||||
// Encode to bech32
|
||||
char bech32[1000];
|
||||
int ret = encode_structured_bech32("naddr", data, data_len, bech32, sizeof(bech32));
|
||||
free(data);
|
||||
if (ret != NOSTR_SUCCESS) return ret;
|
||||
|
||||
return build_uri_with_prefix(bech32, output, output_size);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* NOSTR Core Library - NIP-021: nostr: URI scheme
|
||||
*/
|
||||
|
||||
#ifndef NIP021_H
|
||||
#define NIP021_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include "nip001.h"
|
||||
|
||||
// URI type enumeration
|
||||
typedef enum {
|
||||
NOSTR_URI_NPUB, // Simple 32-byte pubkey
|
||||
NOSTR_URI_NSEC, // Simple 32-byte privkey
|
||||
NOSTR_URI_NOTE, // Simple 32-byte event ID
|
||||
NOSTR_URI_NPROFILE, // Structured: pubkey + relays
|
||||
NOSTR_URI_NEVENT, // Structured: event ID + relays + metadata
|
||||
NOSTR_URI_NADDR, // Structured: address + relays + metadata
|
||||
NOSTR_URI_INVALID
|
||||
} nostr_uri_type_t;
|
||||
|
||||
// Structured data types for complex URIs
|
||||
typedef struct {
|
||||
unsigned char pubkey[32];
|
||||
char** relays;
|
||||
int relay_count;
|
||||
} nostr_nprofile_t;
|
||||
|
||||
typedef struct {
|
||||
unsigned char event_id[32];
|
||||
char** relays;
|
||||
int relay_count;
|
||||
unsigned char* author; // Optional, 32 bytes if present
|
||||
int* kind; // Optional
|
||||
time_t* created_at; // Optional
|
||||
} nostr_nevent_t;
|
||||
|
||||
typedef struct {
|
||||
char* identifier;
|
||||
unsigned char pubkey[32];
|
||||
int kind;
|
||||
char** relays;
|
||||
int relay_count;
|
||||
} nostr_naddr_t;
|
||||
|
||||
// Unified URI result structure
|
||||
typedef struct {
|
||||
nostr_uri_type_t type;
|
||||
union {
|
||||
unsigned char pubkey[32]; // For NPUB
|
||||
unsigned char privkey[32]; // For NSEC
|
||||
unsigned char event_id[32]; // For NOTE
|
||||
nostr_nprofile_t nprofile; // For NPROFILE
|
||||
nostr_nevent_t nevent; // For NEVENT
|
||||
nostr_naddr_t naddr; // For NADDR
|
||||
} data;
|
||||
} nostr_uri_result_t;
|
||||
|
||||
// Function declarations
|
||||
|
||||
// Main parsing function - unified entry point
|
||||
int nostr_parse_uri(const char* uri, nostr_uri_result_t* result);
|
||||
|
||||
// URI construction functions
|
||||
int nostr_build_uri_npub(const unsigned char* pubkey, char* output, size_t output_size);
|
||||
int nostr_build_uri_nsec(const unsigned char* privkey, char* output, size_t output_size);
|
||||
int nostr_build_uri_note(const unsigned char* event_id, char* output, size_t output_size);
|
||||
int nostr_build_uri_nprofile(const unsigned char* pubkey, const char** relays, int relay_count,
|
||||
char* output, size_t output_size);
|
||||
int nostr_build_uri_nevent(const unsigned char* event_id, const char** relays, int relay_count,
|
||||
const unsigned char* author, int kind, time_t created_at,
|
||||
char* output, size_t output_size);
|
||||
int nostr_build_uri_naddr(const char* identifier, const unsigned char* pubkey, int kind,
|
||||
const char** relays, int relay_count, char* output, size_t output_size);
|
||||
|
||||
// Utility functions
|
||||
void nostr_uri_result_free(nostr_uri_result_t* result);
|
||||
nostr_uri_type_t nostr_detect_uri_type(const char* uri);
|
||||
|
||||
#endif // NIP021_H
|
||||
+1
-1
@@ -13,7 +13,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
// NIP-44 constants
|
||||
// #define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 65535
|
||||
// #define NOSTR_NIP44_MAX_PLAINTEXT_SIZE 1048576
|
||||
|
||||
/**
|
||||
* NIP-44: Encrypt a message using ECDH + ChaCha20 + HMAC
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
+16
-10
@@ -26,12 +26,18 @@ static void memory_clear(const void *p, size_t len) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random timestamp within 2 days in the past (as per NIP-59 spec)
|
||||
* Create a random timestamp within max_delay_sec in the past (configurable)
|
||||
*/
|
||||
static time_t random_past_timestamp(void) {
|
||||
static time_t random_past_timestamp(long max_delay_sec) {
|
||||
time_t now = time(NULL);
|
||||
// Random time up to 2 days (172800 seconds) in the past
|
||||
long random_offset = (long)(rand() % 172800);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -104,8 +110,8 @@ cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Use provided timestamp or random past timestamp
|
||||
time_t event_time = (created_at == 0) ? random_past_timestamp() : created_at;
|
||||
// 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();
|
||||
@@ -142,7 +148,7 @@ cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
|
||||
* 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) {
|
||||
const unsigned char* recipient_public_key, long max_delay_sec) {
|
||||
if (!rumor || !sender_private_key || !recipient_public_key) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -178,7 +184,7 @@ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private
|
||||
return NULL;
|
||||
}
|
||||
|
||||
time_t seal_time = random_past_timestamp();
|
||||
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);
|
||||
@@ -217,7 +223,7 @@ cJSON* nostr_nip59_create_seal(cJSON* rumor, const unsigned char* sender_private
|
||||
/**
|
||||
* 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) {
|
||||
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;
|
||||
}
|
||||
@@ -272,7 +278,7 @@ cJSON* nostr_nip59_create_gift_wrap(cJSON* seal, const char* recipient_public_ke
|
||||
return NULL;
|
||||
}
|
||||
|
||||
time_t wrap_time = random_past_timestamp();
|
||||
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);
|
||||
|
||||
+4
-2
@@ -33,19 +33,21 @@ cJSON* nostr_nip59_create_rumor(int kind, const char* content, cJSON* tags,
|
||||
* @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);
|
||||
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);
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 17
|
||||
#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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+32
-4
@@ -2,9 +2,9 @@
|
||||
#define NOSTR_CORE_H
|
||||
|
||||
// Version information (auto-updated by increment_and_push.sh)
|
||||
#define VERSION "v0.4.4"
|
||||
#define VERSION "v0.5.4"
|
||||
#define VERSION_MAJOR 0
|
||||
#define VERSION_MINOR 4
|
||||
#define VERSION_MINOR 5
|
||||
#define VERSION_PATCH 4
|
||||
|
||||
/*
|
||||
@@ -49,6 +49,13 @@
|
||||
* - 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)
|
||||
@@ -177,13 +184,21 @@ extern "C" {
|
||||
#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,
|
||||
@@ -240,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
|
||||
@@ -287,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(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "nostr_log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define NOSTR_LOG_BUFFER_SIZE 4096
|
||||
|
||||
static nostr_log_callback_t g_log_callback = NULL;
|
||||
static void* g_log_user_data = NULL;
|
||||
static int g_log_min_level = NOSTR_LOG_LEVEL_ERROR;
|
||||
|
||||
void nostr_set_log_callback(nostr_log_callback_t cb, void* user_data) {
|
||||
g_log_callback = cb;
|
||||
g_log_user_data = user_data;
|
||||
}
|
||||
|
||||
void nostr_set_log_level(nostr_log_level_t min_level) {
|
||||
if (min_level < NOSTR_LOG_LEVEL_ERROR) {
|
||||
g_log_min_level = NOSTR_LOG_LEVEL_ERROR;
|
||||
return;
|
||||
}
|
||||
if (min_level > NOSTR_LOG_LEVEL_TRACE) {
|
||||
g_log_min_level = NOSTR_LOG_LEVEL_TRACE;
|
||||
return;
|
||||
}
|
||||
g_log_min_level = (int)min_level;
|
||||
}
|
||||
|
||||
void nostr_log_vemitf(int level, const char* component, const char* format, va_list args) {
|
||||
if (!g_log_callback || !format) {
|
||||
return;
|
||||
}
|
||||
if (level < g_log_min_level) {
|
||||
return;
|
||||
}
|
||||
|
||||
char message[NOSTR_LOG_BUFFER_SIZE];
|
||||
int written = vsnprintf(message, sizeof(message), format, args);
|
||||
if (written < 0) {
|
||||
return;
|
||||
}
|
||||
message[sizeof(message) - 1] = '\0';
|
||||
|
||||
g_log_callback(level,
|
||||
component ? component : "unknown",
|
||||
message,
|
||||
g_log_user_data);
|
||||
}
|
||||
|
||||
void nostr_log_emitf(int level, const char* component, const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
nostr_log_vemitf(level, component, format, args);
|
||||
va_end(args);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef NOSTR_LOG_H
|
||||
#define NOSTR_LOG_H
|
||||
|
||||
#include <stdarg.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
NOSTR_LOG_LEVEL_ERROR = 1,
|
||||
NOSTR_LOG_LEVEL_WARN = 2,
|
||||
NOSTR_LOG_LEVEL_INFO = 3,
|
||||
NOSTR_LOG_LEVEL_DEBUG = 4,
|
||||
NOSTR_LOG_LEVEL_TRACE = 5
|
||||
} nostr_log_level_t;
|
||||
|
||||
typedef void (*nostr_log_callback_t)(
|
||||
int level,
|
||||
const char* component,
|
||||
const char* message,
|
||||
void* user_data
|
||||
);
|
||||
|
||||
void nostr_set_log_callback(nostr_log_callback_t cb, void* user_data);
|
||||
void nostr_set_log_level(nostr_log_level_t min_level);
|
||||
|
||||
void nostr_log_emitf(int level, const char* component, const char* format, ...);
|
||||
void nostr_log_vemitf(int level, const char* component, const char* format, va_list args);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NOSTR_LOG_H */
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,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
|
||||
|
||||
@@ -901,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) {
|
||||
@@ -918,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) {
|
||||
@@ -936,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
|
||||
}
|
||||
@@ -974,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
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Logging Delegation Plan for `nostr_core_lib`
|
||||
|
||||
## Objective
|
||||
|
||||
Refactor `nostr_core_lib` so it does not own log destinations such as `debug.log`, and instead emits logs through a consumer-provided callback. This allows host applications such as [`didactyl`](../README.md) to route all library logs into their own logging system with consistent formatting, level control, and file policy.
|
||||
|
||||
## Current State and Problem
|
||||
|
||||
The library currently writes directly to `debug.log` in multiple places:
|
||||
|
||||
- [`nostr_core_lib/nostr_websocket/nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:1008)
|
||||
- [`nostr_core_lib/nostr_core/nip013.c`](../nostr_core/nip013.c:178)
|
||||
- [`nostr_core_lib/nostr_core/nip013.c`](../nostr_core/nip013.c:242)
|
||||
- [`nostr_core_lib/nostr_core/nip013.c`](../nostr_core/nip013.c:270)
|
||||
|
||||
This creates collisions with consumer logs when the host app also uses `debug.log` such as [`didactyl/src/debug.c`](../../src/debug.c:18).
|
||||
|
||||
## Target Design
|
||||
|
||||
Adopt a callback-based logging API inside `nostr_core_lib`:
|
||||
|
||||
1. Library owns no file handle for logs.
|
||||
2. Library emits structured log events by level and source.
|
||||
3. Consumer registers a callback once during startup.
|
||||
4. If no callback is set, logs are dropped by default.
|
||||
|
||||
### High-Level Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[nostr_core_lib internal code] --> B[nostr_log_emit level source message]
|
||||
B --> C{callback configured}
|
||||
C -->|Yes| D[consumer callback]
|
||||
C -->|No| E[drop log event]
|
||||
D --> F[didactyl debug_log routing]
|
||||
F --> G[didactyl log file or stdout]
|
||||
```
|
||||
|
||||
## API Additions
|
||||
|
||||
Add a small public logging API in [`nostr_core_lib/nostr_core/nostr_core.h`](../nostr_core/nostr_core.h):
|
||||
|
||||
- `nostr_log_level_t` enum with levels compatible with host systems
|
||||
- `nostr_log_callback_t` callback type
|
||||
- `void nostr_set_log_callback(nostr_log_callback_t cb, void* user_data);`
|
||||
- `void nostr_set_log_level(nostr_log_level_t min_level);`
|
||||
|
||||
Recommended callback shape:
|
||||
|
||||
```c
|
||||
typedef void (*nostr_log_callback_t)(
|
||||
int level,
|
||||
const char* component,
|
||||
const char* message,
|
||||
void* user_data
|
||||
);
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Keep ABI simple by formatting message inside the library before callback invocation.
|
||||
- Include `component` values such as `websocket` and `nip013`.
|
||||
|
||||
## Internal Implementation Plan
|
||||
|
||||
## Phase 1: Logging Core
|
||||
|
||||
1. Create `nostr_core_lib/nostr_core/nostr_log.h` and `nostr_core_lib/nostr_core/nostr_log.c`.
|
||||
2. Add internal singleton state:
|
||||
- callback pointer
|
||||
- callback user data
|
||||
- minimum log level
|
||||
3. Add internal helpers:
|
||||
- `nostr_log_emitf level component fmt ...`
|
||||
- stack buffer formatting with truncation safety
|
||||
4. Default behavior when no callback set: no-op.
|
||||
|
||||
## Phase 2: Replace File Logging in WebSocket Layer
|
||||
|
||||
1. Remove file-specific static globals from [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:27).
|
||||
2. Remove `debug_log_init` and `debug_log_message` path at [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:1005).
|
||||
3. Replace call sites:
|
||||
- outgoing frame logging near [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:871)
|
||||
- incoming frame logging near [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:964)
|
||||
4. Emit through `nostr_log_emitf` with component `websocket`.
|
||||
|
||||
## Phase 3: Replace File Logging in NIP-13
|
||||
|
||||
1. Replace `fopen`/`fprintf` blocks in [`nip013.c`](../nostr_core/nip013.c:178), [`nip013.c`](../nostr_core/nip013.c:242), and [`nip013.c`](../nostr_core/nip013.c:270).
|
||||
2. Emit equivalent debug lines via `nostr_log_emitf` with component `nip013`.
|
||||
3. Preserve existing compile-time verbosity guards if needed, but route output through callback.
|
||||
|
||||
## Phase 4: Wire Public API
|
||||
|
||||
1. Export new functions in [`nostr_core.h`](../nostr_core/nostr_core.h:1).
|
||||
2. Ensure object file inclusion in `nostr_core_lib` build scripts and static archive.
|
||||
3. Document behavior in [`nostr_core_lib/README.md`](../README.md).
|
||||
|
||||
## Phase 5: Consumer Integration in Didactyl
|
||||
|
||||
1. In didactyl initialization path near [`src/nostr_handler.c`](../../src/nostr_handler.c:1519), register `nostr_set_log_callback`.
|
||||
2. Adapter callback maps library levels to didactyl levels from [`src/debug.h`](../../src/debug.h:7).
|
||||
3. Prefix messages with source namespace such as `[nostr:websocket]` and `[nostr:nip013]`.
|
||||
|
||||
## Backward Compatibility Strategy
|
||||
|
||||
- Keep existing compile guards but change behavior from file output to callback emission.
|
||||
- If callback not set, behavior remains safe and silent.
|
||||
- No breaking changes for consumers not using logging.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Unit Tests in `nostr_core_lib`
|
||||
|
||||
1. Add tests for callback registration and null behavior.
|
||||
2. Add tests for level filtering.
|
||||
3. Add tests that message formatting truncates safely and remains null terminated.
|
||||
|
||||
### Integration Checks in Didactyl
|
||||
|
||||
1. Start didactyl with debug enabled and callback wired.
|
||||
2. Verify websocket SEND and RECV lines appear through didactyl logger format from [`src/debug.c`](../../src/debug.c:31).
|
||||
3. Verify no separate websocket-only banner line from old path near [`nostr_websocket_openssl.c`](../nostr_websocket/nostr_websocket_openssl.c:1010).
|
||||
4. Verify LLM request log lines and nostr logs coexist in one log destination.
|
||||
|
||||
## Rollout Steps
|
||||
|
||||
1. Implement callback infrastructure and migrate websocket logging first.
|
||||
2. Validate no direct file writes remain in websocket module.
|
||||
3. Migrate nip013 logging.
|
||||
4. Add documentation and examples.
|
||||
5. Update didactyl wiring.
|
||||
6. Remove obsolete references to `ENABLE_FILE_LOGGING` for websocket logging behavior if no longer needed.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- Risk: excessive callback volume from websocket trace traffic.
|
||||
- Mitigation: implement minimum level filter in library and map websocket traffic to trace level.
|
||||
- Risk: callback reentrancy or thread concerns.
|
||||
- Mitigation: document callback must be fast and thread-safe, avoid heavy blocking in callback.
|
||||
- Risk: API drift across consumers.
|
||||
- Mitigation: keep callback signature minimal and stable.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- No direct `fopen("debug.log", ...)` remains in production logging paths in `nostr_core_lib`.
|
||||
- Consumer can set callback and receive websocket and nip013 logs.
|
||||
- Didactyl routes nostr_core_lib logs through its logger and single configured destination.
|
||||
- Documentation updated with migration and usage examples.
|
||||
@@ -0,0 +1,209 @@
|
||||
# NIP-42 Authentication Support for core_relay_pool.c
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The relay pool ([`core_relay_pool.c`](nostr_core/core_relay_pool.c)) does not handle NIP-42 authentication. When a relay sends an `AUTH` challenge message, the pool silently ignores it, causing:
|
||||
|
||||
- Subscriptions to fail on auth-required relays (events never delivered)
|
||||
- Publishes to be rejected with `auth-required:` errors
|
||||
- No visibility into authentication state per relay
|
||||
|
||||
The synchronous relay functions in [`core_relays.c`](nostr_core/core_relays.c) already have full NIP-42 support (lines 58-63, 211-239, 595-624). The NIP-42 primitives in [`nip042.c`](nostr_core/nip042.c) / [`nip042.h`](nostr_core/nip042.h) are production-ready. This plan wires them into the pool.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Relay sends AUTH challenge] --> B[process_relay_message parses AUTH]
|
||||
B --> C{pool has private_key?}
|
||||
C -->|No| D[Log warning, mark relay auth_state = CHALLENGE_RECEIVED]
|
||||
C -->|Yes| E[nostr_nip42_create_auth_event]
|
||||
E --> F[nostr_nip42_create_auth_message]
|
||||
F --> G[nostr_ws_send_text to relay]
|
||||
G --> H[Mark relay auth_state = AUTHENTICATING]
|
||||
H --> I[Relay sends OK for auth event]
|
||||
I --> J[Mark relay auth_state = AUTHENTICATED]
|
||||
|
||||
K[Relay sends OK with auth-required] --> L[process_relay_message detects prefix]
|
||||
L --> M{relay already authenticated?}
|
||||
M -->|No| N[Trigger auth flow if private_key available]
|
||||
M -->|Yes| O[Surface as publish failure]
|
||||
|
||||
P[Relay sends NOTICE] --> Q{Contains auth-required?}
|
||||
Q -->|Yes| R[Log auth-required notice]
|
||||
Q -->|No| S[Ignore or log]
|
||||
```
|
||||
|
||||
## Detailed Changes
|
||||
|
||||
### 1. Add NIP-42 include to core_relay_pool.c
|
||||
|
||||
Add `#include "nip042.h"` alongside the existing includes at the top of [`core_relay_pool.c`](nostr_core/core_relay_pool.c:27).
|
||||
|
||||
### 2. Add auth fields to `relay_connection_t` struct
|
||||
|
||||
Modeled after [`core_relays.c:58-63`](nostr_core/core_relays.c:58), add to the [`relay_connection_t`](nostr_core/core_relay_pool.c:76) struct:
|
||||
|
||||
```c
|
||||
// NIP-42 Authentication fields
|
||||
nostr_auth_state_t auth_state;
|
||||
char auth_challenge[NOSTR_NIP42_MAX_CHALLENGE_LENGTH];
|
||||
time_t auth_challenge_time;
|
||||
int nip42_enabled;
|
||||
```
|
||||
|
||||
These go after the existing `last_connection_error_time` field (around line 106).
|
||||
|
||||
### 3. Add private key and NIP-42 config to `nostr_relay_pool` struct
|
||||
|
||||
Add to the [`nostr_relay_pool`](nostr_core/core_relay_pool.c:146) struct:
|
||||
|
||||
```c
|
||||
// NIP-42 Authentication configuration
|
||||
int nip42_enabled;
|
||||
unsigned char private_key[32];
|
||||
int has_private_key;
|
||||
```
|
||||
|
||||
### 4. Add public API: `nostr_relay_pool_set_auth()`
|
||||
|
||||
Add to [`nostr_core.h`](nostr_core/nostr_core.h) after the existing pool management functions (around line 252):
|
||||
|
||||
```c
|
||||
// NIP-42 Authentication configuration for relay pool
|
||||
int nostr_relay_pool_set_auth(nostr_relay_pool_t* pool,
|
||||
const unsigned char* private_key,
|
||||
int enable);
|
||||
```
|
||||
|
||||
Implement in [`core_relay_pool.c`](nostr_core/core_relay_pool.c):
|
||||
- Copies the 32-byte private key into the pool struct
|
||||
- Sets `nip42_enabled` flag
|
||||
- Propagates `nip42_enabled` to all existing relay connections
|
||||
|
||||
### 5. Handle AUTH in `process_relay_message()`
|
||||
|
||||
Add an `AUTH` handler in [`process_relay_message()`](nostr_core/core_relay_pool.c:912) between the existing `OK` and `PONG` handlers (around line 1097). The logic mirrors [`core_relays.c:211-239`](nostr_core/core_relays.c:211):
|
||||
|
||||
```c
|
||||
} else if (strcmp(msg_type, "AUTH") == 0) {
|
||||
// Handle AUTH challenge: ["AUTH", <challenge-string>]
|
||||
if (pool->nip42_enabled && pool->has_private_key &&
|
||||
cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* challenge_json = cJSON_GetArrayItem(parsed, 1);
|
||||
if (cJSON_IsString(challenge_json)) {
|
||||
const char* challenge = cJSON_GetStringValue(challenge_json);
|
||||
|
||||
// Store challenge
|
||||
strncpy(relay->auth_challenge, challenge,
|
||||
sizeof(relay->auth_challenge) - 1);
|
||||
relay->auth_challenge[sizeof(relay->auth_challenge) - 1] = '\0';
|
||||
relay->auth_challenge_time = time(NULL);
|
||||
relay->auth_state = NOSTR_AUTH_STATE_CHALLENGE_RECEIVED;
|
||||
|
||||
// Create and send auth event
|
||||
cJSON* auth_event = nostr_nip42_create_auth_event(
|
||||
challenge, relay->url, pool->private_key, 0);
|
||||
if (auth_event) {
|
||||
char* auth_message = nostr_nip42_create_auth_message(auth_event);
|
||||
if (auth_message) {
|
||||
if (nostr_ws_send_text(relay->ws_client, auth_message) >= 0) {
|
||||
relay->auth_state = NOSTR_AUTH_STATE_AUTHENTICATING;
|
||||
}
|
||||
free(auth_message);
|
||||
}
|
||||
cJSON_Delete(auth_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Handle AUTH in `nostr_relay_pool_query_sync()` inline loop
|
||||
|
||||
The [`nostr_relay_pool_query_sync()`](nostr_core/core_relay_pool.c:1107) function has its own inline message parsing loop (lines 1178-1227) that bypasses `process_relay_message()`. Add AUTH handling there too, similar to the pattern above.
|
||||
|
||||
### 7. Handle NOTICE messages
|
||||
|
||||
Add a `NOTICE` handler in [`process_relay_message()`](nostr_core/core_relay_pool.c:912):
|
||||
|
||||
```c
|
||||
} else if (strcmp(msg_type, "NOTICE") == 0) {
|
||||
// Handle NOTICE: ["NOTICE", <message>]
|
||||
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 2) {
|
||||
cJSON* notice_msg = cJSON_GetArrayItem(parsed, 1);
|
||||
if (cJSON_IsString(notice_msg)) {
|
||||
const char* notice = cJSON_GetStringValue(notice_msg);
|
||||
// Detect auth-required notices
|
||||
if (strstr(notice, "auth-required") != NULL) {
|
||||
relay->auth_state = NOSTR_AUTH_STATE_NONE; // Reset for re-auth
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Handle OK auth-required rejections
|
||||
|
||||
Enhance the existing [`OK` handler](nostr_core/core_relay_pool.c:1048) to detect `auth-required:` prefix in error messages. When detected:
|
||||
- If the pool has a private key and the relay hasn't been authenticated yet, trigger the auth flow
|
||||
- Surface the rejection through the publish callback with the auth-required message
|
||||
|
||||
### 9. Initialize auth fields in `nostr_relay_pool_add_relay()`
|
||||
|
||||
In [`nostr_relay_pool_add_relay()`](nostr_core/core_relay_pool.c:605), initialize the new auth fields:
|
||||
|
||||
```c
|
||||
// Initialize NIP-42 authentication fields
|
||||
relay->auth_state = NOSTR_AUTH_STATE_NONE;
|
||||
memset(relay->auth_challenge, 0, sizeof(relay->auth_challenge));
|
||||
relay->auth_challenge_time = 0;
|
||||
relay->nip42_enabled = pool->nip42_enabled;
|
||||
```
|
||||
|
||||
### 10. Secure cleanup in `nostr_relay_pool_destroy()`
|
||||
|
||||
In [`nostr_relay_pool_destroy()`](nostr_core/core_relay_pool.c:684), zero the private key before freeing:
|
||||
|
||||
```c
|
||||
// Securely zero private key
|
||||
if (pool->has_private_key) {
|
||||
memset(pool->private_key, 0, sizeof(pool->private_key));
|
||||
pool->has_private_key = 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Test file
|
||||
|
||||
Create `tests/nip42_pool_test.c` that:
|
||||
- Creates a pool with auth configured
|
||||
- Verifies `nostr_relay_pool_set_auth()` stores the key
|
||||
- Simulates AUTH challenge parsing logic (unit-level, no live relay needed)
|
||||
- Verifies auth event creation with the pool's private key
|
||||
|
||||
### 12. Documentation updates
|
||||
|
||||
- Update [`POOL_API.md`](POOL_API.md) with `nostr_relay_pool_set_auth()` documentation
|
||||
- Update [`README.md`](README.md) with a relay pool NIP-42 usage example
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Change Type | Description |
|
||||
|------|-------------|-------------|
|
||||
| [`nostr_core/core_relay_pool.c`](nostr_core/core_relay_pool.c) | Modified | Add auth fields, AUTH/NOTICE handlers, auth config API |
|
||||
| [`nostr_core/nostr_core.h`](nostr_core/nostr_core.h) | Modified | Add `nostr_relay_pool_set_auth()` declaration |
|
||||
| [`tests/nip42_pool_test.c`](tests/nip42_pool_test.c) | New | NIP-42 pool authentication tests |
|
||||
| [`POOL_API.md`](POOL_API.md) | Modified | Document new auth API |
|
||||
| [`README.md`](README.md) | Modified | Add pool auth usage example |
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
1. **Private key stored in pool, not per-relay**: A single identity authenticates to all relays in the pool. This matches the common use case and mirrors how `core_relays.c` accepts a single `private_key` parameter.
|
||||
|
||||
2. **Auth state tracked per-relay**: Each relay connection tracks its own `nostr_auth_state_t` since different relays may challenge at different times.
|
||||
|
||||
3. **Automatic re-auth on reconnect**: When a relay reconnects (via the existing reconnection logic), the auth state resets to `NOSTR_AUTH_STATE_NONE`, allowing the relay to re-challenge.
|
||||
|
||||
4. **No blocking on auth**: The AUTH response is sent asynchronously. If a relay requires auth before accepting subscriptions, the relay will re-send events after authentication succeeds. The pool does not block waiting for auth confirmation.
|
||||
|
||||
5. **Reuse existing NIP-42 primitives**: All crypto and message formatting uses [`nostr_nip42_create_auth_event()`](nostr_core/nip042.c:26) and [`nostr_nip42_create_auth_message()`](nostr_core/nip042.c:84) — no new crypto code needed.
|
||||
@@ -0,0 +1,350 @@
|
||||
# NIP-46 Remote Signing — Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
NIP-46 defines a protocol for **Nostr Remote Signing**, enabling 2-way communication between a client application and a remote signer (bunker) over Nostr relays. The remote signer holds the user's private keys and performs cryptographic operations on behalf of the client, reducing the attack surface by keeping keys off the client device.
|
||||
|
||||
This plan covers implementing **both sides** of the protocol:
|
||||
- **Client-side**: sends requests to a remote signer and processes responses
|
||||
- **Remote-signer-side**: receives requests and produces responses (for building bunker applications)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Protocol Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client
|
||||
participant R as Relay
|
||||
participant S as Remote Signer
|
||||
|
||||
Note over C: Generate client-keypair
|
||||
C->>R: Subscribe to kind:24133 p-tagged to client-pubkey
|
||||
C->>R: Publish connect request, kind:24133, p-tag remote-signer-pubkey
|
||||
R->>S: Deliver connect request
|
||||
S->>R: Publish connect response, kind:24133, p-tag client-pubkey
|
||||
R->>C: Deliver connect response
|
||||
Note over C: Connection established
|
||||
|
||||
C->>R: Publish get_public_key request
|
||||
R->>S: Deliver request
|
||||
S->>R: Publish response with user-pubkey
|
||||
R->>C: Deliver response
|
||||
Note over C: Now knows user-pubkey
|
||||
|
||||
C->>R: Publish sign_event request
|
||||
R->>S: Deliver request
|
||||
S->>R: Publish signed event response
|
||||
R->>C: Deliver signed event
|
||||
```
|
||||
|
||||
### Connection Initiation
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Connection Initiation] --> B{Who initiates?}
|
||||
B -->|Remote Signer| C[bunker:// URL]
|
||||
B -->|Client| D[nostrconnect:// URL]
|
||||
|
||||
C --> E[Client parses bunker URL]
|
||||
E --> F[Extract remote-signer-pubkey + relays + secret]
|
||||
F --> G[Client sends connect request to remote-signer]
|
||||
|
||||
D --> H[Remote signer parses nostrconnect URL]
|
||||
H --> I[Extract client-pubkey + relays + secret + perms]
|
||||
I --> J[Remote signer sends connect response to client]
|
||||
|
||||
G --> K[Connection Established]
|
||||
J --> K
|
||||
```
|
||||
|
||||
### Component Dependency Map
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
NIP46[nip046.c/h] --> NIP44[nip044 - NIP-44 Encryption]
|
||||
NIP46 --> NIP04[nip004 - NIP-04 Encryption]
|
||||
NIP46 --> NIP01[nip001 - Event Creation/Signing]
|
||||
NIP46 --> POOL[core_relay_pool - Relay Communication]
|
||||
NIP46 --> UTILS[utils - Hex/Bytes/Random]
|
||||
NIP46 --> CJSON[cJSON - JSON Handling]
|
||||
NIP46 --> COMMON[nostr_common - Error Codes/Constants]
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
### Core Types
|
||||
|
||||
```c
|
||||
// NIP-46 event kind
|
||||
#define NOSTR_NIP46_REQUEST_KIND 24133
|
||||
|
||||
// Connection types
|
||||
typedef enum {
|
||||
NOSTR_NIP46_CONN_BUNKER, // bunker:// initiated by remote-signer
|
||||
NOSTR_NIP46_CONN_NOSTRCONNECT // nostrconnect:// initiated by client
|
||||
} nostr_nip46_connection_type_t;
|
||||
|
||||
// RPC Method types
|
||||
typedef enum {
|
||||
NOSTR_NIP46_METHOD_CONNECT,
|
||||
NOSTR_NIP46_METHOD_SIGN_EVENT,
|
||||
NOSTR_NIP46_METHOD_PING,
|
||||
NOSTR_NIP46_METHOD_GET_PUBLIC_KEY,
|
||||
NOSTR_NIP46_METHOD_NIP04_ENCRYPT,
|
||||
NOSTR_NIP46_METHOD_NIP04_DECRYPT,
|
||||
NOSTR_NIP46_METHOD_NIP44_ENCRYPT,
|
||||
NOSTR_NIP46_METHOD_NIP44_DECRYPT
|
||||
} nostr_nip46_method_t;
|
||||
|
||||
// Parsed bunker:// URL
|
||||
typedef struct {
|
||||
char remote_signer_pubkey[65]; // hex pubkey
|
||||
char relays[8][256]; // up to 8 relay URLs
|
||||
int relay_count;
|
||||
char secret[128]; // optional secret
|
||||
} nostr_nip46_bunker_url_t;
|
||||
|
||||
// Parsed nostrconnect:// URL
|
||||
typedef struct {
|
||||
char client_pubkey[65]; // hex pubkey
|
||||
char relays[8][256]; // relay URLs
|
||||
int relay_count;
|
||||
char secret[128]; // required secret
|
||||
char perms[512]; // optional permissions
|
||||
char name[128]; // optional app name
|
||||
char url[256]; // optional app URL
|
||||
char image[256]; // optional app image
|
||||
} nostr_nip46_nostrconnect_url_t;
|
||||
|
||||
// JSON-RPC Request
|
||||
typedef struct {
|
||||
char id[65]; // random request ID
|
||||
nostr_nip46_method_t method;
|
||||
char method_str[32]; // string form of method
|
||||
char** params; // array of string params
|
||||
int param_count;
|
||||
} nostr_nip46_request_t;
|
||||
|
||||
// JSON-RPC Response
|
||||
typedef struct {
|
||||
char id[65]; // matching request ID
|
||||
char* result; // result string
|
||||
char* error; // error string, NULL if success
|
||||
} nostr_nip46_response_t;
|
||||
|
||||
// Client session state
|
||||
typedef struct {
|
||||
unsigned char client_private_key[32];
|
||||
char client_pubkey_hex[65];
|
||||
char remote_signer_pubkey_hex[65];
|
||||
unsigned char remote_signer_pubkey[32];
|
||||
char user_pubkey_hex[65]; // learned via get_public_key
|
||||
char relays[8][256];
|
||||
int relay_count;
|
||||
int connected;
|
||||
} nostr_nip46_client_session_t;
|
||||
|
||||
// Signer session state
|
||||
typedef struct {
|
||||
unsigned char signer_private_key[32];
|
||||
char signer_pubkey_hex[65];
|
||||
unsigned char user_private_key[32];
|
||||
char user_pubkey_hex[65];
|
||||
char client_pubkey_hex[65];
|
||||
unsigned char client_pubkey[32];
|
||||
char relays[8][256];
|
||||
int relay_count;
|
||||
int connected;
|
||||
} nostr_nip46_signer_session_t;
|
||||
```
|
||||
|
||||
## Public API Functions
|
||||
|
||||
### URL Parsing
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_parse_bunker_url()` | Parse `bunker://<pubkey>?relay=...&secret=...` |
|
||||
| `nostr_nip46_parse_nostrconnect_url()` | Parse `nostrconnect://<pubkey>?relay=...&secret=...&perms=...` |
|
||||
| `nostr_nip46_create_bunker_url()` | Generate a bunker:// connection token |
|
||||
| `nostr_nip46_create_nostrconnect_url()` | Generate a nostrconnect:// connection token |
|
||||
|
||||
### JSON-RPC Message Construction
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_create_request()` | Build a JSON-RPC request object |
|
||||
| `nostr_nip46_create_response()` | Build a JSON-RPC response object |
|
||||
| `nostr_nip46_parse_request()` | Parse decrypted JSON into request struct |
|
||||
| `nostr_nip46_parse_response()` | Parse decrypted JSON into response struct |
|
||||
| `nostr_nip46_free_request()` | Free request struct memory |
|
||||
| `nostr_nip46_free_response()` | Free response struct memory |
|
||||
|
||||
### Event Construction (kind: 24133)
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_create_request_event()` | Create NIP-44 encrypted kind:24133 request event |
|
||||
| `nostr_nip46_create_response_event()` | Create NIP-44 encrypted kind:24133 response event |
|
||||
| `nostr_nip46_decrypt_event()` | Decrypt a kind:24133 event content |
|
||||
|
||||
### Client-Side Operations
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_client_session_init()` | Initialize client session from bunker URL |
|
||||
| `nostr_nip46_client_session_destroy()` | Clean up client session |
|
||||
| `nostr_nip46_client_connect()` | Send connect request to remote signer |
|
||||
| `nostr_nip46_client_get_public_key()` | Request user pubkey from remote signer |
|
||||
| `nostr_nip46_client_sign_event()` | Request event signing from remote signer |
|
||||
| `nostr_nip46_client_ping()` | Send ping to remote signer |
|
||||
| `nostr_nip46_client_nip04_encrypt()` | Request NIP-04 encryption |
|
||||
| `nostr_nip46_client_nip04_decrypt()` | Request NIP-04 decryption |
|
||||
| `nostr_nip46_client_nip44_encrypt()` | Request NIP-44 encryption |
|
||||
| `nostr_nip46_client_nip44_decrypt()` | Request NIP-44 decryption |
|
||||
|
||||
### Remote-Signer-Side Operations
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_signer_session_init()` | Initialize signer session |
|
||||
| `nostr_nip46_signer_session_destroy()` | Clean up signer session |
|
||||
| `nostr_nip46_signer_handle_request()` | Process an incoming request and produce a response |
|
||||
| `nostr_nip46_signer_create_bunker_url()` | Generate bunker URL for distribution |
|
||||
|
||||
### Utility
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `nostr_nip46_generate_request_id()` | Generate random request ID string |
|
||||
| `nostr_nip46_method_to_string()` | Convert method enum to string |
|
||||
| `nostr_nip46_string_to_method()` | Convert string to method enum |
|
||||
|
||||
## Error Codes
|
||||
|
||||
New error codes to add to `nostr_common.h`:
|
||||
|
||||
```c
|
||||
// NIP-46 Remote Signing error codes
|
||||
#define NOSTR_ERROR_NIP46_INVALID_BUNKER_URL -300
|
||||
#define NOSTR_ERROR_NIP46_INVALID_NOSTRCONNECT -301
|
||||
#define NOSTR_ERROR_NIP46_INVALID_REQUEST -302
|
||||
#define NOSTR_ERROR_NIP46_INVALID_RESPONSE -303
|
||||
#define NOSTR_ERROR_NIP46_ENCRYPTION_FAILED -304
|
||||
#define NOSTR_ERROR_NIP46_DECRYPTION_FAILED -305
|
||||
#define NOSTR_ERROR_NIP46_CONNECTION_FAILED -306
|
||||
#define NOSTR_ERROR_NIP46_TIMEOUT -307
|
||||
#define NOSTR_ERROR_NIP46_SECRET_MISMATCH -308
|
||||
#define NOSTR_ERROR_NIP46_UNKNOWN_METHOD -309
|
||||
#define NOSTR_ERROR_NIP46_AUTH_CHALLENGE -310
|
||||
#define NOSTR_ERROR_NIP46_NOT_CONNECTED -311
|
||||
```
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `nostr_core/nip046.h` | Header with all NIP-46 types, constants, and function declarations |
|
||||
| `nostr_core/nip046.c` | Implementation of all NIP-46 functions |
|
||||
| `tests/nip46_test.c` | Comprehensive test suite |
|
||||
| `examples/nip46_remote_signer.c` | Example showing both client and signer usage |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `nostr_core/nostr_core.h` | Add `#include "nip046.h"` and NIP-46 API docs in header comment |
|
||||
| `nostr_core/nostr_common.h` | Add NIP-46 error code defines |
|
||||
| `build.sh` | Add NIP-046 to auto-detection, `--nips=all` list, and description mapping |
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### URL Parsing Strategy
|
||||
|
||||
The `bunker://` and `nostrconnect://` URLs use standard URI format with query parameters. Implementation will:
|
||||
1. Validate the scheme prefix
|
||||
2. Extract the pubkey from the authority section
|
||||
3. Parse query parameters using simple string splitting — no external URL parsing library needed
|
||||
4. URL-decode relay values since they contain `://` characters
|
||||
|
||||
### NIP-44 Encryption Integration
|
||||
|
||||
All kind:24133 event content is NIP-44 encrypted. The implementation will use the existing `nostr_nip44_encrypt()` and `nostr_nip44_decrypt()` functions from `nip044.h`. The flow:
|
||||
1. Build JSON-RPC payload as a string
|
||||
2. Encrypt with `nostr_nip44_encrypt(client_privkey, remote_signer_pubkey, payload, ...)`
|
||||
3. Set as event content
|
||||
4. On receive: `nostr_nip44_decrypt(my_privkey, sender_pubkey, content, ...)`
|
||||
5. Parse decrypted JSON-RPC payload
|
||||
|
||||
### Relay Communication
|
||||
|
||||
For the initial implementation, the NIP-46 module will provide **event construction and parsing only** — it will not manage relay connections directly. Users will use the existing relay pool API to:
|
||||
1. Subscribe to kind:24133 events p-tagged to their pubkey
|
||||
2. Publish kind:24133 request/response events
|
||||
|
||||
This keeps the module focused and composable, matching the pattern used by NIP-59 and NIP-17.
|
||||
|
||||
A higher-level convenience layer could be added later that wraps the relay pool for a fully managed NIP-46 session.
|
||||
|
||||
### Auth Challenge Handling
|
||||
|
||||
When a response has `result: "auth_url"`, the error field contains a URL. The client-side API will return a specific error code `NOSTR_ERROR_NIP46_AUTH_CHALLENGE` and provide the URL in the response struct's error field, allowing the application to handle it appropriately.
|
||||
|
||||
## Test Plan
|
||||
|
||||
The test suite will cover:
|
||||
|
||||
1. **URL Parsing Tests**
|
||||
- Parse valid bunker:// URLs with single and multiple relays
|
||||
- Parse valid nostrconnect:// URLs with all optional fields
|
||||
- Reject malformed URLs
|
||||
- Handle URL-encoded relay values
|
||||
|
||||
2. **URL Generation Tests**
|
||||
- Generate bunker:// URLs and verify round-trip parsing
|
||||
- Generate nostrconnect:// URLs and verify round-trip parsing
|
||||
|
||||
3. **JSON-RPC Message Tests**
|
||||
- Create and parse each method type request
|
||||
- Create and parse success responses
|
||||
- Create and parse error responses
|
||||
- Verify request ID matching
|
||||
|
||||
4. **Event Construction Tests**
|
||||
- Create kind:24133 request events with proper encryption
|
||||
- Create kind:24133 response events with proper encryption
|
||||
- Decrypt events and verify content matches
|
||||
|
||||
5. **Method Enum Conversion Tests**
|
||||
- Round-trip all method enum values through string conversion
|
||||
|
||||
6. **Signer Request Handling Tests**
|
||||
- Handle connect request and produce ack response
|
||||
- Handle sign_event request and produce signed event response
|
||||
- Handle ping request and produce pong response
|
||||
- Handle get_public_key and return correct pubkey
|
||||
- Handle NIP-04/NIP-44 encrypt/decrypt requests
|
||||
|
||||
7. **End-to-End Flow Tests**
|
||||
- Client creates connect request → Signer handles → Client processes response
|
||||
- Client creates sign_event request → Signer signs → Client gets signed event
|
||||
- Full bunker:// connection flow
|
||||
- Secret validation on connect
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Error codes in `nostr_common.h`
|
||||
2. Header file `nip046.h` with all declarations
|
||||
3. URL parsing functions
|
||||
4. JSON-RPC request/response construction and parsing
|
||||
5. Kind:24133 event creation and decryption
|
||||
6. Client session management
|
||||
7. Signer session and request handling
|
||||
8. Utility functions
|
||||
9. Build system updates
|
||||
10. Test suite
|
||||
11. Example program
|
||||
12. Documentation updates in `nostr_core.h`
|
||||
@@ -0,0 +1,778 @@
|
||||
# NIP-60 Cashu Wallet & NIP-61 Nutzaps — Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
**NIP-60** defines a protocol for storing Cashu wallet state on Nostr relays, enabling cross-application wallet portability. The wallet stores unspent proofs as encrypted events, tracks spending history, and manages mint relationships.
|
||||
|
||||
**NIP-61** defines "Nutzaps" — P2PK-locked Cashu tokens sent as zaps between Nostr users, where the payment itself serves as the receipt.
|
||||
|
||||
This implementation covers:
|
||||
- **NIP-60**: Wallet event creation/parsing (kind:17375), token events (kind:7375), spending history (kind:7376), quote events (kind:7374)
|
||||
- **NIP-61**: Nutzap info events (kind:10019), nutzap events (kind:9321), nutzap redemption
|
||||
- **Cashu Mint HTTP Client**: Mint info, token swap, melt, and mint operations via curl
|
||||
|
||||
## Architecture
|
||||
|
||||
### Component Dependency Map
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
NIP60[nip060.c/h - Cashu Wallet Events] --> NIP44[nip044 - NIP-44 Encryption]
|
||||
NIP60 --> NIP01[nip001 - Event Creation/Signing]
|
||||
NIP60 --> CJSON[cJSON - JSON Handling]
|
||||
NIP60 --> UTILS[utils - Hex/Bytes/Base64]
|
||||
NIP60 --> COMMON[nostr_common - Error Codes]
|
||||
|
||||
NIP61[nip061.c/h - Nutzaps] --> NIP44
|
||||
NIP61 --> NIP01
|
||||
NIP61 --> CJSON
|
||||
NIP61 --> UTILS
|
||||
NIP61 --> COMMON
|
||||
|
||||
CASHU[cashu_mint.c/h - Mint HTTP Client] --> CJSON
|
||||
CASHU --> CURL[libcurl - HTTP]
|
||||
CASHU --> COMMON
|
||||
|
||||
NIP60 -.->|optional| CASHU
|
||||
NIP61 -.->|optional| CASHU
|
||||
```
|
||||
|
||||
### NIP-60 High-Level Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as Application
|
||||
participant W as NIP-60 Wallet Lib
|
||||
participant R as Relay
|
||||
participant M as Cashu Mint
|
||||
|
||||
Note over App: Initialize wallet
|
||||
App->>W: nostr_nip60_create_wallet_event
|
||||
W->>R: Publish kind:17375
|
||||
|
||||
Note over App: Receive tokens
|
||||
App->>M: Mint tokens via cashu_mint_mint_tokens
|
||||
M-->>App: Cashu proofs
|
||||
App->>W: nostr_nip60_create_token_event
|
||||
W->>R: Publish kind:7375
|
||||
|
||||
Note over App: Spend tokens
|
||||
App->>W: nostr_nip60_spend_token
|
||||
W->>M: Swap proofs via cashu_mint_swap
|
||||
M-->>W: Change proofs
|
||||
W->>R: Delete old kind:7375
|
||||
W->>R: Publish new kind:7375 with change
|
||||
W->>R: Publish kind:7376 history
|
||||
```
|
||||
|
||||
### NIP-61 Nutzap Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice as Alice - Sender
|
||||
participant R as Relay
|
||||
participant Bob as Bob - Recipient
|
||||
participant M as Cashu Mint
|
||||
|
||||
Note over Bob: Setup
|
||||
Bob->>R: Publish kind:10019 with mints + p2pk pubkey
|
||||
|
||||
Note over Alice: Send nutzap
|
||||
Alice->>R: Fetch Bobs kind:10019
|
||||
Alice->>M: Mint/swap P2PK-locked tokens
|
||||
Alice->>R: Publish kind:9321 nutzap
|
||||
|
||||
Note over Bob: Receive nutzap
|
||||
Bob->>R: Fetch kind:9321 events
|
||||
Bob->>M: Swap P2PK tokens into wallet
|
||||
Bob->>R: Publish kind:7376 redemption history
|
||||
```
|
||||
|
||||
### Token State Transition
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Created: Mint/Receive tokens
|
||||
Created --> Published: Publish kind 7375
|
||||
Published --> Spending: Spend some proofs
|
||||
Spending --> Deleted: NIP-09 delete old event
|
||||
Spending --> RolledOver: New kind 7375 with unspent + change
|
||||
RolledOver --> Published
|
||||
Deleted --> [*]
|
||||
Published --> Redeemed: All proofs spent
|
||||
Redeemed --> Deleted
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
nostr_core/
|
||||
nip060.h # NIP-60 Cashu wallet types and function declarations
|
||||
nip060.c # NIP-60 implementation
|
||||
nip061.h # NIP-61 Nutzap types and function declarations
|
||||
nip061.c # NIP-61 implementation
|
||||
cashu_mint.h # Cashu mint HTTP client types and functions
|
||||
cashu_mint.c # Cashu mint HTTP client implementation
|
||||
tests/
|
||||
nip60_test.c # NIP-60 unit tests
|
||||
nip61_test.c # NIP-61 unit tests
|
||||
cashu_mint_test.c # Cashu mint client tests
|
||||
examples/
|
||||
cashu_wallet.c # Complete wallet example
|
||||
```
|
||||
|
||||
## Data Structures
|
||||
|
||||
### NIP-60 Core Types
|
||||
|
||||
```c
|
||||
// Event kinds
|
||||
#define NOSTR_NIP60_WALLET_KIND 17375
|
||||
#define NOSTR_NIP60_TOKEN_KIND 7375
|
||||
#define NOSTR_NIP60_HISTORY_KIND 7376
|
||||
#define NOSTR_NIP60_QUOTE_KIND 7374
|
||||
|
||||
// A single Cashu proof
|
||||
typedef struct {
|
||||
char id[17]; // Keyset ID, e.g. "005c2502034d4f12"
|
||||
uint64_t amount; // Denomination amount
|
||||
char* secret; // Proof secret (base64 or P2PK JSON)
|
||||
char* C; // Blinded signature (hex compressed point)
|
||||
} nostr_cashu_proof_t;
|
||||
|
||||
// Token content - decrypted payload of kind:7375
|
||||
typedef struct {
|
||||
char* mint_url; // Mint URL
|
||||
nostr_cashu_proof_t* proofs; // Array of proofs
|
||||
int proof_count; // Number of proofs
|
||||
char** deleted_token_ids; // Event IDs of tokens destroyed
|
||||
int deleted_count; // Number of deleted token IDs
|
||||
} nostr_nip60_token_data_t;
|
||||
|
||||
// Wallet content - decrypted payload of kind:17375
|
||||
typedef struct {
|
||||
char privkey[65]; // Hex private key for P2PK
|
||||
char** mint_urls; // Array of mint URLs
|
||||
int mint_count; // Number of mints
|
||||
} nostr_nip60_wallet_data_t;
|
||||
|
||||
// Spending history entry direction
|
||||
typedef enum {
|
||||
NOSTR_NIP60_DIRECTION_IN, // Received funds
|
||||
NOSTR_NIP60_DIRECTION_OUT // Sent funds
|
||||
} nostr_nip60_direction_t;
|
||||
|
||||
// History event reference
|
||||
typedef enum {
|
||||
NOSTR_NIP60_REF_CREATED, // New token event created
|
||||
NOSTR_NIP60_REF_DESTROYED, // Token event destroyed
|
||||
NOSTR_NIP60_REF_REDEEMED // Nutzap redeemed
|
||||
} nostr_nip60_ref_marker_t;
|
||||
|
||||
typedef struct {
|
||||
char event_id[65]; // Referenced event ID
|
||||
char relay_hint[256]; // Optional relay hint
|
||||
nostr_nip60_ref_marker_t marker; // Reference type
|
||||
} nostr_nip60_history_ref_t;
|
||||
|
||||
// Spending history data - decrypted payload of kind:7376
|
||||
typedef struct {
|
||||
nostr_nip60_direction_t direction; // in or out
|
||||
uint64_t amount; // Amount in sats
|
||||
nostr_nip60_history_ref_t* refs; // Event references
|
||||
int ref_count; // Number of references
|
||||
} nostr_nip60_history_data_t;
|
||||
```
|
||||
|
||||
### NIP-61 Core Types
|
||||
|
||||
```c
|
||||
// Event kinds
|
||||
#define NOSTR_NIP61_NUTZAP_INFO_KIND 10019
|
||||
#define NOSTR_NIP61_NUTZAP_KIND 9321
|
||||
|
||||
// Mint entry in kind:10019
|
||||
typedef struct {
|
||||
char* url; // Mint URL
|
||||
char** units; // Supported units, e.g. "sat", "usd"
|
||||
int unit_count; // Number of units
|
||||
} nostr_nip61_mint_entry_t;
|
||||
|
||||
// Nutzap info - parsed kind:10019
|
||||
typedef struct {
|
||||
char** relay_urls; // Relays for receiving nutzaps
|
||||
int relay_count;
|
||||
nostr_nip61_mint_entry_t* mints; // Trusted mints
|
||||
int mint_count;
|
||||
char pubkey[67]; // P2PK pubkey (with 02 prefix)
|
||||
} nostr_nip61_nutzap_info_t;
|
||||
|
||||
// Nutzap event data - parsed kind:9321
|
||||
typedef struct {
|
||||
char* content; // Optional comment
|
||||
nostr_cashu_proof_t* proofs; // P2PK-locked proofs
|
||||
int proof_count;
|
||||
char* mint_url; // Mint URL
|
||||
char recipient_pubkey[65]; // Recipient Nostr pubkey
|
||||
char nutzapped_event_id[65]; // Event being nutzapped (optional)
|
||||
char nutzapped_relay_hint[256]; // Relay hint for nutzapped event
|
||||
int nutzapped_kind; // Kind of nutzapped event
|
||||
} nostr_nip61_nutzap_data_t;
|
||||
```
|
||||
|
||||
### Cashu Mint Client Types
|
||||
|
||||
```c
|
||||
// Cashu protocol version
|
||||
#define CASHU_API_VERSION "v1"
|
||||
|
||||
// Mint keyset
|
||||
typedef struct {
|
||||
char id[17]; // Keyset ID
|
||||
char unit[8]; // Unit, e.g. "sat"
|
||||
int active; // Whether keyset is active
|
||||
uint64_t* amounts; // Denomination amounts
|
||||
char** pubkeys; // Corresponding public keys
|
||||
int key_count; // Number of keys
|
||||
} cashu_keyset_t;
|
||||
|
||||
// Mint info
|
||||
typedef struct {
|
||||
char* name;
|
||||
char* description;
|
||||
char* version;
|
||||
char** supported_nuts; // NUT numbers supported
|
||||
int nut_count;
|
||||
cashu_keyset_t* keysets;
|
||||
int keyset_count;
|
||||
} cashu_mint_info_t;
|
||||
|
||||
// Mint quote (for minting new tokens)
|
||||
typedef struct {
|
||||
char quote_id[128];
|
||||
char payment_request[1024]; // Lightning invoice
|
||||
int paid;
|
||||
uint64_t amount;
|
||||
time_t expiry;
|
||||
} cashu_mint_quote_t;
|
||||
|
||||
// Melt quote (for paying Lightning invoices)
|
||||
typedef struct {
|
||||
char quote_id[128];
|
||||
uint64_t amount;
|
||||
uint64_t fee_reserve;
|
||||
int paid;
|
||||
char* payment_preimage;
|
||||
time_t expiry;
|
||||
} cashu_melt_quote_t;
|
||||
|
||||
// Blind message for minting/swapping
|
||||
typedef struct {
|
||||
uint64_t amount;
|
||||
char* B_; // Blinded secret (hex)
|
||||
char id[17]; // Keyset ID
|
||||
} cashu_blinded_message_t;
|
||||
|
||||
// Blind signature from mint
|
||||
typedef struct {
|
||||
uint64_t amount;
|
||||
char* C_; // Blinded signature (hex)
|
||||
char id[17]; // Keyset ID
|
||||
} cashu_blind_signature_t;
|
||||
```
|
||||
|
||||
## API Functions
|
||||
|
||||
### NIP-60 Wallet Functions
|
||||
|
||||
```c
|
||||
// === Wallet Event (kind:17375) ===
|
||||
|
||||
// Create a wallet event with encrypted content
|
||||
cJSON* nostr_nip60_create_wallet_event(
|
||||
const nostr_nip60_wallet_data_t* wallet_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse and decrypt a wallet event
|
||||
int nostr_nip60_parse_wallet_event(
|
||||
cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_wallet_data_t* wallet_data_out);
|
||||
|
||||
// Free wallet data
|
||||
void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data);
|
||||
|
||||
// === Token Event (kind:7375) ===
|
||||
|
||||
// Create a token event with encrypted proofs
|
||||
cJSON* nostr_nip60_create_token_event(
|
||||
const nostr_nip60_token_data_t* token_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse and decrypt a token event
|
||||
int nostr_nip60_parse_token_event(
|
||||
cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_token_data_t* token_data_out);
|
||||
|
||||
// Free token data
|
||||
void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data);
|
||||
|
||||
// === Token Spending ===
|
||||
|
||||
// Create a NIP-09 deletion event for a spent token
|
||||
cJSON* nostr_nip60_create_token_deletion(
|
||||
const char* token_event_id,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Create a rollover token event (unspent proofs + change from old token)
|
||||
cJSON* nostr_nip60_create_rollover_token(
|
||||
const nostr_nip60_token_data_t* remaining_proofs,
|
||||
const char** deleted_event_ids,
|
||||
int deleted_count,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// === Spending History (kind:7376) ===
|
||||
|
||||
// Create a spending history event
|
||||
cJSON* nostr_nip60_create_history_event(
|
||||
const nostr_nip60_history_data_t* history_data,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse and decrypt a spending history event
|
||||
int nostr_nip60_parse_history_event(
|
||||
cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
nostr_nip60_history_data_t* history_data_out);
|
||||
|
||||
// Free history data
|
||||
void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data);
|
||||
|
||||
// === Quote Event (kind:7374) ===
|
||||
|
||||
// Create a mint quote event
|
||||
cJSON* nostr_nip60_create_quote_event(
|
||||
const char* quote_id,
|
||||
const char* mint_url,
|
||||
time_t expiration,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse a quote event
|
||||
int nostr_nip60_parse_quote_event(
|
||||
cJSON* event,
|
||||
const unsigned char* private_key,
|
||||
char* quote_id_out,
|
||||
size_t quote_id_size,
|
||||
char* mint_url_out,
|
||||
size_t mint_url_size);
|
||||
|
||||
// === Utility: Proof helpers ===
|
||||
|
||||
// Calculate total amount from an array of proofs
|
||||
uint64_t nostr_nip60_sum_proofs(
|
||||
const nostr_cashu_proof_t* proofs,
|
||||
int proof_count);
|
||||
|
||||
// Serialize proofs to JSON array
|
||||
cJSON* nostr_nip60_proofs_to_json(
|
||||
const nostr_cashu_proof_t* proofs,
|
||||
int proof_count);
|
||||
|
||||
// Parse proofs from JSON array
|
||||
int nostr_nip60_proofs_from_json(
|
||||
cJSON* json_array,
|
||||
nostr_cashu_proof_t** proofs_out,
|
||||
int* proof_count_out);
|
||||
|
||||
// Free proof array
|
||||
void nostr_nip60_free_proofs(
|
||||
nostr_cashu_proof_t* proofs,
|
||||
int proof_count);
|
||||
|
||||
// === Filter helpers ===
|
||||
|
||||
// Create a filter to fetch wallet and token events
|
||||
cJSON* nostr_nip60_create_wallet_filter(const char* pubkey_hex);
|
||||
|
||||
// Create a filter to fetch spending history
|
||||
cJSON* nostr_nip60_create_history_filter(const char* pubkey_hex, time_t since);
|
||||
```
|
||||
|
||||
### NIP-61 Nutzap Functions
|
||||
|
||||
```c
|
||||
// === Nutzap Info Event (kind:10019) ===
|
||||
|
||||
// Create nutzap info event
|
||||
cJSON* nostr_nip61_create_nutzap_info_event(
|
||||
const nostr_nip61_nutzap_info_t* info,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse nutzap info event
|
||||
int nostr_nip61_parse_nutzap_info_event(
|
||||
cJSON* event,
|
||||
nostr_nip61_nutzap_info_t* info_out);
|
||||
|
||||
// Free nutzap info
|
||||
void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info);
|
||||
|
||||
// === Nutzap Event (kind:9321) ===
|
||||
|
||||
// Create a nutzap event
|
||||
cJSON* nostr_nip61_create_nutzap_event(
|
||||
const nostr_nip61_nutzap_data_t* nutzap_data,
|
||||
const unsigned char* sender_private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// Parse a nutzap event
|
||||
int nostr_nip61_parse_nutzap_event(
|
||||
cJSON* event,
|
||||
nostr_nip61_nutzap_data_t* nutzap_data_out);
|
||||
|
||||
// Free nutzap data
|
||||
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data);
|
||||
|
||||
// === Nutzap Redemption ===
|
||||
|
||||
// Create a redemption history event (kind:7376 with redeemed marker)
|
||||
cJSON* nostr_nip61_create_redemption_event(
|
||||
const char* nutzap_event_id,
|
||||
const char* nutzap_relay_hint,
|
||||
const char* sender_pubkey,
|
||||
const char* created_token_event_id,
|
||||
const char* created_token_relay_hint,
|
||||
uint64_t amount,
|
||||
const unsigned char* private_key,
|
||||
time_t timestamp);
|
||||
|
||||
// === Nutzap Verification ===
|
||||
|
||||
// Verify a nutzap against recipients kind:10019
|
||||
int nostr_nip61_verify_nutzap(
|
||||
cJSON* nutzap_event,
|
||||
cJSON* nutzap_info_event);
|
||||
|
||||
// === Filter helpers ===
|
||||
|
||||
// Create filter to fetch nutzap info for a user
|
||||
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex);
|
||||
|
||||
// Create filter to fetch incoming nutzaps
|
||||
cJSON* nostr_nip61_create_nutzap_filter(
|
||||
const char* recipient_pubkey_hex,
|
||||
const char** mint_urls,
|
||||
int mint_count,
|
||||
time_t since);
|
||||
```
|
||||
|
||||
### Cashu Mint HTTP Client Functions
|
||||
|
||||
```c
|
||||
// === Mint Connection ===
|
||||
|
||||
// Get mint info
|
||||
int cashu_mint_get_info(
|
||||
const char* mint_url,
|
||||
cashu_mint_info_t* info_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Free mint info
|
||||
void cashu_mint_free_info(cashu_mint_info_t* info);
|
||||
|
||||
// === Keysets ===
|
||||
|
||||
// Get active keysets from mint
|
||||
int cashu_mint_get_keysets(
|
||||
const char* mint_url,
|
||||
cashu_keyset_t** keysets_out,
|
||||
int* keyset_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Get specific keyset keys
|
||||
int cashu_mint_get_keys(
|
||||
const char* mint_url,
|
||||
const char* keyset_id,
|
||||
cashu_keyset_t* keyset_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Free keysets
|
||||
void cashu_mint_free_keysets(cashu_keyset_t* keysets, int count);
|
||||
|
||||
// === Minting (receiving Lightning) ===
|
||||
|
||||
// Request a mint quote (get Lightning invoice)
|
||||
int cashu_mint_request_mint_quote(
|
||||
const char* mint_url,
|
||||
uint64_t amount,
|
||||
const char* unit,
|
||||
cashu_mint_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Check mint quote status
|
||||
int cashu_mint_check_mint_quote(
|
||||
const char* mint_url,
|
||||
const char* quote_id,
|
||||
cashu_mint_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Mint tokens (after quote is paid)
|
||||
int cashu_mint_mint_tokens(
|
||||
const char* mint_url,
|
||||
const char* quote_id,
|
||||
const cashu_blinded_message_t* blinded_messages,
|
||||
int message_count,
|
||||
cashu_blind_signature_t** signatures_out,
|
||||
int* signature_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// === Melting (paying Lightning) ===
|
||||
|
||||
// Request a melt quote (estimate fee for paying invoice)
|
||||
int cashu_mint_request_melt_quote(
|
||||
const char* mint_url,
|
||||
const char* payment_request,
|
||||
const char* unit,
|
||||
cashu_melt_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Check melt quote status
|
||||
int cashu_mint_check_melt_quote(
|
||||
const char* mint_url,
|
||||
const char* quote_id,
|
||||
cashu_melt_quote_t* quote_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// Melt tokens (pay Lightning invoice)
|
||||
int cashu_mint_melt_tokens(
|
||||
const char* mint_url,
|
||||
const char* quote_id,
|
||||
const nostr_cashu_proof_t* proofs,
|
||||
int proof_count,
|
||||
cashu_melt_quote_t* result_out,
|
||||
cashu_blind_signature_t** change_out,
|
||||
int* change_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// === Swapping ===
|
||||
|
||||
// Swap proofs (split/combine denominations)
|
||||
int cashu_mint_swap(
|
||||
const char* mint_url,
|
||||
const nostr_cashu_proof_t* inputs,
|
||||
int input_count,
|
||||
const cashu_blinded_message_t* outputs,
|
||||
int output_count,
|
||||
cashu_blind_signature_t** signatures_out,
|
||||
int* signature_count_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// === Token State ===
|
||||
|
||||
// Check if proofs have been spent
|
||||
int cashu_mint_check_proofs_state(
|
||||
const char* mint_url,
|
||||
const nostr_cashu_proof_t* proofs,
|
||||
int proof_count,
|
||||
int* states_out,
|
||||
int timeout_seconds);
|
||||
|
||||
// === Crypto Helpers ===
|
||||
|
||||
// Generate blinded messages for a target amount
|
||||
int cashu_create_blinded_messages(
|
||||
uint64_t amount,
|
||||
const char* keyset_id,
|
||||
cashu_blinded_message_t** messages_out,
|
||||
int* message_count_out,
|
||||
char*** secrets_out,
|
||||
char*** rs_out);
|
||||
|
||||
// Unblind signatures to get proofs
|
||||
int cashu_unblind_signatures(
|
||||
const cashu_blind_signature_t* signatures,
|
||||
int signature_count,
|
||||
const char** secrets,
|
||||
const char** rs,
|
||||
const cashu_keyset_t* keyset,
|
||||
nostr_cashu_proof_t** proofs_out,
|
||||
int* proof_count_out);
|
||||
|
||||
// Create P2PK-locked blinded messages
|
||||
int cashu_create_p2pk_blinded_messages(
|
||||
uint64_t amount,
|
||||
const char* keyset_id,
|
||||
const char* recipient_pubkey_hex,
|
||||
cashu_blinded_message_t** messages_out,
|
||||
int* message_count_out,
|
||||
char*** secrets_out,
|
||||
char*** rs_out);
|
||||
|
||||
// Free blinded messages
|
||||
void cashu_free_blinded_messages(cashu_blinded_message_t* messages, int count);
|
||||
|
||||
// Free blind signatures
|
||||
void cashu_free_blind_signatures(cashu_blind_signature_t* signatures, int count);
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
New error codes to add to `nostr_common.h`:
|
||||
|
||||
```c
|
||||
// NIP-60 Cashu Wallet error codes
|
||||
#define NOSTR_ERROR_NIP60_INVALID_WALLET -400
|
||||
#define NOSTR_ERROR_NIP60_INVALID_TOKEN -401
|
||||
#define NOSTR_ERROR_NIP60_INVALID_HISTORY -402
|
||||
#define NOSTR_ERROR_NIP60_INVALID_QUOTE -403
|
||||
#define NOSTR_ERROR_NIP60_DECRYPT_FAILED -404
|
||||
#define NOSTR_ERROR_NIP60_INVALID_PROOFS -405
|
||||
#define NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS -406
|
||||
|
||||
// NIP-61 Nutzap error codes
|
||||
#define NOSTR_ERROR_NIP61_INVALID_INFO -410
|
||||
#define NOSTR_ERROR_NIP61_INVALID_NUTZAP -411
|
||||
#define NOSTR_ERROR_NIP61_MINT_MISMATCH -412
|
||||
#define NOSTR_ERROR_NIP61_PUBKEY_MISMATCH -413
|
||||
#define NOSTR_ERROR_NIP61_VERIFICATION_FAILED -414
|
||||
|
||||
// Cashu Mint client error codes
|
||||
#define NOSTR_ERROR_CASHU_HTTP_FAILED -420
|
||||
#define NOSTR_ERROR_CASHU_JSON_PARSE_FAILED -421
|
||||
#define NOSTR_ERROR_CASHU_MINT_ERROR -422
|
||||
#define NOSTR_ERROR_CASHU_QUOTE_NOT_PAID -423
|
||||
#define NOSTR_ERROR_CASHU_QUOTE_EXPIRED -424
|
||||
#define NOSTR_ERROR_CASHU_PROOFS_SPENT -425
|
||||
#define NOSTR_ERROR_CASHU_CRYPTO_FAILED -426
|
||||
#define NOSTR_ERROR_CASHU_INVALID_KEYSET -427
|
||||
```
|
||||
|
||||
## Build System Changes
|
||||
|
||||
### build.sh Updates
|
||||
|
||||
1. Add `060` and `061` to the NIP detection patterns
|
||||
2. Add `cashu_mint.c` as a source when NIP-60 or NIP-61 is detected
|
||||
3. Add NIP descriptions: `NIP-060(Cashu-Wallet)`, `NIP-061(Nutzaps)`
|
||||
4. Update the `--nips=all` list to include `060` and `061`
|
||||
5. Update the help text with new NIP descriptions
|
||||
|
||||
### nostr_core.h Updates
|
||||
|
||||
Add includes:
|
||||
```c
|
||||
#include "nip060.h" // Cashu Wallet
|
||||
#include "nip061.h" // Nutzaps
|
||||
#include "cashu_mint.h" // Cashu Mint HTTP Client
|
||||
```
|
||||
|
||||
Add NIP-60/61 functions to the quick reference comment block.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
The implementation should proceed in this order due to dependencies:
|
||||
|
||||
1. **Error codes** in `nostr_common.h` — no dependencies
|
||||
2. **Cashu proof types** — shared between NIP-60 and NIP-61
|
||||
3. **`cashu_mint.h/c`** — Cashu mint HTTP client (depends on curl, cJSON)
|
||||
4. **`nip060.h/c`** — NIP-60 wallet events (depends on NIP-44, NIP-01, cashu types)
|
||||
5. **`nip061.h/c`** — NIP-61 nutzaps (depends on NIP-60 types, cashu types)
|
||||
6. **Build system** — update `build.sh` and `nostr_core.h`
|
||||
7. **Tests** — `nip60_test.c`, `nip61_test.c`, `cashu_mint_test.c`
|
||||
8. **Example** — `cashu_wallet.c`
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Self-encryption for NIP-44
|
||||
NIP-60 encrypts content to the user's own key (sender = recipient). The NIP-44 encrypt/decrypt functions take sender_private_key and recipient_public_key. For self-encryption, we derive the user's public key from their private key and use that as the recipient.
|
||||
|
||||
### 2. Cashu Crypto (Blinding)
|
||||
The Cashu protocol requires elliptic curve blinding operations (hash-to-curve, blind/unblind). These use secp256k1 which is already a dependency. The blinding math is:
|
||||
- `B_ = Y + r*G` where Y = hash_to_curve(secret), r = random blinding factor
|
||||
- `C_ = k*B_` (mint signs)
|
||||
- `C = C_ - r*K` where K = mint's public key (unblind)
|
||||
|
||||
### 3. P2PK Secrets
|
||||
For NIP-61 nutzaps, proof secrets use the P2PK format:
|
||||
```json
|
||||
["P2PK", {"nonce": "<random>", "data": "02<pubkey>"}]
|
||||
```
|
||||
The `02` prefix is required for nostr-cashu compatibility.
|
||||
|
||||
### 4. Curl Usage
|
||||
Following the same pattern as `nip005.c` — direct curl usage with write callbacks. No abstraction layer needed since curl is already a project dependency.
|
||||
|
||||
## Test Strategy
|
||||
|
||||
### NIP-60 Tests
|
||||
- Create and parse wallet events (round-trip encryption)
|
||||
- Create and parse token events with multiple proofs
|
||||
- Token deletion event creation
|
||||
- Rollover token creation with del references
|
||||
- History event creation and parsing
|
||||
- Quote event creation and parsing
|
||||
- Proof sum calculation
|
||||
- Filter creation helpers
|
||||
|
||||
### NIP-61 Tests
|
||||
- Create and parse nutzap info events
|
||||
- Create and parse nutzap events
|
||||
- Nutzap verification against kind:10019
|
||||
- Redemption history event creation
|
||||
- Filter creation helpers
|
||||
|
||||
### Cashu Mint Tests
|
||||
- Mint info parsing from JSON
|
||||
- Keyset parsing
|
||||
- Blinded message creation
|
||||
- Signature unblinding
|
||||
- P2PK secret generation
|
||||
- Proof state checking (mock or live mint)
|
||||
|
||||
## Example Usage
|
||||
|
||||
```c
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
// Create a wallet
|
||||
nostr_nip60_wallet_data_t wallet = {0};
|
||||
strcpy(wallet.privkey, "<generated-hex-privkey>");
|
||||
wallet.mint_urls = (char*[]){"https://mint.example.com"};
|
||||
wallet.mint_count = 1;
|
||||
|
||||
cJSON* wallet_event = nostr_nip60_create_wallet_event(&wallet, my_privkey, 0);
|
||||
// Publish wallet_event to relays...
|
||||
|
||||
// Store tokens
|
||||
nostr_cashu_proof_t proofs[2] = {
|
||||
{.id = "00ad268c4d1f5826", .amount = 1, .secret = "...", .C = "02..."},
|
||||
{.id = "00ad268c4d1f5826", .amount = 4, .secret = "...", .C = "02..."}
|
||||
};
|
||||
nostr_nip60_token_data_t token = {
|
||||
.mint_url = "https://mint.example.com",
|
||||
.proofs = proofs,
|
||||
.proof_count = 2
|
||||
};
|
||||
|
||||
cJSON* token_event = nostr_nip60_create_token_event(&token, my_privkey, 0);
|
||||
// Publish token_event to relays...
|
||||
|
||||
// Send a nutzap
|
||||
nostr_nip61_nutzap_data_t nutzap = {
|
||||
.content = "Great post!",
|
||||
.proofs = p2pk_locked_proofs,
|
||||
.proof_count = 1,
|
||||
.mint_url = "https://mint.example.com",
|
||||
.recipient_pubkey = "<bob-pubkey>",
|
||||
.nutzapped_event_id = "<event-id>"
|
||||
};
|
||||
|
||||
cJSON* nutzap_event = nostr_nip61_create_nutzap_event(&nutzap, my_privkey, 0);
|
||||
// Publish nutzap_event to relays...
|
||||
```
|
||||
@@ -1,74 +1,47 @@
|
||||
[Thu Oct 2 19:30:49 2025] 🚀 Pool test started
|
||||
[Tue Oct 7 05:51:04 2025] 🚀 Pool test started
|
||||
|
||||
[Thu Oct 2 19:30:52 2025] 🏊 Pool started with default relay
|
||||
[Tue Oct 7 05:51:07 2025] 🏊 Pool started with default relay
|
||||
|
||||
[Thu Oct 2 19:31:05 2025] ➕ Relay added: ws://127.0.0.1:7777
|
||||
|
||||
[Thu Oct 2 19:31:24 2025] 🔍 New subscription created (ID: 1)
|
||||
[Tue Oct 7 05:52:03 2025] 🔍 New subscription created (ID: 1)
|
||||
Filter: {
|
||||
"kinds": [15555],
|
||||
"limit": 10
|
||||
}
|
||||
|
||||
[Thu Oct 2 19:31:24 2025] 📨 EVENT from ws://127.0.0.1:7777
|
||||
├── ID: 9af94cf163a2...
|
||||
├── Pubkey: e568a76a4f88...
|
||||
├── Kind: 15555
|
||||
├── Created: 1759447876
|
||||
└── Content: 📊 Bitcoin Mempool Status
|
||||
[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
|
||||
|
||||
Pending Transactions: 3,255
|
||||
Total Size: 1.82 MB
|
||||
Memory Usage: 3.4%
|
||||
A...
|
||||
[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
|
||||
|
||||
[Thu Oct 2 19:31:24 2025] 📨 EVENT from wss://relay.laantungir.net
|
||||
├── ID: 93472348ded1...
|
||||
├── Pubkey: b3f282fe77f4...
|
||||
├── Kind: 15555
|
||||
├── Created: 1753542367
|
||||
└── Content: 📊 Bitcoin Mempool Status
|
||||
[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
|
||||
|
||||
Pending Transactions: 9,882
|
||||
Total Size: 2.85 MB
|
||||
Memory Usage: 6.0%
|
||||
A...
|
||||
[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
|
||||
|
||||
[Thu Oct 2 19:31:24 2025] 📋 EOSE received - 0 events collected
|
||||
[Tue Oct 7 05:52:03 2025] 📋 EOSE received - 0 events collected
|
||||
|
||||
[Thu Oct 2 19:31:26 2025] 📨 EVENT from ws://127.0.0.1:7777
|
||||
├── ID: 90336970f3cf...
|
||||
├── Pubkey: e568a76a4f88...
|
||||
├── Kind: 15555
|
||||
├── Created: 1759447886
|
||||
└── Content: 📊 Bitcoin Mempool Status
|
||||
[Tue Oct 7 05:52:31 2025] 🔍 New subscription created (ID: 2)
|
||||
Filter: {
|
||||
"since": 1759830747,
|
||||
"limit": 10
|
||||
}
|
||||
|
||||
Pending Transactions: 3,295
|
||||
Total Size: 1.83 MB
|
||||
Memory Usage: 3.4%
|
||||
A...
|
||||
|
||||
[Thu Oct 2 19:32:06 2025] 📨 EVENT from ws://127.0.0.1:7777
|
||||
├── ID: 56e8e931f211...
|
||||
├── Pubkey: e568a76a4f88...
|
||||
├── Kind: 15555
|
||||
├── Created: 1759447926
|
||||
└── Content: 📊 Bitcoin Mempool Status
|
||||
|
||||
Pending Transactions: 3,357
|
||||
Total Size: 1.91 MB
|
||||
Memory Usage: 3.5%
|
||||
A...
|
||||
|
||||
[Thu Oct 2 19:32:16 2025] 📨 EVENT from ws://127.0.0.1:7777
|
||||
├── ID: 76682921a11b...
|
||||
├── Pubkey: e568a76a4f88...
|
||||
├── Kind: 15555
|
||||
├── Created: 1759447936
|
||||
└── Content: 📊 Bitcoin Mempool Status
|
||||
|
||||
Pending Transactions: 3,363
|
||||
Total Size: 1.91 MB
|
||||
Memory Usage: 3.5%
|
||||
A...
|
||||
[Tue Oct 7 05:52:31 2025] 📋 EOSE received - 0 events collected
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s <relay_url> [event_id]\n", argv[0]);
|
||||
fprintf(stderr, "Example: %s wss://relay.laantungir.net 06cdf2cdd095ddb1ebe15d5b3c736b27a34de2683e847b871fe37d86ac998772\n");
|
||||
fprintf(stderr, "Example: websocket_debug wss://relay.laantungir.net 06cdf2cdd095ddb1ebe15d5b3c736b27a34de2683e847b871fe37d86ac998772\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user