diff --git a/build.sh b/build.sh index 08171f82..ff7bad20 100755 --- a/build.sh +++ b/build.sh @@ -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 ' ' ',')" diff --git a/nostr_core/cashu_mint.c b/nostr_core/cashu_mint.c index 4910b72a..709f23a7 100644 --- a/nostr_core/cashu_mint.c +++ b/nostr_core/cashu_mint.c @@ -4,9 +4,11 @@ #include "cashu_mint.h" #include "../cjson/cJSON.h" +#include "utils.h" #include #include #include +#include #include @@ -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)); +} diff --git a/nostr_core/cashu_mint.h b/nostr_core/cashu_mint.h index 635c6c18..c650b432 100644 --- a/nostr_core/cashu_mint.h +++ b/nostr_core/cashu_mint.h @@ -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