Add live NIP-60 Cashu receive integration test using nofee.testnut mint

This commit is contained in:
2026-03-20 10:23:21 -04:00
parent e3b9248004
commit afe3e4787f
3 changed files with 390 additions and 3 deletions
+1 -1
View File
@@ -1 +1 @@
0.5.4
0.5.5
+2 -2
View File
@@ -2,10 +2,10 @@
#define NOSTR_CORE_H
// Version information (auto-updated by increment_and_push.sh)
#define VERSION "v0.5.4"
#define VERSION "v0.5.5"
#define VERSION_MAJOR 0
#define VERSION_MINOR 5
#define VERSION_PATCH 4
#define VERSION_PATCH 5
/*
* NOSTR Core Library - Complete API Reference
+387
View File
@@ -0,0 +1,387 @@
/*
* NIP-60 Live Cashu Receive Integration Test
*
* This test performs a live mint against testnut mints, constructs a cashuA token
* string from the returned signature, and verifies our receive/decode path.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <curl/curl.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)
typedef struct {
char* data;
size_t size;
size_t capacity;
} http_buf_t;
static size_t write_cb(void* contents, size_t size, size_t nmemb, void* userp) {
http_buf_t* b = (http_buf_t*)userp;
size_t n = size * nmemb;
if (b->size + n + 1 > b->capacity) {
size_t cap = b->capacity ? b->capacity * 2 : 1024;
while (cap < b->size + n + 1) cap *= 2;
char* p = (char*)realloc(b->data, cap);
if (!p) return 0;
b->data = p;
b->capacity = cap;
}
memcpy(b->data + b->size, contents, n);
b->size += n;
b->data[b->size] = '\0';
return n;
}
static int http_get_json(const char* url, cJSON** out_json) {
if (!url || !out_json) return NOSTR_ERROR_INVALID_INPUT;
*out_json = NULL;
CURL* curl = curl_easy_init();
if (!curl) return NOSTR_ERROR_NETWORK_FAILED;
http_buf_t b;
memset(&b, 0, sizeof(b));
b.capacity = 2048;
b.data = (char*)malloc(b.capacity);
if (!b.data) {
curl_easy_cleanup(curl);
return NOSTR_ERROR_MEMORY_FAILED;
}
b.data[0] = '\0';
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &b);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
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);
CURLcode rc = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
curl_easy_cleanup(curl);
if (rc != CURLE_OK || status < 200 || status >= 300) {
free(b.data);
return NOSTR_ERROR_CASHU_HTTP_FAILED;
}
cJSON* json = cJSON_Parse(b.data);
free(b.data);
if (!json) return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
*out_json = json;
return NOSTR_SUCCESS;
}
static int get_active_keyset_id(const char* mint_url, char out_id[65]) {
if (!mint_url || !out_id) return NOSTR_ERROR_INVALID_INPUT;
out_id[0] = '\0';
char url[1024];
snprintf(url, sizeof(url), "%s/v1/keysets", mint_url);
cJSON* root = NULL;
int rc = http_get_json(url, &root);
if (rc != NOSTR_SUCCESS) return rc;
cJSON* arr = cJSON_GetObjectItem(root, "keysets");
if (!arr || !cJSON_IsArray(arr)) {
cJSON_Delete(root);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
cJSON* selected = NULL;
int n = cJSON_GetArraySize(arr);
for (int i = 0; i < n; i++) {
cJSON* it = cJSON_GetArrayItem(arr, i);
if (!it || !cJSON_IsObject(it)) continue;
cJSON* active = cJSON_GetObjectItem(it, "active");
if (active && cJSON_IsBool(active) && cJSON_IsTrue(active)) {
selected = it;
break;
}
}
if (!selected && n > 0) {
selected = cJSON_GetArrayItem(arr, 0);
}
if (!selected) {
cJSON_Delete(root);
return NOSTR_ERROR_CASHU_INVALID_KEYSET;
}
cJSON* id = cJSON_GetObjectItem(selected, "id");
const char* id_s = (id && cJSON_IsString(id)) ? cJSON_GetStringValue(id) : NULL;
if (!id_s || id_s[0] == '\0') {
cJSON_Delete(root);
return NOSTR_ERROR_CASHU_INVALID_KEYSET;
}
snprintf(out_id, 65, "%s", id_s);
cJSON_Delete(root);
return NOSTR_SUCCESS;
}
static int base64_to_base64url_no_pad(const unsigned char* in, size_t in_len, char* out, size_t out_size) {
if (!in || !out || out_size == 0) return NOSTR_ERROR_INVALID_INPUT;
size_t needed = ((in_len + 2) / 3) * 4 + 1;
char* b64 = (char*)malloc(needed);
if (!b64) return NOSTR_ERROR_MEMORY_FAILED;
size_t n = base64_encode(in, in_len, b64, needed);
if (n == 0) {
free(b64);
return NOSTR_ERROR_INVALID_INPUT;
}
size_t j = 0;
for (size_t i = 0; i < n && b64[i] != '\0'; i++) {
char c = b64[i];
if (c == '=') continue;
if (c == '+') c = '-';
else if (c == '/') c = '_';
if (j + 1 >= out_size) {
free(b64);
return NOSTR_ERROR_INVALID_INPUT;
}
out[j++] = c;
}
out[j] = '\0';
free(b64);
return NOSTR_SUCCESS;
}
static int build_cashuA_token_string(const char* mint_url,
const char* keyset_id,
uint64_t amount,
const char* secret,
const char* C_hex,
char* out_token,
size_t out_token_size) {
if (!mint_url || !keyset_id || !secret || !C_hex || !out_token || out_token_size == 0) {
return NOSTR_ERROR_INVALID_INPUT;
}
cJSON* root = cJSON_CreateObject();
cJSON* token_arr = cJSON_CreateArray();
cJSON* entry = cJSON_CreateObject();
cJSON* proofs = cJSON_CreateArray();
cJSON* proof = cJSON_CreateObject();
if (!root || !token_arr || !entry || !proofs || !proof) {
cJSON_Delete(root);
cJSON_Delete(token_arr);
cJSON_Delete(entry);
cJSON_Delete(proofs);
cJSON_Delete(proof);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON_AddStringToObject(entry, "mint", mint_url);
cJSON_AddStringToObject(proof, "id", keyset_id);
cJSON_AddNumberToObject(proof, "amount", (double)amount);
cJSON_AddStringToObject(proof, "secret", secret);
cJSON_AddStringToObject(proof, "C", C_hex);
cJSON_AddItemToArray(proofs, proof);
cJSON_AddItemToObject(entry, "proofs", proofs);
cJSON_AddItemToArray(token_arr, entry);
cJSON_AddItemToObject(root, "token", token_arr);
char* json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!json) return NOSTR_ERROR_MEMORY_FAILED;
char b64url[8192];
int rc = base64_to_base64url_no_pad((const unsigned char*)json, strlen(json), b64url, sizeof(b64url));
free(json);
if (rc != NOSTR_SUCCESS) return rc;
int written = snprintf(out_token, out_token_size, "cashuA%s", b64url);
if (written < 0 || (size_t)written >= out_token_size) {
return NOSTR_ERROR_INVALID_INPUT;
}
return NOSTR_SUCCESS;
}
static void test_live_mint_and_receive(const char* mint_url) {
printf("\n=== test_live_mint_and_receive: %s ===\n", mint_url);
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
int rc = cashu_mint_get_info(mint_url, &info, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "get mint info");
if (rc != NOSTR_SUCCESS) return;
TEST_ASSERT(info.pubkey != NULL && strlen(info.pubkey) == 66, "mint pubkey available");
if (!info.pubkey || strlen(info.pubkey) != 66) {
cashu_mint_free_info(&info);
return;
}
char keyset_id[65];
memset(keyset_id, 0, sizeof(keyset_id));
rc = get_active_keyset_id(mint_url, keyset_id);
TEST_ASSERT(rc == NOSTR_SUCCESS, "fetch active keyset id");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_mint_quote_t quote;
memset(&quote, 0, sizeof(quote));
rc = cashu_mint_request_mint_quote(mint_url, 1, "sat", &quote, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "request mint quote");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_mint_quote_t quote_check;
memset(&quote_check, 0, sizeof(quote_check));
rc = cashu_mint_check_mint_quote(mint_url, quote.quote_id, &quote_check, 15);
TEST_ASSERT(rc == NOSTR_SUCCESS, "check mint quote");
TEST_ASSERT(quote_check.paid == 1, "mint quote paid");
if (rc != NOSTR_SUCCESS || quote_check.paid != 1) {
cashu_mint_free_info(&info);
return;
}
char secret[160];
snprintf(secret, sizeof(secret), "nostr-live-receive-test-%ld-%s", (long)time(NULL), mint_url);
char B_hex[67];
unsigned char r[32];
rc = cashu_blind_message(secret, info.pubkey, B_hex, r);
TEST_ASSERT(rc == NOSTR_SUCCESS, "blind message");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_info(&info);
return;
}
cashu_blinded_output_t output;
memset(&output, 0, sizeof(output));
size_t keyset_len = strlen(keyset_id);
TEST_ASSERT(keyset_len < sizeof(output.id), "keyset id fits library struct");
if (keyset_len >= sizeof(output.id)) {
cashu_mint_free_info(&info);
return;
}
memcpy(output.id, keyset_id, keyset_len + 1);
output.amount = 1;
snprintf(output.B_, sizeof(output.B_), "%s", B_hex);
cashu_mint_tokens_request_t mint_req;
memset(&mint_req, 0, sizeof(mint_req));
snprintf(mint_req.quote_id, sizeof(mint_req.quote_id), "%s", quote.quote_id);
snprintf(mint_req.unit, sizeof(mint_req.unit), "sat");
mint_req.outputs = &output;
mint_req.output_count = 1;
cJSON* mint_body = NULL;
rc = cashu_build_mint_tokens_request(&mint_req, &mint_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && mint_body != NULL, "build mint tokens request");
if (rc != NOSTR_SUCCESS || !mint_body) {
cashu_mint_free_info(&info);
return;
}
cJSON* mint_resp_json = NULL;
rc = cashu_mint_mint_tokens(mint_url, mint_body, &mint_resp_json, 15);
cJSON_Delete(mint_body);
TEST_ASSERT(rc == NOSTR_SUCCESS && mint_resp_json != NULL, "mint tokens call");
if (rc != NOSTR_SUCCESS || !mint_resp_json) {
cJSON_Delete(mint_resp_json);
cashu_mint_free_info(&info);
return;
}
cashu_mint_tokens_response_t mint_resp;
memset(&mint_resp, 0, sizeof(mint_resp));
rc = cashu_parse_mint_tokens_response(mint_resp_json, &mint_resp);
cJSON_Delete(mint_resp_json);
TEST_ASSERT(rc == NOSTR_SUCCESS, "parse mint tokens response");
TEST_ASSERT(mint_resp.signature_count >= 1, "mint returned signatures");
if (rc != NOSTR_SUCCESS || mint_resp.signature_count < 1) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_info(&info);
return;
}
char C_hex[67];
rc = cashu_unblind_signature(mint_resp.signatures[0].C_, info.pubkey, r, C_hex);
TEST_ASSERT(rc == NOSTR_SUCCESS, "unblind signature");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_info(&info);
return;
}
char token_string[10000];
rc = build_cashuA_token_string(mint_url, keyset_id, 1, secret, C_hex, token_string, sizeof(token_string));
TEST_ASSERT(rc == NOSTR_SUCCESS, "build cashuA token string");
if (rc != NOSTR_SUCCESS) {
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_info(&info);
return;
}
cashu_decoded_token_t decoded;
memset(&decoded, 0, sizeof(decoded));
rc = cashu_decode_token(token_string, &decoded);
TEST_ASSERT(rc == NOSTR_SUCCESS, "decode token (receive path)");
TEST_ASSERT(decoded.mint_url != NULL && strcmp(decoded.mint_url, mint_url) == 0, "decoded mint url matches");
TEST_ASSERT(decoded.proof_count == 1, "decoded proof count");
TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].amount == 1, "decoded proof amount");
TEST_ASSERT(decoded.proofs != NULL && strcmp(decoded.proofs[0].id, keyset_id) == 0, "decoded keyset id");
TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].secret != NULL && strcmp(decoded.proofs[0].secret, secret) == 0,
"decoded secret matches");
cashu_free_decoded_token(&decoded);
cashu_mint_free_mint_tokens_response(&mint_resp);
cashu_mint_free_info(&info);
}
int main(void) {
printf("NIP-60 Live Cashu Receive Integration Test\n");
printf("========================================\n");
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize NOSTR library\n");
return 1;
}
test_live_mint_and_receive("https://nofee.testnut.cashu.space");
printf("\n=== Test Summary ===\n");
printf("Passed: %d/%d\n", tests_passed, tests_run);
nostr_cleanup();
return (tests_passed == tests_run) ? 0 : 1;
}