diff options
Diffstat (limited to 'main/cashu.c')
| -rw-r--r-- | main/cashu.c | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/main/cashu.c b/main/cashu.c new file mode 100644 index 0000000..bafd000 --- /dev/null +++ b/main/cashu.c | |||
| @@ -0,0 +1,241 @@ | |||
| 1 | #include "cashu.h" | ||
| 2 | #include "config.h" | ||
| 3 | #include "esp_log.h" | ||
| 4 | #include "esp_http_client.h" | ||
| 5 | #include "cJSON.h" | ||
| 6 | #include "mbedtls/base64.h" | ||
| 7 | #include "mbedtls/sha256.h" | ||
| 8 | #include <string.h> | ||
| 9 | |||
| 10 | static const char *TAG = "cashu"; | ||
| 11 | |||
| 12 | static const char V3_PREFIX[] = "cashuA"; | ||
| 13 | static const size_t V3_PREFIX_LEN = 6; | ||
| 14 | |||
| 15 | static int b64url_decode(const char *input, size_t input_len, char *out, size_t out_size, size_t *out_len) | ||
| 16 | { | ||
| 17 | char b64[1024]; | ||
| 18 | size_t b64_len = input_len; | ||
| 19 | if (b64_len >= sizeof(b64)) return -1; | ||
| 20 | memcpy(b64, input, b64_len); | ||
| 21 | b64[b64_len] = '\0'; | ||
| 22 | |||
| 23 | for (size_t i = 0; i < b64_len; i++) { | ||
| 24 | if (b64[i] == '-') b64[i] = '+'; | ||
| 25 | else if (b64[i] == '_') b64[i] = '/'; | ||
| 26 | } | ||
| 27 | while (b64_len % 4 != 0 && b64_len < sizeof(b64) - 1) { | ||
| 28 | b64[b64_len++] = '='; | ||
| 29 | } | ||
| 30 | b64[b64_len] = '\0'; | ||
| 31 | |||
| 32 | size_t olen = 0; | ||
| 33 | int ret = mbedtls_base64_decode((unsigned char *)out, out_size, &olen, | ||
| 34 | (const unsigned char *)b64, b64_len); | ||
| 35 | if (ret != 0) return -1; | ||
| 36 | *out_len = olen; | ||
| 37 | return 0; | ||
| 38 | } | ||
| 39 | |||
| 40 | static esp_err_t parse_proofs_array(cJSON *arr, cashu_token_t *out) | ||
| 41 | { | ||
| 42 | if (!cJSON_IsArray(arr)) return ESP_FAIL; | ||
| 43 | int count = cJSON_GetArraySize(arr); | ||
| 44 | if (count > CASHU_MAX_PROOFS) return ESP_FAIL; | ||
| 45 | |||
| 46 | out->proof_count = 0; | ||
| 47 | out->total_amount = 0; | ||
| 48 | for (int i = 0; i < count; i++) { | ||
| 49 | cJSON *proof = cJSON_GetArrayItem(arr, i); | ||
| 50 | cJSON *amt = cJSON_GetObjectItemCaseSensitive(proof, "amount"); | ||
| 51 | cJSON *id = cJSON_GetObjectItemCaseSensitive(proof, "id"); | ||
| 52 | cJSON *secret = cJSON_GetObjectItemCaseSensitive(proof, "secret"); | ||
| 53 | cJSON *c = cJSON_GetObjectItemCaseSensitive(proof, "C"); | ||
| 54 | |||
| 55 | if (!amt || !cJSON_IsNumber(amt)) return ESP_FAIL; | ||
| 56 | |||
| 57 | out->proofs[i].amount = (uint64_t)amt->valuedouble; | ||
| 58 | out->total_amount += out->proofs[i].amount; | ||
| 59 | |||
| 60 | if (id && cJSON_IsString(id)) { | ||
| 61 | strncpy(out->proofs[i].id, id->valuestring, sizeof(out->proofs[i].id) - 1); | ||
| 62 | } | ||
| 63 | if (secret && cJSON_IsString(secret)) { | ||
| 64 | strncpy(out->proofs[i].secret, secret->valuestring, sizeof(out->proofs[i].secret) - 1); | ||
| 65 | } | ||
| 66 | if (c && cJSON_IsString(c)) { | ||
| 67 | strncpy(out->proofs[i].c, c->valuestring, sizeof(out->proofs[i].c) - 1); | ||
| 68 | } | ||
| 69 | out->proof_count++; | ||
| 70 | } | ||
| 71 | return ESP_OK; | ||
| 72 | } | ||
| 73 | |||
| 74 | esp_err_t cashu_decode_token(const char *token_str, cashu_token_t *out) | ||
| 75 | { | ||
| 76 | if (!token_str || !out) return ESP_FAIL; | ||
| 77 | memset(out, 0, sizeof(*out)); | ||
| 78 | |||
| 79 | size_t len = strlen(token_str); | ||
| 80 | if (len <= V3_PREFIX_LEN) { | ||
| 81 | ESP_LOGE(TAG, "Token too short"); | ||
| 82 | return ESP_FAIL; | ||
| 83 | } | ||
| 84 | if (strncmp(token_str, V3_PREFIX, V3_PREFIX_LEN) != 0) { | ||
| 85 | ESP_LOGE(TAG, "Token missing cashuA prefix"); | ||
| 86 | return ESP_FAIL; | ||
| 87 | } | ||
| 88 | |||
| 89 | char json_buf[2048]; | ||
| 90 | size_t json_len = 0; | ||
| 91 | if (b64url_decode(token_str + V3_PREFIX_LEN, len - V3_PREFIX_LEN, | ||
| 92 | json_buf, sizeof(json_buf) - 1, &json_len) != 0) { | ||
| 93 | ESP_LOGE(TAG, "Base64url decode failed"); | ||
| 94 | return ESP_FAIL; | ||
| 95 | } | ||
| 96 | json_buf[json_len] = '\0'; | ||
| 97 | |||
| 98 | cJSON *root = cJSON_Parse(json_buf); | ||
| 99 | if (!root) { | ||
| 100 | ESP_LOGE(TAG, "JSON parse failed"); | ||
| 101 | return ESP_FAIL; | ||
| 102 | } | ||
| 103 | |||
| 104 | cJSON *token_arr = cJSON_GetObjectItemCaseSensitive(root, "token"); | ||
| 105 | if (token_arr && cJSON_IsArray(token_arr)) { | ||
| 106 | cJSON *first = cJSON_GetArrayItem(token_arr, 0); | ||
| 107 | if (!first) { cJSON_Delete(root); return ESP_FAIL; } | ||
| 108 | |||
| 109 | cJSON *mint = cJSON_GetObjectItemCaseSensitive(first, "mint"); | ||
| 110 | if (mint && cJSON_IsString(mint)) { | ||
| 111 | strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1); | ||
| 112 | } | ||
| 113 | |||
| 114 | cJSON *proofs = cJSON_GetObjectItemCaseSensitive(first, "proofs"); | ||
| 115 | if (proofs) { | ||
| 116 | esp_err_t ret = parse_proofs_array(proofs, out); | ||
| 117 | if (ret != ESP_OK) { cJSON_Delete(root); return ret; } | ||
| 118 | } | ||
| 119 | } else { | ||
| 120 | cJSON *mint = cJSON_GetObjectItemCaseSensitive(root, "mint"); | ||
| 121 | if (mint && cJSON_IsString(mint)) { | ||
| 122 | strncpy(out->mint_url, mint->valuestring, sizeof(out->mint_url) - 1); | ||
| 123 | } | ||
| 124 | |||
| 125 | cJSON *proofs = cJSON_GetObjectItemCaseSensitive(root, "proofs"); | ||
| 126 | if (proofs) { | ||
| 127 | esp_err_t ret = parse_proofs_array(proofs, out); | ||
| 128 | if (ret != ESP_OK) { cJSON_Delete(root); return ret; } | ||
| 129 | } | ||
| 130 | } | ||
| 131 | |||
| 132 | cJSON_Delete(root); | ||
| 133 | |||
| 134 | if (out->proof_count == 0) { | ||
| 135 | ESP_LOGE(TAG, "No proofs in token"); | ||
| 136 | return ESP_FAIL; | ||
| 137 | } | ||
| 138 | |||
| 139 | ESP_LOGI(TAG, "Decoded token: %d proofs, total=%llu, mint=%s", | ||
| 140 | out->proof_count, (unsigned long long)out->total_amount, out->mint_url); | ||
| 141 | return ESP_OK; | ||
| 142 | } | ||
| 143 | |||
| 144 | static void sha256_hex(const char *data, size_t data_len, char *hex_out) | ||
| 145 | { | ||
| 146 | uint8_t hash[32]; | ||
| 147 | mbedtls_sha256((const unsigned char *)data, data_len, hash, 0); | ||
| 148 | for (int i = 0; i < 32; i++) { | ||
| 149 | sprintf(hex_out + i * 2, "%02x", hash[i]); | ||
| 150 | } | ||
| 151 | hex_out[64] = '\0'; | ||
| 152 | } | ||
| 153 | |||
| 154 | esp_err_t cashu_check_proof_states(const char *mint_url, const cashu_token_t *token, | ||
| 155 | cashu_proof_state_t *states, int *state_count) | ||
| 156 | { | ||
| 157 | cJSON *ys_arr = cJSON_CreateArray(); | ||
| 158 | for (int i = 0; i < token->proof_count; i++) { | ||
| 159 | char y_hex[65]; | ||
| 160 | sha256_hex(token->proofs[i].secret, strlen(token->proofs[i].secret), y_hex); | ||
| 161 | cJSON_AddItemToArray(ys_arr, cJSON_CreateString(y_hex)); | ||
| 162 | strncpy(states[i].y_hex, y_hex, sizeof(states[i].y_hex) - 1); | ||
| 163 | states[i].spent = false; | ||
| 164 | } | ||
| 165 | *state_count = token->proof_count; | ||
| 166 | |||
| 167 | char *ys_json = cJSON_PrintUnformatted(ys_arr); | ||
| 168 | cJSON_Delete(ys_arr); | ||
| 169 | |||
| 170 | char post_body[2048]; | ||
| 171 | snprintf(post_body, sizeof(post_body), "{\"Ys\":%s}", ys_json); | ||
| 172 | cJSON_free(ys_json); | ||
| 173 | |||
| 174 | char url[512]; | ||
| 175 | snprintf(url, sizeof(url), "%s/v1/checkstate", mint_url); | ||
| 176 | |||
| 177 | char resp_buf[4096]; | ||
| 178 | int resp_len = 0; | ||
| 179 | |||
| 180 | esp_http_client_config_t config = { | ||
| 181 | .url = url, | ||
| 182 | .method = HTTP_METHOD_POST, | ||
| 183 | .timeout_ms = 10000, | ||
| 184 | }; | ||
| 185 | esp_http_client_handle_t client = esp_http_client_init(&config); | ||
| 186 | if (!client) return ESP_FAIL; | ||
| 187 | |||
| 188 | esp_http_client_set_header(client, "Content-Type", "application/json"); | ||
| 189 | esp_err_t err = esp_http_client_open(client, strlen(post_body)); | ||
| 190 | if (err != ESP_OK) { | ||
| 191 | esp_http_client_cleanup(client); | ||
| 192 | return err; | ||
| 193 | } | ||
| 194 | esp_http_client_write(client, post_body, strlen(post_body)); | ||
| 195 | |||
| 196 | resp_len = esp_http_client_read(client, resp_buf, sizeof(resp_buf) - 1); | ||
| 197 | int status = esp_http_client_get_status_code(client); | ||
| 198 | esp_http_client_cleanup(client); | ||
| 199 | |||
| 200 | if (status != 200 || resp_len <= 0) { | ||
| 201 | ESP_LOGE(TAG, "checkstate returned %d", status); | ||
| 202 | return ESP_FAIL; | ||
| 203 | } | ||
| 204 | resp_buf[resp_len] = '\0'; | ||
| 205 | |||
| 206 | cJSON *root = cJSON_Parse(resp_buf); | ||
| 207 | if (!root) return ESP_FAIL; | ||
| 208 | |||
| 209 | cJSON *states_arr = cJSON_GetObjectItemCaseSensitive(root, "states"); | ||
| 210 | if (!states_arr || !cJSON_IsArray(states_arr)) { | ||
| 211 | cJSON_Delete(root); | ||
| 212 | return ESP_FAIL; | ||
| 213 | } | ||
| 214 | |||
| 215 | int n = cJSON_GetArraySize(states_arr); | ||
| 216 | for (int i = 0; i < n && i < token->proof_count; i++) { | ||
| 217 | cJSON *s = cJSON_GetArrayItem(states_arr, i); | ||
| 218 | cJSON *state = cJSON_GetObjectItemCaseSensitive(s, "state"); | ||
| 219 | if (state && cJSON_IsString(state)) { | ||
| 220 | states[i].spent = (strcmp(state->valuestring, "SPENT") == 0); | ||
| 221 | } | ||
| 222 | } | ||
| 223 | |||
| 224 | cJSON_Delete(root); | ||
| 225 | return ESP_OK; | ||
| 226 | } | ||
| 227 | |||
| 228 | uint64_t cashu_calculate_allotment_ms(uint64_t token_amount, uint64_t price_per_step, | ||
| 229 | uint64_t step_size_ms) | ||
| 230 | { | ||
| 231 | if (price_per_step == 0) return 0; | ||
| 232 | return (token_amount / price_per_step) * step_size_ms; | ||
| 233 | } | ||
| 234 | |||
| 235 | bool cashu_is_mint_accepted(const char *mint_url) | ||
| 236 | { | ||
| 237 | if (!mint_url || mint_url[0] == '\0') return false; | ||
| 238 | const tollgate_config_t *cfg = tollgate_config_get(); | ||
| 239 | if (strstr(mint_url, cfg->mint_url) != NULL) return true; | ||
| 240 | return (strcmp(mint_url, cfg->mint_url) == 0); | ||
| 241 | } | ||