upleb.uk

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

summaryrefslogtreecommitdiff
path: root/main/cashu.c
diff options
context:
space:
mode:
Diffstat (limited to 'main/cashu.c')
-rw-r--r--main/cashu.c241
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
10static const char *TAG = "cashu";
11
12static const char V3_PREFIX[] = "cashuA";
13static const size_t V3_PREFIX_LEN = 6;
14
15static 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
40static 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
74esp_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
144static 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
154esp_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
228uint64_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
235bool 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}