cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
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
11
12/******************************************************************
13 * 1. Included files
14 ******************************************************************/
15
16#include "eth_rpc.h"
17#include "eth_json.h"
18#include "https_post.h"
19
20#include <string.h>
21#include <strings.h> /* strncasecmp */
22#include <stdlib.h> /* strtoull, malloc, free */
23#include <stdio.h> /* snprintf */
24#include <inttypes.h> /* PRIu64 */
25
26/* CW_Utils.h pulls in Arduino.h (via platform_compat.h); it must come before
27 * any lwip-including IDF header so that IPAddress.h declares INADDR_NONE
28 * before lwip defines it as a macro. */
29#include "CW_Utils.h" /* hardened memory primitives (CODING_RULES §1.4) */
30
31#include "esp_log.h"
32
33static const char *const TAG = "eth_rpc";
34
35/* JSON-RPC response buffer — large enough for any expected response. */
36#define RESP_BUF_SIZE 1024U
37
38/* Hex chars per byte */
39#define HEX_PER_BYTE 2U
40
41/* never dump full RPC responses (they can echo credentials embedded
42 * in the URL) — log at most this many bytes on parse failures. */
43#define RESP_LOG_MAX 80
44
45/* sanity bound for the account nonce — a real terminal never gets
46 * anywhere near 2^32 transactions, so anything above is a bogus response. */
47#define NONCE_MAX 0xFFFFFFFFULL
48
49/* Largest expected "result" string: 0x + 64 hex chars + NUL, rounded up. */
50#define RESULT_STR_MAX 80U
51
52/******************************************************************
53 * 2. Constants and module state
54 ******************************************************************/
55
56static const char *s_rpc_url = NULL;
57static const char *s_from_addr = NULL;
58static const char *s_project_id = NULL;
59static const char *s_api_secret = NULL;
60static const char *s_ca_cert = NULL; /* pinned cert; NULL = CA bundle */
61
62/******************************************************************
63 * 4. HTTP helper
64 ******************************************************************/
65
77static bool do_post(const char *body, char *resp_buf, size_t resp_buf_size)
78{
79 return https_post_json(s_rpc_url, body, resp_buf, resp_buf_size,
81}
82
83/******************************************************************
84 * 5. Hex utilities
85 ******************************************************************/
86
93static char hex_nibble(uint8_t n)
94{
95 return (n < 10U) ? static_cast<char>('0' + n)
96 : static_cast<char>('a' + n - 10U);
97}
98
106static void bytes_to_hex(const uint8_t *data, size_t len, char *out)
107{
108 size_t i;
109 for (i = 0U; i < len; i++) {
110 out[i * HEX_PER_BYTE] = hex_nibble((data[i] >> 4U) & 0x0FU);
111 out[i * HEX_PER_BYTE + 1U] = hex_nibble(data[i] & 0x0FU);
112 }
113}
114
115/* The JSON-RPC "result" string extractor lives in eth_json.cpp (a pure,
116 * host-fuzzable unit — see fuzz/fuzz_eth_rpc_json.cpp). */
117
118/******************************************************************
119 * 7. Public API
120 ******************************************************************/
121
122void eth_rpc_init(const char *rpc_url, const char *from_addr)
123{
124 s_rpc_url = rpc_url;
125 s_from_addr = from_addr;
126}
127
128/* The payer, copied. eth_rpc_init keeps a pointer to its caller's storage,
129 * which suits a config.h literal and not an address derived into a stack buffer
130 * during one sale — so the per-tap override owns its bytes. */
131static char s_from_buf[43]; /* "0x" + 40 hex + NUL */
132
133bool eth_rpc_set_from(const char *addr)
134{
135 if (addr == NULL) { return false; }
136 if ((addr[0] != '0') || ((addr[1] != 'x') && (addr[1] != 'X'))) {
137 return false;
138 }
139 if (strlen(addr) != 42U) { return false; }
140
141 (void)snprintf(s_from_buf, sizeof(s_from_buf), "%s", addr);
143 return true;
144}
145
146void eth_rpc_set_auth(const char *project_id, const char *api_secret)
147{
148 s_project_id = project_id;
149 s_api_secret = api_secret;
150}
151
152void eth_rpc_set_ca_cert(const char *ca_pem)
153{
154 s_ca_cert = ca_pem;
155}
156
157bool eth_rpc_get_nonce(uint64_t *nonce_out)
158{
159 char body[256];
160 (void)snprintf(body, sizeof(body),
161 "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionCount\","
162 "\"params\":[\"%s\",\"latest\"],\"id\":1}",
164
165 char resp[RESP_BUF_SIZE];
166 if (!do_post(body, resp, sizeof(resp))) {
167 return false;
168 }
169
170 char result[RESULT_STR_MAX];
171 if (!eth_json_result_string(resp, result, sizeof(result))) {
172 ESP_LOGE(TAG, "nonce: no result in: %.*s", RESP_LOG_MAX, resp);
173 return false;
174 }
175 if (strncmp(result, "0x", 2U) != 0) {
176 ESP_LOGE(TAG, "nonce: result not hex: %.*s", RESP_LOG_MAX, result);
177 return false;
178 }
179
180 char *end = NULL;
181 uint64_t nonce = strtoull(result + 2, &end, 16);
182 if ((end == (result + 2)) || (*end != '\0')) {
183 ESP_LOGE(TAG, "nonce: malformed hex: %.*s", RESP_LOG_MAX, result);
184 return false;
185 }
186 /* strtoull saturates silently — reject absurd values outright. */
187 if (nonce > NONCE_MAX) {
188 ESP_LOGE(TAG, "nonce: out of range: %" PRIu64, nonce);
189 return false;
190 }
191
192 *nonce_out = nonce;
193 ESP_LOGI(TAG, "Nonce: %" PRIu64, nonce);
194 return true;
195}
196
203static const char *from_no_prefix(void)
204{
205 const char *p = s_from_addr;
206 if ((p != NULL) && (p[0] == '0') && ((p[1] == 'x') || (p[1] == 'X'))) {
207 p += 2;
208 }
209 return p;
210}
211
212bool eth_rpc_get_balance(uint64_t *wei_out)
213{
214 if ((wei_out == NULL) || (s_from_addr == NULL)) { return false; }
215
216 char body[256];
217 (void)snprintf(body, sizeof(body),
218 "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getBalance\","
219 "\"params\":[\"%s\",\"latest\"],\"id\":4}",
221
222 char resp[RESP_BUF_SIZE];
223 if (!do_post(body, resp, sizeof(resp))) {
224 return false;
225 }
226
227 char result[RESULT_STR_MAX];
228 if (!eth_json_result_string(resp, result, sizeof(result))) {
229 ESP_LOGE(TAG, "balance: no result in: %.*s", RESP_LOG_MAX, resp);
230 return false;
231 }
232 if (!eth_json_hex_quantity(result, wei_out)) {
233 ESP_LOGE(TAG, "balance: malformed quantity: %.*s", RESP_LOG_MAX, result);
234 return false;
235 }
236 return true;
237}
238
239bool eth_rpc_get_token_balance(const char *token_addr, uint64_t *units_out)
240{
241 if ((token_addr == NULL) || (units_out == NULL) || (s_from_addr == NULL)) {
242 return false;
243 }
244
245 /* The ABI argument is the address left-padded to 32 bytes, so the bare 40
246 * characters have to be exactly that. A short one would shift the padding
247 * and ask the contract about a different account — which would answer, and
248 * the answer would be about somebody else. */
249 const char *from_hex = from_no_prefix();
250 if (strlen(from_hex) != 40U) {
251 ESP_LOGE(TAG, "token balance: from_addr is not 20 bytes");
252 return false;
253 }
254
255 /* balanceOf(address): selector 70a08231, then 12 zero bytes + the address. */
256 char body[320];
257 (void)snprintf(body, sizeof(body),
258 "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\","
259 "\"params\":[{\"to\":\"%s\",\"data\":\"0x70a08231"
260 "000000000000000000000000%s\"},\"latest\"],\"id\":5}",
261 token_addr, from_hex);
262
263 char resp[RESP_BUF_SIZE];
264 if (!do_post(body, resp, sizeof(resp))) {
265 return false;
266 }
267
268 char result[RESULT_STR_MAX];
269 if (!eth_json_result_string(resp, result, sizeof(result))) {
270 ESP_LOGE(TAG, "token balance: no result in: %.*s", RESP_LOG_MAX, resp);
271 return false;
272 }
273 /* A call to an address with no code returns "0x" — an answer this must not
274 * read as a zero balance, or a mistyped contract would look like an empty
275 * account instead of like a misconfiguration. eth_json_hex_quantity rejects
276 * it, and the caller treats a failed read as "cannot tell" rather than as
277 * grounds to refuse. */
278 if (!eth_json_hex_quantity(result, units_out)) {
279 ESP_LOGE(TAG, "token balance: malformed quantity: %.*s",
280 RESP_LOG_MAX, result);
281 return false;
282 }
283 return true;
284}
285
286bool eth_rpc_send_raw_tx(const uint8_t *tx, size_t tx_len,
287 char *tx_hash_out, size_t tx_hash_max,
288 char *err_out, size_t err_max)
289{
290 /* Cleared up front so every failure path below leaves a defined value: the
291 * caller distinguishes "the node said why" from "we never got that far" by
292 * whether this is empty, and a stale message from a previous sale would be
293 * worse than none at all. */
294 if ((err_out != NULL) && (err_max > 0U)) { err_out[0] = '\0'; }
295
296 /* "0x" + 2 hex chars per byte + NUL */
297 size_t hex_str_size = 2U + tx_len * HEX_PER_BYTE + 1U;
298 char *tx_hex = static_cast<char *>(malloc(hex_str_size));
299 if (tx_hex == NULL) { return false; }
300
301 tx_hex[0] = '0';
302 tx_hex[1] = 'x';
303 bytes_to_hex(tx, tx_len, tx_hex + 2U);
304 tx_hex[hex_str_size - 1U] = '\0';
305
306 /* JSON body */
307 size_t body_size = hex_str_size + 128U;
308 char *body = static_cast<char *>(malloc(body_size));
309 if (body == NULL) { free(tx_hex); return false; }
310
311 (void)snprintf(body, body_size,
312 "{\"jsonrpc\":\"2.0\",\"method\":\"eth_sendRawTransaction\","
313 "\"params\":[\"%s\"],\"id\":2}",
314 tx_hex);
315 free(tx_hex);
316
317 char resp[RESP_BUF_SIZE];
318 bool ok = do_post(body, resp, sizeof(resp));
319 free(body);
320
321 if (!ok) { return false; }
322
323 /* Extract the "result" string (the tx hash) with a real JSON parse, so
324 * a JSON-RPC error object is reported as a failure. */
325 char result[RESULT_STR_MAX];
326 if (!eth_json_result_string(resp, result, sizeof(result))) {
327 ESP_LOGE(TAG, "send_raw_tx: no result in: %.*s", RESP_LOG_MAX, resp);
328 /* A refusal, as opposed to a malformed body, carries the node's reason —
329 * the one thing that tells an operator whether to top up gas, lower the
330 * amount or just try again. Hand it back rather than log it and forget. */
331 if ((err_out != NULL) && (err_max > 0U)) {
332 (void)eth_json_error_message(resp, err_out, err_max);
333 }
334 return false;
335 }
336 if (strncmp(result, "0x", 2U) != 0) {
337 ESP_LOGE(TAG, "send_raw_tx: result not a hash: %.*s",
338 RESP_LOG_MAX, result);
339 return false;
340 }
341
342 size_t hash_len = strlen(result);
343 if ((hash_len + 1U) > tx_hash_max) { return false; }
344
345 (void)CW_Utils::safe_memcpy(reinterpret_cast<uint8_t *>(tx_hash_out),
346 tx_hash_max,
347 reinterpret_cast<const uint8_t *>(result),
348 hash_len);
349 tx_hash_out[hash_len] = '\0';
350 ESP_LOGI(TAG, "Tx hash: %s", tx_hash_out);
351 return true;
352}
353
354bool eth_rpc_get_token_decimals(const char *token_addr, uint64_t *dec_out)
355{
356 if ((token_addr == NULL) || (dec_out == NULL)) { return false; }
357 char body[224];
358 int k = snprintf(body, sizeof(body),
359 "{\"jsonrpc\":\"2.0\",\"method\":\"eth_call\","
360 "\"params\":[{\"to\":\"%s\",\"data\":\"0x313ce567\"},"
361 "\"latest\"],\"id\":6}",
362 token_addr);
363 if ((k <= 0) || (static_cast<size_t>(k) >= sizeof(body))) { return false; }
364
365 char resp[RESP_BUF_SIZE];
366 if (!do_post(body, resp, sizeof(resp))) { return false; }
367 char result[RESULT_STR_MAX];
368 /* "0x" from an address with no code is refused by eth_json_hex_quantity. */
369 if (!eth_json_result_string(resp, result, sizeof(result)) ||
370 !eth_json_hex_quantity(result, dec_out)) {
371 ESP_LOGE(TAG, "decimals: no usable answer: %.*s", RESP_LOG_MAX, resp);
372 return false;
373 }
374 return true;
375}
376
377bool eth_rpc_err_already_known(const char *node_err)
378{
379 return (node_err != NULL) &&
380 ((strstr(node_err, "already known") != NULL) ||
381 (strstr(node_err, "known transaction") != NULL) ||
382 (strstr(node_err, "ALREADY_EXISTS") != NULL));
383}
384
386{
387 if ((want == NULL) || (want->tx_hash == NULL)) { return ETH_RPC_RECEIPT_RPC_ERROR; }
388 char body[160];
389 (void)snprintf(body, sizeof(body),
390 "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getTransactionReceipt\","
391 "\"params\":[\"%s\"],\"id\":3}",
392 want->tx_hash);
393
394 /* Receipts are large (the logsBloom field alone is 512 hex chars, plus
395 * the ERC-20 Transfer log) — use a dedicated heap buffer, a truncated
396 * body would fail the JSON parse. */
397 const size_t resp_size = 4096U;
398 char *resp = static_cast<char *>(malloc(resp_size));
399 if (resp == NULL) { return ETH_RPC_RECEIPT_RPC_ERROR; }
400
402 if (do_post(body, resp, resp_size)) {
403 switch (eth_json_receipt_check(resp, want)) {
408 ESP_LOGE(TAG, "receipt is not our payment: %.*s", RESP_LOG_MAX, resp);
409 verdict = ETH_RPC_RECEIPT_MISMATCH;
410 break;
411 case ETH_JSON_RECEIPT_ERROR: /* fall through */
412 default: verdict = ETH_RPC_RECEIPT_RPC_ERROR; break;
413 }
414 }
415 free(resp);
416 return verdict;
417}
bool eth_json_error_message(const char *resp, char *out, size_t out_size)
Extract the JSON-RPC error.message from a response body.
Definition eth_json.cpp:39
bool eth_json_result_string(const char *resp, char *out, size_t out_size)
Extract the top-level "result" string from a JSON-RPC response.
Definition eth_json.cpp:18
eth_json_receipt_t eth_json_receipt_check(const char *resp, const eth_receipt_expect_t *want)
Classify a receipt AND check it is the payment that was asked for.
Definition eth_json.cpp:162
JSON-RPC response parsing helpers — kept in their own unit (cJSON + CW_Utils only,...
static bool eth_json_hex_quantity(const char *hex, uint64_t *out)
Parse a JSON-RPC QUANTITY ("0x0", "0x1a", 64 hex chars) into a uint64, saturating instead of wrapping...
Definition eth_json.h:135
@ ETH_JSON_RECEIPT_REVERTED
Definition eth_json.h:32
@ ETH_JSON_RECEIPT_PENDING
Definition eth_json.h:30
@ ETH_JSON_RECEIPT_SUCCESS
Definition eth_json.h:31
@ ETH_JSON_RECEIPT_ERROR
Definition eth_json.h:27
@ ETH_JSON_RECEIPT_MISMATCH
Definition eth_json.h:33
static bool do_post(const char *body, char *resp_buf, size_t resp_buf_size)
POST a JSON-RPC body to the configured endpoint over HTTPS.
Definition eth_rpc.cpp:77
static const char * s_api_secret
Definition eth_rpc.cpp:59
static const char * s_from_addr
Definition eth_rpc.cpp:57
eth_rpc_receipt_result_t eth_rpc_get_tx_receipt(const eth_receipt_expect_t *want)
Poll the receipt of a broadcast transaction (one shot).
Definition eth_rpc.cpp:385
bool eth_rpc_get_nonce(uint64_t *nonce_out)
Fetch the confirmed transaction count (nonce) for from_addr.
Definition eth_rpc.cpp:157
static const char *const TAG
Definition eth_rpc.cpp:33
static const char * s_rpc_url
Definition eth_rpc.cpp:56
void eth_rpc_init(const char *rpc_url, const char *from_addr)
Set the RPC URL and the from-address used for nonce queries and ecrecover comparison.
Definition eth_rpc.cpp:122
void eth_rpc_set_auth(const char *project_id, const char *api_secret)
Optional: set Infura-style HTTP Basic Auth credentials.
Definition eth_rpc.cpp:146
static char hex_nibble(uint8_t n)
Convert a nibble value to its lowercase ASCII hex digit.
Definition eth_rpc.cpp:93
#define RESULT_STR_MAX
Definition eth_rpc.cpp:50
static const char * s_project_id
Definition eth_rpc.cpp:58
static void bytes_to_hex(const uint8_t *data, size_t len, char *out)
Hex-encode a byte buffer (lowercase, no prefix, no NUL).
Definition eth_rpc.cpp:106
#define RESP_BUF_SIZE
Definition eth_rpc.cpp:36
bool eth_rpc_get_token_decimals(const char *token_addr, uint64_t *dec_out)
Read an ERC-20 contract's decimals().
Definition eth_rpc.cpp:354
#define HEX_PER_BYTE
Definition eth_rpc.cpp:39
bool eth_rpc_get_balance(uint64_t *wei_out)
Fetch the native balance of from_addr, in wei.
Definition eth_rpc.cpp:212
bool eth_rpc_get_token_balance(const char *token_addr, uint64_t *units_out)
Fetch from_addr's balance of an ERC-20, via balanceOf over eth_call.
Definition eth_rpc.cpp:239
bool eth_rpc_send_raw_tx(const uint8_t *tx, size_t tx_len, char *tx_hash_out, size_t tx_hash_max, char *err_out, size_t err_max)
Broadcast a raw signed transaction (type-prefixed RLP bytes).
Definition eth_rpc.cpp:286
bool eth_rpc_err_already_known(const char *node_err)
true if a broadcast error message means the node already HAS this transaction ("already known",...
Definition eth_rpc.cpp:377
static const char * from_no_prefix(void)
The configured from-address with any "0x" prefix removed.
Definition eth_rpc.cpp:203
void eth_rpc_set_ca_cert(const char *ca_pem)
Optional: pin the RPC endpoint's TLS certificate.
Definition eth_rpc.cpp:152
static const char * s_ca_cert
Definition eth_rpc.cpp:60
#define RESP_LOG_MAX
Definition eth_rpc.cpp:43
static char s_from_buf[43]
Definition eth_rpc.cpp:131
bool eth_rpc_set_from(const char *addr)
Replace the from-address — the account a sale spends from.
Definition eth_rpc.cpp:133
#define NONCE_MAX
Definition eth_rpc.cpp:47
Ethereum JSON-RPC client over HTTPS (nonce / ecrecover parity / raw-tx broadcast / receipt polling)....
eth_rpc_receipt_result_t
Outcome of one eth_getTransactionReceipt poll.
Definition eth_rpc.h:36
@ ETH_RPC_RECEIPT_RPC_ERROR
Definition eth_rpc.h:40
@ ETH_RPC_RECEIPT_MISMATCH
Definition eth_rpc.h:41
@ ETH_RPC_RECEIPT_REVERTED
Definition eth_rpc.h:39
@ ETH_RPC_RECEIPT_PENDING
Definition eth_rpc.h:37
@ ETH_RPC_RECEIPT_SUCCESS
Definition eth_rpc.h:38
bool https_post_json(const char *url, const char *body, char *resp_buf, size_t resp_buf_size, const char *user, const char *pass, const char *ca_pem)
POST a JSON body over HTTPS and read the response.
One HTTPS JSON POST, shared by every RPC client in the firmware.
static esp_err_t ok(httpd_req_t *req, const char *msg)
200 with a plain-text message.
What a receipt must show for the payment to count — see eth_json_receipt_check.
Definition eth_json.h:40
const char * tx_hash
Definition eth_json.h:41