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