Add Cashu token decode support for cashuA and cashuB

This commit is contained in:
Your Name
2026-03-19 18:19:45 -04:00
parent 4d0d794a9a
commit cb4ff93906
3 changed files with 464 additions and 2 deletions
+2 -2
View File
@@ -197,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 060 061"
NEEDED_NIPS="001 004 005 006 011 013 017 019 021 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
@@ -216,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 060 061"
NEEDED_NIPS="001 004 005 006 011 013 019 021 042 044 046 059 060 061"
elif [ -n "$DETECTED" ]; then
NEEDED_NIPS="$DETECTED"
print_success "Auto-detected NIPs: $(echo $NEEDED_NIPS | tr ' ' ',')"
+451
View File
@@ -4,9 +4,11 @@
#include "cashu_mint.h"
#include "../cjson/cJSON.h"
#include "utils.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <curl/curl.h>
@@ -819,3 +821,452 @@ void cashu_mint_free_checkstate_response(cashu_checkstate_response_t* response)
free(response->states);
memset(response, 0, sizeof(*response));
}
static char* cashu_trim_copy(const char* s) {
if (!s) return NULL;
const char* start = s;
while (*start && isspace((unsigned char)*start)) start++;
const char* end = start + strlen(start);
while (end > start && isspace((unsigned char)*(end - 1))) end--;
size_t len = (size_t)(end - start);
char* out = (char*)malloc(len + 1);
if (!out) return NULL;
memcpy(out, start, len);
out[len] = '\0';
return out;
}
static int cashu_base64url_decode_alloc(const char* input, unsigned char** out, size_t* out_len) {
if (!input || !out || !out_len) return NOSTR_ERROR_INVALID_INPUT;
*out = NULL;
*out_len = 0;
size_t in_len = strlen(input);
if (in_len == 0) return NOSTR_ERROR_INVALID_INPUT;
size_t pad = (4 - (in_len % 4)) % 4;
size_t b64_len = in_len + pad;
char* b64 = (char*)malloc(b64_len + 1);
if (!b64) return NOSTR_ERROR_MEMORY_FAILED;
for (size_t i = 0; i < in_len; i++) {
char c = input[i];
if (c == '-') c = '+';
else if (c == '_') c = '/';
b64[i] = c;
}
for (size_t i = 0; i < pad; i++) b64[in_len + i] = '=';
b64[b64_len] = '\0';
size_t max_out = (b64_len / 4) * 3;
unsigned char* decoded = (unsigned char*)malloc(max_out + 1);
if (!decoded) {
free(b64);
return NOSTR_ERROR_MEMORY_FAILED;
}
size_t n = base64_decode(b64, decoded);
free(b64);
if (n == 0) {
free(decoded);
return NOSTR_ERROR_INVALID_INPUT;
}
*out = decoded;
*out_len = n;
return NOSTR_SUCCESS;
}
typedef struct {
const unsigned char* data;
size_t len;
size_t pos;
int depth;
} cashu_cbor_cursor_t;
static int cashu_cbor_read_uint(cashu_cbor_cursor_t* c, uint8_t ai, uint64_t* out) {
if (!c || !out) return NOSTR_ERROR_INVALID_INPUT;
if (ai < 24) {
*out = ai;
return NOSTR_SUCCESS;
}
if (ai == 24) {
if (c->pos + 1 > c->len) return NOSTR_ERROR_INVALID_INPUT;
*out = c->data[c->pos++];
return NOSTR_SUCCESS;
}
if (ai == 25) {
if (c->pos + 2 > c->len) return NOSTR_ERROR_INVALID_INPUT;
*out = ((uint64_t)c->data[c->pos] << 8) | (uint64_t)c->data[c->pos + 1];
c->pos += 2;
return NOSTR_SUCCESS;
}
if (ai == 26) {
if (c->pos + 4 > c->len) return NOSTR_ERROR_INVALID_INPUT;
*out = ((uint64_t)c->data[c->pos] << 24) |
((uint64_t)c->data[c->pos + 1] << 16) |
((uint64_t)c->data[c->pos + 2] << 8) |
(uint64_t)c->data[c->pos + 3];
c->pos += 4;
return NOSTR_SUCCESS;
}
if (ai == 27) {
if (c->pos + 8 > c->len) return NOSTR_ERROR_INVALID_INPUT;
*out = ((uint64_t)c->data[c->pos] << 56) |
((uint64_t)c->data[c->pos + 1] << 48) |
((uint64_t)c->data[c->pos + 2] << 40) |
((uint64_t)c->data[c->pos + 3] << 32) |
((uint64_t)c->data[c->pos + 4] << 24) |
((uint64_t)c->data[c->pos + 5] << 16) |
((uint64_t)c->data[c->pos + 6] << 8) |
(uint64_t)c->data[c->pos + 7];
c->pos += 8;
return NOSTR_SUCCESS;
}
return NOSTR_ERROR_INVALID_INPUT;
}
static int cashu_cbor_parse_item(cashu_cbor_cursor_t* c, cJSON** out);
static int cashu_cbor_parse_text(cashu_cbor_cursor_t* c, uint8_t ai, cJSON** out) {
uint64_t n = 0;
int rc = cashu_cbor_read_uint(c, ai, &n);
if (rc != NOSTR_SUCCESS) return rc;
if (c->pos + (size_t)n > c->len) return NOSTR_ERROR_INVALID_INPUT;
char* s = (char*)malloc((size_t)n + 1);
if (!s) return NOSTR_ERROR_MEMORY_FAILED;
memcpy(s, c->data + c->pos, (size_t)n);
s[n] = '\0';
c->pos += (size_t)n;
*out = cJSON_CreateString(s);
free(s);
return *out ? NOSTR_SUCCESS : NOSTR_ERROR_MEMORY_FAILED;
}
static int cashu_cbor_parse_bytes(cashu_cbor_cursor_t* c, uint8_t ai, cJSON** out) {
uint64_t n = 0;
int rc = cashu_cbor_read_uint(c, ai, &n);
if (rc != NOSTR_SUCCESS) return rc;
if (c->pos + (size_t)n > c->len) return NOSTR_ERROR_INVALID_INPUT;
size_t hex_len = (size_t)n * 2;
char* hex = (char*)malloc(hex_len + 1);
if (!hex) return NOSTR_ERROR_MEMORY_FAILED;
nostr_bytes_to_hex(c->data + c->pos, (size_t)n, hex);
c->pos += (size_t)n;
*out = cJSON_CreateString(hex);
free(hex);
return *out ? NOSTR_SUCCESS : NOSTR_ERROR_MEMORY_FAILED;
}
static int cashu_cbor_parse_array(cashu_cbor_cursor_t* c, uint8_t ai, cJSON** out) {
uint64_t n = 0;
int rc = cashu_cbor_read_uint(c, ai, &n);
if (rc != NOSTR_SUCCESS) return rc;
cJSON* arr = cJSON_CreateArray();
if (!arr) return NOSTR_ERROR_MEMORY_FAILED;
for (uint64_t i = 0; i < n; i++) {
cJSON* item = NULL;
rc = cashu_cbor_parse_item(c, &item);
if (rc != NOSTR_SUCCESS || !item) {
cJSON_Delete(arr);
return rc != NOSTR_SUCCESS ? rc : NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
cJSON_AddItemToArray(arr, item);
}
*out = arr;
return NOSTR_SUCCESS;
}
static int cashu_cbor_parse_map(cashu_cbor_cursor_t* c, uint8_t ai, cJSON** out) {
uint64_t n = 0;
int rc = cashu_cbor_read_uint(c, ai, &n);
if (rc != NOSTR_SUCCESS) return rc;
cJSON* obj = cJSON_CreateObject();
if (!obj) return NOSTR_ERROR_MEMORY_FAILED;
for (uint64_t i = 0; i < n; i++) {
cJSON* k = NULL;
cJSON* v = NULL;
rc = cashu_cbor_parse_item(c, &k);
if (rc != NOSTR_SUCCESS || !k) {
cJSON_Delete(obj);
return rc != NOSTR_SUCCESS ? rc : NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
rc = cashu_cbor_parse_item(c, &v);
if (rc != NOSTR_SUCCESS || !v) {
cJSON_Delete(k);
cJSON_Delete(obj);
return rc != NOSTR_SUCCESS ? rc : NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
char keybuf[32];
const char* key = NULL;
if (cJSON_IsString(k) && k->valuestring) {
key = k->valuestring;
} else if (cJSON_IsNumber(k)) {
snprintf(keybuf, sizeof(keybuf), "%.0f", cJSON_GetNumberValue(k));
key = keybuf;
}
if (!key || key[0] == '\0') {
cJSON_Delete(k);
cJSON_Delete(v);
cJSON_Delete(obj);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
cJSON_AddItemToObject(obj, key, v);
cJSON_Delete(k);
}
*out = obj;
return NOSTR_SUCCESS;
}
static int cashu_cbor_parse_item(cashu_cbor_cursor_t* c, cJSON** out) {
if (!c || !out || c->pos >= c->len) return NOSTR_ERROR_INVALID_INPUT;
if (c->depth > 64) return NOSTR_ERROR_INVALID_INPUT;
uint8_t ib = c->data[c->pos++];
uint8_t mt = (uint8_t)(ib >> 5);
uint8_t ai = (uint8_t)(ib & 0x1f);
c->depth++;
int rc = NOSTR_SUCCESS;
cJSON* node = NULL;
if (mt == 0) {
uint64_t v = 0;
rc = cashu_cbor_read_uint(c, ai, &v);
if (rc == NOSTR_SUCCESS) node = cJSON_CreateNumber((double)v);
} else if (mt == 1) {
uint64_t v = 0;
rc = cashu_cbor_read_uint(c, ai, &v);
if (rc == NOSTR_SUCCESS) node = cJSON_CreateNumber(-(double)(v + 1));
} else if (mt == 2) {
rc = cashu_cbor_parse_bytes(c, ai, &node);
} else if (mt == 3) {
rc = cashu_cbor_parse_text(c, ai, &node);
} else if (mt == 4) {
rc = cashu_cbor_parse_array(c, ai, &node);
} else if (mt == 5) {
rc = cashu_cbor_parse_map(c, ai, &node);
} else if (mt == 7 && ai == 20) {
node = cJSON_CreateFalse();
} else if (mt == 7 && ai == 21) {
node = cJSON_CreateTrue();
} else if (mt == 7 && ai == 22) {
node = cJSON_CreateNull();
} else {
rc = NOSTR_ERROR_INVALID_INPUT;
}
c->depth--;
if (rc != NOSTR_SUCCESS) {
cJSON_Delete(node);
return rc;
}
if (!node) return NOSTR_ERROR_MEMORY_FAILED;
*out = node;
return NOSTR_SUCCESS;
}
static cJSON* cashu_token_get_case(cJSON* obj, const char* a, const char* b) {
if (!obj || !cJSON_IsObject(obj)) return NULL;
cJSON* x = cJSON_GetObjectItemCaseSensitive(obj, a);
if (x) return x;
return cJSON_GetObjectItemCaseSensitive(obj, b);
}
static int cashu_extract_token_json(cJSON* root, cashu_decoded_token_t* out) {
if (!root || !out) return NOSTR_ERROR_INVALID_INPUT;
memset(out, 0, sizeof(*out));
cJSON* mint = cashu_token_get_case(root, "mint", "m");
cJSON* token_arr = cashu_token_get_case(root, "token", "t");
cJSON* token_entry = NULL;
if (token_arr && cJSON_IsArray(token_arr) && cJSON_GetArraySize(token_arr) > 0) {
token_entry = cJSON_GetArrayItem(token_arr, 0);
} else if (cJSON_IsObject(root)) {
token_entry = root;
}
if (!token_entry || !cJSON_IsObject(token_entry)) {
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
cJSON* entry_mint = cashu_token_get_case(token_entry, "mint", "m");
if (entry_mint && cJSON_IsString(entry_mint) && entry_mint->valuestring) mint = entry_mint;
if (!mint || !cJSON_IsString(mint) || !mint->valuestring || mint->valuestring[0] == '\0') {
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
out->mint_url = cashu_strdup(mint->valuestring);
if (!out->mint_url) return NOSTR_ERROR_MEMORY_FAILED;
cJSON* proofs = cashu_token_get_case(token_entry, "proofs", "p");
if (!proofs || !cJSON_IsArray(proofs)) {
cashu_free_decoded_token(out);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
int n = cJSON_GetArraySize(proofs);
if (n <= 0) {
cashu_free_decoded_token(out);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
out->proofs = (nostr_cashu_proof_t*)calloc((size_t)n, sizeof(nostr_cashu_proof_t));
if (!out->proofs) {
cashu_free_decoded_token(out);
return NOSTR_ERROR_MEMORY_FAILED;
}
cJSON* token_entry_id = cashu_token_get_case(token_entry, "id", "i");
const char* inherited_id = (token_entry_id && cJSON_IsString(token_entry_id) && token_entry_id->valuestring)
? token_entry_id->valuestring
: NULL;
for (int i = 0; i < n; i++) {
cJSON* p = cJSON_GetArrayItem(proofs, i);
if (!p || !cJSON_IsObject(p)) {
cashu_free_decoded_token(out);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
cJSON* id = cashu_token_get_case(p, "id", "i");
cJSON* amount = cashu_token_get_case(p, "amount", "a");
cJSON* secret = cashu_token_get_case(p, "secret", "s");
cJSON* C = cashu_token_get_case(p, "C", "c");
const char* id_s = (id && cJSON_IsString(id) && id->valuestring) ? id->valuestring : inherited_id;
if (!id_s || id_s[0] == '\0' || !amount || !cJSON_IsNumber(amount) ||
!secret || !cJSON_IsString(secret) || !secret->valuestring ||
!C || !cJSON_IsString(C) || !C->valuestring) {
cashu_free_decoded_token(out);
return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
snprintf(out->proofs[i].id, sizeof(out->proofs[i].id), "%s", id_s);
out->proofs[i].amount = (uint64_t)cJSON_GetNumberValue(amount);
out->proofs[i].secret = cashu_strdup(secret->valuestring);
out->proofs[i].C = cashu_strdup(C->valuestring);
if (!out->proofs[i].secret || !out->proofs[i].C) {
cashu_free_decoded_token(out);
return NOSTR_ERROR_MEMORY_FAILED;
}
}
out->proof_count = n;
return NOSTR_SUCCESS;
}
static int cashu_decode_token_a(const char* token_payload, cashu_decoded_token_t* token_out) {
unsigned char* decoded = NULL;
size_t decoded_len = 0;
int rc = cashu_base64url_decode_alloc(token_payload, &decoded, &decoded_len);
if (rc != NOSTR_SUCCESS) return rc;
char* json_text = (char*)malloc(decoded_len + 1);
if (!json_text) {
free(decoded);
return NOSTR_ERROR_MEMORY_FAILED;
}
memcpy(json_text, decoded, decoded_len);
json_text[decoded_len] = '\0';
free(decoded);
cJSON* root = cJSON_Parse(json_text);
free(json_text);
if (!root) return NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
rc = cashu_extract_token_json(root, token_out);
cJSON_Delete(root);
return rc;
}
static int cashu_decode_token_b(const char* token_payload, cashu_decoded_token_t* token_out) {
unsigned char* decoded = NULL;
size_t decoded_len = 0;
int rc = cashu_base64url_decode_alloc(token_payload, &decoded, &decoded_len);
if (rc != NOSTR_SUCCESS) return rc;
cashu_cbor_cursor_t cur;
memset(&cur, 0, sizeof(cur));
cur.data = decoded;
cur.len = decoded_len;
cJSON* root = NULL;
rc = cashu_cbor_parse_item(&cur, &root);
free(decoded);
if (rc != NOSTR_SUCCESS || !root) {
cJSON_Delete(root);
return rc != NOSTR_SUCCESS ? rc : NOSTR_ERROR_CASHU_JSON_PARSE_FAILED;
}
rc = cashu_extract_token_json(root, token_out);
cJSON_Delete(root);
return rc;
}
int cashu_decode_token(const char* token_string,
cashu_decoded_token_t* token_out) {
if (!token_string || !token_out) {
return NOSTR_ERROR_INVALID_INPUT;
}
char* t = cashu_trim_copy(token_string);
if (!t) return NOSTR_ERROR_MEMORY_FAILED;
const char* s = t;
if (strncmp(s, "cashu:", 6) == 0) {
s += 6;
}
int rc = NOSTR_ERROR_INVALID_INPUT;
if (strncmp(s, "cashuA", 6) == 0) {
rc = cashu_decode_token_a(s + 6, token_out);
} else if (strncmp(s, "cashuB", 6) == 0) {
rc = cashu_decode_token_b(s + 6, token_out);
}
free(t);
return rc;
}
void cashu_free_decoded_token(cashu_decoded_token_t* token) {
if (!token) return;
free(token->mint_url);
if (token->proofs) {
for (int i = 0; i < token->proof_count; i++) {
free(token->proofs[i].secret);
free(token->proofs[i].C);
}
}
free(token->proofs);
memset(token, 0, sizeof(*token));
}
+11
View File
@@ -106,6 +106,12 @@ typedef struct {
int state_count;
} cashu_checkstate_response_t;
typedef struct {
char* mint_url;
nostr_cashu_proof_t* proofs;
int proof_count;
} cashu_decoded_token_t;
int cashu_mint_get_info(const char* mint_url,
cashu_mint_info_t* info_out,
int timeout_seconds);
@@ -203,6 +209,11 @@ int cashu_parse_checkstate_response(cJSON* response_body,
void cashu_mint_free_checkstate_response(cashu_checkstate_response_t* response);
int cashu_decode_token(const char* token_string,
cashu_decoded_token_t* token_out);
void cashu_free_decoded_token(cashu_decoded_token_t* token);
#ifdef __cplusplus
}
#endif