cryptnox-sdk-esp32 1.0.0
ESP32 SDK for Cryptnox Hardware Wallet
Loading...
Searching...
No Matches
eth_rpc.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
6#include "eth_rpc.h"
7
8#include <string.h>
9#include <strings.h> /* strncasecmp */
10#include <stdlib.h> /* strtoull, malloc, free */
11#include <stdio.h> /* snprintf */
12#include <inttypes.h> /* PRIu64 */
13
14#include "freertos/FreeRTOS.h"
15#include "freertos/event_groups.h"
16#include "esp_wifi.h"
17#include "esp_event.h"
18#include "esp_netif.h"
19#include "esp_log.h"
20#include "esp_http_client.h"
21#include "esp_crt_bundle.h"
22
23static const char *const TAG = "eth_rpc";
24
25#define WIFI_CONNECTED_BIT BIT0
26#define WIFI_FAIL_BIT BIT1
27#define WIFI_MAX_RETRY 5
28#define WIFI_TIMEOUT_MS 30000
29
30/* JSON-RPC response buffer — large enough for any expected response. */
31#define RESP_BUF_SIZE 1024U
32
33/* Hex chars per byte */
34#define HEX_PER_BYTE 2U
35
36/******************************************************************
37 * Module state
38 ******************************************************************/
39
40static const char *s_rpc_url = NULL;
41static const char *s_from_addr = NULL;
42static const char *s_project_id = NULL;
43static const char *s_api_secret = NULL;
44
45static EventGroupHandle_t s_wifi_event_group;
46static int s_retry_num = 0;
47
48/******************************************************************
49 * WiFi event handler
50 ******************************************************************/
51
52static void wifi_event_handler(void *arg, esp_event_base_t event_base,
53 int32_t event_id, void *event_data)
54{
55 (void)arg;
56 (void)event_data;
57
58 if ((event_base == WIFI_EVENT) && (event_id == WIFI_EVENT_STA_START)) {
59 esp_wifi_connect();
60 } else if ((event_base == WIFI_EVENT) &&
61 (event_id == WIFI_EVENT_STA_DISCONNECTED)) {
63 esp_wifi_connect();
65 ESP_LOGW(TAG, "WiFi retry %d/%d", s_retry_num, WIFI_MAX_RETRY);
66 } else {
67 xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
68 }
69 } else if ((event_base == IP_EVENT) && (event_id == IP_EVENT_STA_GOT_IP)) {
70 s_retry_num = 0;
71 xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
72 } else {
73 /* other events ignored */
74 }
75}
76
77/******************************************************************
78 * HTTP helper
79 ******************************************************************/
80
81/*
82 * POST 'body' to s_rpc_url over HTTPS and read the response into
83 * resp_buf (NUL-terminated on success). Returns true if data was read.
84 */
85static bool do_post(const char *body, char *resp_buf, size_t resp_buf_size)
86{
87 bool success = false;
88
89 bool use_auth = ((s_project_id != NULL) && (s_project_id[0] != '\0') &&
90 (s_api_secret != NULL) && (s_api_secret[0] != '\0'));
91
92 esp_http_client_config_t cfg;
93 (void)memset(&cfg, 0, sizeof(cfg));
94 cfg.url = s_rpc_url;
95 cfg.method = HTTP_METHOD_POST;
96 cfg.timeout_ms = 15000;
97 cfg.crt_bundle_attach = esp_crt_bundle_attach;
98 if (use_auth) {
99 cfg.username = s_project_id;
100 cfg.password = s_api_secret;
101 cfg.auth_type = HTTP_AUTH_TYPE_BASIC;
102 }
103
104 esp_http_client_handle_t client = esp_http_client_init(&cfg);
105 if (client == NULL) {
106 ESP_LOGE(TAG, "HTTP client init failed");
107 return false;
108 }
109
110 (void)esp_http_client_set_header(client, "Content-Type", "application/json");
111
112 int body_len = (int)strlen(body);
113 esp_err_t err = esp_http_client_open(client, body_len);
114 if (err != ESP_OK) {
115 ESP_LOGE(TAG, "HTTP open: %s", esp_err_to_name(err));
116 goto cleanup;
117 }
118
119 if (esp_http_client_write(client, body, body_len) != body_len) {
120 ESP_LOGE(TAG, "HTTP write incomplete");
121 goto cleanup;
122 }
123
124 {
125 int64_t content_length = esp_http_client_fetch_headers(client);
126 (void)content_length; /* may be -1 for chunked; we read until EOF */
127
128 int total = 0;
129 int read;
130 do {
131 int space = (int)(resp_buf_size - 1U) - total;
132 if (space <= 0) { break; }
133 read = esp_http_client_read(client, resp_buf + total, space);
134 if (read > 0) { total += read; }
135 } while (read > 0);
136
137 resp_buf[total] = '\0';
138 success = (total > 0);
139 }
140
141cleanup:
142 esp_http_client_close(client);
143 esp_http_client_cleanup(client);
144 return success;
145}
146
147/******************************************************************
148 * Hex utilities
149 ******************************************************************/
150
151static char hex_nibble(uint8_t n)
152{
153 return (n < 10U) ? (char)('0' + n) : (char)('a' + n - 10U);
154}
155
156static void bytes_to_hex(const uint8_t *data, size_t len, char *out)
157{
158 size_t i;
159 for (i = 0U; i < len; i++) {
160 out[i * HEX_PER_BYTE] = hex_nibble((data[i] >> 4U) & 0x0FU);
161 out[i * HEX_PER_BYTE + 1U] = hex_nibble(data[i] & 0x0FU);
162 }
163}
164
165/******************************************************************
166 * Public API
167 ******************************************************************/
168
169void eth_rpc_init(const char *rpc_url, const char *from_addr)
170{
171 s_rpc_url = rpc_url;
172 s_from_addr = from_addr;
173}
174
175void eth_rpc_set_auth(const char *project_id, const char *api_secret)
176{
177 s_project_id = project_id;
178 s_api_secret = api_secret;
179}
180
181bool eth_rpc_wifi_connect(const char *ssid, const char *password)
182{
183 s_wifi_event_group = xEventGroupCreate();
184 s_retry_num = 0;
185
186 ESP_ERROR_CHECK(esp_netif_init());
187 ESP_ERROR_CHECK(esp_event_loop_create_default());
188 (void)esp_netif_create_default_wifi_sta();
189
190 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
191 ESP_ERROR_CHECK(esp_wifi_init(&cfg));
192
193 esp_event_handler_instance_t h_any;
194 esp_event_handler_instance_t h_ip;
195 ESP_ERROR_CHECK(esp_event_handler_instance_register(
196 WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, &h_any));
197 ESP_ERROR_CHECK(esp_event_handler_instance_register(
198 IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, &h_ip));
199
200 wifi_config_t wifi_cfg;
201 (void)memset(&wifi_cfg, 0, sizeof(wifi_cfg));
202 (void)strncpy((char *)wifi_cfg.sta.ssid, ssid, sizeof(wifi_cfg.sta.ssid) - 1U);
203 (void)strncpy((char *)wifi_cfg.sta.password, password, sizeof(wifi_cfg.sta.password) - 1U);
204 wifi_cfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
205
206 ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
207 ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg));
208 ESP_ERROR_CHECK(esp_wifi_start());
209
210 ESP_LOGI(TAG, "Connecting to \"%s\"...", ssid);
211
212 EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
214 pdFALSE, pdFALSE,
215 pdMS_TO_TICKS(WIFI_TIMEOUT_MS));
216
217 bool connected = ((bits & WIFI_CONNECTED_BIT) != 0U);
218 if (connected) {
219 ESP_LOGI(TAG, "WiFi connected");
220 } else {
221 ESP_LOGE(TAG, "WiFi connect failed");
222 }
223
224 (void)esp_event_handler_instance_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, h_ip);
225 (void)esp_event_handler_instance_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, h_any);
226 vEventGroupDelete(s_wifi_event_group);
227
228 return connected;
229}
230
231bool eth_rpc_get_nonce(uint64_t *nonce_out)
232{
233 char body[256];
234 (void)snprintf(body, sizeof(body),
235 "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionCount\","
236 "\"params\":[\"%s\",\"pending\"],\"id\":1}",
238
239 char resp[RESP_BUF_SIZE];
240 if (!do_post(body, resp, sizeof(resp))) {
241 return false;
242 }
243
244 /* Extract hex value after "result":"0x */
245 char *result = strstr(resp, "\"result\":\"0x");
246 if (result == NULL) {
247 ESP_LOGE(TAG, "nonce: no result in: %s", resp);
248 return false;
249 }
250 result += 12; /* skip past "result":"0x */
251
252 *nonce_out = strtoull(result, NULL, 16);
253 ESP_LOGI(TAG, "Nonce: %" PRIu64, *nonce_out);
254 return true;
255}
256
257uint8_t eth_rpc_ecrecover_parity(const uint8_t hash[32],
258 const uint8_t r[32],
259 const uint8_t s[32])
260{
261 /* ecrecover precompile input: hash(32) || v_uint256(32) || r(32) || s(32) */
262 uint8_t input[128];
263 (void)memset(input, 0, sizeof(input));
264 (void)memcpy(input, hash, 32U);
265 /* v occupies the last byte of the second 32-byte slot (index 63) */
266 (void)memcpy(input + 64U, r, 32U);
267 (void)memcpy(input + 96U, s, 32U);
268
269 /* Hex-encode the 128-byte input */
270 char input_hex[257];
271 bytes_to_hex(input, sizeof(input), input_hex);
272 input_hex[256] = '\0';
273
274 /* from_addr without "0x" prefix for comparison */
275 const char *from_hex = s_from_addr;
276 if ((from_hex[0] == '0') && ((from_hex[1] == 'x') || (from_hex[1] == 'X'))) {
277 from_hex += 2;
278 }
279
280 uint8_t v_raw;
281 for (v_raw = 0U; v_raw < 2U; v_raw++) {
282 /* Set v byte (27 or 28) in slot [32..63] last byte */
283 input_hex[63U * HEX_PER_BYTE] = hex_nibble(((27U + v_raw) >> 4U) & 0x0FU);
284 input_hex[63U * HEX_PER_BYTE + 1U] = hex_nibble((27U + v_raw) & 0x0FU);
285
286 char body[600];
287 (void)snprintf(body, sizeof(body),
288 "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\","
289 "\"params\":[{\"to\":"
290 "\"0x0000000000000000000000000000000000000001\","
291 "\"data\":\"0x%s\"},\"latest\"],\"id\":3}",
292 input_hex);
293
294 char resp[RESP_BUF_SIZE];
295 if (!do_post(body, resp, sizeof(resp))) {
296 continue;
297 }
298
299 /* Response: "result":"0x" + 64 hex chars (32 bytes ABI address).
300 * The address occupies the last 40 hex chars (bytes 12-31). */
301 char *result = strstr(resp, "\"result\":\"0x");
302 if (result == NULL) { continue; }
303 result += 12U; /* skip to hex digits */
304
305 size_t result_hex_len = 0U;
306 {
307 const char *p = result;
308 while ((*p != '"') && (*p != '\0') && (*p != ',')) {
309 p++;
310 result_hex_len++;
311 }
312 }
313
314 if (result_hex_len < 64U) {
315 /* Empty result — address not recovered (try other v) */
316 continue;
317 }
318
319 /* ABI address: 24 hex chars of zeros + 40 hex chars of address */
320 const char *recovered_hex = result + 24U;
321
322 if (strncasecmp(recovered_hex, from_hex, 40U) == 0) {
323 ESP_LOGI(TAG, "v=%u matched ecrecover", v_raw);
324 return v_raw;
325 }
326 }
327
328 ESP_LOGW(TAG, "ecrecover did not match either parity, defaulting v=0");
329 return 0U;
330}
331
332bool eth_rpc_send_raw_tx(const uint8_t *tx, size_t tx_len,
333 char *tx_hash_out, size_t tx_hash_max)
334{
335 /* "0x" + 2 hex chars per byte + NUL */
336 size_t hex_str_size = 2U + tx_len * HEX_PER_BYTE + 1U;
337 char *tx_hex = (char *)malloc(hex_str_size);
338 if (tx_hex == NULL) { return false; }
339
340 tx_hex[0] = '0';
341 tx_hex[1] = 'x';
342 bytes_to_hex(tx, tx_len, tx_hex + 2U);
343 tx_hex[hex_str_size - 1U] = '\0';
344
345 /* JSON body */
346 size_t body_size = hex_str_size + 128U;
347 char *body = (char *)malloc(body_size);
348 if (body == NULL) { free(tx_hex); return false; }
349
350 (void)snprintf(body, body_size,
351 "{\"jsonrpc\":\"2.0\",\"method\":\"eth_sendRawTransaction\","
352 "\"params\":[\"%s\"],\"id\":2}",
353 tx_hex);
354 free(tx_hex);
355
356 char resp[RESP_BUF_SIZE];
357 bool ok = do_post(body, resp, sizeof(resp));
358 free(body);
359
360 if (!ok) { return false; }
361
362 /* Extract "result":"0x..." (the tx hash) */
363 char *result = strstr(resp, "\"result\":\"");
364 if (result == NULL) {
365 ESP_LOGE(TAG, "send_raw_tx: no result in: %s", resp);
366 return false;
367 }
368 result += 10U; /* skip past "result":" — now at '0x...' */
369
370 char *end = strchr(result, '"');
371 if (end == NULL) { return false; }
372
373 size_t hash_len = (size_t)(end - result);
374 if ((hash_len + 1U) > tx_hash_max) { return false; }
375
376 (void)memcpy(tx_hash_out, result, hash_len);
377 tx_hash_out[hash_len] = '\0';
378 return true;
379}
static EventGroupHandle_t s_wifi_event_group
Definition main.cpp:80
#define WIFI_FAIL_BIT
Definition main.cpp:78
static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
FreeRTOS event handler driving the Wi-Fi station state machine.
Definition main.cpp:95
#define WIFI_CONNECTED_BIT
Definition main.cpp:77
static int s_retry_num
Definition main.cpp:81
static const char *const TAG
bool eth_rpc_send_raw_tx(const uint8_t *tx, size_t tx_len, char *tx_hash_out, size_t tx_hash_max)
Definition eth_rpc.cpp:332
bool eth_rpc_wifi_connect(const char *ssid, const char *password)
Definition eth_rpc.cpp:181
static bool do_post(const char *body, char *resp_buf, size_t resp_buf_size)
Definition eth_rpc.cpp:85
static const char * s_api_secret
Definition eth_rpc.cpp:43
static const char * s_from_addr
Definition eth_rpc.cpp:41
bool eth_rpc_get_nonce(uint64_t *nonce_out)
Definition eth_rpc.cpp:231
#define WIFI_TIMEOUT_MS
Definition eth_rpc.cpp:28
static const char * s_rpc_url
Definition eth_rpc.cpp:40
void eth_rpc_init(const char *rpc_url, const char *from_addr)
Definition eth_rpc.cpp:169
void eth_rpc_set_auth(const char *project_id, const char *api_secret)
Definition eth_rpc.cpp:175
static char hex_nibble(uint8_t n)
Definition eth_rpc.cpp:151
static const char * s_project_id
Definition eth_rpc.cpp:42
#define WIFI_MAX_RETRY
Definition eth_rpc.cpp:27
static void bytes_to_hex(const uint8_t *data, size_t len, char *out)
Definition eth_rpc.cpp:156
#define RESP_BUF_SIZE
Definition eth_rpc.cpp:31
#define HEX_PER_BYTE
Definition eth_rpc.cpp:34
uint8_t eth_rpc_ecrecover_parity(const uint8_t hash[32], const uint8_t r[32], const uint8_t s[32])
Definition eth_rpc.cpp:257
static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
Definition eth_rpc.cpp:52