diff --git a/VERSION b/VERSION index b49b2533..d3532a10 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.6 +0.5.7 diff --git a/nostr_core/cashu_mint.c b/nostr_core/cashu_mint.c index 00a5b308..3f0bd372 100644 --- a/nostr_core/cashu_mint.c +++ b/nostr_core/cashu_mint.c @@ -1089,6 +1089,44 @@ static int cashu_base64url_decode_alloc(const char* input, unsigned char** out, return NOSTR_SUCCESS; } +static int cashu_base64url_encode_alloc(const unsigned char* input, + size_t input_len, + char** out) { + if (!input || input_len == 0 || !out) return NOSTR_ERROR_INVALID_INPUT; + + *out = NULL; + + size_t b64_cap = ((input_len + 2) / 3) * 4 + 4; + char* b64 = (char*)malloc(b64_cap); + if (!b64) return NOSTR_ERROR_MEMORY_FAILED; + + size_t n = base64_encode(input, input_len, b64, b64_cap); + if (n == 0) { + free(b64); + return NOSTR_ERROR_INVALID_INPUT; + } + + while (n > 0 && b64[n - 1] == '=') n--; + + char* out_s = (char*)malloc(n + 1); + if (!out_s) { + free(b64); + return NOSTR_ERROR_MEMORY_FAILED; + } + + for (size_t i = 0; i < n; i++) { + char c = b64[i]; + if (c == '+') c = '-'; + else if (c == '/') c = '_'; + out_s[i] = c; + } + out_s[n] = '\0'; + + free(b64); + *out = out_s; + return NOSTR_SUCCESS; +} + typedef struct { const unsigned char* data; size_t len; @@ -1394,6 +1432,246 @@ static int cashu_extract_token_json(cJSON* root, cashu_decoded_token_t* out) { return NOSTR_SUCCESS; } +typedef struct { + unsigned char* data; + size_t len; + size_t cap; +} cashu_cbor_buf_t; + +static int cashu_cbor_buf_reserve(cashu_cbor_buf_t* b, size_t add) { + if (!b) return NOSTR_ERROR_INVALID_INPUT; + if (b->len + add <= b->cap) return NOSTR_SUCCESS; + + size_t new_cap = b->cap ? b->cap : 256; + while (new_cap < b->len + add) new_cap *= 2; + + unsigned char* p = (unsigned char*)realloc(b->data, new_cap); + if (!p) return NOSTR_ERROR_MEMORY_FAILED; + + b->data = p; + b->cap = new_cap; + return NOSTR_SUCCESS; +} + +static int cashu_cbor_buf_put_u8(cashu_cbor_buf_t* b, unsigned char v) { + int rc = cashu_cbor_buf_reserve(b, 1); + if (rc != NOSTR_SUCCESS) return rc; + b->data[b->len++] = v; + return NOSTR_SUCCESS; +} + +static int cashu_cbor_write_type_uint(cashu_cbor_buf_t* b, uint8_t mt, uint64_t v) { + if (!b || mt > 7) return NOSTR_ERROR_INVALID_INPUT; + + if (v < 24) { + return cashu_cbor_buf_put_u8(b, (unsigned char)((mt << 5) | (uint8_t)v)); + } + + if (v <= 0xff) { + int rc = cashu_cbor_buf_reserve(b, 2); + if (rc != NOSTR_SUCCESS) return rc; + b->data[b->len++] = (unsigned char)((mt << 5) | 24); + b->data[b->len++] = (unsigned char)v; + return NOSTR_SUCCESS; + } + + if (v <= 0xffff) { + int rc = cashu_cbor_buf_reserve(b, 3); + if (rc != NOSTR_SUCCESS) return rc; + b->data[b->len++] = (unsigned char)((mt << 5) | 25); + b->data[b->len++] = (unsigned char)((v >> 8) & 0xff); + b->data[b->len++] = (unsigned char)(v & 0xff); + return NOSTR_SUCCESS; + } + + if (v <= 0xffffffffULL) { + int rc = cashu_cbor_buf_reserve(b, 5); + if (rc != NOSTR_SUCCESS) return rc; + b->data[b->len++] = (unsigned char)((mt << 5) | 26); + b->data[b->len++] = (unsigned char)((v >> 24) & 0xff); + b->data[b->len++] = (unsigned char)((v >> 16) & 0xff); + b->data[b->len++] = (unsigned char)((v >> 8) & 0xff); + b->data[b->len++] = (unsigned char)(v & 0xff); + return NOSTR_SUCCESS; + } + + int rc = cashu_cbor_buf_reserve(b, 9); + if (rc != NOSTR_SUCCESS) return rc; + b->data[b->len++] = (unsigned char)((mt << 5) | 27); + b->data[b->len++] = (unsigned char)((v >> 56) & 0xff); + b->data[b->len++] = (unsigned char)((v >> 48) & 0xff); + b->data[b->len++] = (unsigned char)((v >> 40) & 0xff); + b->data[b->len++] = (unsigned char)((v >> 32) & 0xff); + b->data[b->len++] = (unsigned char)((v >> 24) & 0xff); + b->data[b->len++] = (unsigned char)((v >> 16) & 0xff); + b->data[b->len++] = (unsigned char)((v >> 8) & 0xff); + b->data[b->len++] = (unsigned char)(v & 0xff); + return NOSTR_SUCCESS; +} + +static int cashu_cbor_write_text(cashu_cbor_buf_t* b, const char* s) { + if (!b || !s) return NOSTR_ERROR_INVALID_INPUT; + size_t n = strlen(s); + int rc = cashu_cbor_write_type_uint(b, 3, (uint64_t)n); + if (rc != NOSTR_SUCCESS) return rc; + + rc = cashu_cbor_buf_reserve(b, n); + if (rc != NOSTR_SUCCESS) return rc; + memcpy(b->data + b->len, s, n); + b->len += n; + return NOSTR_SUCCESS; +} + +static int cashu_encode_token_a(const cashu_decoded_token_t* token, + char** token_string_out) { + cJSON* root = cJSON_CreateObject(); + cJSON* token_arr = cJSON_CreateArray(); + cJSON* entry = cJSON_CreateObject(); + cJSON* proofs = cJSON_CreateArray(); + if (!root || !token_arr || !entry || !proofs) { + cJSON_Delete(root); + cJSON_Delete(token_arr); + cJSON_Delete(entry); + cJSON_Delete(proofs); + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_AddStringToObject(entry, "mint", token->mint_url); + + for (int i = 0; i < token->proof_count; i++) { + const nostr_cashu_proof_t* p = &token->proofs[i]; + cJSON* pobj = cJSON_CreateObject(); + if (!pobj) { + cJSON_Delete(root); + cJSON_Delete(token_arr); + cJSON_Delete(entry); + cJSON_Delete(proofs); + return NOSTR_ERROR_MEMORY_FAILED; + } + + cJSON_AddStringToObject(pobj, "id", p->id); + cJSON_AddNumberToObject(pobj, "amount", (double)p->amount); + cJSON_AddStringToObject(pobj, "secret", p->secret); + cJSON_AddStringToObject(pobj, "C", p->C); + cJSON_AddItemToArray(proofs, pobj); + } + + cJSON_AddItemToObject(entry, "proofs", proofs); + cJSON_AddItemToArray(token_arr, entry); + cJSON_AddItemToObject(root, "token", token_arr); + + char* json_text = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + if (!json_text) return NOSTR_ERROR_MEMORY_FAILED; + + char* b64url = NULL; + int rc = cashu_base64url_encode_alloc((const unsigned char*)json_text, strlen(json_text), &b64url); + free(json_text); + if (rc != NOSTR_SUCCESS) return rc; + + size_t total = 6 + strlen(b64url) + 1; + char* out = (char*)malloc(total); + if (!out) { + free(b64url); + return NOSTR_ERROR_MEMORY_FAILED; + } + + snprintf(out, total, "cashuA%s", b64url); + free(b64url); + *token_string_out = out; + return NOSTR_SUCCESS; +} + +static int cashu_encode_token_b(const cashu_decoded_token_t* token, + char** token_string_out) { + cashu_cbor_buf_t b; + memset(&b, 0, sizeof(b)); + + int rc = NOSTR_SUCCESS; + + /* root map: { "m": mint, "t": [ {"i": id, "p": [proofs]} ] } */ + rc = cashu_cbor_write_type_uint(&b, 5, 2); + if (rc != NOSTR_SUCCESS) goto fail; + + rc = cashu_cbor_write_text(&b, "m"); + if (rc != NOSTR_SUCCESS) goto fail; + rc = cashu_cbor_write_text(&b, token->mint_url); + if (rc != NOSTR_SUCCESS) goto fail; + + rc = cashu_cbor_write_text(&b, "t"); + if (rc != NOSTR_SUCCESS) goto fail; + rc = cashu_cbor_write_type_uint(&b, 4, 1); + if (rc != NOSTR_SUCCESS) goto fail; + + const char* inherited_id = token->proofs[0].id; + + rc = cashu_cbor_write_type_uint(&b, 5, 2); + if (rc != NOSTR_SUCCESS) goto fail; + rc = cashu_cbor_write_text(&b, "i"); + if (rc != NOSTR_SUCCESS) goto fail; + rc = cashu_cbor_write_text(&b, inherited_id); + if (rc != NOSTR_SUCCESS) goto fail; + + rc = cashu_cbor_write_text(&b, "p"); + if (rc != NOSTR_SUCCESS) goto fail; + rc = cashu_cbor_write_type_uint(&b, 4, (uint64_t)token->proof_count); + if (rc != NOSTR_SUCCESS) goto fail; + + for (int i = 0; i < token->proof_count; i++) { + const nostr_cashu_proof_t* p = &token->proofs[i]; + int fields = 3; + int has_local_id = strcmp(p->id, inherited_id) != 0; + if (has_local_id) fields++; + + rc = cashu_cbor_write_type_uint(&b, 5, (uint64_t)fields); + if (rc != NOSTR_SUCCESS) goto fail; + + if (has_local_id) { + rc = cashu_cbor_write_text(&b, "i"); + if (rc != NOSTR_SUCCESS) goto fail; + rc = cashu_cbor_write_text(&b, p->id); + if (rc != NOSTR_SUCCESS) goto fail; + } + + rc = cashu_cbor_write_text(&b, "a"); + if (rc != NOSTR_SUCCESS) goto fail; + rc = cashu_cbor_write_type_uint(&b, 0, p->amount); + if (rc != NOSTR_SUCCESS) goto fail; + + rc = cashu_cbor_write_text(&b, "s"); + if (rc != NOSTR_SUCCESS) goto fail; + rc = cashu_cbor_write_text(&b, p->secret); + if (rc != NOSTR_SUCCESS) goto fail; + + rc = cashu_cbor_write_text(&b, "c"); + if (rc != NOSTR_SUCCESS) goto fail; + rc = cashu_cbor_write_text(&b, p->C); + if (rc != NOSTR_SUCCESS) goto fail; + } + + char* b64url = NULL; + rc = cashu_base64url_encode_alloc(b.data, b.len, &b64url); + if (rc != NOSTR_SUCCESS) goto fail; + + size_t total = 6 + strlen(b64url) + 1; + char* out = (char*)malloc(total); + if (!out) { + free(b64url); + rc = NOSTR_ERROR_MEMORY_FAILED; + goto fail; + } + + snprintf(out, total, "cashuB%s", b64url); + free(b64url); + free(b.data); + *token_string_out = out; + return NOSTR_SUCCESS; + +fail: + free(b.data); + return rc; +} + 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; @@ -1467,6 +1745,35 @@ int cashu_decode_token(const char* token_string, return rc; } +int cashu_encode_token(const cashu_decoded_token_t* token, + cashu_token_format_t format, + char** token_string_out) { + if (!token || !token_string_out || !token->mint_url || !token->proofs || token->proof_count <= 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + + *token_string_out = NULL; + + for (int i = 0; i < token->proof_count; i++) { + if (token->proofs[i].id[0] == '\0' || + !token->proofs[i].secret || token->proofs[i].secret[0] == '\0' || + !token->proofs[i].C || token->proofs[i].C[0] == '\0' || + token->proofs[i].amount == 0) { + return NOSTR_ERROR_INVALID_INPUT; + } + } + + if (format == CASHU_TOKEN_FORMAT_A) { + return cashu_encode_token_a(token, token_string_out); + } + + if (format == CASHU_TOKEN_FORMAT_B) { + return cashu_encode_token_b(token, token_string_out); + } + + return NOSTR_ERROR_INVALID_INPUT; +} + void cashu_free_decoded_token(cashu_decoded_token_t* token) { if (!token) return; diff --git a/nostr_core/cashu_mint.h b/nostr_core/cashu_mint.h index 39d14782..6c35e96e 100644 --- a/nostr_core/cashu_mint.h +++ b/nostr_core/cashu_mint.h @@ -17,11 +17,16 @@ extern "C" { #define CASHU_API_VERSION "v1" typedef struct { - char id[17]; + char id[NOSTR_CASHU_KEYSET_ID_HEX_SIZE]; char unit[16]; int active; } cashu_keyset_t; +typedef enum { + CASHU_TOKEN_FORMAT_A = 0, + CASHU_TOKEN_FORMAT_B = 1 +} cashu_token_format_t; + typedef struct { uint64_t amount; char pubkey[67]; @@ -244,6 +249,10 @@ 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); +int cashu_encode_token(const cashu_decoded_token_t* token, + cashu_token_format_t format, + char** token_string_out); + void cashu_free_decoded_token(cashu_decoded_token_t* token); int cashu_blind_message(const char* secret, diff --git a/nostr_core/nostr_core.h b/nostr_core/nostr_core.h index 24004b3c..3410c706 100644 --- a/nostr_core/nostr_core.h +++ b/nostr_core/nostr_core.h @@ -2,10 +2,10 @@ #define NOSTR_CORE_H // Version information (auto-updated by increment_and_push.sh) -#define VERSION "v0.5.6" +#define VERSION "v0.5.7" #define VERSION_MAJOR 0 #define VERSION_MINOR 5 -#define VERSION_PATCH 6 +#define VERSION_PATCH 7 /* * NOSTR Core Library - Complete API Reference diff --git a/tests/nip60_live_receive_test b/tests/nip60_live_receive_test index 1795ac75..4a7e5800 100755 Binary files a/tests/nip60_live_receive_test and b/tests/nip60_live_receive_test differ diff --git a/tests/nip60_live_receive_test.c b/tests/nip60_live_receive_test.c index 02d4b3b7..d21e0f7d 100644 --- a/tests/nip60_live_receive_test.c +++ b/tests/nip60_live_receive_test.c @@ -294,10 +294,34 @@ static void test_live_mint_and_receive(const char* mint_url) { return; } - char token_string[10000]; - rc = build_cashuA_token_string(mint_url, active_keyset.id, 1, secret, C_hex, token_string, sizeof(token_string)); - TEST_ASSERT(rc == NOSTR_SUCCESS, "build cashuA token string"); - if (rc != NOSTR_SUCCESS) { + nostr_cashu_proof_t send_proof; + memset(&send_proof, 0, sizeof(send_proof)); + snprintf(send_proof.id, sizeof(send_proof.id), "%s", active_keyset.id); + send_proof.amount = 1; + send_proof.secret = secret; + send_proof.C = C_hex; + + cashu_decoded_token_t token_for_encode; + memset(&token_for_encode, 0, sizeof(token_for_encode)); + token_for_encode.mint_url = (char*)mint_url; + token_for_encode.proofs = &send_proof; + token_for_encode.proof_count = 1; + + char* token_a = NULL; + rc = cashu_encode_token(&token_for_encode, CASHU_TOKEN_FORMAT_A, &token_a); + TEST_ASSERT(rc == NOSTR_SUCCESS && token_a != NULL, "encode cashuA token"); + if (rc != NOSTR_SUCCESS || !token_a) { + cashu_mint_free_mint_tokens_response(&mint_resp); + cashu_mint_free_keyset_keys(&keyset_keys); + cashu_mint_free_info(&info); + return; + } + + char* token_b = NULL; + rc = cashu_encode_token(&token_for_encode, CASHU_TOKEN_FORMAT_B, &token_b); + TEST_ASSERT(rc == NOSTR_SUCCESS && token_b != NULL, "encode cashuB token"); + if (rc != NOSTR_SUCCESS || !token_b) { + free(token_a); cashu_mint_free_mint_tokens_response(&mint_resp); cashu_mint_free_keyset_keys(&keyset_keys); cashu_mint_free_info(&info); @@ -306,14 +330,25 @@ static void test_live_mint_and_receive(const char* mint_url) { 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, active_keyset.id) == 0, "decoded keyset id"); + rc = cashu_decode_token(token_a, &decoded); + TEST_ASSERT(rc == NOSTR_SUCCESS, "decode cashuA token"); + TEST_ASSERT(decoded.mint_url != NULL && strcmp(decoded.mint_url, mint_url) == 0, "cashuA decoded mint url matches"); + TEST_ASSERT(decoded.proof_count == 1, "cashuA decoded proof count"); + TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].amount == 1, "cashuA decoded proof amount"); + TEST_ASSERT(decoded.proofs != NULL && strcmp(decoded.proofs[0].id, active_keyset.id) == 0, "cashuA decoded keyset id"); TEST_ASSERT(decoded.proofs != NULL && decoded.proofs[0].secret != NULL && strcmp(decoded.proofs[0].secret, secret) == 0, - "decoded secret matches"); + "cashuA decoded secret matches"); + + cashu_decoded_token_t decoded_b; + memset(&decoded_b, 0, sizeof(decoded_b)); + rc = cashu_decode_token(token_b, &decoded_b); + TEST_ASSERT(rc == NOSTR_SUCCESS, "decode cashuB token"); + TEST_ASSERT(decoded_b.mint_url != NULL && strcmp(decoded_b.mint_url, mint_url) == 0, "cashuB decoded mint url matches"); + TEST_ASSERT(decoded_b.proof_count == 1, "cashuB decoded proof count"); + TEST_ASSERT(decoded_b.proofs != NULL && decoded_b.proofs[0].amount == 1, "cashuB decoded proof amount"); + TEST_ASSERT(decoded_b.proofs != NULL && strcmp(decoded_b.proofs[0].id, active_keyset.id) == 0, "cashuB decoded keyset id"); + TEST_ASSERT(decoded_b.proofs != NULL && decoded_b.proofs[0].secret != NULL && strcmp(decoded_b.proofs[0].secret, secret) == 0, + "cashuB decoded secret matches"); char Y_hex[67]; rc = secret_to_Y_hex(secret, Y_hex); @@ -344,7 +379,10 @@ static void test_live_mint_and_receive(const char* mint_url) { } } + cashu_free_decoded_token(&decoded_b); cashu_free_decoded_token(&decoded); + free(token_b); + free(token_a); cashu_mint_free_mint_tokens_response(&mint_resp); cashu_mint_free_keyset_keys(&keyset_keys); cashu_mint_free_info(&info);