upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/main/wallet.c
diff options
context:
space:
mode:
Diffstat (limited to 'main/wallet.c')
-rw-r--r--main/wallet.c639
1 files changed, 639 insertions, 0 deletions
diff --git a/main/wallet.c b/main/wallet.c
new file mode 100644
index 0000000..3f65220
--- /dev/null
+++ b/main/wallet.c
@@ -0,0 +1,639 @@
1#include "wallet.h"
2#include "wallet_persist.h"
3#include "config.h"
4#include "esp_log.h"
5#include "esp_random.h"
6#include "esp_http_client.h"
7#include "esp_crt_bundle.h"
8#include "cJSON.h"
9#include "mbedtls/ecp.h"
10#include "mbedtls/bignum.h"
11#include "mbedtls/sha256.h"
12#include "mbedtls/base64.h"
13#include "freertos/FreeRTOS.h"
14#include "freertos/task.h"
15#include "freertos/semphr.h"
16#include "esp_heap_caps.h"
17#include <string.h>
18#include <stdio.h>
19
20static const char *TAG = "wallet";
21static wallet_t s_wallet;
22
23static const char DOMAIN_SEPARATOR[] = "Secp256k1_HashToCurve_Cashu_";
24
25static mbedtls_ecp_group s_grp;
26static mbedtls_mpi s_order;
27static bool s_grp_loaded = false;
28
29static esp_err_t init_ecp_group(void)
30{
31 if (s_grp_loaded) return ESP_OK;
32 mbedtls_ecp_group_init(&s_grp);
33 mbedtls_mpi_init(&s_order);
34 int ret = mbedtls_ecp_group_load(&s_grp, MBEDTLS_ECP_DP_SECP256K1);
35 if (ret != 0) {
36 ESP_LOGE(TAG, "Failed to load secp256k1 group: -0x%x", -ret);
37 return ESP_FAIL;
38 }
39 mbedtls_mpi_copy(&s_order, &s_grp.N);
40 s_grp_loaded = true;
41 return ESP_OK;
42}
43
44static void random_bytes(uint8_t *buf, size_t len)
45{
46 esp_fill_random(buf, len);
47}
48
49static esp_err_t random_scalar(mbedtls_mpi *r)
50{
51 uint8_t buf[32];
52 random_bytes(buf, 32);
53 mbedtls_mpi_init(r);
54 int ret = mbedtls_mpi_read_binary(r, buf, 32);
55 if (ret != 0) return ESP_FAIL;
56 ret = mbedtls_mpi_mod_mpi(r, r, &s_order);
57 if (ret != 0) return ESP_FAIL;
58 if (mbedtls_mpi_cmp_int(r, 1) < 0) {
59 mbedtls_mpi_add_int(r, r, 1);
60 }
61 return ESP_OK;
62}
63
64static esp_err_t hash_to_curve(const uint8_t *msg, size_t msg_len, mbedtls_ecp_point *Y)
65{
66 uint8_t msg_hash[32];
67 size_t ds_len = strlen(DOMAIN_SEPARATOR);
68 uint8_t *hash_input = malloc(ds_len + msg_len);
69 if (!hash_input) return ESP_FAIL;
70 memcpy(hash_input, DOMAIN_SEPARATOR, ds_len);
71 memcpy(hash_input + ds_len, msg, msg_len);
72 mbedtls_sha256(hash_input, ds_len + msg_len, msg_hash, 0);
73 free(hash_input);
74
75 mbedtls_ecp_point_init(Y);
76 for (uint32_t counter = 0; counter < 256; counter++) {
77 uint8_t counter_bytes[4];
78 counter_bytes[0] = counter & 0xFF;
79 counter_bytes[1] = (counter >> 8) & 0xFF;
80 counter_bytes[2] = (counter >> 16) & 0xFF;
81 counter_bytes[3] = (counter >> 24) & 0xFF;
82
83 uint8_t to_hash[32 + 4 + 1];
84 memcpy(to_hash, msg_hash, 32);
85 memcpy(to_hash + 32, counter_bytes, 4);
86
87 uint8_t point_hash[32];
88 mbedtls_sha256(to_hash, 36, point_hash, 0);
89
90 uint8_t compressed[33];
91 compressed[0] = 0x02;
92 memcpy(compressed + 1, point_hash, 32);
93
94 int ret = mbedtls_ecp_point_read_binary(&s_grp, Y, compressed, 33);
95 if (ret == 0) {
96 ret = mbedtls_ecp_check_pubkey(&s_grp, Y);
97 if (ret == 0) return ESP_OK;
98 }
99
100 compressed[0] = 0x03;
101 ret = mbedtls_ecp_point_read_binary(&s_grp, Y, compressed, 33);
102 if (ret == 0) {
103 ret = mbedtls_ecp_check_pubkey(&s_grp, Y);
104 if (ret == 0) return ESP_OK;
105 }
106 }
107
108 ESP_LOGE(TAG, "hash_to_curve failed after 256 attempts");
109 return ESP_FAIL;
110}
111
112static esp_err_t point_add(const mbedtls_ecp_point *A, const mbedtls_ecp_point *B,
113 mbedtls_ecp_point *R)
114{
115 mbedtls_mpi one;
116 mbedtls_mpi_init(&one);
117 mbedtls_mpi_lset(&one, 1);
118 int ret = mbedtls_ecp_muladd(&s_grp, R, &one, A, &one, B);
119 if (ret != 0) {
120 ESP_LOGE(TAG, "point_add failed: -0x%x", -ret);
121 }
122 mbedtls_mpi_free(&one);
123 return (ret == 0) ? ESP_OK : ESP_FAIL;
124}
125
126static esp_err_t scalar_mul(const mbedtls_mpi *m, const mbedtls_ecp_point *P,
127 mbedtls_ecp_point *R)
128{
129 int ret = mbedtls_ecp_mul(&s_grp, R, m, P, NULL, NULL);
130 if (ret != 0) {
131 ESP_LOGE(TAG, "scalar_mul failed: -0x%x", -ret);
132 }
133 return (ret == 0) ? ESP_OK : ESP_FAIL;
134}
135
136static int hex_to_bytes(const char *hex, uint8_t *bytes, size_t bytes_len)
137{
138 size_t hex_len = strlen(hex);
139 if (hex_len / 2 > bytes_len) return -1;
140 for (size_t i = 0; i < hex_len / 2; i++) {
141 unsigned int b;
142 sscanf(hex + i * 2, "%02x", &b);
143 bytes[i] = (uint8_t)b;
144 }
145 return hex_len / 2;
146}
147
148static void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex)
149{
150 for (size_t i = 0; i < len; i++) {
151 sprintf(hex + i * 2, "%02x", bytes[i]);
152 }
153 hex[len * 2] = '\0';
154}
155
156esp_err_t wallet_init(void)
157{
158 memset(&s_wallet, 0, sizeof(s_wallet));
159 esp_err_t err = init_ecp_group();
160 if (err != ESP_OK) return err;
161 wallet_persist_load();
162 ESP_LOGI(TAG, "Wallet initialized (secp256k1 loaded)");
163 return ESP_OK;
164}
165
166wallet_t *wallet_get(void)
167{
168 return &s_wallet;
169}
170
171uint64_t wallet_balance(void)
172{
173 return s_wallet.balance;
174}
175
176esp_err_t wallet_add_proofs(const wallet_proof_t *proofs, int count)
177{
178 for (int i = 0; i < count; i++) {
179 if (s_wallet.proof_count >= WALLET_MAX_PROOFS) {
180 ESP_LOGW(TAG, "Wallet full, cannot add more proofs");
181 return ESP_ERR_NO_MEM;
182 }
183 memcpy(&s_wallet.proofs[s_wallet.proof_count], &proofs[i], sizeof(wallet_proof_t));
184 s_wallet.balance += proofs[i].amount;
185 s_wallet.proof_count++;
186 ESP_LOGI(TAG, "Added proof: amount=%llu, total_balance=%llu",
187 (unsigned long long)proofs[i].amount,
188 (unsigned long long)s_wallet.balance);
189 }
190 wallet_persist_save();
191 return ESP_OK;
192}
193
194esp_err_t wallet_remove_proof(int index)
195{
196 if (index < 0 || index >= s_wallet.proof_count) return ESP_ERR_INVALID_ARG;
197 s_wallet.balance -= s_wallet.proofs[index].amount;
198 for (int i = index; i < s_wallet.proof_count - 1; i++) {
199 memcpy(&s_wallet.proofs[i], &s_wallet.proofs[i + 1], sizeof(wallet_proof_t));
200 }
201 memset(&s_wallet.proofs[s_wallet.proof_count - 1], 0, sizeof(wallet_proof_t));
202 s_wallet.proof_count--;
203 wallet_persist_save();
204 return ESP_OK;
205}
206
207void wallet_clear(void)
208{
209 s_wallet.balance = 0;
210 s_wallet.proof_count = 0;
211 wallet_persist_save();
212}
213
214esp_err_t wallet_fetch_keysets(const char *mint_url)
215{
216 char url[512];
217 snprintf(url, sizeof(url), "%s/v1/keysets", mint_url);
218
219 char *resp_buf = malloc(8192);
220 if (!resp_buf) return ESP_ERR_NO_MEM;
221
222 esp_http_client_config_t config = {
223 .url = url,
224 .method = HTTP_METHOD_GET,
225 .timeout_ms = 10000,
226 .crt_bundle_attach = esp_crt_bundle_attach,
227 };
228 esp_http_client_handle_t client = esp_http_client_init(&config);
229 if (!client) { free(resp_buf); return ESP_FAIL; }
230
231 esp_err_t err = esp_http_client_open(client, 0);
232 if (err != ESP_OK) {
233 ESP_LOGE(TAG, "Keyset fetch open failed: %s", esp_err_to_name(err));
234 esp_http_client_cleanup(client);
235 free(resp_buf);
236 return err;
237 }
238
239 int content_length = esp_http_client_fetch_headers(client);
240 int status = esp_http_client_get_status_code(client);
241 ESP_LOGI(TAG, "Keyset fetch: status=%d content_length=%d", status, content_length);
242
243 int resp_len = esp_http_client_read(client, resp_buf, 8191);
244 ESP_LOGI(TAG, "Keyset fetch: read %d bytes", resp_len);
245 esp_http_client_cleanup(client);
246
247 if (status != 200 || resp_len <= 0) {
248 ESP_LOGE(TAG, "Keyset fetch failed: status=%d len=%d", status, resp_len);
249 free(resp_buf);
250 return ESP_FAIL;
251 }
252 resp_buf[resp_len] = '\0';
253
254 cJSON *root = cJSON_Parse(resp_buf);
255 free(resp_buf);
256 if (!root) return ESP_FAIL;
257
258 cJSON *keysets = cJSON_GetObjectItemCaseSensitive(root, "keysets");
259 if (!keysets || !cJSON_IsArray(keysets)) {
260 cJSON_Delete(root);
261 return ESP_FAIL;
262 }
263
264 s_wallet.keyset_count = 0;
265 int n = cJSON_GetArraySize(keysets);
266 for (int i = 0; i < n && i < WALLET_MAX_KEYSETS; i++) {
267 cJSON *ks = cJSON_GetArrayItem(keysets, i);
268 cJSON *id = cJSON_GetObjectItemCaseSensitive(ks, "id");
269 if (id && cJSON_IsString(id)) {
270 strncpy(s_wallet.keysets[s_wallet.keyset_count].id, id->valuestring,
271 WALLET_KEYSET_ID_LEN - 1);
272 cJSON *fee = cJSON_GetObjectItemCaseSensitive(ks, "input_fee_ppk");
273 s_wallet.keysets[s_wallet.keyset_count].input_fee_ppk = fee ? fee->valueint : 0;
274 s_wallet.keyset_count++;
275 }
276 }
277
278 cJSON_Delete(root);
279 ESP_LOGI(TAG, "Fetched %d keysets from %s", s_wallet.keyset_count, mint_url);
280 return ESP_OK;
281}
282
283esp_err_t wallet_swap_proofs(const char *mint_url, int start_index, int count)
284{
285 ESP_LOGI(TAG, "wallet_swap_proofs called: start=%d count=%d keysets=%d proofs=%d",
286 start_index, count, s_wallet.keyset_count, s_wallet.proof_count);
287
288 if (s_wallet.keyset_count == 0) {
289 ESP_LOGE(TAG, "No keysets loaded, fetch first");
290 return ESP_FAIL;
291 }
292 if (start_index < 0 || start_index + count > s_wallet.proof_count) {
293 return ESP_ERR_INVALID_ARG;
294 }
295
296 wallet_proof_t *old_proofs = &s_wallet.proofs[start_index];
297 int n = count;
298
299 uint64_t total_input = 0;
300 for (int i = 0; i < n; i++) total_input += old_proofs[i].amount;
301
302 int fee_ppk = s_wallet.keysets[0].input_fee_ppk;
303 uint64_t fee_sats = (total_input * fee_ppk + 999) / 1000;
304 uint64_t total_output = total_input - fee_sats;
305 ESP_LOGI(TAG, "Swap: total_input=%llu fee_ppk=%d fee=%llu total_output=%llu",
306 (unsigned long long)total_input, fee_ppk,
307 (unsigned long long)fee_sats, (unsigned long long)total_output);
308
309 cJSON *inputs = cJSON_CreateArray();
310 for (int i = 0; i < n; i++) {
311 cJSON *p = cJSON_CreateObject();
312 cJSON_AddNumberToObject(p, "amount", (double)old_proofs[i].amount);
313 cJSON_AddStringToObject(p, "id", old_proofs[i].id);
314 cJSON_AddStringToObject(p, "secret", old_proofs[i].secret);
315 cJSON_AddStringToObject(p, "C", old_proofs[i].c);
316 cJSON_AddItemToArray(inputs, p);
317 }
318
319 typedef struct {
320 uint8_t secret[32];
321 mbedtls_mpi r;
322 mbedtls_ecp_point Y;
323 } swap_output_t;
324
325 swap_output_t *outputs = heap_caps_malloc(n * sizeof(swap_output_t), MALLOC_CAP_SPIRAM);
326 if (!outputs) { cJSON_Delete(inputs); return ESP_ERR_NO_MEM; }
327
328 cJSON *blinded_msgs = cJSON_CreateArray();
329 for (int i = 0; i < n; i++) {
330 random_bytes(outputs[i].secret, 32);
331 mbedtls_ecp_point_init(&outputs[i].Y);
332 esp_err_t htc_ret = hash_to_curve(outputs[i].secret, 32, &outputs[i].Y);
333 if (htc_ret != ESP_OK) {
334 ESP_LOGE(TAG, "hash_to_curve failed for output %d", i);
335 }
336 mbedtls_mpi_init(&outputs[i].r);
337 random_scalar(&outputs[i].r);
338
339 mbedtls_ecp_point rG, B_;
340 mbedtls_ecp_point_init(&rG);
341 mbedtls_ecp_point_init(&B_);
342
343 esp_err_t sm_ret = scalar_mul(&outputs[i].r, &s_grp.G, &rG);
344 if (sm_ret != ESP_OK) {
345 ESP_LOGE(TAG, "scalar_mul failed for output %d", i);
346 }
347 esp_err_t pa_ret = point_add(&outputs[i].Y, &rG, &B_);
348 if (pa_ret != ESP_OK) {
349 ESP_LOGE(TAG, "point_add failed for output %d", i);
350 }
351
352 uint8_t b_bytes[33];
353 size_t olen = 0;
354 int wret = mbedtls_ecp_point_write_binary(&s_grp, &B_, MBEDTLS_ECP_PF_COMPRESSED, &olen, b_bytes, 33);
355 if (wret != 0 || olen == 0) {
356 ESP_LOGE(TAG, "Blinded point write failed: ret=-0x%x olen=%zu", -wret, olen);
357 olen = 1;
358 b_bytes[0] = 0x00;
359 }
360 char b_hex[67];
361 bytes_to_hex(b_bytes, olen, b_hex);
362
363 uint64_t out_amount = old_proofs[i].amount;
364 if (i == n - 1) {
365 uint64_t running = 0;
366 for (int j = 0; j < n - 1; j++) running += old_proofs[j].amount;
367 out_amount = total_output - running;
368 }
369
370 cJSON *bm = cJSON_CreateObject();
371 cJSON_AddNumberToObject(bm, "amount", (double)out_amount);
372 cJSON_AddStringToObject(bm, "id", s_wallet.keysets[0].id);
373 cJSON_AddStringToObject(bm, "B_", b_hex);
374 cJSON_AddItemToArray(blinded_msgs, bm);
375
376 mbedtls_ecp_point_free(&rG);
377 mbedtls_ecp_point_free(&B_);
378 }
379
380 cJSON *body = cJSON_CreateObject();
381 cJSON_AddItemToObject(body, "inputs", inputs);
382 cJSON_AddItemToObject(body, "outputs", blinded_msgs);
383 char *body_str = cJSON_PrintUnformatted(body);
384 cJSON_Delete(body);
385
386 ESP_LOGI(TAG, "Swap request body (%zu bytes): %s", strlen(body_str), body_str);
387
388 char url[512];
389 snprintf(url, sizeof(url), "%s/v1/swap", mint_url);
390
391 char *resp_buf = malloc(8192);
392 if (!resp_buf) {
393 free(body_str);
394 for (int i = 0; i < n; i++) {
395 mbedtls_mpi_free(&outputs[i].r);
396 mbedtls_ecp_point_free(&outputs[i].Y);
397 }
398 free(outputs);
399 return ESP_ERR_NO_MEM;
400 }
401
402 esp_http_client_config_t config = {
403 .url = url,
404 .method = HTTP_METHOD_POST,
405 .timeout_ms = 15000,
406 .crt_bundle_attach = esp_crt_bundle_attach,
407 };
408 esp_http_client_handle_t client = esp_http_client_init(&config);
409 if (!client) {
410 free(body_str);
411 free(resp_buf);
412 for (int i = 0; i < n; i++) {
413 mbedtls_mpi_free(&outputs[i].r);
414 mbedtls_ecp_point_free(&outputs[i].Y);
415 }
416 free(outputs);
417 return ESP_FAIL;
418 }
419
420 esp_http_client_set_header(client, "Content-Type", "application/json");
421 esp_http_client_open(client, strlen(body_str));
422 esp_http_client_write(client, body_str, strlen(body_str));
423 free(body_str);
424
425 esp_http_client_fetch_headers(client);
426 int resp_len = esp_http_client_read(client, resp_buf, 8191);
427 int status = esp_http_client_get_status_code(client);
428 esp_http_client_cleanup(client);
429
430 if (status != 200 || resp_len <= 0) {
431 if (resp_len > 0) {
432 resp_buf[resp_len] = '\0';
433 ESP_LOGE(TAG, "Swap failed: status=%d body=%s", status, resp_buf);
434 } else {
435 ESP_LOGE(TAG, "Swap failed: status=%d len=%d", status, resp_len);
436 }
437 free(resp_buf);
438 for (int i = 0; i < n; i++) {
439 mbedtls_mpi_free(&outputs[i].r);
440 mbedtls_ecp_point_free(&outputs[i].Y);
441 }
442 free(outputs);
443 return ESP_FAIL;
444 }
445 resp_buf[resp_len] = '\0';
446
447 cJSON *root = cJSON_Parse(resp_buf);
448 free(resp_buf);
449 if (!root) {
450 for (int i = 0; i < n; i++) {
451 mbedtls_mpi_free(&outputs[i].r);
452 mbedtls_ecp_point_free(&outputs[i].Y);
453 }
454 free(outputs);
455 return ESP_FAIL;
456 }
457
458 cJSON *signatures = cJSON_GetObjectItemCaseSensitive(root, "signatures");
459 if (!signatures || !cJSON_IsArray(signatures)) {
460 ESP_LOGE(TAG, "No signatures in swap response");
461 cJSON_Delete(root);
462 for (int i = 0; i < n; i++) {
463 mbedtls_mpi_free(&outputs[i].r);
464 mbedtls_ecp_point_free(&outputs[i].Y);
465 }
466 free(outputs);
467 return ESP_FAIL;
468 }
469
470 for (int i = start_index; i < start_index + n; i++) {
471 s_wallet.balance -= s_wallet.proofs[i].amount;
472 }
473
474 int sig_count = cJSON_GetArraySize(signatures);
475 for (int i = 0; i < sig_count && i < n; i++) {
476 cJSON *sig = cJSON_GetArrayItem(signatures, i);
477 cJSON *c_ = cJSON_GetObjectItemCaseSensitive(sig, "C_");
478 cJSON *amt = cJSON_GetObjectItemCaseSensitive(sig, "amount");
479 cJSON *id = cJSON_GetObjectItemCaseSensitive(sig, "id");
480
481 if (!c_ || !cJSON_IsString(c_)) continue;
482
483 uint8_t c_bytes[33];
484 int c_len = hex_to_bytes(c_->valuestring, c_bytes, 33);
485
486 mbedtls_ecp_point C_;
487 mbedtls_ecp_point_init(&C_);
488 mbedtls_ecp_point_read_binary(&s_grp, &C_, c_bytes, c_len);
489
490 char ks_id[WALLET_KEYSET_ID_LEN] = {0};
491 if (id && cJSON_IsString(id)) {
492 strncpy(ks_id, id->valuestring, WALLET_KEYSET_ID_LEN - 1);
493 }
494
495 mbedtls_mpi neg_r;
496 mbedtls_mpi_init(&neg_r);
497 mbedtls_mpi_sub_mpi(&neg_r, &s_order, &outputs[i].r);
498
499 mbedtls_ecp_point neg_rG;
500 mbedtls_ecp_point_init(&neg_rG);
501 scalar_mul(&neg_r, &s_grp.G, &neg_rG);
502
503 mbedtls_ecp_point C;
504 mbedtls_ecp_point_init(&C);
505 point_add(&C_, &neg_rG, &C);
506
507 uint8_t c_final[33];
508 size_t c_final_len;
509 mbedtls_ecp_point_write_binary(&s_grp, &C, MBEDTLS_ECP_PF_COMPRESSED,
510 &c_final_len, c_final, 33);
511
512 if (s_wallet.proof_count < WALLET_MAX_PROOFS) {
513 wallet_proof_t *wp = &s_wallet.proofs[s_wallet.proof_count];
514 if (amt && cJSON_IsNumber(amt)) {
515 wp->amount = (uint64_t)amt->valuedouble;
516 }
517 strncpy(wp->id, ks_id, WALLET_KEYSET_ID_LEN - 1);
518 bytes_to_hex(outputs[i].secret, 32, wp->secret);
519 bytes_to_hex(c_final, c_final_len, wp->c);
520 s_wallet.balance += wp->amount;
521 s_wallet.proof_count++;
522 }
523
524 mbedtls_mpi_free(&neg_r);
525 mbedtls_ecp_point_free(&C_);
526 mbedtls_ecp_point_free(&neg_rG);
527 mbedtls_ecp_point_free(&C);
528 }
529
530 for (int i = 0; i < n; i++) {
531 int idx = start_index;
532 for (int j = idx; j < s_wallet.proof_count - 1; j++) {
533 memcpy(&s_wallet.proofs[j], &s_wallet.proofs[j + 1], sizeof(wallet_proof_t));
534 }
535 s_wallet.proof_count--;
536 }
537
538 for (int i = 0; i < n; i++) {
539 mbedtls_mpi_free(&outputs[i].r);
540 mbedtls_ecp_point_free(&outputs[i].Y);
541 }
542 free(outputs);
543 cJSON_Delete(root);
544
545 ESP_LOGI(TAG, "Swap complete: %d proofs swapped, balance=%llu",
546 n, (unsigned long long)s_wallet.balance);
547 wallet_persist_save();
548 return ESP_OK;
549}
550
551esp_err_t wallet_create_token(char *out, size_t out_size, uint64_t amount,
552 const char *mint_url)
553{
554 if (s_wallet.proof_count == 0 || s_wallet.balance < amount) {
555 ESP_LOGE(TAG, "Insufficient balance: have=%llu need=%llu",
556 (unsigned long long)s_wallet.balance, (unsigned long long)amount);
557 return ESP_FAIL;
558 }
559
560 cJSON *proofs_arr = cJSON_CreateArray();
561 uint64_t remaining = amount;
562 int indices_to_remove[10];
563 int remove_count = 0;
564
565 for (int i = 0; i < s_wallet.proof_count && remaining > 0 && remove_count < 10; i++) {
566 if (s_wallet.proofs[i].amount <= remaining) {
567 cJSON *p = cJSON_CreateObject();
568 cJSON_AddNumberToObject(p, "amount", (double)s_wallet.proofs[i].amount);
569 cJSON_AddStringToObject(p, "id", s_wallet.proofs[i].id);
570 cJSON_AddStringToObject(p, "secret", s_wallet.proofs[i].secret);
571 cJSON_AddStringToObject(p, "C", s_wallet.proofs[i].c);
572 cJSON_AddItemToArray(proofs_arr, p);
573 remaining -= s_wallet.proofs[i].amount;
574 indices_to_remove[remove_count++] = i;
575 }
576 }
577
578 if (remaining > 0) {
579 cJSON_Delete(proofs_arr);
580 ESP_LOGE(TAG, "Cannot make exact amount: %llu remaining", (unsigned long long)remaining);
581 return ESP_FAIL;
582 }
583
584 cJSON *token_obj = cJSON_CreateObject();
585 cJSON *token_arr = cJSON_CreateArray();
586 cJSON *mint_proofs = cJSON_CreateObject();
587 cJSON_AddStringToObject(mint_proofs, "mint", mint_url);
588 cJSON_AddItemToObject(mint_proofs, "proofs", proofs_arr);
589 cJSON_AddItemToArray(token_arr, mint_proofs);
590 cJSON_AddItemToObject(token_obj, "token", token_arr);
591
592 char *json_str = cJSON_PrintUnformatted(token_obj);
593 cJSON_Delete(token_obj);
594
595 size_t b64_len;
596 mbedtls_base64_encode((unsigned char *)out + 6, out_size - 6, &b64_len,
597 (const unsigned char *)json_str, strlen(json_str));
598 free(json_str);
599
600 memcpy(out, "cashuA", 6);
601 for (size_t i = 0; i < b64_len; i++) {
602 if (out[6 + i] == '+') out[6 + i] = '-';
603 else if (out[6 + i] == '/') out[6 + i] = '_';
604 else if (out[6 + i] == '=') { out[6 + i] = '\0'; break; }
605 }
606 out[6 + b64_len] = '\0';
607
608 for (int i = remove_count - 1; i >= 0; i--) {
609 s_wallet.balance -= s_wallet.proofs[indices_to_remove[i]].amount;
610 for (int j = indices_to_remove[i]; j < s_wallet.proof_count - 1; j++) {
611 memcpy(&s_wallet.proofs[j], &s_wallet.proofs[j + 1], sizeof(wallet_proof_t));
612 }
613 s_wallet.proof_count--;
614 }
615
616 ESP_LOGI(TAG, "Created token for %llu sats, remaining balance=%llu",
617 (unsigned long long)amount, (unsigned long long)s_wallet.balance);
618 wallet_persist_save();
619 return ESP_OK;
620}
621
622esp_err_t wallet_send(const char *mint_url, uint64_t amount,
623 char *token_out, size_t token_out_size)
624{
625 return wallet_create_token(token_out, token_out_size, amount, mint_url);
626}
627
628void wallet_print_status(void)
629{
630 ESP_LOGI(TAG, "Wallet: %d proofs, balance=%llu sats, %d keysets",
631 s_wallet.proof_count,
632 (unsigned long long)s_wallet.balance,
633 s_wallet.keyset_count);
634 for (int i = 0; i < s_wallet.proof_count; i++) {
635 ESP_LOGI(TAG, " [%d] amount=%llu id=%s", i,
636 (unsigned long long)s_wallet.proofs[i].amount,
637 s_wallet.proofs[i].id);
638 }
639}