Add cashu_encode_token with cashuA/cashuB formats and live encode-decode validation

This commit is contained in:
2026-03-20 10:59:13 -04:00
parent a595747aff
commit a3f7a1bbfd
6 changed files with 369 additions and 15 deletions

View File

@@ -1 +1 @@
0.5.6
0.5.7

View File

@@ -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;

View File

@@ -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,

View File

@@ -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

Binary file not shown.

View File

@@ -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);