Add live NIP-60 Cashu receive integration test using nofee.testnut mint
This commit is contained in:
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -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("e, 0, sizeof(quote));
|
||||
rc = cashu_mint_request_mint_quote(mint_url, 1, "sat", "e, 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("e_check, 0, sizeof(quote_check));
|
||||
rc = cashu_mint_check_mint_quote(mint_url, quote.quote_id, "e_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;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user