diff options
Diffstat (limited to 'main/wallet.c')
| -rw-r--r-- | main/wallet.c | 639 |
1 files changed, 0 insertions, 639 deletions
diff --git a/main/wallet.c b/main/wallet.c deleted file mode 100644 index 3f65220..0000000 --- a/main/wallet.c +++ /dev/null | |||
| @@ -1,639 +0,0 @@ | |||
| 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 | |||
| 20 | static const char *TAG = "wallet"; | ||
| 21 | static wallet_t s_wallet; | ||
| 22 | |||
| 23 | static const char DOMAIN_SEPARATOR[] = "Secp256k1_HashToCurve_Cashu_"; | ||
| 24 | |||
| 25 | static mbedtls_ecp_group s_grp; | ||
| 26 | static mbedtls_mpi s_order; | ||
| 27 | static bool s_grp_loaded = false; | ||
| 28 | |||
| 29 | static 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 | |||
| 44 | static void random_bytes(uint8_t *buf, size_t len) | ||
| 45 | { | ||
| 46 | esp_fill_random(buf, len); | ||
| 47 | } | ||
| 48 | |||
| 49 | static 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 | |||
| 64 | static 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 | |||
| 112 | static 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 | |||
| 126 | static 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 | |||
| 136 | static 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 | |||
| 148 | static 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 | |||
| 156 | esp_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 | |||
| 166 | wallet_t *wallet_get(void) | ||
| 167 | { | ||
| 168 | return &s_wallet; | ||
| 169 | } | ||
| 170 | |||
| 171 | uint64_t wallet_balance(void) | ||
| 172 | { | ||
| 173 | return s_wallet.balance; | ||
| 174 | } | ||
| 175 | |||
| 176 | esp_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 | |||
| 194 | esp_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 | |||
| 207 | void wallet_clear(void) | ||
| 208 | { | ||
| 209 | s_wallet.balance = 0; | ||
| 210 | s_wallet.proof_count = 0; | ||
| 211 | wallet_persist_save(); | ||
| 212 | } | ||
| 213 | |||
| 214 | esp_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 | |||
| 283 | esp_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 | |||
| 551 | esp_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 | |||
| 622 | esp_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 | |||
| 628 | void 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 | } | ||