cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
pay_tron.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
15
16#include "pos_app.h"
17
25void tron_addr_to_hex(const uint8_t *addr21, char *out, size_t n)
26{
27 if (n < (TRON_ADDR_HEX_LEN + 1U)) { if (n > 0U) { out[0] = '\0'; } return; }
28 for (size_t i = 0U; i < CW_TRON_ADDRESS_BYTES; i++) {
29 (void)snprintf(&out[i * 2U], 3U, "%02x",
30 static_cast<unsigned>(addr21[i]));
31 }
32}
33
51static bool tron_balance_ok(const char *owner_hex, const char *token_hex,
52 uint64_t amount, char *err, size_t err_max)
53{
54 const bool is_token = (token_hex != NULL) && (token_hex[0] != '\0');
55
56 uint64_t trx_sun = 0U;
57 if (!tron_rpc_get_balance(owner_hex, &trx_sun)) {
58 ESP_LOGW(TAG, "pre-flight: TRX balance read failed - letting it run");
59 return true;
60 }
61
62 if (!is_token) {
63 /* A TRX transfer pays its amount out of this balance, and its bandwidth
64 * out of the free daily allowance — so the balance is the whole test. */
65 if (trx_sun < amount) {
66 (void)snprintf(err, err_max, "Not enough TRX for this amount");
67 return false;
68 }
69 return true;
70 }
71
72 uint64_t have = 0U;
73 if (!tron_rpc_get_trc20_balance(owner_hex, token_hex, &have)) {
74 ESP_LOGW(TAG, "pre-flight: token balance read failed - letting it run");
75 } else if (have < amount) {
76 (void)snprintf(err, err_max, "Not enough %s on the card",
78 return false;
79 } else {
80 /* enough tokens — the fee is the remaining question */
81 }
82
83 /* The fee: TRC-20 burns energy from a stake or TRX, so the only certain
84 * refusal is an account with neither. Pricing the burn is left out: getting
85 * it wrong would refuse sales that would have settled.
86 *
87 * ponytail: zero-or-not. Price the energy properly if a thin-but-nonzero
88 * balance turns out to be a real support case. */
89 if (trx_sun == 0U) {
90 uint64_t energy = 0U;
91 if (tron_rpc_get_energy(owner_hex, &energy) && (energy == 0U)) {
92 (void)snprintf(err, err_max, "No TRX for the network fee");
93 return false;
94 }
95 }
96 return true;
97}
98
125bcast_t sign_and_broadcast_tron(CryptnoxWallet &wallet,
126 Pn532NfcTransport &transport,
127 CW_CryptoProvider &crypto,
128 const pos_amount_t *amount,
129 const pos_addr_t *to,
130 const token_t *token,
131 const char *pin, size_t pin_chars,
132 inflight_t *fl,
133 char *err_out, size_t err_max)
134{
135 if (!IS_TRUE32(amount_consistent(amount)) ||
137 ((token != NULL) && !IS_TRUE32(address_consistent(&token->addr)))) {
138 pos_handle_anomaly("pre-create reconcile (tron)");
139 (void)snprintf(err_out, err_max, "Integrity check failed");
140 return BCAST_FAILED;
141 }
142 if ((token != NULL) && !token->ok) {
143 (void)snprintf(err_out, err_max, "Token contract not configured");
144 return BCAST_FAILED;
145 }
146 const uint64_t amount_sun = amount->amount_minor; /* both 6 decimals */
147
148 /* Recipient in Tron form, built from the reconciled copy. */
149 uint8_t to21[CW_TRON_ADDRESS_BYTES];
150 to21[0] = CW_TRON_ADDRESS_PREFIX;
151 (void)CW_Utils::safe_memcpy(&to21[1], sizeof(to21) - 1U,
152 to->addr, ETH_ADDR_LEN);
153 char to_hex[TRON_ADDR_HEX_LEN + 1U];
154 tron_addr_to_hex(to21, to_hex, sizeof(to_hex));
155
156 /* Same for the token contract — the node is told which contract to call,
157 * so that address has to come from the reconciled store too. */
158 char token_hex[TRON_ADDR_HEX_LEN + 1U] = { 0 };
159 if (token != NULL) {
160 uint8_t c21[CW_TRON_ADDRESS_BYTES];
161 c21[0] = CW_TRON_ADDRESS_PREFIX;
162 (void)CW_Utils::safe_memcpy(&c21[1], sizeof(c21) - 1U,
163 token->addr.addr, ETH_ADDR_LEN);
164 tron_addr_to_hex(c21, token_hex, sizeof(token_hex));
165 }
166
167 /* The card comes before the RPC on this path: Tron will not serialise a
168 * transfer without the sender, and the sender is whoever tapped. */
169 CW_SecureSession session;
170 if (!card_connect(wallet, transport, session)) {
171 if (!s_user_cancelled) {
172 (void)snprintf(err_out, err_max, "%s",
173 (s_card_fault != NULL) ? s_card_fault
174 : "Card not found");
175 }
176 return BCAST_FAILED;
177 }
178
179 if (!s_user_cancelled) {
181 }
182
183 /* Read this card's Tron account. The export needs the PIN verified for the
184 * session first; the same PIN then rides along with the SIGN APDU. */
185 char owner_hex[TRON_ADDR_HEX_LEN + 1] = { 0 };
186 char owner_b58[CW_TRON_ADDRESS_STR_SIZE] = { 0 };
187 uint8_t pubkey[64];
188 uint8_t owner21[CW_TRON_ADDRESS_BYTES];
189 WipeGuard g_pub(pubkey, sizeof(pubkey));
190 if (!wallet.verifyPin(session,
191 reinterpret_cast<const uint8_t *>(pin),
192 static_cast<uint8_t>(pin_chars))) {
193 (void)snprintf(err_out, err_max, "%s", pin_fail_text(transport, "Wrong PIN"));
194 wallet.disconnect(session);
195 return BCAST_FAILED;
196 }
197 if (!wallet.getPublicKey(session, CW_TRON_DERIVE_PATH,
198 CW_TRON_PATH_LENGTH, pubkey) ||
199 !CW_Tron::addressBytesFromPublicKey(pubkey, owner21) ||
200 !CW_Tron::encodeAddress(owner21, crypto,
201 owner_b58, sizeof(owner_b58))) {
202 wallet.disconnect(session);
203 (void)snprintf(err_out, err_max, "Cannot read card address");
204 return BCAST_FAILED;
205 }
206 tron_addr_to_hex(owner21, owner_hex, sizeof(owner_hex));
207 ESP_LOGI(TAG, "Tron sender (this card): %s", owner_b58);
208
209 /* Before create, signature and broadcast: nothing is spent on a no. */
210 if (!tron_balance_ok(owner_hex, (token != NULL) ? token_hex : NULL,
211 amount_sun, err_out, err_max)) {
212 wallet.disconnect(session);
213 return BCAST_FAILED;
214 }
215
216 tron_tx_ctx_t tx;
217 const bool built = (token == NULL)
218 ? tron_rpc_create_transfer(owner_hex, to_hex, amount_sun, &tx)
219 : tron_rpc_create_trc20_transfer(owner_hex, token_hex, to_hex,
220 amount_sun, TRON_TRC20_FEE_LIMIT_SUN,
221 &tx);
222 if (!built) {
223 wallet.disconnect(session);
224 /* Most often an account the chain has never seen funded: Tron will not
225 * build a transfer from it, and a TRC-20 call additionally needs TRX
226 * for the energy the transfer burns. */
227 (void)snprintf(err_out, err_max, "No %s on %.12s...",
228 (token == NULL) ? "TRX" : "funds/TRX", owner_b58);
229 return BCAST_FAILED;
230 }
231
232 /* Last reconcile before the card produces an irreversible signature.
233 * Immediately before card_sign(), which does nothing else first. */
234 if (!IS_TRUE32(amount_consistent(amount)) ||
236 ((token != NULL) && !IS_TRUE32(address_consistent(&token->addr)))) {
237 pos_handle_anomaly("pre-sign reconcile (tron)");
238 (void)snprintf(err_out, err_max, "Integrity check failed");
239 wallet.disconnect(session);
240 return BCAST_FAILED;
241 }
242
243 uint8_t rs[64];
244 WipeGuard g_rs(rs, sizeof(rs));
245 if (!card_sign(wallet, session, tx.txid,
246 static_cast<uint8_t>(sizeof(tx.txid)),
247 CW_TRON_DERIVE_PATH, CW_TRON_PATH_LENGTH,
248 pin, pin_chars, rs, err_out, err_max)) {
249 return BCAST_FAILED;
250 }
251 const uint8_t *sig_r = rs;
252 const uint8_t *sig_s = rs + 32U;
253
254 if (!s_user_cancelled) {
256 }
257
258 /* Tron wants r || s || recovery id, and nothing on the card or in the API
259 * tells us the id — so try 0 and fall back to 1, the same way the SDK's
260 * TronSigning example does. A wrong id recovers to some other account, the
261 * node answers SIGERROR and nothing is committed, so the retry is safe. The
262 * two attempts carry identical raw_data, hence the identical txID: only one
263 * transaction can ever exist. */
264 uint8_t sig[65];
265 WipeGuard g_sig65(sig, sizeof(sig));
266 (void)CW_Utils::safe_memcpy(sig, sizeof(sig), sig_r, 32U);
267 (void)CW_Utils::safe_memcpy(sig + 32U, sizeof(sig) - 32U, sig_s, 32U);
268
269 /* last cancel check right before the irreversible broadcast. */
270 if (s_user_cancelled) {
271 return BCAST_FAILED;
272 }
273
274 /* Before the broadcast: the txID is fixed by the verified raw_data, and a
275 * reset between the first attempt and its answer is what this record is for. */
276 (void)snprintf(fl->hash, sizeof(fl->hash), "%s", tx.txid_hex);
277 fl->tron = true;
280
281 bool sent = false;
282 for (uint8_t v = 0U; (v < 2U) && !sent; v++) {
283 sig[64] = v;
284 sent = tron_rpc_broadcast(&tx, sig);
285 if (!sent) {
286 ESP_LOGW(TAG, "broadcast rejected with v=%u", static_cast<unsigned>(v));
287 }
288 }
289 /* ~4 KB of locals on top of TLS in a 16 KB task: log the headroom. */
290 ESP_LOGI(TAG, "main stack after Tron broadcast: %u bytes free",
291 static_cast<unsigned>(uxTaskGetStackHighWaterMark(NULL)));
292
293 /* "Refused" and "we never heard back" are not the same thing, and only the
294 * chain can tell them apart. A v=0 broadcast that landed but whose response
295 * was lost leaves this loop with sent == false, and the v=1 retry is then
296 * refused as a duplicate. The txID is fixed by the verified raw_data, so
297 * both attempts are the same transaction: the receipt poll settles it, and
298 * waits past raw_data's expiration before calling it not sent — until then
299 * the chain can still include it. */
300 if (!sent) {
301 ESP_LOGW(TAG, "broadcast not confirmed - polling %s until it expires",
302 tx.txid_hex);
303 return BCAST_UNKNOWN;
304 }
305 return BCAST_SENT;
306}
307
static const pos_asset_t * pos_asset_of(pos_chain_t chain)
The row describing chain.
Definition assets.h:145
bool card_sign(CryptnoxWallet &wallet, CW_SecureSession &session, const uint8_t *hash, uint8_t hash_len, const uint8_t *path, uint8_t path_len, const char *pin, size_t pin_chars, uint8_t rs_out[64], char *err_out, size_t err_max)
Have the card sign hash, then close the session.
Definition card_io.cpp:160
const char * s_card_fault
Definition card_io.cpp:18
bool card_connect(CryptnoxWallet &wallet, Pn532NfcTransport &transport, CW_SecureSession &session, bool setup)
Wait for a card and open a secure channel, cancellable from the UI.
Definition card_io.cpp:79
const char * pin_fail_text(Pn532NfcTransport &transport, const char *wrong)
Why verifyPin said no: the PIN, or the card leaving the field.
Definition card_io.cpp:52
#define TRON_TRC20_FEE_LIMIT_SUN
#define ETH_ADDR_LEN
Length of a raw (binary) Ethereum address, in bytes.
Definition eth_addr.h:25
static const char *const TAG
Definition eth_rpc.cpp:33
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_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 amount_consistent(const pos_amount_t *a)
Definition hardening.h:93
static bool32 address_consistent(const pos_addr_t *a)
Definition hardening.h:98
std::atomic< bool > s_user_cancelled
Definition main.cpp:27
void inflight_persist(const inflight_t *fl)
Write the sale to NVS just before it leaves the terminal.
Definition pay.cpp:228
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
static bool tron_balance_ok(const char *owner_hex, const char *token_hex, uint64_t amount, char *err, size_t err_max)
Refuse a Tron sale the tapped card cannot fund, before it signs.
Definition pay_tron.cpp:51
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 ...
bcast_t
Definition pos_app.h:148
@ BCAST_UNKNOWN
Definition pos_app.h:151
@ BCAST_FAILED
Definition pos_app.h:149
@ BCAST_SENT
Definition pos_app.h:150
pos_chain_t settings_get_chain(void)
Selected chain, or POS_CHAIN_ETH_USDC if never set.
Definition settings.cpp:165
Scrubs a buffer with CW_Utils::secure_wipe when it leaves scope.
Definition pos_app.h:215
bool tron
Definition pos_app.h:159
char hash[72]
Definition pos_app.h:160
uint64_t expiration_ms
Definition pos_app.h:166
uint8_t addr[ETH_ADDR_LEN]
Definition hardening.h:65
uint64_t amount_minor
Definition hardening.h:59
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
A created-and-verified transfer, ready to sign and broadcast.
Definition tron_rpc.h:38
uint8_t txid[32]
Definition tron_rpc.h:39
char txid_hex[65]
Definition tron_rpc.h:40
uint64_t expiration_ms
Definition tron_rpc.h:42
bool tron_rpc_get_trc20_balance(const char *owner_hex, const char *contract_hex, uint64_t *units_out)
Read an account's TRC-20 balance via a constant balanceOf call.
Definition tron_rpc.cpp:269
bool tron_rpc_get_balance(const char *owner_hex, uint64_t *sun_out)
Read an account's TRX balance, in sun.
Definition tron_rpc.cpp:218
bool tron_rpc_create_trc20_transfer(const char *owner_hex, const char *contract_hex, const char *to_hex, uint64_t amount, uint64_t fee_limit_sun, tron_tx_ctx_t *out)
Create a TRC-20 transfer (USDT, USDC, …) and verify what the node serialised for us.
Definition tron_rpc.cpp:411
bool tron_rpc_get_energy(const char *owner_hex, uint64_t *energy_out)
Read the energy an account still has available.
Definition tron_rpc.cpp:241
bool tron_rpc_broadcast(const tron_tx_ctx_t *tx, const uint8_t sig[65])
Broadcast a created transfer with its 65-byte signature.
Definition tron_rpc.cpp:471
bool tron_rpc_create_transfer(const char *owner_hex, const char *to_hex, uint64_t amount_sun, tron_tx_ctx_t *out)
Create a TRX transfer and verify what the node serialised for us.
Definition tron_rpc.cpp:366
tron_receipt_t
Outcome of one gettransactioninfobyid poll.
Definition tron_rpc.h:47
@ TRON_RECEIPT_FAILED
Definition tron_rpc.h:50
@ TRON_RECEIPT_SUCCESS
Definition tron_rpc.h:49
@ TRON_RECEIPT_PENDING
Definition tron_rpc.h:48
#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_show_tx_status(ui_tx_state_t state, const char *info)
Switch to the transaction-status screen.
Definition ui.cpp:898
@ UI_TX_STATE_SIGNING
Definition ui.h:97
@ UI_TX_STATE_SENDING
Definition ui.h:98