Compare commits

...
5 Commits
Author SHA1 Message Date
laantungir 363b7e9adf Release v0.5.0 2026-03-19 09:13:59 -04:00
laantungir 3186c2ead6 just want to increment 2026-03-19 09:09:59 -04:00
laantungir f07433578a just want to increment 2026-03-19 09:09:46 -04:00
laantungir d264d9f5ed nip60 2026-03-19 09:01:11 -04:00
Your Name 98094cac86 fix relay pool sync query repeat behavior and add regression test 2026-03-16 05:27:23 -04:00
18 changed files with 3714 additions and 12 deletions
+1 -1
View File
@@ -1 +1 @@
0.4.13
0.5.0
+11 -3
View File
@@ -147,6 +147,8 @@ if [ "$HELP" = true ]; then
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"
@@ -195,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 046 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
@@ -214,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 046 059"
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 ' ' ',')"
@@ -232,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 021 042 044 046 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
@@ -526,12 +528,18 @@ for nip in $NEEDED_NIPS; do
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"
+121
View File
@@ -0,0 +1,121 @@
/*
* Example: NIP-60/NIP-61 Cashu Wallet Flow
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "../nostr_core/nostr_core.h"
static void print_event(const char* title, cJSON* evt) {
if (!evt) {
printf("%s: <null>\n", title);
return;
}
char* s = cJSON_Print(evt);
if (s) {
printf("\n%s\n%s\n", title, s);
free(s);
}
}
int main(void) {
if (nostr_init() != NOSTR_SUCCESS) {
printf("Failed to initialize library\n");
return 1;
}
const char* user_sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
unsigned char user_sk[32];
if (nostr_hex_to_bytes(user_sk_hex, user_sk, 32) != 0) {
printf("Invalid private key\n");
nostr_cleanup();
return 1;
}
/* 1) Create wallet event (kind:17375) */
char* mint_urls[] = {
"https://mint1.example.com",
"https://mint2.example.com"
};
nostr_nip60_wallet_data_t wallet_data;
memset(&wallet_data, 0, sizeof(wallet_data));
strcpy(wallet_data.privkey, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
wallet_data.mint_urls = mint_urls;
wallet_data.mint_count = 2;
cJSON* wallet_event = nostr_nip60_create_wallet_event(&wallet_data, user_sk, 0);
print_event("Wallet Event (kind:17375)", wallet_event);
/* 2) Create token event (kind:7375) */
nostr_cashu_proof_t proofs[2];
memset(proofs, 0, sizeof(proofs));
strcpy(proofs[0].id, "005c2502034d4f12");
proofs[0].amount = 1;
proofs[0].secret = "secret-1";
proofs[0].C = "0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46";
strcpy(proofs[1].id, "005c2502034d4f12");
proofs[1].amount = 2;
proofs[1].secret = "secret-2";
proofs[1].C = "02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f";
nostr_nip60_token_data_t token_data;
memset(&token_data, 0, sizeof(token_data));
token_data.mint_url = "https://mint1.example.com";
token_data.proofs = proofs;
token_data.proof_count = 2;
cJSON* token_event = nostr_nip60_create_token_event(&token_data, user_sk, 0);
print_event("Token Event (kind:7375)", token_event);
/* 3) Create spend history event (kind:7376) */
nostr_nip60_history_ref_t refs[1];
memset(refs, 0, sizeof(refs));
strcpy(refs[0].event_id, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
refs[0].marker = NOSTR_NIP60_REF_CREATED;
nostr_nip60_history_data_t hist;
memset(&hist, 0, sizeof(hist));
hist.direction = NOSTR_NIP60_DIRECTION_IN;
hist.amount = 3;
hist.refs = refs;
hist.ref_count = 1;
cJSON* history_event = nostr_nip60_create_history_event(&hist, user_sk, 0);
print_event("History Event (kind:7376)", history_event);
/* 4) Create nutzap info event (kind:10019) */
char* relays[] = {"wss://relay1.example.com", "wss://relay2.example.com"};
char* mint_units[] = {"sat"};
nostr_nip61_mint_entry_t mint_entry;
memset(&mint_entry, 0, sizeof(mint_entry));
mint_entry.url = "https://mint1.example.com";
mint_entry.units = mint_units;
mint_entry.unit_count = 1;
nostr_nip61_nutzap_info_t info;
memset(&info, 0, sizeof(info));
info.relay_urls = relays;
info.relay_count = 2;
info.mints = &mint_entry;
info.mint_count = 1;
strcpy(info.pubkey, "02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed");
cJSON* info_event = nostr_nip61_create_nutzap_info_event(&info, user_sk, 0);
print_event("Nutzap Info Event (kind:10019)", info_event);
/* 5) Optionally call Cashu mint HTTP endpoints with [cashu_mint_get_info()] */
printf("\nCashu mint integration is available via cashu_mint_* APIs.\n");
cJSON_Delete(info_event);
cJSON_Delete(history_event);
cJSON_Delete(token_event);
cJSON_Delete(wallet_event);
nostr_cleanup();
return 0;
}
+395
View File
@@ -0,0 +1,395 @@
/*
* 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;
}
+102
View File
@@ -0,0 +1,102 @@
/*
* 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;
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);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_CASHU_MINT_H */
+93 -5
View File
@@ -19,6 +19,7 @@
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <stdarg.h>
// Our production-ready WebSocket implementation
#include "../nostr_websocket/nostr_websocket_tls.h"
@@ -43,6 +44,36 @@
#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) {
struct timespec ts;
@@ -1247,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];
@@ -1272,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
@@ -1281,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[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';
@@ -1308,6 +1352,16 @@ cJSON** nostr_relay_pool_query_sync(
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) {
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
@@ -1322,8 +1376,33 @@ cJSON** nostr_relay_pool_query_sync(
if (event_id_json && cJSON_IsString(event_id_json)) {
const char* event_id = cJSON_GetStringValue(event_id_json);
if (!is_event_seen(pool, event_id)) {
mark_event_seen(pool, event_id);
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;
}
}
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) {
@@ -1347,6 +1426,7 @@ cJSON** nostr_relay_pool_query_sync(
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) {
@@ -1386,6 +1466,7 @@ cJSON** nostr_relay_pool_query_sync(
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);
}
}
@@ -1393,6 +1474,13 @@ cJSON** nostr_relay_pool_query_sync(
}
}
}
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++) {
+791
View File
@@ -0,0 +1,791 @@
/*
* NIP-60: Cashu Wallet Implementation
* https://github.com/nostr-protocol/nips/blob/master/60.md
*/
#include "nip060.h"
#include "nip044.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int nostr_ec_public_key_from_private_key(const unsigned char* private_key, unsigned char* public_key);
static char* nip60_strdup(const char* s) {
if (!s) return NULL;
size_t len = strlen(s);
char* out = (char*)malloc(len + 1);
if (!out) return NULL;
memcpy(out, s, len + 1);
return out;
}
static const char* nip60_marker_to_string(nostr_nip60_ref_marker_t marker) {
switch (marker) {
case NOSTR_NIP60_REF_CREATED: return "created";
case NOSTR_NIP60_REF_DESTROYED: return "destroyed";
case NOSTR_NIP60_REF_REDEEMED: return "redeemed";
default: return "created";
}
}
static nostr_nip60_ref_marker_t nip60_string_to_marker(const char* s) {
if (!s) return NOSTR_NIP60_REF_CREATED;
if (strcmp(s, "created") == 0) return NOSTR_NIP60_REF_CREATED;
if (strcmp(s, "destroyed") == 0) return NOSTR_NIP60_REF_DESTROYED;
if (strcmp(s, "redeemed") == 0) return NOSTR_NIP60_REF_REDEEMED;
return NOSTR_NIP60_REF_CREATED;
}
static const char* nip60_direction_to_string(nostr_nip60_direction_t direction) {
return (direction == NOSTR_NIP60_DIRECTION_OUT) ? "out" : "in";
}
static nostr_nip60_direction_t nip60_string_to_direction(const char* s) {
if (s && strcmp(s, "out") == 0) return NOSTR_NIP60_DIRECTION_OUT;
return NOSTR_NIP60_DIRECTION_IN;
}
static int nip60_encrypt_self(const unsigned char* private_key,
const char* plaintext,
char* output,
size_t output_size) {
if (!private_key || !plaintext || !output || output_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
unsigned char pubkey[32];
if (nostr_ec_public_key_from_private_key(private_key, pubkey) != 0) {
return NOSTR_ERROR_CRYPTO_FAILED;
}
return nostr_nip44_encrypt(private_key, pubkey, plaintext, output, output_size);
}
static int nip60_decrypt_event_content(cJSON* event,
const unsigned char* private_key,
char* output,
size_t output_size) {
if (!event || !private_key || !output || output_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* pubkey_item = cJSON_GetObjectItem(event, "pubkey");
cJSON* content_item = cJSON_GetObjectItem(event, "content");
if (!pubkey_item || !content_item || !cJSON_IsString(pubkey_item) || !cJSON_IsString(content_item)) {
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
}
const char* pubkey_hex = cJSON_GetStringValue(pubkey_item);
const char* content = cJSON_GetStringValue(content_item);
if (!pubkey_hex || !content) {
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
}
unsigned char sender_pubkey[32];
if (nostr_hex_to_bytes(pubkey_hex, sender_pubkey, 32) != 0) {
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
}
int rc = nostr_nip44_decrypt(private_key, sender_pubkey, content, output, output_size);
if (rc != NOSTR_SUCCESS) {
return NOSTR_ERROR_NIP60_DECRYPT_FAILED;
}
return NOSTR_SUCCESS;
}
uint64_t nostr_nip60_sum_proofs(const nostr_cashu_proof_t* proofs, int proof_count) {
if (!proofs || proof_count <= 0) return 0;
uint64_t total = 0;
for (int i = 0; i < proof_count; i++) {
total += proofs[i].amount;
}
return total;
}
void nostr_nip60_free_proofs(nostr_cashu_proof_t* proofs, int proof_count) {
if (!proofs) return;
for (int i = 0; i < proof_count; i++) {
free(proofs[i].secret);
free(proofs[i].C);
proofs[i].secret = NULL;
proofs[i].C = NULL;
}
free(proofs);
}
cJSON* nostr_nip60_proofs_to_json(const nostr_cashu_proof_t* proofs, int proof_count) {
if (!proofs || proof_count < 0) return NULL;
cJSON* arr = cJSON_CreateArray();
if (!arr) return NULL;
for (int i = 0; i < proof_count; i++) {
cJSON* obj = cJSON_CreateObject();
if (!obj) {
cJSON_Delete(arr);
return NULL;
}
cJSON_AddStringToObject(obj, "id", proofs[i].id);
cJSON_AddNumberToObject(obj, "amount", (double)proofs[i].amount);
cJSON_AddStringToObject(obj, "secret", proofs[i].secret ? proofs[i].secret : "");
cJSON_AddStringToObject(obj, "C", proofs[i].C ? proofs[i].C : "");
cJSON_AddItemToArray(arr, obj);
}
return arr;
}
int nostr_nip60_proofs_from_json(cJSON* json_array,
nostr_cashu_proof_t** proofs_out,
int* proof_count_out) {
if (!json_array || !cJSON_IsArray(json_array) || !proofs_out || !proof_count_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
*proofs_out = NULL;
*proof_count_out = 0;
int count = cJSON_GetArraySize(json_array);
if (count <= 0) {
return NOSTR_SUCCESS;
}
nostr_cashu_proof_t* proofs = (nostr_cashu_proof_t*)calloc((size_t)count, sizeof(nostr_cashu_proof_t));
if (!proofs) {
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < count; i++) {
cJSON* p = cJSON_GetArrayItem(json_array, i);
if (!p || !cJSON_IsObject(p)) {
nostr_nip60_free_proofs(proofs, count);
return NOSTR_ERROR_NIP60_INVALID_PROOFS;
}
cJSON* id_item = cJSON_GetObjectItem(p, "id");
cJSON* amount_item = cJSON_GetObjectItem(p, "amount");
cJSON* secret_item = cJSON_GetObjectItem(p, "secret");
cJSON* c_item = cJSON_GetObjectItem(p, "C");
if (!id_item || !amount_item || !secret_item || !c_item ||
!cJSON_IsString(id_item) || !cJSON_IsNumber(amount_item) ||
!cJSON_IsString(secret_item) || !cJSON_IsString(c_item)) {
nostr_nip60_free_proofs(proofs, count);
return NOSTR_ERROR_NIP60_INVALID_PROOFS;
}
const char* id = cJSON_GetStringValue(id_item);
const char* secret = cJSON_GetStringValue(secret_item);
const char* C = cJSON_GetStringValue(c_item);
if (!id || !secret || !C) {
nostr_nip60_free_proofs(proofs, count);
return NOSTR_ERROR_NIP60_INVALID_PROOFS;
}
strncpy(proofs[i].id, id, sizeof(proofs[i].id) - 1);
proofs[i].id[sizeof(proofs[i].id) - 1] = '\0';
proofs[i].amount = (uint64_t)cJSON_GetNumberValue(amount_item);
proofs[i].secret = nip60_strdup(secret);
proofs[i].C = nip60_strdup(C);
if (!proofs[i].secret || !proofs[i].C) {
nostr_nip60_free_proofs(proofs, count);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
*proofs_out = proofs;
*proof_count_out = count;
return NOSTR_SUCCESS;
}
cJSON* nostr_nip60_create_wallet_event(const nostr_nip60_wallet_data_t* wallet_data,
const unsigned char* private_key,
time_t timestamp) {
if (!wallet_data || !private_key || wallet_data->mint_count <= 0) {
return NULL;
}
cJSON* payload = cJSON_CreateArray();
if (!payload) return NULL;
cJSON* priv_row = cJSON_CreateArray();
if (!priv_row) {
cJSON_Delete(payload);
return NULL;
}
cJSON_AddItemToArray(priv_row, cJSON_CreateString("privkey"));
cJSON_AddItemToArray(priv_row, cJSON_CreateString(wallet_data->privkey));
cJSON_AddItemToArray(payload, priv_row);
for (int i = 0; i < wallet_data->mint_count; i++) {
if (!wallet_data->mint_urls || !wallet_data->mint_urls[i]) continue;
cJSON* mint_row = cJSON_CreateArray();
if (!mint_row) {
cJSON_Delete(payload);
return NULL;
}
cJSON_AddItemToArray(mint_row, cJSON_CreateString("mint"));
cJSON_AddItemToArray(mint_row, cJSON_CreateString(wallet_data->mint_urls[i]));
cJSON_AddItemToArray(payload, mint_row);
}
char* plain = cJSON_PrintUnformatted(payload);
cJSON_Delete(payload);
if (!plain) return NULL;
char encrypted[65536];
int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted));
free(plain);
if (rc != NOSTR_SUCCESS) return NULL;
return nostr_create_and_sign_event(NOSTR_NIP60_WALLET_KIND, encrypted, NULL, private_key, timestamp);
}
int nostr_nip60_parse_wallet_event(cJSON* event,
const unsigned char* private_key,
nostr_nip60_wallet_data_t* wallet_data_out) {
if (!event || !private_key || !wallet_data_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
memset(wallet_data_out, 0, sizeof(*wallet_data_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_WALLET_KIND) {
return NOSTR_ERROR_NIP60_INVALID_WALLET;
}
char decrypted[65536];
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
if (rc != NOSTR_SUCCESS) return rc;
cJSON* payload = cJSON_Parse(decrypted);
if (!payload || !cJSON_IsArray(payload)) {
cJSON_Delete(payload);
return NOSTR_ERROR_NIP60_INVALID_WALLET;
}
int mint_count = 0;
cJSON* row = NULL;
cJSON_ArrayForEach(row, payload) {
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
cJSON* k = cJSON_GetArrayItem(row, 0);
cJSON* v = cJSON_GetArrayItem(row, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
const char* key = cJSON_GetStringValue(k);
if (key && strcmp(key, "mint") == 0) mint_count++;
}
if (mint_count > 0) {
wallet_data_out->mint_urls = (char**)calloc((size_t)mint_count, sizeof(char*));
if (!wallet_data_out->mint_urls) {
cJSON_Delete(payload);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
int mint_idx = 0;
cJSON_ArrayForEach(row, payload) {
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
cJSON* k = cJSON_GetArrayItem(row, 0);
cJSON* v = cJSON_GetArrayItem(row, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
const char* key = cJSON_GetStringValue(k);
const char* val = cJSON_GetStringValue(v);
if (!key || !val) continue;
if (strcmp(key, "privkey") == 0) {
strncpy(wallet_data_out->privkey, val, sizeof(wallet_data_out->privkey) - 1);
wallet_data_out->privkey[sizeof(wallet_data_out->privkey) - 1] = '\0';
} else if (strcmp(key, "mint") == 0 && mint_idx < mint_count) {
wallet_data_out->mint_urls[mint_idx] = nip60_strdup(val);
if (!wallet_data_out->mint_urls[mint_idx]) {
cJSON_Delete(payload);
nostr_nip60_free_wallet_data(wallet_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
mint_idx++;
}
}
wallet_data_out->mint_count = mint_idx;
cJSON_Delete(payload);
if (wallet_data_out->privkey[0] == '\0' || wallet_data_out->mint_count == 0) {
nostr_nip60_free_wallet_data(wallet_data_out);
return NOSTR_ERROR_NIP60_INVALID_WALLET;
}
return NOSTR_SUCCESS;
}
void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data) {
if (!data) return;
if (data->mint_urls) {
for (int i = 0; i < data->mint_count; i++) {
free(data->mint_urls[i]);
}
free(data->mint_urls);
}
memset(data, 0, sizeof(*data));
}
cJSON* nostr_nip60_create_token_event(const nostr_nip60_token_data_t* token_data,
const unsigned char* private_key,
time_t timestamp) {
if (!token_data || !private_key || !token_data->mint_url ||
!token_data->proofs || token_data->proof_count <= 0) {
return NULL;
}
cJSON* payload = cJSON_CreateObject();
if (!payload) return NULL;
cJSON_AddStringToObject(payload, "mint", token_data->mint_url);
cJSON* proofs = nostr_nip60_proofs_to_json(token_data->proofs, token_data->proof_count);
if (!proofs) {
cJSON_Delete(payload);
return NULL;
}
cJSON_AddItemToObject(payload, "proofs", proofs);
if (token_data->deleted_token_ids && token_data->deleted_count > 0) {
cJSON* del = cJSON_CreateArray();
if (!del) {
cJSON_Delete(payload);
return NULL;
}
for (int i = 0; i < token_data->deleted_count; i++) {
if (token_data->deleted_token_ids[i]) {
cJSON_AddItemToArray(del, cJSON_CreateString(token_data->deleted_token_ids[i]));
}
}
cJSON_AddItemToObject(payload, "del", del);
}
char* plain = cJSON_PrintUnformatted(payload);
cJSON_Delete(payload);
if (!plain) return NULL;
char encrypted[131072];
int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted));
free(plain);
if (rc != NOSTR_SUCCESS) return NULL;
return nostr_create_and_sign_event(NOSTR_NIP60_TOKEN_KIND, encrypted, NULL, private_key, timestamp);
}
int nostr_nip60_parse_token_event(cJSON* event,
const unsigned char* private_key,
nostr_nip60_token_data_t* token_data_out) {
if (!event || !private_key || !token_data_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
memset(token_data_out, 0, sizeof(*token_data_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_TOKEN_KIND) {
return NOSTR_ERROR_NIP60_INVALID_TOKEN;
}
char decrypted[131072];
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
if (rc != NOSTR_SUCCESS) return rc;
cJSON* payload = cJSON_Parse(decrypted);
if (!payload || !cJSON_IsObject(payload)) {
cJSON_Delete(payload);
return NOSTR_ERROR_NIP60_INVALID_TOKEN;
}
cJSON* mint_item = cJSON_GetObjectItem(payload, "mint");
cJSON* proofs_item = cJSON_GetObjectItem(payload, "proofs");
if (!mint_item || !proofs_item || !cJSON_IsString(mint_item) || !cJSON_IsArray(proofs_item)) {
cJSON_Delete(payload);
return NOSTR_ERROR_NIP60_INVALID_TOKEN;
}
const char* mint_url = cJSON_GetStringValue(mint_item);
token_data_out->mint_url = nip60_strdup(mint_url ? mint_url : "");
if (!token_data_out->mint_url) {
cJSON_Delete(payload);
return NOSTR_ERROR_MEMORY_FAILED;
}
rc = nostr_nip60_proofs_from_json(proofs_item, &token_data_out->proofs, &token_data_out->proof_count);
if (rc != NOSTR_SUCCESS) {
cJSON_Delete(payload);
nostr_nip60_free_token_data(token_data_out);
return rc;
}
cJSON* del_item = cJSON_GetObjectItem(payload, "del");
if (del_item && cJSON_IsArray(del_item)) {
int del_count = cJSON_GetArraySize(del_item);
if (del_count > 0) {
token_data_out->deleted_token_ids = (char**)calloc((size_t)del_count, sizeof(char*));
if (!token_data_out->deleted_token_ids) {
cJSON_Delete(payload);
nostr_nip60_free_token_data(token_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int i = 0; i < del_count; i++) {
cJSON* it = cJSON_GetArrayItem(del_item, i);
if (!it || !cJSON_IsString(it)) continue;
const char* s = cJSON_GetStringValue(it);
if (!s) continue;
token_data_out->deleted_token_ids[token_data_out->deleted_count] = nip60_strdup(s);
if (!token_data_out->deleted_token_ids[token_data_out->deleted_count]) {
cJSON_Delete(payload);
nostr_nip60_free_token_data(token_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
token_data_out->deleted_count++;
}
}
}
cJSON_Delete(payload);
return NOSTR_SUCCESS;
}
void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data) {
if (!data) return;
free(data->mint_url);
nostr_nip60_free_proofs(data->proofs, data->proof_count);
if (data->deleted_token_ids) {
for (int i = 0; i < data->deleted_count; i++) {
free(data->deleted_token_ids[i]);
}
free(data->deleted_token_ids);
}
memset(data, 0, sizeof(*data));
}
cJSON* nostr_nip60_create_token_deletion(const char* token_event_id,
const unsigned char* private_key,
time_t timestamp) {
if (!token_event_id || !private_key) return NULL;
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
cJSON* e_tag = cJSON_CreateArray();
cJSON_AddItemToArray(e_tag, cJSON_CreateString("e"));
cJSON_AddItemToArray(e_tag, cJSON_CreateString(token_event_id));
cJSON_AddItemToArray(tags, e_tag);
cJSON* k_tag = cJSON_CreateArray();
cJSON_AddItemToArray(k_tag, cJSON_CreateString("k"));
cJSON_AddItemToArray(k_tag, cJSON_CreateString("7375"));
cJSON_AddItemToArray(tags, k_tag);
cJSON* evt = nostr_create_and_sign_event(5, "NIP-60 token spent", tags, private_key, timestamp);
cJSON_Delete(tags);
return evt;
}
cJSON* nostr_nip60_create_rollover_token(const nostr_nip60_token_data_t* remaining_proofs,
const char** deleted_event_ids,
int deleted_count,
const unsigned char* private_key,
time_t timestamp) {
if (!remaining_proofs || !private_key) return NULL;
nostr_nip60_token_data_t token = *remaining_proofs;
token.deleted_token_ids = (char**)deleted_event_ids;
token.deleted_count = deleted_count;
return nostr_nip60_create_token_event(&token, private_key, timestamp);
}
cJSON* nostr_nip60_create_history_event(const nostr_nip60_history_data_t* history_data,
const unsigned char* private_key,
time_t timestamp) {
if (!history_data || !private_key) return NULL;
cJSON* payload = cJSON_CreateArray();
if (!payload) return NULL;
cJSON* dir = cJSON_CreateArray();
cJSON_AddItemToArray(dir, cJSON_CreateString("direction"));
cJSON_AddItemToArray(dir, cJSON_CreateString(nip60_direction_to_string(history_data->direction)));
cJSON_AddItemToArray(payload, dir);
char amount_buf[32];
snprintf(amount_buf, sizeof(amount_buf), "%llu", (unsigned long long)history_data->amount);
cJSON* amt = cJSON_CreateArray();
cJSON_AddItemToArray(amt, cJSON_CreateString("amount"));
cJSON_AddItemToArray(amt, cJSON_CreateString(amount_buf));
cJSON_AddItemToArray(payload, amt);
for (int i = 0; i < history_data->ref_count; i++) {
cJSON* e = cJSON_CreateArray();
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].event_id));
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].relay_hint));
cJSON_AddItemToArray(e, cJSON_CreateString(nip60_marker_to_string(history_data->refs[i].marker)));
cJSON_AddItemToArray(payload, e);
}
char* plain = cJSON_PrintUnformatted(payload);
cJSON_Delete(payload);
if (!plain) return NULL;
char encrypted[65536];
int rc = nip60_encrypt_self(private_key, plain, encrypted, sizeof(encrypted));
free(plain);
if (rc != NOSTR_SUCCESS) return NULL;
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
for (int i = 0; i < history_data->ref_count; i++) {
if (history_data->refs[i].marker != NOSTR_NIP60_REF_REDEEMED) continue;
cJSON* e = cJSON_CreateArray();
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].event_id));
cJSON_AddItemToArray(e, cJSON_CreateString(history_data->refs[i].relay_hint));
cJSON_AddItemToArray(e, cJSON_CreateString("redeemed"));
cJSON_AddItemToArray(tags, e);
}
cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP60_HISTORY_KIND, encrypted, tags, private_key, timestamp);
cJSON_Delete(tags);
return evt;
}
int nostr_nip60_parse_history_event(cJSON* event,
const unsigned char* private_key,
nostr_nip60_history_data_t* history_data_out) {
if (!event || !private_key || !history_data_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
memset(history_data_out, 0, sizeof(*history_data_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_HISTORY_KIND) {
return NOSTR_ERROR_NIP60_INVALID_HISTORY;
}
char decrypted[65536];
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
if (rc != NOSTR_SUCCESS) return rc;
cJSON* payload = cJSON_Parse(decrypted);
if (!payload || !cJSON_IsArray(payload)) {
cJSON_Delete(payload);
return NOSTR_ERROR_NIP60_INVALID_HISTORY;
}
int ref_count = 0;
cJSON* row = NULL;
cJSON_ArrayForEach(row, payload) {
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
cJSON* k = cJSON_GetArrayItem(row, 0);
if (!k || !cJSON_IsString(k)) continue;
const char* key = cJSON_GetStringValue(k);
if (key && strcmp(key, "e") == 0) ref_count++;
}
if (ref_count > 0) {
history_data_out->refs = (nostr_nip60_history_ref_t*)calloc((size_t)ref_count, sizeof(nostr_nip60_history_ref_t));
if (!history_data_out->refs) {
cJSON_Delete(payload);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
int ref_idx = 0;
cJSON_ArrayForEach(row, payload) {
if (!cJSON_IsArray(row) || cJSON_GetArraySize(row) < 2) continue;
cJSON* k = cJSON_GetArrayItem(row, 0);
cJSON* v = cJSON_GetArrayItem(row, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
const char* key = cJSON_GetStringValue(k);
const char* val = cJSON_GetStringValue(v);
if (!key || !val) continue;
if (strcmp(key, "direction") == 0) {
history_data_out->direction = nip60_string_to_direction(val);
} else if (strcmp(key, "amount") == 0) {
history_data_out->amount = (uint64_t)strtoull(val, NULL, 10);
} else if (strcmp(key, "e") == 0 && ref_idx < ref_count) {
strncpy(history_data_out->refs[ref_idx].event_id, val,
sizeof(history_data_out->refs[ref_idx].event_id) - 1);
cJSON* relay_item = cJSON_GetArrayItem(row, 2);
cJSON* marker_item = cJSON_GetArrayItem(row, 3);
if (relay_item && cJSON_IsString(relay_item)) {
const char* relay = cJSON_GetStringValue(relay_item);
if (relay) {
strncpy(history_data_out->refs[ref_idx].relay_hint, relay,
sizeof(history_data_out->refs[ref_idx].relay_hint) - 1);
}
}
if (marker_item && cJSON_IsString(marker_item)) {
const char* m = cJSON_GetStringValue(marker_item);
history_data_out->refs[ref_idx].marker = nip60_string_to_marker(m);
} else {
history_data_out->refs[ref_idx].marker = NOSTR_NIP60_REF_CREATED;
}
ref_idx++;
}
}
history_data_out->ref_count = ref_idx;
cJSON_Delete(payload);
return NOSTR_SUCCESS;
}
void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data) {
if (!data) return;
free(data->refs);
memset(data, 0, sizeof(*data));
}
cJSON* nostr_nip60_create_quote_event(const char* quote_id,
const char* mint_url,
time_t expiration,
const unsigned char* private_key,
time_t timestamp) {
if (!quote_id || !mint_url || !private_key || expiration <= 0) {
return NULL;
}
char encrypted[4096];
int rc = nip60_encrypt_self(private_key, quote_id, encrypted, sizeof(encrypted));
if (rc != NOSTR_SUCCESS) return NULL;
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
cJSON* exp_tag = cJSON_CreateArray();
cJSON_AddItemToArray(exp_tag, cJSON_CreateString("expiration"));
char exp_buf[32];
snprintf(exp_buf, sizeof(exp_buf), "%lld", (long long)expiration);
cJSON_AddItemToArray(exp_tag, cJSON_CreateString(exp_buf));
cJSON_AddItemToArray(tags, exp_tag);
cJSON* mint_tag = cJSON_CreateArray();
cJSON_AddItemToArray(mint_tag, cJSON_CreateString("mint"));
cJSON_AddItemToArray(mint_tag, cJSON_CreateString(mint_url));
cJSON_AddItemToArray(tags, mint_tag);
cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP60_QUOTE_KIND, encrypted, tags, private_key, timestamp);
cJSON_Delete(tags);
return evt;
}
int nostr_nip60_parse_quote_event(cJSON* event,
const unsigned char* private_key,
char* quote_id_out,
size_t quote_id_size,
char* mint_url_out,
size_t mint_url_size,
time_t* expiration_out) {
if (!event || !private_key || !quote_id_out || quote_id_size == 0 ||
!mint_url_out || mint_url_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP60_QUOTE_KIND) {
return NOSTR_ERROR_NIP60_INVALID_QUOTE;
}
char decrypted[4096];
int rc = nip60_decrypt_event_content(event, private_key, decrypted, sizeof(decrypted));
if (rc != NOSTR_SUCCESS) return rc;
strncpy(quote_id_out, decrypted, quote_id_size - 1);
quote_id_out[quote_id_size - 1] = '\0';
mint_url_out[0] = '\0';
if (expiration_out) *expiration_out = 0;
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (tags && cJSON_IsArray(tags)) {
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* k = cJSON_GetArrayItem(tag, 0);
cJSON* v = cJSON_GetArrayItem(tag, 1);
if (!k || !v || !cJSON_IsString(k) || !cJSON_IsString(v)) continue;
const char* key = cJSON_GetStringValue(k);
const char* val = cJSON_GetStringValue(v);
if (!key || !val) continue;
if (strcmp(key, "mint") == 0) {
strncpy(mint_url_out, val, mint_url_size - 1);
mint_url_out[mint_url_size - 1] = '\0';
} else if (strcmp(key, "expiration") == 0 && expiration_out) {
*expiration_out = (time_t)strtoll(val, NULL, 10);
}
}
}
return NOSTR_SUCCESS;
}
cJSON* nostr_nip60_create_wallet_filter(const char* pubkey_hex) {
if (!pubkey_hex) return NULL;
cJSON* filter = cJSON_CreateObject();
if (!filter) return NULL;
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_WALLET_KIND));
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_TOKEN_KIND));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* authors = cJSON_CreateArray();
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
return filter;
}
cJSON* nostr_nip60_create_history_filter(const char* pubkey_hex, time_t since) {
if (!pubkey_hex) return NULL;
cJSON* filter = cJSON_CreateObject();
if (!filter) return NULL;
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP60_HISTORY_KIND));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* authors = cJSON_CreateArray();
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
if (since > 0) {
cJSON_AddNumberToObject(filter, "since", (double)since);
}
return filter;
}
+146
View File
@@ -0,0 +1,146 @@
/*
* NIP-60: Cashu Wallet
* https://github.com/nostr-protocol/nips/blob/master/60.md
*/
#ifndef NOSTR_NIP060_H
#define NOSTR_NIP060_H
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include "nip001.h"
#include "nostr_common.h"
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
#define NOSTR_NIP60_WALLET_KIND 17375
#define NOSTR_NIP60_TOKEN_KIND 7375
#define NOSTR_NIP60_HISTORY_KIND 7376
#define NOSTR_NIP60_QUOTE_KIND 7374
#define NOSTR_CASHU_KEYSET_ID_HEX_SIZE 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 */
+540
View File
@@ -0,0 +1,540 @@
/*
* NIP-61: Nutzaps Implementation
* https://github.com/nostr-protocol/nips/blob/master/61.md
*/
#include "nip061.h"
#include "nip060.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
static char* nip61_strdup(const char* s) {
if (!s) return NULL;
size_t len = strlen(s);
char* out = (char*)malloc(len + 1);
if (!out) return NULL;
memcpy(out, s, len + 1);
return out;
}
static int nip61_mint_in_info(const char* mint_url, const nostr_nip61_nutzap_info_t* info) {
if (!mint_url || !info) return 0;
for (int i = 0; i < info->mint_count; i++) {
if (info->mints[i].url && strcmp(info->mints[i].url, mint_url) == 0) {
return 1;
}
}
return 0;
}
cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info,
const unsigned char* private_key,
time_t timestamp) {
if (!info || !private_key || info->mint_count <= 0 || info->relay_count <= 0 || info->pubkey[0] == '\0') {
return NULL;
}
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
for (int i = 0; i < info->relay_count; i++) {
if (!info->relay_urls || !info->relay_urls[i]) continue;
cJSON* relay = cJSON_CreateArray();
cJSON_AddItemToArray(relay, cJSON_CreateString("relay"));
cJSON_AddItemToArray(relay, cJSON_CreateString(info->relay_urls[i]));
cJSON_AddItemToArray(tags, relay);
}
for (int i = 0; i < info->mint_count; i++) {
if (!info->mints || !info->mints[i].url) continue;
cJSON* mint = cJSON_CreateArray();
cJSON_AddItemToArray(mint, cJSON_CreateString("mint"));
cJSON_AddItemToArray(mint, cJSON_CreateString(info->mints[i].url));
for (int u = 0; u < info->mints[i].unit_count; u++) {
if (info->mints[i].units && info->mints[i].units[u]) {
cJSON_AddItemToArray(mint, cJSON_CreateString(info->mints[i].units[u]));
}
}
cJSON_AddItemToArray(tags, mint);
}
cJSON* pub = cJSON_CreateArray();
cJSON_AddItemToArray(pub, cJSON_CreateString("pubkey"));
cJSON_AddItemToArray(pub, cJSON_CreateString(info->pubkey));
cJSON_AddItemToArray(tags, pub);
cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP61_NUTZAP_INFO_KIND, "", tags, private_key, timestamp);
cJSON_Delete(tags);
return evt;
}
int nostr_nip61_parse_nutzap_info_event(cJSON* event,
nostr_nip61_nutzap_info_t* info_out) {
if (!event || !info_out) return NOSTR_ERROR_INVALID_INPUT;
memset(info_out, 0, sizeof(*info_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags = cJSON_GetObjectItem(event, "tags");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP61_NUTZAP_INFO_KIND ||
!tags || !cJSON_IsArray(tags)) {
return NOSTR_ERROR_NIP61_INVALID_INFO;
}
int relay_count = 0;
int mint_count = 0;
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
if (!t0 || !cJSON_IsString(t0)) continue;
const char* key = cJSON_GetStringValue(t0);
if (!key) continue;
if (strcmp(key, "relay") == 0) relay_count++;
if (strcmp(key, "mint") == 0) mint_count++;
}
if (relay_count > 0) {
info_out->relay_urls = (char**)calloc((size_t)relay_count, sizeof(char*));
if (!info_out->relay_urls) return NOSTR_ERROR_MEMORY_FAILED;
}
if (mint_count > 0) {
info_out->mints = (nostr_nip61_mint_entry_t*)calloc((size_t)mint_count, sizeof(nostr_nip61_mint_entry_t));
if (!info_out->mints) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
int relay_idx = 0;
int mint_idx = 0;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
cJSON* t1 = cJSON_GetArrayItem(tag, 1);
if (!t0 || !t1 || !cJSON_IsString(t0) || !cJSON_IsString(t1)) continue;
const char* key = cJSON_GetStringValue(t0);
const char* val = cJSON_GetStringValue(t1);
if (!key || !val) continue;
if (strcmp(key, "relay") == 0 && relay_idx < relay_count) {
info_out->relay_urls[relay_idx] = nip61_strdup(val);
if (!info_out->relay_urls[relay_idx]) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
relay_idx++;
} else if (strcmp(key, "mint") == 0 && mint_idx < mint_count) {
info_out->mints[mint_idx].url = nip61_strdup(val);
if (!info_out->mints[mint_idx].url) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
int total = cJSON_GetArraySize(tag);
int unit_count = (total > 2) ? (total - 2) : 0;
if (unit_count > 0) {
info_out->mints[mint_idx].units = (char**)calloc((size_t)unit_count, sizeof(char*));
if (!info_out->mints[mint_idx].units) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
for (int u = 0; u < unit_count; u++) {
cJSON* unit_item = cJSON_GetArrayItem(tag, u + 2);
if (unit_item && cJSON_IsString(unit_item)) {
const char* us = cJSON_GetStringValue(unit_item);
if (us) {
info_out->mints[mint_idx].units[info_out->mints[mint_idx].unit_count] = nip61_strdup(us);
if (!info_out->mints[mint_idx].units[info_out->mints[mint_idx].unit_count]) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
info_out->mints[mint_idx].unit_count++;
}
}
}
}
mint_idx++;
} else if (strcmp(key, "pubkey") == 0) {
strncpy(info_out->pubkey, val, sizeof(info_out->pubkey) - 1);
info_out->pubkey[sizeof(info_out->pubkey) - 1] = '\0';
}
}
info_out->relay_count = relay_idx;
info_out->mint_count = mint_idx;
if (info_out->pubkey[0] == '\0' || info_out->mint_count == 0) {
nostr_nip61_free_nutzap_info(info_out);
return NOSTR_ERROR_NIP61_INVALID_INFO;
}
return NOSTR_SUCCESS;
}
void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info) {
if (!info) return;
if (info->relay_urls) {
for (int i = 0; i < info->relay_count; i++) {
free(info->relay_urls[i]);
}
free(info->relay_urls);
}
if (info->mints) {
for (int i = 0; i < info->mint_count; i++) {
free(info->mints[i].url);
if (info->mints[i].units) {
for (int u = 0; u < info->mints[i].unit_count; u++) {
free(info->mints[i].units[u]);
}
free(info->mints[i].units);
}
}
free(info->mints);
}
memset(info, 0, sizeof(*info));
}
cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data,
const unsigned char* sender_private_key,
time_t timestamp) {
if (!nutzap_data || !sender_private_key || !nutzap_data->mint_url ||
!nutzap_data->proofs || nutzap_data->proof_count <= 0 ||
nutzap_data->recipient_pubkey[0] == '\0') {
return NULL;
}
cJSON* tags = cJSON_CreateArray();
if (!tags) return NULL;
for (int i = 0; i < nutzap_data->proof_count; i++) {
cJSON* proof_json = cJSON_CreateObject();
cJSON_AddStringToObject(proof_json, "id", nutzap_data->proofs[i].id);
cJSON_AddNumberToObject(proof_json, "amount", (double)nutzap_data->proofs[i].amount);
cJSON_AddStringToObject(proof_json, "secret", nutzap_data->proofs[i].secret ? nutzap_data->proofs[i].secret : "");
cJSON_AddStringToObject(proof_json, "C", nutzap_data->proofs[i].C ? nutzap_data->proofs[i].C : "");
char* proof_str = cJSON_PrintUnformatted(proof_json);
cJSON_Delete(proof_json);
if (!proof_str) {
cJSON_Delete(tags);
return NULL;
}
cJSON* proof_tag = cJSON_CreateArray();
cJSON_AddItemToArray(proof_tag, cJSON_CreateString("proof"));
cJSON_AddItemToArray(proof_tag, cJSON_CreateString(proof_str));
cJSON_AddItemToArray(tags, proof_tag);
free(proof_str);
}
cJSON* u = cJSON_CreateArray();
cJSON_AddItemToArray(u, cJSON_CreateString("u"));
cJSON_AddItemToArray(u, cJSON_CreateString(nutzap_data->mint_url));
cJSON_AddItemToArray(tags, u);
cJSON* p = cJSON_CreateArray();
cJSON_AddItemToArray(p, cJSON_CreateString("p"));
cJSON_AddItemToArray(p, cJSON_CreateString(nutzap_data->recipient_pubkey));
cJSON_AddItemToArray(tags, p);
if (nutzap_data->nutzapped_event_id[0] != '\0') {
cJSON* e = cJSON_CreateArray();
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_data->nutzapped_event_id));
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_data->nutzapped_relay_hint));
cJSON_AddItemToArray(tags, e);
if (nutzap_data->nutzapped_kind > 0) {
cJSON* k = cJSON_CreateArray();
char kind_buf[16];
snprintf(kind_buf, sizeof(kind_buf), "%d", nutzap_data->nutzapped_kind);
cJSON_AddItemToArray(k, cJSON_CreateString("k"));
cJSON_AddItemToArray(k, cJSON_CreateString(kind_buf));
cJSON_AddItemToArray(tags, k);
}
}
const char* content = nutzap_data->content ? nutzap_data->content : "";
cJSON* evt = nostr_create_and_sign_event(NOSTR_NIP61_NUTZAP_KIND, content, tags, sender_private_key, timestamp);
cJSON_Delete(tags);
return evt;
}
int nostr_nip61_parse_nutzap_event(cJSON* event,
nostr_nip61_nutzap_data_t* nutzap_data_out) {
if (!event || !nutzap_data_out) return NOSTR_ERROR_INVALID_INPUT;
memset(nutzap_data_out, 0, sizeof(*nutzap_data_out));
cJSON* kind_item = cJSON_GetObjectItem(event, "kind");
cJSON* tags = cJSON_GetObjectItem(event, "tags");
cJSON* content = cJSON_GetObjectItem(event, "content");
if (!kind_item || !cJSON_IsNumber(kind_item) || (int)cJSON_GetNumberValue(kind_item) != NOSTR_NIP61_NUTZAP_KIND ||
!tags || !cJSON_IsArray(tags) || !content || !cJSON_IsString(content)) {
return NOSTR_ERROR_NIP61_INVALID_NUTZAP;
}
const char* content_str = cJSON_GetStringValue(content);
nutzap_data_out->content = nip61_strdup(content_str ? content_str : "");
if (!nutzap_data_out->content) return NOSTR_ERROR_MEMORY_FAILED;
int proof_count = 0;
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
if (t0 && cJSON_IsString(t0) && strcmp(cJSON_GetStringValue(t0), "proof") == 0) {
proof_count++;
}
}
if (proof_count > 0) {
nutzap_data_out->proofs = (nostr_cashu_proof_t*)calloc((size_t)proof_count, sizeof(nostr_cashu_proof_t));
if (!nutzap_data_out->proofs) {
nostr_nip61_free_nutzap_data(nutzap_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
int proof_idx = 0;
cJSON_ArrayForEach(tag, tags) {
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
cJSON* t1 = cJSON_GetArrayItem(tag, 1);
if (!t0 || !t1 || !cJSON_IsString(t0) || !cJSON_IsString(t1)) continue;
const char* key = cJSON_GetStringValue(t0);
const char* val = cJSON_GetStringValue(t1);
if (!key || !val) continue;
if (strcmp(key, "proof") == 0 && proof_idx < proof_count) {
cJSON* proof_obj = cJSON_Parse(val);
if (!proof_obj || !cJSON_IsObject(proof_obj)) {
cJSON_Delete(proof_obj);
continue;
}
cJSON* id = cJSON_GetObjectItem(proof_obj, "id");
cJSON* amount = cJSON_GetObjectItem(proof_obj, "amount");
cJSON* secret = cJSON_GetObjectItem(proof_obj, "secret");
cJSON* C = cJSON_GetObjectItem(proof_obj, "C");
if (id && cJSON_IsString(id) && amount && cJSON_IsNumber(amount) &&
secret && cJSON_IsString(secret) && C && cJSON_IsString(C)) {
const char* id_s = cJSON_GetStringValue(id);
const char* sec_s = cJSON_GetStringValue(secret);
const char* c_s = cJSON_GetStringValue(C);
if (id_s && sec_s && c_s) {
strncpy(nutzap_data_out->proofs[proof_idx].id, id_s,
sizeof(nutzap_data_out->proofs[proof_idx].id) - 1);
nutzap_data_out->proofs[proof_idx].amount = (uint64_t)cJSON_GetNumberValue(amount);
nutzap_data_out->proofs[proof_idx].secret = nip61_strdup(sec_s);
nutzap_data_out->proofs[proof_idx].C = nip61_strdup(c_s);
if (!nutzap_data_out->proofs[proof_idx].secret || !nutzap_data_out->proofs[proof_idx].C) {
cJSON_Delete(proof_obj);
nostr_nip61_free_nutzap_data(nutzap_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
proof_idx++;
}
}
cJSON_Delete(proof_obj);
} else if (strcmp(key, "u") == 0) {
free(nutzap_data_out->mint_url);
nutzap_data_out->mint_url = nip61_strdup(val);
if (!nutzap_data_out->mint_url) {
nostr_nip61_free_nutzap_data(nutzap_data_out);
return NOSTR_ERROR_MEMORY_FAILED;
}
} else if (strcmp(key, "p") == 0) {
strncpy(nutzap_data_out->recipient_pubkey, val, sizeof(nutzap_data_out->recipient_pubkey) - 1);
} else if (strcmp(key, "e") == 0) {
strncpy(nutzap_data_out->nutzapped_event_id, val, sizeof(nutzap_data_out->nutzapped_event_id) - 1);
cJSON* t2 = cJSON_GetArrayItem(tag, 2);
if (t2 && cJSON_IsString(t2)) {
const char* relay = cJSON_GetStringValue(t2);
if (relay) strncpy(nutzap_data_out->nutzapped_relay_hint, relay,
sizeof(nutzap_data_out->nutzapped_relay_hint) - 1);
}
} else if (strcmp(key, "k") == 0) {
nutzap_data_out->nutzapped_kind = atoi(val);
}
}
nutzap_data_out->proof_count = proof_idx;
if (!nutzap_data_out->mint_url || nutzap_data_out->recipient_pubkey[0] == '\0' || proof_idx == 0) {
nostr_nip61_free_nutzap_data(nutzap_data_out);
return NOSTR_ERROR_NIP61_INVALID_NUTZAP;
}
return NOSTR_SUCCESS;
}
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data) {
if (!data) return;
free(data->content);
free(data->mint_url);
nostr_nip60_free_proofs(data->proofs, data->proof_count);
memset(data, 0, sizeof(*data));
}
cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id,
const char* nutzap_relay_hint,
const char* sender_pubkey,
const char* created_token_event_id,
const char* created_token_relay_hint,
uint64_t amount,
const unsigned char* private_key,
time_t timestamp) {
if (!nutzap_event_id || !sender_pubkey || !created_token_event_id || !private_key) {
return NULL;
}
nostr_nip60_history_ref_t refs[1];
memset(refs, 0, sizeof(refs));
strncpy(refs[0].event_id, created_token_event_id, sizeof(refs[0].event_id) - 1);
if (created_token_relay_hint) {
strncpy(refs[0].relay_hint, created_token_relay_hint, sizeof(refs[0].relay_hint) - 1);
}
refs[0].marker = NOSTR_NIP60_REF_CREATED;
nostr_nip60_history_data_t hist;
memset(&hist, 0, sizeof(hist));
hist.direction = NOSTR_NIP60_DIRECTION_IN;
hist.amount = amount;
hist.refs = refs;
hist.ref_count = 1;
cJSON* evt = nostr_nip60_create_history_event(&hist, private_key, timestamp);
if (!evt) return NULL;
cJSON* tags = cJSON_GetObjectItem(evt, "tags");
if (!tags || !cJSON_IsArray(tags)) return evt;
cJSON* e = cJSON_CreateArray();
cJSON_AddItemToArray(e, cJSON_CreateString("e"));
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_event_id));
cJSON_AddItemToArray(e, cJSON_CreateString(nutzap_relay_hint ? nutzap_relay_hint : ""));
cJSON_AddItemToArray(e, cJSON_CreateString("redeemed"));
cJSON_AddItemToArray(tags, e);
cJSON* p = cJSON_CreateArray();
cJSON_AddItemToArray(p, cJSON_CreateString("p"));
cJSON_AddItemToArray(p, cJSON_CreateString(sender_pubkey));
cJSON_AddItemToArray(tags, p);
return evt;
}
int nostr_nip61_verify_nutzap(cJSON* nutzap_event, cJSON* nutzap_info_event) {
if (!nutzap_event || !nutzap_info_event) return NOSTR_ERROR_INVALID_INPUT;
nostr_nip61_nutzap_data_t nutzap;
memset(&nutzap, 0, sizeof(nutzap));
int rc = nostr_nip61_parse_nutzap_event(nutzap_event, &nutzap);
if (rc != NOSTR_SUCCESS) return rc;
nostr_nip61_nutzap_info_t info;
memset(&info, 0, sizeof(info));
rc = nostr_nip61_parse_nutzap_info_event(nutzap_info_event, &info);
if (rc != NOSTR_SUCCESS) {
nostr_nip61_free_nutzap_data(&nutzap);
return rc;
}
if (!nip61_mint_in_info(nutzap.mint_url, &info)) {
nostr_nip61_free_nutzap_data(&nutzap);
nostr_nip61_free_nutzap_info(&info);
return NOSTR_ERROR_NIP61_MINT_MISMATCH;
}
int key_match = 0;
if (strlen(info.pubkey) == 64) {
for (int i = 0; i < nutzap.proof_count; i++) {
if (nutzap.proofs[i].secret && strstr(nutzap.proofs[i].secret, info.pubkey) != NULL) {
key_match = 1;
break;
}
}
} else if (strlen(info.pubkey) == 66 && strncmp(info.pubkey, "02", 2) == 0) {
const char* xonly = info.pubkey + 2;
for (int i = 0; i < nutzap.proof_count; i++) {
if (nutzap.proofs[i].secret && strstr(nutzap.proofs[i].secret, xonly) != NULL) {
key_match = 1;
break;
}
}
}
nostr_nip61_free_nutzap_data(&nutzap);
nostr_nip61_free_nutzap_info(&info);
if (!key_match) {
return NOSTR_ERROR_NIP61_PUBKEY_MISMATCH;
}
return NOSTR_SUCCESS;
}
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex) {
if (!pubkey_hex) return NULL;
cJSON* filter = cJSON_CreateObject();
if (!filter) return NULL;
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP61_NUTZAP_INFO_KIND));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* authors = cJSON_CreateArray();
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
return filter;
}
cJSON* nostr_nip61_create_nutzap_filter(const char* recipient_pubkey_hex,
const char** mint_urls,
int mint_count,
time_t since) {
if (!recipient_pubkey_hex) return NULL;
cJSON* filter = cJSON_CreateObject();
if (!filter) return NULL;
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(NOSTR_NIP61_NUTZAP_KIND));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON* pvals = cJSON_CreateArray();
cJSON_AddItemToArray(pvals, cJSON_CreateString(recipient_pubkey_hex));
cJSON_AddItemToObject(filter, "#p", pvals);
if (mint_urls && mint_count > 0) {
cJSON* uvals = cJSON_CreateArray();
for (int i = 0; i < mint_count; i++) {
if (mint_urls[i]) cJSON_AddItemToArray(uvals, cJSON_CreateString(mint_urls[i]));
}
cJSON_AddItemToObject(filter, "#u", uvals);
}
if (since > 0) {
cJSON_AddNumberToObject(filter, "since", (double)since);
}
return filter;
}
+89
View File
@@ -0,0 +1,89 @@
/*
* NIP-61: Nutzaps
* https://github.com/nostr-protocol/nips/blob/master/61.md
*/
#ifndef NOSTR_NIP061_H
#define NOSTR_NIP061_H
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include "nip001.h"
#include "nip060.h"
#include "nostr_common.h"
#include "../cjson/cJSON.h"
#ifdef __cplusplus
extern "C" {
#endif
#define NOSTR_NIP61_NUTZAP_INFO_KIND 10019
#define NOSTR_NIP61_NUTZAP_KIND 9321
typedef struct {
char* url;
char** units;
int unit_count;
} nostr_nip61_mint_entry_t;
typedef struct {
char** relay_urls;
int relay_count;
nostr_nip61_mint_entry_t* mints;
int mint_count;
char pubkey[NOSTR_CASHU_PUBKEY_HEX_SIZE];
} nostr_nip61_nutzap_info_t;
typedef struct {
char* content;
nostr_cashu_proof_t* proofs;
int proof_count;
char* mint_url;
char recipient_pubkey[65];
char nutzapped_event_id[NOSTR_CASHU_EVENT_ID_HEX_SIZE];
char nutzapped_relay_hint[256];
int nutzapped_kind;
} nostr_nip61_nutzap_data_t;
cJSON* nostr_nip61_create_nutzap_info_event(const nostr_nip61_nutzap_info_t* info,
const unsigned char* private_key,
time_t timestamp);
int nostr_nip61_parse_nutzap_info_event(cJSON* event,
nostr_nip61_nutzap_info_t* info_out);
void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info);
cJSON* nostr_nip61_create_nutzap_event(const nostr_nip61_nutzap_data_t* nutzap_data,
const unsigned char* sender_private_key,
time_t timestamp);
int nostr_nip61_parse_nutzap_event(cJSON* event,
nostr_nip61_nutzap_data_t* nutzap_data_out);
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data);
cJSON* nostr_nip61_create_redemption_event(const char* nutzap_event_id,
const char* nutzap_relay_hint,
const char* sender_pubkey,
const char* created_token_event_id,
const char* created_token_relay_hint,
uint64_t amount,
const unsigned char* private_key,
time_t timestamp);
int nostr_nip61_verify_nutzap(cJSON* nutzap_event, cJSON* nutzap_info_event);
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex);
cJSON* nostr_nip61_create_nutzap_filter(const char* recipient_pubkey_hex,
const char** mint_urls,
int mint_count,
time_t since);
#ifdef __cplusplus
}
#endif
#endif /* NOSTR_NIP061_H */
+20
View File
@@ -64,6 +64,26 @@ const char* nostr_strerror(int error_code) {
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";
}
}
+26
View File
@@ -76,6 +76,32 @@
#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
+6 -3
View File
@@ -2,10 +2,10 @@
#define NOSTR_CORE_H
// Version information (auto-updated by increment_and_push.sh)
#define VERSION "v0.4.13"
#define VERSION "v0.5.0"
#define VERSION_MAJOR 0
#define VERSION_MINOR 4
#define VERSION_PATCH 13
#define VERSION_MINOR 5
#define VERSION_PATCH 0
/*
* NOSTR Core Library - Complete API Reference
@@ -189,6 +189,9 @@ extern "C" {
#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
+778
View File
@@ -0,0 +1,778 @@
# NIP-60 Cashu Wallet & NIP-61 Nutzaps — Implementation Plan
## Overview
**NIP-60** defines a protocol for storing Cashu wallet state on Nostr relays, enabling cross-application wallet portability. The wallet stores unspent proofs as encrypted events, tracks spending history, and manages mint relationships.
**NIP-61** defines "Nutzaps" — P2PK-locked Cashu tokens sent as zaps between Nostr users, where the payment itself serves as the receipt.
This implementation covers:
- **NIP-60**: Wallet event creation/parsing (kind:17375), token events (kind:7375), spending history (kind:7376), quote events (kind:7374)
- **NIP-61**: Nutzap info events (kind:10019), nutzap events (kind:9321), nutzap redemption
- **Cashu Mint HTTP Client**: Mint info, token swap, melt, and mint operations via curl
## Architecture
### Component Dependency Map
```mermaid
graph TD
NIP60[nip060.c/h - Cashu Wallet Events] --> NIP44[nip044 - NIP-44 Encryption]
NIP60 --> NIP01[nip001 - Event Creation/Signing]
NIP60 --> CJSON[cJSON - JSON Handling]
NIP60 --> UTILS[utils - Hex/Bytes/Base64]
NIP60 --> COMMON[nostr_common - Error Codes]
NIP61[nip061.c/h - Nutzaps] --> NIP44
NIP61 --> NIP01
NIP61 --> CJSON
NIP61 --> UTILS
NIP61 --> COMMON
CASHU[cashu_mint.c/h - Mint HTTP Client] --> CJSON
CASHU --> CURL[libcurl - HTTP]
CASHU --> COMMON
NIP60 -.->|optional| CASHU
NIP61 -.->|optional| CASHU
```
### NIP-60 High-Level Flow
```mermaid
sequenceDiagram
participant App as Application
participant W as NIP-60 Wallet Lib
participant R as Relay
participant M as Cashu Mint
Note over App: Initialize wallet
App->>W: nostr_nip60_create_wallet_event
W->>R: Publish kind:17375
Note over App: Receive tokens
App->>M: Mint tokens via cashu_mint_mint_tokens
M-->>App: Cashu proofs
App->>W: nostr_nip60_create_token_event
W->>R: Publish kind:7375
Note over App: Spend tokens
App->>W: nostr_nip60_spend_token
W->>M: Swap proofs via cashu_mint_swap
M-->>W: Change proofs
W->>R: Delete old kind:7375
W->>R: Publish new kind:7375 with change
W->>R: Publish kind:7376 history
```
### NIP-61 Nutzap Flow
```mermaid
sequenceDiagram
participant Alice as Alice - Sender
participant R as Relay
participant Bob as Bob - Recipient
participant M as Cashu Mint
Note over Bob: Setup
Bob->>R: Publish kind:10019 with mints + p2pk pubkey
Note over Alice: Send nutzap
Alice->>R: Fetch Bobs kind:10019
Alice->>M: Mint/swap P2PK-locked tokens
Alice->>R: Publish kind:9321 nutzap
Note over Bob: Receive nutzap
Bob->>R: Fetch kind:9321 events
Bob->>M: Swap P2PK tokens into wallet
Bob->>R: Publish kind:7376 redemption history
```
### Token State Transition
```mermaid
stateDiagram-v2
[*] --> Created: Mint/Receive tokens
Created --> Published: Publish kind 7375
Published --> Spending: Spend some proofs
Spending --> Deleted: NIP-09 delete old event
Spending --> RolledOver: New kind 7375 with unspent + change
RolledOver --> Published
Deleted --> [*]
Published --> Redeemed: All proofs spent
Redeemed --> Deleted
```
## File Structure
```
nostr_core/
nip060.h # NIP-60 Cashu wallet types and function declarations
nip060.c # NIP-60 implementation
nip061.h # NIP-61 Nutzap types and function declarations
nip061.c # NIP-61 implementation
cashu_mint.h # Cashu mint HTTP client types and functions
cashu_mint.c # Cashu mint HTTP client implementation
tests/
nip60_test.c # NIP-60 unit tests
nip61_test.c # NIP-61 unit tests
cashu_mint_test.c # Cashu mint client tests
examples/
cashu_wallet.c # Complete wallet example
```
## Data Structures
### NIP-60 Core Types
```c
// Event kinds
#define NOSTR_NIP60_WALLET_KIND 17375
#define NOSTR_NIP60_TOKEN_KIND 7375
#define NOSTR_NIP60_HISTORY_KIND 7376
#define NOSTR_NIP60_QUOTE_KIND 7374
// A single Cashu proof
typedef struct {
char id[17]; // Keyset ID, e.g. "005c2502034d4f12"
uint64_t amount; // Denomination amount
char* secret; // Proof secret (base64 or P2PK JSON)
char* C; // Blinded signature (hex compressed point)
} nostr_cashu_proof_t;
// Token content - decrypted payload of kind:7375
typedef struct {
char* mint_url; // Mint URL
nostr_cashu_proof_t* proofs; // Array of proofs
int proof_count; // Number of proofs
char** deleted_token_ids; // Event IDs of tokens destroyed
int deleted_count; // Number of deleted token IDs
} nostr_nip60_token_data_t;
// Wallet content - decrypted payload of kind:17375
typedef struct {
char privkey[65]; // Hex private key for P2PK
char** mint_urls; // Array of mint URLs
int mint_count; // Number of mints
} nostr_nip60_wallet_data_t;
// Spending history entry direction
typedef enum {
NOSTR_NIP60_DIRECTION_IN, // Received funds
NOSTR_NIP60_DIRECTION_OUT // Sent funds
} nostr_nip60_direction_t;
// History event reference
typedef enum {
NOSTR_NIP60_REF_CREATED, // New token event created
NOSTR_NIP60_REF_DESTROYED, // Token event destroyed
NOSTR_NIP60_REF_REDEEMED // Nutzap redeemed
} nostr_nip60_ref_marker_t;
typedef struct {
char event_id[65]; // Referenced event ID
char relay_hint[256]; // Optional relay hint
nostr_nip60_ref_marker_t marker; // Reference type
} nostr_nip60_history_ref_t;
// Spending history data - decrypted payload of kind:7376
typedef struct {
nostr_nip60_direction_t direction; // in or out
uint64_t amount; // Amount in sats
nostr_nip60_history_ref_t* refs; // Event references
int ref_count; // Number of references
} nostr_nip60_history_data_t;
```
### NIP-61 Core Types
```c
// Event kinds
#define NOSTR_NIP61_NUTZAP_INFO_KIND 10019
#define NOSTR_NIP61_NUTZAP_KIND 9321
// Mint entry in kind:10019
typedef struct {
char* url; // Mint URL
char** units; // Supported units, e.g. "sat", "usd"
int unit_count; // Number of units
} nostr_nip61_mint_entry_t;
// Nutzap info - parsed kind:10019
typedef struct {
char** relay_urls; // Relays for receiving nutzaps
int relay_count;
nostr_nip61_mint_entry_t* mints; // Trusted mints
int mint_count;
char pubkey[67]; // P2PK pubkey (with 02 prefix)
} nostr_nip61_nutzap_info_t;
// Nutzap event data - parsed kind:9321
typedef struct {
char* content; // Optional comment
nostr_cashu_proof_t* proofs; // P2PK-locked proofs
int proof_count;
char* mint_url; // Mint URL
char recipient_pubkey[65]; // Recipient Nostr pubkey
char nutzapped_event_id[65]; // Event being nutzapped (optional)
char nutzapped_relay_hint[256]; // Relay hint for nutzapped event
int nutzapped_kind; // Kind of nutzapped event
} nostr_nip61_nutzap_data_t;
```
### Cashu Mint Client Types
```c
// Cashu protocol version
#define CASHU_API_VERSION "v1"
// Mint keyset
typedef struct {
char id[17]; // Keyset ID
char unit[8]; // Unit, e.g. "sat"
int active; // Whether keyset is active
uint64_t* amounts; // Denomination amounts
char** pubkeys; // Corresponding public keys
int key_count; // Number of keys
} cashu_keyset_t;
// Mint info
typedef struct {
char* name;
char* description;
char* version;
char** supported_nuts; // NUT numbers supported
int nut_count;
cashu_keyset_t* keysets;
int keyset_count;
} cashu_mint_info_t;
// Mint quote (for minting new tokens)
typedef struct {
char quote_id[128];
char payment_request[1024]; // Lightning invoice
int paid;
uint64_t amount;
time_t expiry;
} cashu_mint_quote_t;
// Melt quote (for paying Lightning invoices)
typedef struct {
char quote_id[128];
uint64_t amount;
uint64_t fee_reserve;
int paid;
char* payment_preimage;
time_t expiry;
} cashu_melt_quote_t;
// Blind message for minting/swapping
typedef struct {
uint64_t amount;
char* B_; // Blinded secret (hex)
char id[17]; // Keyset ID
} cashu_blinded_message_t;
// Blind signature from mint
typedef struct {
uint64_t amount;
char* C_; // Blinded signature (hex)
char id[17]; // Keyset ID
} cashu_blind_signature_t;
```
## API Functions
### NIP-60 Wallet Functions
```c
// === Wallet Event (kind:17375) ===
// Create a wallet event with encrypted content
cJSON* nostr_nip60_create_wallet_event(
const nostr_nip60_wallet_data_t* wallet_data,
const unsigned char* private_key,
time_t timestamp);
// Parse and decrypt a wallet event
int nostr_nip60_parse_wallet_event(
cJSON* event,
const unsigned char* private_key,
nostr_nip60_wallet_data_t* wallet_data_out);
// Free wallet data
void nostr_nip60_free_wallet_data(nostr_nip60_wallet_data_t* data);
// === Token Event (kind:7375) ===
// Create a token event with encrypted proofs
cJSON* nostr_nip60_create_token_event(
const nostr_nip60_token_data_t* token_data,
const unsigned char* private_key,
time_t timestamp);
// Parse and decrypt a token event
int nostr_nip60_parse_token_event(
cJSON* event,
const unsigned char* private_key,
nostr_nip60_token_data_t* token_data_out);
// Free token data
void nostr_nip60_free_token_data(nostr_nip60_token_data_t* data);
// === Token Spending ===
// Create a NIP-09 deletion event for a spent token
cJSON* nostr_nip60_create_token_deletion(
const char* token_event_id,
const unsigned char* private_key,
time_t timestamp);
// Create a rollover token event (unspent proofs + change from old token)
cJSON* nostr_nip60_create_rollover_token(
const nostr_nip60_token_data_t* remaining_proofs,
const char** deleted_event_ids,
int deleted_count,
const unsigned char* private_key,
time_t timestamp);
// === Spending History (kind:7376) ===
// Create a spending history event
cJSON* nostr_nip60_create_history_event(
const nostr_nip60_history_data_t* history_data,
const unsigned char* private_key,
time_t timestamp);
// Parse and decrypt a spending history event
int nostr_nip60_parse_history_event(
cJSON* event,
const unsigned char* private_key,
nostr_nip60_history_data_t* history_data_out);
// Free history data
void nostr_nip60_free_history_data(nostr_nip60_history_data_t* data);
// === Quote Event (kind:7374) ===
// Create a mint quote event
cJSON* nostr_nip60_create_quote_event(
const char* quote_id,
const char* mint_url,
time_t expiration,
const unsigned char* private_key,
time_t timestamp);
// Parse a quote event
int nostr_nip60_parse_quote_event(
cJSON* event,
const unsigned char* private_key,
char* quote_id_out,
size_t quote_id_size,
char* mint_url_out,
size_t mint_url_size);
// === Utility: Proof helpers ===
// Calculate total amount from an array of proofs
uint64_t nostr_nip60_sum_proofs(
const nostr_cashu_proof_t* proofs,
int proof_count);
// Serialize proofs to JSON array
cJSON* nostr_nip60_proofs_to_json(
const nostr_cashu_proof_t* proofs,
int proof_count);
// Parse proofs from JSON array
int nostr_nip60_proofs_from_json(
cJSON* json_array,
nostr_cashu_proof_t** proofs_out,
int* proof_count_out);
// Free proof array
void nostr_nip60_free_proofs(
nostr_cashu_proof_t* proofs,
int proof_count);
// === Filter helpers ===
// Create a filter to fetch wallet and token events
cJSON* nostr_nip60_create_wallet_filter(const char* pubkey_hex);
// Create a filter to fetch spending history
cJSON* nostr_nip60_create_history_filter(const char* pubkey_hex, time_t since);
```
### NIP-61 Nutzap Functions
```c
// === Nutzap Info Event (kind:10019) ===
// Create nutzap info event
cJSON* nostr_nip61_create_nutzap_info_event(
const nostr_nip61_nutzap_info_t* info,
const unsigned char* private_key,
time_t timestamp);
// Parse nutzap info event
int nostr_nip61_parse_nutzap_info_event(
cJSON* event,
nostr_nip61_nutzap_info_t* info_out);
// Free nutzap info
void nostr_nip61_free_nutzap_info(nostr_nip61_nutzap_info_t* info);
// === Nutzap Event (kind:9321) ===
// Create a nutzap event
cJSON* nostr_nip61_create_nutzap_event(
const nostr_nip61_nutzap_data_t* nutzap_data,
const unsigned char* sender_private_key,
time_t timestamp);
// Parse a nutzap event
int nostr_nip61_parse_nutzap_event(
cJSON* event,
nostr_nip61_nutzap_data_t* nutzap_data_out);
// Free nutzap data
void nostr_nip61_free_nutzap_data(nostr_nip61_nutzap_data_t* data);
// === Nutzap Redemption ===
// Create a redemption history event (kind:7376 with redeemed marker)
cJSON* nostr_nip61_create_redemption_event(
const char* nutzap_event_id,
const char* nutzap_relay_hint,
const char* sender_pubkey,
const char* created_token_event_id,
const char* created_token_relay_hint,
uint64_t amount,
const unsigned char* private_key,
time_t timestamp);
// === Nutzap Verification ===
// Verify a nutzap against recipients kind:10019
int nostr_nip61_verify_nutzap(
cJSON* nutzap_event,
cJSON* nutzap_info_event);
// === Filter helpers ===
// Create filter to fetch nutzap info for a user
cJSON* nostr_nip61_create_nutzap_info_filter(const char* pubkey_hex);
// Create filter to fetch incoming nutzaps
cJSON* nostr_nip61_create_nutzap_filter(
const char* recipient_pubkey_hex,
const char** mint_urls,
int mint_count,
time_t since);
```
### Cashu Mint HTTP Client Functions
```c
// === Mint Connection ===
// Get mint info
int cashu_mint_get_info(
const char* mint_url,
cashu_mint_info_t* info_out,
int timeout_seconds);
// Free mint info
void cashu_mint_free_info(cashu_mint_info_t* info);
// === Keysets ===
// Get active keysets from mint
int cashu_mint_get_keysets(
const char* mint_url,
cashu_keyset_t** keysets_out,
int* keyset_count_out,
int timeout_seconds);
// Get specific keyset keys
int cashu_mint_get_keys(
const char* mint_url,
const char* keyset_id,
cashu_keyset_t* keyset_out,
int timeout_seconds);
// Free keysets
void cashu_mint_free_keysets(cashu_keyset_t* keysets, int count);
// === Minting (receiving Lightning) ===
// Request a mint quote (get Lightning invoice)
int cashu_mint_request_mint_quote(
const char* mint_url,
uint64_t amount,
const char* unit,
cashu_mint_quote_t* quote_out,
int timeout_seconds);
// Check mint quote status
int cashu_mint_check_mint_quote(
const char* mint_url,
const char* quote_id,
cashu_mint_quote_t* quote_out,
int timeout_seconds);
// Mint tokens (after quote is paid)
int cashu_mint_mint_tokens(
const char* mint_url,
const char* quote_id,
const cashu_blinded_message_t* blinded_messages,
int message_count,
cashu_blind_signature_t** signatures_out,
int* signature_count_out,
int timeout_seconds);
// === Melting (paying Lightning) ===
// Request a melt quote (estimate fee for paying invoice)
int cashu_mint_request_melt_quote(
const char* mint_url,
const char* payment_request,
const char* unit,
cashu_melt_quote_t* quote_out,
int timeout_seconds);
// Check melt quote status
int cashu_mint_check_melt_quote(
const char* mint_url,
const char* quote_id,
cashu_melt_quote_t* quote_out,
int timeout_seconds);
// Melt tokens (pay Lightning invoice)
int cashu_mint_melt_tokens(
const char* mint_url,
const char* quote_id,
const nostr_cashu_proof_t* proofs,
int proof_count,
cashu_melt_quote_t* result_out,
cashu_blind_signature_t** change_out,
int* change_count_out,
int timeout_seconds);
// === Swapping ===
// Swap proofs (split/combine denominations)
int cashu_mint_swap(
const char* mint_url,
const nostr_cashu_proof_t* inputs,
int input_count,
const cashu_blinded_message_t* outputs,
int output_count,
cashu_blind_signature_t** signatures_out,
int* signature_count_out,
int timeout_seconds);
// === Token State ===
// Check if proofs have been spent
int cashu_mint_check_proofs_state(
const char* mint_url,
const nostr_cashu_proof_t* proofs,
int proof_count,
int* states_out,
int timeout_seconds);
// === Crypto Helpers ===
// Generate blinded messages for a target amount
int cashu_create_blinded_messages(
uint64_t amount,
const char* keyset_id,
cashu_blinded_message_t** messages_out,
int* message_count_out,
char*** secrets_out,
char*** rs_out);
// Unblind signatures to get proofs
int cashu_unblind_signatures(
const cashu_blind_signature_t* signatures,
int signature_count,
const char** secrets,
const char** rs,
const cashu_keyset_t* keyset,
nostr_cashu_proof_t** proofs_out,
int* proof_count_out);
// Create P2PK-locked blinded messages
int cashu_create_p2pk_blinded_messages(
uint64_t amount,
const char* keyset_id,
const char* recipient_pubkey_hex,
cashu_blinded_message_t** messages_out,
int* message_count_out,
char*** secrets_out,
char*** rs_out);
// Free blinded messages
void cashu_free_blinded_messages(cashu_blinded_message_t* messages, int count);
// Free blind signatures
void cashu_free_blind_signatures(cashu_blind_signature_t* signatures, int count);
```
## Error Codes
New error codes to add to `nostr_common.h`:
```c
// NIP-60 Cashu Wallet error codes
#define NOSTR_ERROR_NIP60_INVALID_WALLET -400
#define NOSTR_ERROR_NIP60_INVALID_TOKEN -401
#define NOSTR_ERROR_NIP60_INVALID_HISTORY -402
#define NOSTR_ERROR_NIP60_INVALID_QUOTE -403
#define NOSTR_ERROR_NIP60_DECRYPT_FAILED -404
#define NOSTR_ERROR_NIP60_INVALID_PROOFS -405
#define NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS -406
// NIP-61 Nutzap error codes
#define NOSTR_ERROR_NIP61_INVALID_INFO -410
#define NOSTR_ERROR_NIP61_INVALID_NUTZAP -411
#define NOSTR_ERROR_NIP61_MINT_MISMATCH -412
#define NOSTR_ERROR_NIP61_PUBKEY_MISMATCH -413
#define NOSTR_ERROR_NIP61_VERIFICATION_FAILED -414
// Cashu Mint client error codes
#define NOSTR_ERROR_CASHU_HTTP_FAILED -420
#define NOSTR_ERROR_CASHU_JSON_PARSE_FAILED -421
#define NOSTR_ERROR_CASHU_MINT_ERROR -422
#define NOSTR_ERROR_CASHU_QUOTE_NOT_PAID -423
#define NOSTR_ERROR_CASHU_QUOTE_EXPIRED -424
#define NOSTR_ERROR_CASHU_PROOFS_SPENT -425
#define NOSTR_ERROR_CASHU_CRYPTO_FAILED -426
#define NOSTR_ERROR_CASHU_INVALID_KEYSET -427
```
## Build System Changes
### build.sh Updates
1. Add `060` and `061` to the NIP detection patterns
2. Add `cashu_mint.c` as a source when NIP-60 or NIP-61 is detected
3. Add NIP descriptions: `NIP-060(Cashu-Wallet)`, `NIP-061(Nutzaps)`
4. Update the `--nips=all` list to include `060` and `061`
5. Update the help text with new NIP descriptions
### nostr_core.h Updates
Add includes:
```c
#include "nip060.h" // Cashu Wallet
#include "nip061.h" // Nutzaps
#include "cashu_mint.h" // Cashu Mint HTTP Client
```
Add NIP-60/61 functions to the quick reference comment block.
## Implementation Order
The implementation should proceed in this order due to dependencies:
1. **Error codes** in `nostr_common.h` — no dependencies
2. **Cashu proof types** — shared between NIP-60 and NIP-61
3. **`cashu_mint.h/c`** — Cashu mint HTTP client (depends on curl, cJSON)
4. **`nip060.h/c`** — NIP-60 wallet events (depends on NIP-44, NIP-01, cashu types)
5. **`nip061.h/c`** — NIP-61 nutzaps (depends on NIP-60 types, cashu types)
6. **Build system** — update `build.sh` and `nostr_core.h`
7. **Tests**`nip60_test.c`, `nip61_test.c`, `cashu_mint_test.c`
8. **Example**`cashu_wallet.c`
## Key Design Decisions
### 1. Self-encryption for NIP-44
NIP-60 encrypts content to the user's own key (sender = recipient). The NIP-44 encrypt/decrypt functions take sender_private_key and recipient_public_key. For self-encryption, we derive the user's public key from their private key and use that as the recipient.
### 2. Cashu Crypto (Blinding)
The Cashu protocol requires elliptic curve blinding operations (hash-to-curve, blind/unblind). These use secp256k1 which is already a dependency. The blinding math is:
- `B_ = Y + r*G` where Y = hash_to_curve(secret), r = random blinding factor
- `C_ = k*B_` (mint signs)
- `C = C_ - r*K` where K = mint's public key (unblind)
### 3. P2PK Secrets
For NIP-61 nutzaps, proof secrets use the P2PK format:
```json
["P2PK", {"nonce": "<random>", "data": "02<pubkey>"}]
```
The `02` prefix is required for nostr-cashu compatibility.
### 4. Curl Usage
Following the same pattern as `nip005.c` — direct curl usage with write callbacks. No abstraction layer needed since curl is already a project dependency.
## Test Strategy
### NIP-60 Tests
- Create and parse wallet events (round-trip encryption)
- Create and parse token events with multiple proofs
- Token deletion event creation
- Rollover token creation with del references
- History event creation and parsing
- Quote event creation and parsing
- Proof sum calculation
- Filter creation helpers
### NIP-61 Tests
- Create and parse nutzap info events
- Create and parse nutzap events
- Nutzap verification against kind:10019
- Redemption history event creation
- Filter creation helpers
### Cashu Mint Tests
- Mint info parsing from JSON
- Keyset parsing
- Blinded message creation
- Signature unblinding
- P2PK secret generation
- Proof state checking (mock or live mint)
## Example Usage
```c
#include "nostr_core/nostr_core.h"
// Create a wallet
nostr_nip60_wallet_data_t wallet = {0};
strcpy(wallet.privkey, "<generated-hex-privkey>");
wallet.mint_urls = (char*[]){"https://mint.example.com"};
wallet.mint_count = 1;
cJSON* wallet_event = nostr_nip60_create_wallet_event(&wallet, my_privkey, 0);
// Publish wallet_event to relays...
// Store tokens
nostr_cashu_proof_t proofs[2] = {
{.id = "00ad268c4d1f5826", .amount = 1, .secret = "...", .C = "02..."},
{.id = "00ad268c4d1f5826", .amount = 4, .secret = "...", .C = "02..."}
};
nostr_nip60_token_data_t token = {
.mint_url = "https://mint.example.com",
.proofs = proofs,
.proof_count = 2
};
cJSON* token_event = nostr_nip60_create_token_event(&token, my_privkey, 0);
// Publish token_event to relays...
// Send a nutzap
nostr_nip61_nutzap_data_t nutzap = {
.content = "Great post!",
.proofs = p2pk_locked_proofs,
.proof_count = 1,
.mint_url = "https://mint.example.com",
.recipient_pubkey = "<bob-pubkey>",
.nutzapped_event_id = "<event-id>"
};
cJSON* nutzap_event = nostr_nip61_create_nutzap_event(&nutzap, my_privkey, 0);
// Publish nutzap_event to relays...
```
+70
View File
@@ -0,0 +1,70 @@
/*
* Cashu Mint HTTP Client Test Suite
*/
#include <stdio.h>
#include <string.h>
#include "../nostr_core/nostr_core.h"
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static void test_invalid_inputs(void) {
printf("\n=== test_invalid_inputs ===\n");
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
int rc = cashu_mint_get_info(NULL, &info, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "get_info null url");
cashu_mint_quote_t mquote;
memset(&mquote, 0, sizeof(mquote));
rc = cashu_mint_request_mint_quote(NULL, 10, "sat", &mquote, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "mint quote null url");
rc = cashu_mint_request_mint_quote("https://mint.example.com", 10, NULL, &mquote, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "mint quote null unit");
rc = cashu_mint_check_mint_quote("https://mint.example.com", NULL, &mquote, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "check mint quote null id");
cashu_melt_quote_t melt;
memset(&melt, 0, sizeof(melt));
rc = cashu_mint_request_melt_quote("https://mint.example.com", NULL, "sat", &melt, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "melt quote null request");
rc = cashu_mint_check_melt_quote("https://mint.example.com", NULL, &melt, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "check melt quote null id");
cJSON* out = NULL;
rc = cashu_mint_swap("https://mint.example.com", NULL, &out, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "swap null request body");
rc = cashu_mint_mint_tokens("https://mint.example.com", NULL, &out, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "mint tokens null request body");
rc = cashu_mint_melt_tokens("https://mint.example.com", NULL, &out, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "melt tokens null request body");
rc = cashu_mint_check_proofs_state("https://mint.example.com", NULL, &out, 5);
TEST_ASSERT(rc == NOSTR_ERROR_INVALID_INPUT, "checkstate null request body");
}
int main(void) {
printf("Cashu Mint HTTP Client Tests\n");
printf("============================\n");
test_invalid_inputs();
printf("\n=== Test Summary ===\n");
printf("Passed: %d/%d\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}
+185
View File
@@ -0,0 +1,185 @@
/*
* NIP-60 Cashu Wallet Test Suite
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "../nostr_core/nostr_core.h"
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static int hex_to_bytes32(const char* hex, unsigned char out[32]) {
return nostr_hex_to_bytes(hex, out, 32) == 0 ? 0 : -1;
}
static void test_wallet_roundtrip(void) {
printf("\n=== test_wallet_roundtrip ===\n");
const char* sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
char* mints[] = {
"https://mint1.example.com",
"https://mint2.example.com"
};
nostr_nip60_wallet_data_t in;
memset(&in, 0, sizeof(in));
strcpy(in.privkey, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
in.mint_urls = mints;
in.mint_count = 2;
cJSON* evt = nostr_nip60_create_wallet_event(&in, sk, 0);
TEST_ASSERT(evt != NULL, "create wallet event");
nostr_nip60_wallet_data_t out;
memset(&out, 0, sizeof(out));
int rc = nostr_nip60_parse_wallet_event(evt, sk, &out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse wallet event");
TEST_ASSERT(strcmp(out.privkey, in.privkey) == 0, "wallet privkey preserved");
TEST_ASSERT(out.mint_count == 2, "wallet mint count");
TEST_ASSERT(strcmp(out.mint_urls[0], mints[0]) == 0, "wallet mint 0");
TEST_ASSERT(strcmp(out.mint_urls[1], mints[1]) == 0, "wallet mint 1");
nostr_nip60_free_wallet_data(&out);
cJSON_Delete(evt);
}
static void test_token_roundtrip_and_sum(void) {
printf("\n=== test_token_roundtrip_and_sum ===\n");
const char* sk_hex = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
nostr_cashu_proof_t proofs[2];
memset(proofs, 0, sizeof(proofs));
strcpy(proofs[0].id, "005c2502034d4f12");
proofs[0].amount = 1;
proofs[0].secret = "secret-1";
proofs[0].C = "0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46";
strcpy(proofs[1].id, "005c2502034d4f12");
proofs[1].amount = 8;
proofs[1].secret = "secret-8";
proofs[1].C = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
char* del_ids[] = {"event-id-1"};
nostr_nip60_token_data_t token;
memset(&token, 0, sizeof(token));
token.mint_url = "https://mint.example.com";
token.proofs = proofs;
token.proof_count = 2;
token.deleted_token_ids = del_ids;
token.deleted_count = 1;
TEST_ASSERT(nostr_nip60_sum_proofs(proofs, 2) == 9, "proof sum");
cJSON* evt = nostr_nip60_create_token_event(&token, sk, 0);
TEST_ASSERT(evt != NULL, "create token event");
nostr_nip60_token_data_t out;
memset(&out, 0, sizeof(out));
int rc = nostr_nip60_parse_token_event(evt, sk, &out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse token event");
TEST_ASSERT(strcmp(out.mint_url, token.mint_url) == 0, "token mint");
TEST_ASSERT(out.proof_count == 2, "token proof count");
TEST_ASSERT(out.deleted_count == 1, "token del count");
TEST_ASSERT(nostr_nip60_sum_proofs(out.proofs, out.proof_count) == 9, "parsed proof sum");
cJSON* del_evt = nostr_nip60_create_token_deletion("event-id-1", sk, 0);
TEST_ASSERT(del_evt != NULL, "create token deletion event");
nostr_nip60_free_token_data(&out);
cJSON_Delete(del_evt);
cJSON_Delete(evt);
}
static void test_history_quote_and_filters(void) {
printf("\n=== test_history_quote_and_filters ===\n");
const char* sk_hex = "1111111111111111111111111111111111111111111111111111111111111111";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
nostr_nip60_history_ref_t refs[2];
memset(refs, 0, sizeof(refs));
strcpy(refs[0].event_id, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
strcpy(refs[0].relay_hint, "wss://relay.example.com");
refs[0].marker = NOSTR_NIP60_REF_DESTROYED;
strcpy(refs[1].event_id, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
refs[1].marker = NOSTR_NIP60_REF_CREATED;
nostr_nip60_history_data_t hist;
memset(&hist, 0, sizeof(hist));
hist.direction = NOSTR_NIP60_DIRECTION_OUT;
hist.amount = 4;
hist.refs = refs;
hist.ref_count = 2;
cJSON* hist_evt = nostr_nip60_create_history_event(&hist, sk, 0);
TEST_ASSERT(hist_evt != NULL, "create history event");
nostr_nip60_history_data_t parsed;
memset(&parsed, 0, sizeof(parsed));
int rc = nostr_nip60_parse_history_event(hist_evt, sk, &parsed);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse history event");
TEST_ASSERT(parsed.direction == NOSTR_NIP60_DIRECTION_OUT, "history direction");
TEST_ASSERT(parsed.amount == 4, "history amount");
TEST_ASSERT(parsed.ref_count == 2, "history refs");
time_t exp = 2000000000;
cJSON* quote_evt = nostr_nip60_create_quote_event("quote-123", "https://mint.example.com", exp, sk, 0);
TEST_ASSERT(quote_evt != NULL, "create quote event");
char quote_id[128];
char mint[256];
time_t exp_out = 0;
rc = nostr_nip60_parse_quote_event(quote_evt, sk, quote_id, sizeof(quote_id), mint, sizeof(mint), &exp_out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse quote event");
TEST_ASSERT(strcmp(quote_id, "quote-123") == 0, "quote id");
TEST_ASSERT(strcmp(mint, "https://mint.example.com") == 0, "quote mint");
TEST_ASSERT(exp_out == exp, "quote expiration");
cJSON* wf = nostr_nip60_create_wallet_filter("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
cJSON* hf = nostr_nip60_create_history_filter("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1700000000);
TEST_ASSERT(wf != NULL, "wallet filter");
TEST_ASSERT(hf != NULL, "history filter");
cJSON_Delete(wf);
cJSON_Delete(hf);
cJSON_Delete(quote_evt);
nostr_nip60_free_history_data(&parsed);
cJSON_Delete(hist_evt);
}
int main(void) {
printf("NIP-60 Cashu Wallet Tests\n");
printf("==========================\n");
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize NOSTR library\n");
return 1;
}
test_wallet_roundtrip();
test_token_roundtrip_and_sum();
test_history_quote_and_filters();
printf("\n=== Test Summary ===\n");
printf("Passed: %d/%d\n", tests_passed, tests_run);
nostr_cleanup();
return (tests_passed == tests_run) ? 0 : 1;
}
+199
View File
@@ -0,0 +1,199 @@
/*
* NIP-61 Nutzaps Test Suite
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "../nostr_core/nostr_core.h"
static int tests_run = 0;
static int tests_passed = 0;
#define TEST_ASSERT(cond, msg) do { \
tests_run++; \
if (cond) { tests_passed++; printf("✅ %s\n", msg); } \
else { printf("❌ %s\n", msg); } \
} while (0)
static int hex_to_bytes32(const char* hex, unsigned char out[32]) {
return nostr_hex_to_bytes(hex, out, 32) == 0 ? 0 : -1;
}
static void test_nutzap_info_roundtrip(void) {
printf("\n=== test_nutzap_info_roundtrip ===\n");
const char* sk_hex = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
char* relays[] = {"wss://relay1.example.com", "wss://relay2.example.com"};
char* mint1_units[] = {"usd", "sat"};
char* mint2_units[] = {"sat"};
nostr_nip61_mint_entry_t mints[2];
memset(mints, 0, sizeof(mints));
mints[0].url = "https://mint1.example.com";
mints[0].units = mint1_units;
mints[0].unit_count = 2;
mints[1].url = "https://mint2.example.com";
mints[1].units = mint2_units;
mints[1].unit_count = 1;
nostr_nip61_nutzap_info_t info;
memset(&info, 0, sizeof(info));
info.relay_urls = relays;
info.relay_count = 2;
info.mints = mints;
info.mint_count = 2;
strcpy(info.pubkey, "02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed");
cJSON* evt = nostr_nip61_create_nutzap_info_event(&info, sk, 0);
TEST_ASSERT(evt != NULL, "create nutzap info event");
nostr_nip61_nutzap_info_t out;
memset(&out, 0, sizeof(out));
int rc = nostr_nip61_parse_nutzap_info_event(evt, &out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse nutzap info event");
TEST_ASSERT(out.relay_count == 2, "relay count");
TEST_ASSERT(out.mint_count == 2, "mint count");
TEST_ASSERT(strcmp(out.pubkey, info.pubkey) == 0, "pubkey value");
nostr_nip61_free_nutzap_info(&out);
cJSON_Delete(evt);
}
static void test_nutzap_event_and_filters(void) {
printf("\n=== test_nutzap_event_and_filters ===\n");
const char* sk_hex = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
nostr_cashu_proof_t proofs[1];
memset(proofs, 0, sizeof(proofs));
strcpy(proofs[0].id, "005c2502034d4f12");
proofs[0].amount = 1;
proofs[0].secret = "[\"P2PK\",{\"nonce\":\"n1\",\"data\":\"02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed\"}]";
proofs[0].C = "02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f";
nostr_nip61_nutzap_data_t in;
memset(&in, 0, sizeof(in));
in.content = "Thanks for this great idea.";
in.proofs = proofs;
in.proof_count = 1;
in.mint_url = "https://mint1.example.com";
strcpy(in.recipient_pubkey, "e9fbced3a42dcf551486650cc752ab354347dd413b307484e4fd1818ab53f991");
strcpy(in.nutzapped_event_id, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
strcpy(in.nutzapped_relay_hint, "wss://relay.example.com");
in.nutzapped_kind = 1;
cJSON* evt = nostr_nip61_create_nutzap_event(&in, sk, 0);
TEST_ASSERT(evt != NULL, "create nutzap event");
nostr_nip61_nutzap_data_t out;
memset(&out, 0, sizeof(out));
int rc = nostr_nip61_parse_nutzap_event(evt, &out);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse nutzap event");
TEST_ASSERT(out.proof_count == 1, "proof count");
TEST_ASSERT(strcmp(out.mint_url, in.mint_url) == 0, "mint url");
TEST_ASSERT(strcmp(out.recipient_pubkey, in.recipient_pubkey) == 0, "recipient pubkey");
cJSON* info_filter = nostr_nip61_create_nutzap_info_filter(in.recipient_pubkey);
TEST_ASSERT(info_filter != NULL, "create nutzap info filter");
const char* mints[] = {"https://mint1.example.com", "https://mint2.example.com"};
cJSON* nz_filter = nostr_nip61_create_nutzap_filter(in.recipient_pubkey, mints, 2, 1700000000);
TEST_ASSERT(nz_filter != NULL, "create nutzap filter");
cJSON* redeem_evt = nostr_nip61_create_redemption_event(
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"wss://sender-relay.example.com",
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"wss://my-relay.example.com",
1,
sk,
0
);
TEST_ASSERT(redeem_evt != NULL, "create redemption event");
nostr_nip61_free_nutzap_data(&out);
cJSON_Delete(redeem_evt);
cJSON_Delete(nz_filter);
cJSON_Delete(info_filter);
cJSON_Delete(evt);
}
static void test_nutzap_verify(void) {
printf("\n=== test_nutzap_verify ===\n");
const char* sk_hex = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220";
unsigned char sk[32];
TEST_ASSERT(hex_to_bytes32(sk_hex, sk) == 0, "parse private key");
char* relays[] = {"wss://relay.example.com"};
char* mint_units[] = {"sat"};
nostr_nip61_mint_entry_t mint;
memset(&mint, 0, sizeof(mint));
mint.url = "https://mint-verify.example.com";
mint.units = mint_units;
mint.unit_count = 1;
nostr_nip61_nutzap_info_t info;
memset(&info, 0, sizeof(info));
info.relay_urls = relays;
info.relay_count = 1;
info.mints = &mint;
info.mint_count = 1;
strcpy(info.pubkey, "02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed");
cJSON* info_evt = nostr_nip61_create_nutzap_info_event(&info, sk, 0);
TEST_ASSERT(info_evt != NULL, "create info event for verify");
nostr_cashu_proof_t proof;
memset(&proof, 0, sizeof(proof));
strcpy(proof.id, "005c2502034d4f12");
proof.amount = 1;
proof.secret = "[\"P2PK\",{\"nonce\":\"n1\",\"data\":\"02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed\"}]";
proof.C = "02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f";
nostr_nip61_nutzap_data_t nz;
memset(&nz, 0, sizeof(nz));
nz.content = "zap";
nz.proofs = &proof;
nz.proof_count = 1;
nz.mint_url = "https://mint-verify.example.com";
strcpy(nz.recipient_pubkey, "e9fbced3a42dcf551486650cc752ab354347dd413b307484e4fd1818ab53f991");
cJSON* nz_evt = nostr_nip61_create_nutzap_event(&nz, sk, 0);
TEST_ASSERT(nz_evt != NULL, "create nutzap event for verify");
int rc = nostr_nip61_verify_nutzap(nz_evt, info_evt);
TEST_ASSERT(rc == NOSTR_SUCCESS, "verify nutzap against info");
cJSON_Delete(nz_evt);
cJSON_Delete(info_evt);
}
int main(void) {
printf("NIP-61 Nutzaps Tests\n");
printf("====================\n");
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize NOSTR library\n");
return 1;
}
test_nutzap_info_roundtrip();
test_nutzap_event_and_filters();
test_nutzap_verify();
printf("\n=== Test Summary ===\n");
printf("Passed: %d/%d\n", tests_passed, tests_run);
nostr_cleanup();
return (tests_passed == tests_run) ? 0 : 1;
}
+141
View File
@@ -0,0 +1,141 @@
#define _POSIX_C_SOURCE 200809L
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
const char** relays;
int relay_count;
const char* pubkey;
int timeout_ms;
} test_config_t;
static cJSON* build_kind10002_filter(const char* pubkey_hex) {
if (!pubkey_hex || pubkey_hex[0] == '\0') {
return NULL;
}
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
if (!filter || !kinds || !authors) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
return NULL;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(10002));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToArray(authors, cJSON_CreateString(pubkey_hex));
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(16));
return filter;
}
static void free_events(cJSON** events, int event_count) {
if (!events) return;
for (int i = 0; i < event_count; i++) {
if (events[i]) cJSON_Delete(events[i]);
}
free(events);
}
static void print_event_summary(const char* label, cJSON** events, int event_count) {
printf("%s event_count=%d\n", label, event_count);
for (int i = 0; i < event_count; i++) {
cJSON* id = cJSON_GetObjectItemCaseSensitive(events[i], "id");
cJSON* created_at = cJSON_GetObjectItemCaseSensitive(events[i], "created_at");
const char* id_str = (id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : "(no-id)";
long long ts = (created_at && cJSON_IsNumber(created_at)) ? (long long)created_at->valuedouble : 0;
printf(" [%d] id=%.16s... created_at=%lld\n", i, id_str, ts);
}
}
static int run_query(nostr_relay_pool_t* pool, const test_config_t* cfg, const char* label) {
int event_count = 0;
cJSON* filter = build_kind10002_filter(cfg->pubkey);
if (!filter) {
fprintf(stderr, "[%s] failed to build filter\n", label);
return -1;
}
cJSON** events = nostr_relay_pool_query_sync(
pool,
cfg->relays,
cfg->relay_count,
filter,
&event_count,
cfg->timeout_ms
);
cJSON_Delete(filter);
print_event_summary(label, events, event_count);
free_events(events, event_count);
return event_count;
}
int main(int argc, char** argv) {
const char* pubkey = "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8";
if (argc > 1 && argv[1] && argv[1][0] != '\0') {
pubkey = argv[1];
}
const char* relays[] = {
"wss://relay.damus.io",
"wss://relay.primal.net"
};
test_config_t cfg = {
.relays = relays,
.relay_count = 2,
.pubkey = pubkey,
.timeout_ms = 5000
};
printf("=== repeated_sync_query_test ===\n");
printf("pubkey=%s\n", cfg.pubkey);
printf("relay[0]=%s\n", cfg.relays[0]);
printf("relay[1]=%s\n", cfg.relays[1]);
printf("timeout_ms=%d\n", cfg.timeout_ms);
printf("Set NOSTR_POOL_QUERY_SYNC_DEBUG=1 for internal query-sync tracing.\n\n");
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "nostr_init failed\n");
return 1;
}
nostr_relay_pool_t* pool = nostr_relay_pool_create(NULL);
if (!pool) {
fprintf(stderr, "nostr_relay_pool_create failed\n");
nostr_cleanup();
return 1;
}
int first_count = run_query(pool, &cfg, "first_query");
int second_count = run_query(pool, &cfg, "second_query");
int exit_code = 0;
if (first_count <= 0) {
fprintf(stderr, "FAIL: first query returned no events\n");
exit_code = 2;
} else if (second_count <= 0) {
fprintf(stderr, "FAIL: second query returned no events (regression candidate)\n");
exit_code = 3;
} else {
printf("PASS: both queries returned events\n");
}
nostr_relay_pool_destroy(pool);
nostr_cleanup();
return exit_code;
}