cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
pay.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
12
13#include "pos_app.h"
14
15/* BIP32 Ethereum derivation path: m/44'/60'/0'/0/0 */
16const uint8_t ETH_DERIVE_PATH[20] = {
17 0x80U, 0x00U, 0x00U, 0x2CU,
18 0x80U, 0x00U, 0x00U, 0x3CU,
19 0x80U, 0x00U, 0x00U, 0x00U,
20 0x00U, 0x00U, 0x00U, 0x00U,
21 0x00U, 0x00U, 0x00U, 0x00U,
22};
23
24/* The payout strings the dual stores are built from: operator-set, else config.h.
25 * Resolved once at boot; a change during setup applies through a restart, so
26 * there is one validated path. Ethereum is "0x"-prefixed for eth_addr_parse. */
29
31
33 { POS_CHAIN_ETH_USDC, ADDR_USDC, ADDR_USDC_MAIN, "ADDR_USDC" },
39};
40
41const size_t TOKEN_CFG_COUNT = sizeof(TOKEN_CFG) / sizeof(TOKEN_CFG[0]);
42
43
46 if ((unsigned)chain >= (unsigned)POS_CHAIN__COUNT) { return NULL; }
47 return pos_asset_of(chain)->native ? NULL : &s_token[chain];
48}
49
57static bool token_parse(token_t *t, bool tron, CW_CryptoProvider &crypto) {
58 if (!tron) {
59 t->ok = eth_addr_parse(t->str, t->addr.addr) &&
61 return t->ok;
62 }
63 uint8_t c21[CW_TRON_ADDRESS_BYTES];
64 uint8_t c21_echo[CW_TRON_ADDRESS_BYTES];
65 t->ok = CW_Tron::decodeAddress(t->str, crypto, c21) &&
66 CW_Tron::decodeAddress(t->str, crypto, c21_echo);
67 if (t->ok) {
68 (void)CW_Utils::safe_memcpy(t->addr.addr, sizeof(t->addr.addr),
69 &c21[1], ETH_ADDR_LEN);
70 (void)CW_Utils::safe_memcpy(t->addr.addr_echo, sizeof(t->addr.addr_echo),
71 &c21_echo[1], ETH_ADDR_LEN);
72 }
73 return t->ok;
74}
75
84void token_load(const token_cfg_t *cfg, CW_CryptoProvider &crypto) {
85 token_t *t = &s_token[cfg->chain];
86 const bool tron = pos_chain_is_tron(cfg->chain);
87 if (settings_get_contract(cfg->chain, t->str, sizeof(t->str))) {
88 t->checked = false; /* operator-set: token_decimals_ok asks the chain */
89 if (token_parse(t, tron, crypto)) { return; }
90 ESP_LOGW(TAG, "stored %s contract not usable - trying config.h",
92 }
93 (void)snprintf(t->str, sizeof(t->str), "%s%s", tron ? "" : "0x",
94 settings_net_str(cfg->test, cfg->main));
95 t->checked = true; /* config.h: part of the signed image, vetted at build */
96 if (!token_parse(t, tron, crypto)) {
97 const pos_asset_t *a = pos_asset_of(cfg->chain);
98 ESP_LOGW(TAG, "%s not usable - %s on %s disabled", cfg->name, a->ticker,
99 pos_net_info(a->net)->name);
100 }
101}
102
115bool token_decimals_ok(pos_chain_t chain, char *err, size_t err_max)
116{
117 token_t *t = active_token(chain);
118 if ((t == NULL) || t->checked) { return true; }
119
120 uint64_t dec = 0U;
121 bool asked = false;
122 if (pos_chain_is_tron(chain)) {
123 uint8_t c21[CW_TRON_ADDRESS_BYTES];
124 c21[0] = CW_TRON_ADDRESS_PREFIX;
125 (void)CW_Utils::safe_memcpy(&c21[1], sizeof(c21) - 1U, t->addr.addr,
127 char hex[TRON_ADDR_HEX_LEN + 1U];
128 tron_addr_to_hex(c21, hex, sizeof(hex));
129 asked = tron_rpc_get_trc20_decimals(hex, &dec);
130 } else {
132 asked = eth_rpc_get_token_decimals(t->str, &dec);
134 }
135 if (asked && (dec == 6U)) {
136 t->checked = true;
137 return true;
138 }
139 if (asked) {
140 (void)snprintf(err, err_max, "Token contract has %u decimals - set it again",
141 static_cast<unsigned>((dec > 99U) ? 99U : dec));
142 } else {
143 (void)snprintf(err, err_max, "Could not check the token contract");
144 }
145 ESP_LOGW(TAG, "contract %s refused: %s", t->str, err);
146 return false;
147}
148
155extern "C" void ui_refresh_addresses_for(uint8_t c) {
156 const pos_chain_t chain = static_cast<pos_chain_t>(c);
157 const token_t *token = active_token(chain);
158 const char *payee = pos_chain_is_tron(chain) ? s_payout_tron : s_payout_eth;
159 if (token != NULL) {
160 ui_set_addresses(token->str, payee);
161 } else if (pos_chain_is_tron(chain)) {
162 ui_set_addresses("Native TRX (no contract)", payee);
163 } else {
164 ui_set_addresses(pos_chain_is_polygon(chain) ? "Native POL (no contract)"
165 : "Native ETH (no contract)",
166 payee);
167 }
168}
169
170extern "C" void ui_refresh_addresses(void) {
171 ui_refresh_addresses_for(static_cast<uint8_t>(settings_get_chain()));
172}
173
174/* Dual-stored recipient (§3.2/§7.1): ADDR_TO parsed twice at boot into two
175 * independent copies, reconciled before calldata encode and before signing so
176 * a transient flip on the working copy can't redirect funds. */
178
179/* A stored payout address that failed its checksum at boot, [0] EVM, [1] Tron.
180 * The dual store then holds the config.h fallback so the boot can continue,
181 * and every sale on that family is refused — see resolve_evm_payout. */
182bool s_payout_bad[2] = { false, false };
183
184/* The Tron recipient's own dual store. */
186
189 return chain_is_tron() ? &s_tron_dest : &s_dest;
190}
191
193
194/* fmt_coin() — the fee ceiling as text, rounded up — is in money.h. */
195
201void sale_fee_text(char *out, size_t n)
202{
203 out[0] = '\0';
204 char amt[32];
205 if (chain_is_tron()) {
206 if (active_token() == NULL) { return; }
207 fmt_coin(amt, sizeof(amt), TRON_TRC20_FEE_LIMIT_SUN, 6U, "TRX");
208 } else {
209 /* gas limit ~1e5 at most, max fee a uint32 of Gwei x 1e9: ~25 bits of
210 * headroom in the product, as in evm_balance_ok. */
211 const uint64_t wei = (uint64_t)(chain_is_native_evm() ? GAS_LIMIT_NATIVE
212 : GAS_LIMIT_ERC20)
213 * s_sale_fee.max_fee;
214 fmt_coin(amt, sizeof(amt), wei, 18U, chain_is_polygon() ? "POL" : "ETH");
215 }
216 (void)snprintf(out, n, "Fee up to %s", amt);
217}
218
220
229{
230 inflight_t rec = *fl;
231 rec.active = true;
232 rec.broadcast_known = false;
233 (void)settings_inflight_save(&rec, sizeof(rec));
234}
235
237uint64_t wall_ms(void)
238{
239 struct timeval tv;
240 if ((gettimeofday(&tv, NULL) != 0) || (tv.tv_sec < 1600000000)) { return 0U; }
241 return (static_cast<uint64_t>(tv.tv_sec) * 1000U) +
242 (static_cast<uint64_t>(tv.tv_usec) / 1000U);
243}
244
256{
257 inflight_t *fl = &s_inflight;
258 /* The sale's own endpoint. A no-op straight after a sale; after a reset it
259 * is what points a resumed EVM poll at the network the hash is on. */
260 if (!fl->tron) { eth_rpc_select_for(fl->polygon); }
262 fl->broadcast_known ? "Waiting for the block"
263 : "Checking it was sent");
264 const eth_receipt_expect_t want = {
265 fl->hash, fl->to, fl->token ? fl->payee : NULL, fl->amount
266 };
268 bool expired = false;
269 const int64_t deadline = esp_timer_get_time() + 120LL * 1000000LL;
270 for (;;) {
271 wdt_feed(); /* two minutes of polling is on purpose */
272 /* Count down so the wait does not look like a hang. Set in place:
273 * ui_show_tx_status here would restart the spinner. */
274 char left[32];
275 snprintf(left, sizeof(left), "%d s remaining",
276 static_cast<int>((deadline - esp_timer_get_time() + 999999LL)
277 / 1000000LL));
278 ui_set_tx_info(left);
280 : eth_rpc_get_tx_receipt(&want);
281 if ((rc == ETH_RPC_RECEIPT_SUCCESS) || (rc == ETH_RPC_RECEIPT_REVERTED) ||
282 (rc == ETH_RPC_RECEIPT_MISMATCH)) {
283 break;
284 }
285 /* Tron: the chain refuses a transaction past raw_data.expiration, so a
286 * node that still has never heard of it 10 s after that (one more
287 * block) means it will never land. Only on "not found" — an RPC error
288 * says nothing either way. */
289 const uint64_t now = wall_ms();
290 if (fl->tron && (rc == ETH_RPC_RECEIPT_PENDING) && (now != 0U) &&
291 (now > (fl->expiration_ms + 10000U))) {
292 expired = true;
293 break;
294 }
295 if (esp_timer_get_time() >= deadline) { break; }
296 vTaskDelay(pdMS_TO_TICKS(4000));
297 }
298
299 if (rc == ETH_RPC_RECEIPT_SUCCESS) {
300 /* §4: render PAID only if the monotonic gate holds — the on-chain
301 * APPROVED verdict AND amount/recipient still self-consistent through
302 * the decide→render window. */
303 fl->active = false;
307 if (IS_TRUE32(decision)) {
308 ESP_LOGI(TAG, "Tx confirmed on-chain");
310 } else {
311 /* Mined OK but the integrity gate failed — never show PAID on a
312 * corrupted decision. */
313 pos_handle_anomaly("final decision gate");
314 ESP_LOGE(TAG, "Integrity gate failed post-receipt: %s", fl->hash);
315 ui_show_tx_status(UI_TX_STATE_FAILED, "Integrity check failed");
316 }
317 } else if (rc == ETH_RPC_RECEIPT_REVERTED) {
318 /* A transfer the node accepted and the chain then rejected is, for a
319 * till, almost always the card not holding enough of the token.
320 * ponytail: a guess, not a reason. Reading the revert data back needs
321 * an eth_call replay at the mined block. */
322 fl->active = false;
326 ESP_LOGE(TAG, "Tx reverted on-chain: %s", fl->hash);
328 "Reverted - check the card's balance");
329 } else if (expired) {
330 fl->active = false;
332 ESP_LOGW(TAG, "Tron tx %s expired unseen - not sent", fl->hash);
333 ui_show_tx_status(UI_TX_STATE_FAILED, "Not sent - nothing was charged");
334 } else {
335 if (rc == ETH_RPC_RECEIPT_MISMATCH) {
336 /* The node's receipt is not our transfer. Whether ours landed is
337 * unknown — the node that would say is the one lying. */
338 pos_handle_anomaly("receipt mismatch");
339 }
340 ESP_LOGW(TAG, "Tx %s unconfirmed after 120 s", fl->hash);
342 }
343}
344
345/* The family is read once per payment: the operator can switch it between
346 * sales, but never mid-sale. */
347bcast_t pay_sign_and_broadcast(CryptnoxWallet &wallet, Pn532NfcTransport &transport,
348 CW_CryptoProvider &crypto, const pos_amount_t *amount,
349 const char *pin, size_t pin_chars, inflight_t *fl,
350 char *err_out, size_t err_max)
351{
352 /* Again here, not only at the confirm step: free once checked, and no route
353 * to the card skips it. */
354 if (!token_decimals_ok(settings_get_chain(), err_out, err_max)) {
355 return BCAST_FAILED;
356 }
357 return chain_is_tron()
358 ? sign_and_broadcast_tron(wallet, transport, crypto, amount, &s_tron_dest,
359 active_token(), pin, pin_chars, fl,
360 err_out, err_max)
361 : sign_and_broadcast(wallet, transport, amount, &s_dest, pin, pin_chars,
362 fl, err_out, err_max);
363}
static const pos_asset_t * pos_asset_of(pos_chain_t chain)
The row describing chain.
Definition assets.h:145
static bool pos_chain_is_tron(pos_chain_t c)
true for the Tron selections; every other one is EVM.
Definition assets.h:165
static bool pos_chain_is_polygon(pos_chain_t c)
true for the Polygon selections. EVM like Ethereum, but its own endpoint, chain id and contracts.
Definition assets.h:174
static const pos_net_info_t * pos_net_info(pos_net_t net)
Names for net, or Ethereum's for a value out of range.
Definition assets.h:190
#define GAS_LIMIT_NATIVE
#define TRON_ADDR_USDC_MAIN
#define ADDR_USDT
#define TRON_ADDR_USDT
#define ADDR_USDT_MAIN
#define TRON_ADDR_USDC
#define POLY_ADDR_USDT
#define TRON_TRC20_FEE_LIMIT_SUN
#define ADDR_USDC_MAIN
#define POLY_ADDR_USDC_MAIN
#define POLY_ADDR_USDT_MAIN
#define POLY_ADDR_USDC
#define TRON_ADDR_USDT_MAIN
bool eth_addr_parse(const char *hex, uint8_t out[ETH_ADDR_LEN])
Parse a 20-byte Ethereum address from a hex string.
Definition eth_addr.cpp:34
#define ETH_ADDR_LEN
Length of a raw (binary) Ethereum address, in bytes.
Definition eth_addr.h:25
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
static const char *const TAG
Definition eth_rpc.cpp:33
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
eth_rpc_receipt_result_t
Outcome of one eth_getTransactionReceipt poll.
Definition eth_rpc.h:36
@ 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
void pos_handle_anomaly(const char *where)
Report an anomaly: bump a self-checked persisted counter, log it, and keep a local ring buffer for on...
Definition hardening.cpp:99
#define IS_TRUE32(x)
Definition hardening.h:43
static bool32 run_payment_decision(const pos_amount_t *amount, const pos_addr_t *to, pos_verdict_t verdict)
§4 decision gate: return TRUE32 only if amount and recipient are self-consistent AND the verdict is t...
Definition hardening.h:118
#define POS_VERDICT_APPROVED
Definition hardening.h:47
uint32_t bool32
Definition hardening.h:38
#define POS_VERDICT_DECLINED
Definition hardening.h:48
static void fmt_coin(char *out, size_t n, uint64_t v, unsigned dec, const char *coin)
"0.0013 ETH": v base units of a dec-decimal coin, to six places, rounded UP — it is a ceiling,...
Definition money.h:177
const token_cfg_t TOKEN_CFG[]
Definition pay.cpp:32
static bool token_parse(token_t *t, bool tron, CW_CryptoProvider &crypto)
Parse t->str into its dual store, twice and independently.
Definition pay.cpp:57
token_t * active_token(pos_chain_t chain)
The selection's token, or NULL for a native coin.
Definition pay.cpp:45
void token_load(const token_cfg_t *cfg, CW_CryptoProvider &crypto)
Load one token at boot: the operator's contract if one is set and parses, config.h otherwise.
Definition pay.cpp:84
char s_payout_tron[SETTINGS_PAYOUT_MAX]
Definition pay.cpp:28
const size_t TOKEN_CFG_COUNT
Definition pay.cpp:41
uint64_t wall_ms(void)
Unix time in ms, 0 while the clock is unset.
Definition pay.cpp:237
sale_fee_t s_sale_fee
Definition pay.cpp:192
bool token_decimals_ok(pos_chain_t chain, char *err, size_t err_max)
Before the first sale in an operator-set token, read its decimals() and refuse anything but 6.
Definition pay.cpp:115
const uint8_t ETH_DERIVE_PATH[20]
Definition pay.cpp:16
bcast_t pay_sign_and_broadcast(CryptnoxWallet &wallet, Pn532NfcTransport &transport, CW_CryptoProvider &crypto, const pos_amount_t *amount, const char *pin, size_t pin_chars, inflight_t *fl, char *err_out, size_t err_max)
Sign and broadcast the reconciled sale on whichever family is selected. The family is read once,...
Definition pay.cpp:347
void sale_fee_text(char *out, size_t n)
The most network fee the customer's card can be charged on top of the sale, for the confirm screen....
Definition pay.cpp:201
void ui_refresh_addresses_for(uint8_t c)
Point the UI's address rows at the selected chain.
Definition pay.cpp:155
pos_addr_t s_dest
Definition pay.cpp:177
void ui_refresh_addresses(void)
Implemented by main: repoint those two rows at the selected chain.
Definition pay.cpp:170
void settle_inflight(void)
Poll the in-flight sale for up to 120 s and show what the chain says.
Definition pay.cpp:255
pos_addr_t s_tron_dest
Definition pay.cpp:185
void inflight_persist(const inflight_t *fl)
Write the sale to NVS just before it leaves the terminal.
Definition pay.cpp:228
const pos_addr_t * active_dest(void)
The reconciled recipient for the chain currently selected.
Definition pay.cpp:188
inflight_t s_inflight
Definition pay.cpp:219
bool s_payout_bad[2]
Definition pay.cpp:182
char s_payout_eth[SETTINGS_PAYOUT_MAX]
Definition pay.cpp:27
token_t s_token[POS_CHAIN__COUNT]
Definition pay.cpp:30
void eth_rpc_select_for(bool polygon)
Point eth_rpc at the endpoint for the selected EVM network.
Definition pay_evm.cpp:26
void eth_rpc_select(void)
Definition pay_evm.cpp:51
bcast_t sign_and_broadcast(CryptnoxWallet &wallet, Pn532NfcTransport &transport, const pos_amount_t *amount, const pos_addr_t *to, const char *pin, size_t pin_chars, inflight_t *fl, char *err_out, size_t err_max)
Sign an EVM token or coin transfer on the card and broadcast it.
Definition pay_evm.cpp:165
eth_rpc_receipt_result_t tron_receipt_as_eth(tron_receipt_t r)
Map a Tron receipt onto the Ethereum verdicts the UI flow uses.
Definition pay_tron.cpp:309
bcast_t sign_and_broadcast_tron(CryptnoxWallet &wallet, Pn532NfcTransport &transport, CW_CryptoProvider &crypto, const pos_amount_t *amount, const pos_addr_t *to, const token_t *token, const char *pin, size_t pin_chars, inflight_t *fl, char *err_out, size_t err_max)
Sign a Tron transfer on the card and broadcast it — TRX or TRC-20.
Definition pay_tron.cpp:125
void tron_addr_to_hex(const uint8_t *addr21, char *out, size_t n)
Format a raw 21-byte Tron address as the "41..." hex the API wants.
Definition pay_tron.cpp:25
Private to the application files (main, boot, card_io, pay, pay_evm, pay_tron): the state they share ...
static bool chain_is_tron(void)
true when the operator has switched the terminal to Tron.
Definition pos_app.h:74
bcast_t
Definition pos_app.h:148
@ BCAST_FAILED
Definition pos_app.h:149
static bool chain_is_polygon(void)
true when the terminal is charging on Polygon rather than Ethereum.
Definition pos_app.h:79
static bool chain_is_native_evm(void)
true when charging in the network's own coin (ETH / POL), not a token.
Definition pos_app.h:84
static bool expired(void)
True once the portal's own window has closed.
const char * settings_net_str(const char *testnet, const char *mainnet)
Pick the string belonging to the network the terminal is on.
Definition settings.cpp:210
pos_chain_t settings_get_chain(void)
Selected chain, or POS_CHAIN_ETH_USDC if never set.
Definition settings.cpp:165
bool settings_get_contract(pos_chain_t chain, char *out, size_t n)
Read the token-contract address for a network.
Definition settings.cpp:549
void settings_inflight_clear(void)
Drop the persisted sale. Writes nothing when there is none.
Definition settings.cpp:673
bool settings_inflight_save(const void *rec, size_t n)
Persist the sale between broadcast and verdict (opaque record).
Definition settings.cpp:645
pos_chain_t
Which chain (and therefore which asset) the terminal charges in.
Definition settings.h:33
@ POS_CHAIN_POLY_USDT
Definition settings.h:39
@ POS_CHAIN_POLY_USDC
Definition settings.h:38
@ POS_CHAIN_ETH_USDC
Definition settings.h:34
@ POS_CHAIN__COUNT
Definition settings.h:43
@ POS_CHAIN_TRON_USDT
Definition settings.h:36
@ POS_CHAIN_TRON_USDC
Definition settings.h:40
@ POS_CHAIN_ETH_USDT
Definition settings.h:37
#define SETTINGS_PAYOUT_MAX
Longest payout address plus NUL — "0x" + 40 hex, or 34 base58 Tron.
Definition settings.h:214
What a receipt must show for the payment to count — see eth_json_receipt_check.
Definition eth_json.h:40
bool tron
Definition pos_app.h:159
char hash[72]
Definition pos_app.h:160
uint8_t to[ETH_ADDR_LEN]
Definition pos_app.h:162
uint8_t payee[ETH_ADDR_LEN]
Definition pos_app.h:163
bool active
Definition pos_app.h:158
bool polygon
Definition pos_app.h:168
uint64_t expiration_ms
Definition pos_app.h:166
pos_amount_t decided
Definition pos_app.h:169
bool token
Definition pos_app.h:164
uint64_t amount
Definition pos_app.h:165
bool broadcast_known
Definition pos_app.h:167
uint8_t addr_echo[ETH_ADDR_LEN]
Definition hardening.h:66
uint8_t addr[ETH_ADDR_LEN]
Definition hardening.h:65
One selectable asset.
Definition assets.h:66
pos_net_t net
Definition assets.h:78
bool native
Definition assets.h:79
const char * ticker
Definition assets.h:68
const char * main
Definition pos_app.h:111
const char * name
Definition pos_app.h:112
pos_chain_t chain
Definition pos_app.h:109
const char * test
Definition pos_app.h:110
A token's contract, dual-stored.
Definition pos_app.h:98
pos_addr_t addr
Definition pos_app.h:100
bool ok
Definition pos_app.h:101
bool checked
Definition pos_app.h:102
char str[SETTINGS_PAYOUT_MAX]
Definition pos_app.h:99
tron_receipt_t tron_rpc_get_receipt(const char *txid_hex)
Poll a broadcast transaction's receipt (one shot).
Definition tron_rpc.cpp:511
bool tron_rpc_get_trc20_decimals(const char *contract_hex, uint64_t *dec_out)
Read a TRC-20 contract's decimals() — see eth_rpc_get_token_decimals for why it has to be 6.
Definition tron_rpc.cpp:330
#define TRON_ADDR_HEX_LEN
Hex length of a Tron address (21 bytes: 0x41 || 20-byte key hash).
Definition tron_tx.h:38
void ui_set_addresses(const char *token_contract, const char *dest_addr)
Provide the token contract and destination addresses for the confirm screen and the settings "Tx" tab...
Definition ui.cpp:764
void ui_show_tx_status(ui_tx_state_t state, const char *info)
Switch to the transaction-status screen.
Definition ui.cpp:898
void ui_set_tx_info(const char *info)
Replace the transaction screen's info line without rebuilding it.
Definition ui.cpp:910
@ UI_TX_STATE_UNCONFIRMED
Definition ui.h:102
@ UI_TX_STATE_CONFIRMING
Definition ui.h:99
@ UI_TX_STATE_DONE
Definition ui.h:100
@ UI_TX_STATE_FAILED
Definition ui.h:101
static void wdt_feed(void)
Feed the task watchdog if, and only if, the calling task is subscribed.
Definition wdt.h:28