This commit is contained in:
2026-03-19 09:01:11 -04:00
parent 20006df228
commit e21c8eb5be
34 changed files with 3479 additions and 6 deletions
+1 -1
View File
@@ -1 +1 @@
0.4.13
0.4.11
+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 021 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 021 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 */
+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
+5 -2
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.4.11"
#define VERSION_MAJOR 0
#define VERSION_MINOR 4
#define VERSION_PATCH 13
#define VERSION_PATCH 11
/*
* 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...
```
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+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;
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+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;
}
BIN
View File
Binary file not shown.
+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;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.