Add cashu_encode_token with cashuA/cashuB formats and live encode-decode validation
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user