walletsuite_tx_compiler/types.rs
1//! Input and output types for the transaction compiler.
2//!
3//! The serde layout is the canonical JSON shape of a `WalletSuite`
4//! `PrepareSignResponseDto` so downstream consumers share on-the-wire
5//! vocabulary.
6//!
7//! All public structs and enums are marked `#[non_exhaustive]` so that
8//! future additions (new chains, new tx types, new fee fields) do not
9//! force a major version bump. Downstream code must construct these
10//! types via serde (`serde_json::from_value`) rather than struct
11//! literals, and must use `_ => ...` when matching on enums.
12
13use serde::{Deserialize, Serialize};
14
15/// Supported chain families.
16#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18#[non_exhaustive]
19pub enum Chain {
20 /// Ethereum and EVM-compatible chains (BSC, Polygon, Arbitrum, …).
21 Ethereum,
22 /// The Tron mainnet or testnets.
23 Tron,
24}
25
26/// Fee calculation mode carried by a prepared transaction.
27#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
28#[non_exhaustive]
29pub enum FeeMode {
30 /// Dynamic fee market transactions (EIP-1559, type 2 envelope).
31 #[serde(rename = "EIP1559")]
32 Eip1559,
33 /// Legacy gas-price transactions (type 0 envelope, EIP-155 signed).
34 #[serde(rename = "LEGACY")]
35 Legacy,
36 /// Tron fee structure with fee limit and block reference.
37 #[serde(rename = "TRON")]
38 Tron,
39}
40
41impl FeeMode {
42 /// The stable string discriminator used on the wire and in error
43 /// messages (`"EIP1559"`, `"LEGACY"`, `"TRON"`).
44 #[must_use]
45 pub const fn as_str(self) -> &'static str {
46 match self {
47 Self::Eip1559 => "EIP1559",
48 Self::Legacy => "LEGACY",
49 Self::Tron => "TRON",
50 }
51 }
52}
53
54/// Intent expressed by the prepared payload.
55#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
56#[non_exhaustive]
57pub enum TxType {
58 /// Native token transfer (ETH, BNB, TRX, …).
59 #[serde(rename = "TRANSFER_NATIVE")]
60 TransferNative,
61 /// Fungible token transfer (ERC-20 on EVM, TRC-20 on Tron).
62 #[serde(rename = "TRANSFER_TOKEN")]
63 TransferToken,
64}
65
66/// Tron block-header reference used for replay protection and
67/// `Transaction.raw_data` construction.
68///
69/// Field names match the Tron block-header JSON wire format verbatim so
70/// payloads round-trip without remapping. See the Tron protocol
71/// documentation for the authoritative schema.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[non_exhaustive]
74pub struct TronBlockHeader {
75 /// `h` — block hash (hex string, no `0x` prefix). Used to derive the
76 /// `refBlockHash` field of `Transaction.raw_data`.
77 pub h: String,
78 /// `n` — block number. The low 16 bits become `refBlockBytes` in
79 /// `Transaction.raw_data`.
80 pub n: u64,
81 /// `t` — block timestamp in milliseconds since the Unix epoch.
82 pub t: u64,
83 /// `p` — parent block hash (informational, not used in compilation).
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub p: Option<String>,
86 /// `r` — transaction trie root (informational).
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub r: Option<String>,
89 /// `w` — witness (block producer) address (informational).
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub w: Option<String>,
92 /// `v` — protocol version.
93 pub v: u32,
94}
95
96/// Mode-discriminated fee parameters. Exactly one of the three
97/// mode-specific subsets is populated; the others are `None`.
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100#[non_exhaustive]
101pub struct FeeParams {
102 /// Active fee mode (discriminator for the rest of this struct).
103 pub mode: FeeMode,
104 /// Gas unit limit (EVM only, both EIP-1559 and LEGACY).
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub gas_limit: Option<String>,
107 /// Base fee per gas in wei (EIP-1559 only, informational).
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub base_fee_per_gas: Option<String>,
110 /// Priority fee (tip) per gas in wei (EIP-1559 only).
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub max_priority_fee_per_gas: Option<String>,
113 /// Maximum fee per gas in wei (EIP-1559 only).
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub max_fee_per_gas: Option<String>,
116 /// Gas price in wei (LEGACY only).
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub gas_price: Option<String>,
119 /// `el` — Tron energy fee limit in SUN (Tron only, optional).
120 /// Field name mirrors the wire-format short key.
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub el: Option<String>,
123 /// `rp` — Tron reference-point block header used for replay
124 /// protection and `Transaction.raw_data` construction. Required on
125 /// the Tron path; field name mirrors the wire-format short key.
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub rp: Option<TronBlockHeader>,
128}
129
130/// Canonical prepared transaction payload from the `WalletSuite`
131/// prepare-sign API.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134#[non_exhaustive]
135pub struct PreparedTransaction {
136 /// Target chain family.
137 pub chain: Chain,
138 /// Chain ID (EVM only, `None` for Tron).
139 pub chain_id: Option<i64>,
140 /// Sender address.
141 pub from: String,
142 /// On-envelope recipient address.
143 ///
144 /// For EVM token transfers this equals `token_contract`; the real recipient
145 /// lives inside `data`.
146 pub to: String,
147 /// Amount in smallest units (wei / SUN) as a decimal string.
148 pub value_wei: String,
149 /// ABI-encoded calldata (EVM token transfers); `None` otherwise.
150 pub data: Option<String>,
151 /// Intent.
152 pub tx_type: TxType,
153 /// Token contract address (token transfers only).
154 pub token_contract: Option<String>,
155 /// Transaction nonce as a decimal string (EVM only).
156 pub nonce: Option<String>,
157 /// Fee parameters.
158 pub fee: FeeParams,
159}
160
161/// Result of compiling a prepared transaction.
162///
163/// - For EVM: `unsigned_tx` is the EIP-2718 signing pre-image
164/// (`0x02 || rlp([...])` for EIP-1559, `rlp([..., chainId, 0, 0])` for
165/// legacy EIP-155). To broadcast after signing, the caller must
166/// reconstruct the signed envelope by replacing the zero signature
167/// fields — use `alloy_consensus::TxEnvelope` or an equivalent
168/// library. `tx_hash` is `keccak256(unsigned_tx)`.
169/// - For Tron: `unsigned_tx` is the protobuf-encoded
170/// `Transaction.raw_data` bytes. The caller wraps `raw_data` and the
171/// signature into the outer `Transaction` message to broadcast.
172/// `tx_hash` is `sha256(unsigned_tx)`.
173#[derive(Debug, Clone, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase")]
175#[non_exhaustive]
176pub struct CompilationResult {
177 /// Chain the transaction was compiled for.
178 pub chain: Chain,
179 /// Hex-encoded signing pre-image / raw-data bytes (`0x`-prefixed).
180 /// See the struct-level docs for per-chain semantics.
181 pub unsigned_tx: String,
182 /// Hash the caller signs (`0x`-prefixed). keccak256 for EVM,
183 /// SHA-256 for Tron.
184 pub tx_hash: String,
185 /// Metadata about the compilation choices.
186 pub metadata: CompilationMetadata,
187}
188
189/// Metadata describing how a transaction was compiled.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase")]
192#[non_exhaustive]
193pub struct CompilationMetadata {
194 /// Intent of the compiled transaction.
195 pub tx_type: TxType,
196 /// Fee mode used for compilation.
197 pub fee_mode: FeeMode,
198 /// EVM envelope type: `0` for legacy, `2` for EIP-1559.
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub evm_tx_type: Option<u8>,
201 /// Tron contract type: `1` for `TransferContract`, `31` for
202 /// `TriggerSmartContract`.
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub tron_contract_type: Option<u8>,
205 /// Tron transaction expiration timestamp in milliseconds.
206 #[serde(skip_serializing_if = "Option::is_none")]
207 pub expiration: Option<u64>,
208}
209
210/// Human-reviewable representation of a transaction.
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213#[non_exhaustive]
214pub struct TransactionReview {
215 /// Target chain family.
216 pub chain: Chain,
217 /// Intent.
218 pub tx_type: TxType,
219 /// Sender address.
220 pub from: String,
221 /// Decoded recipient (from calldata for EVM token transfers).
222 pub recipient: String,
223 /// Transfer amount in smallest units (decimal string).
224 pub amount: String,
225 /// Token contract (token transfers only).
226 pub token_contract: Option<String>,
227 /// Transaction nonce (EVM only).
228 pub nonce: Option<String>,
229 /// Chain ID (EVM only).
230 pub chain_id: Option<i64>,
231 /// Fee summary.
232 pub fee: FeeReview,
233}
234
235/// Fee summary sized for display to a human reviewer.
236#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238#[non_exhaustive]
239pub struct FeeReview {
240 /// Active fee mode.
241 pub mode: FeeMode,
242 /// Estimated maximum fee cost in smallest native units.
243 ///
244 /// `None` encodes JSON `null` when the estimate is unavailable (Tron
245 /// without a fee limit).
246 pub estimated_max_cost: Option<String>,
247 /// Gas unit limit (EVM only).
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub gas_limit: Option<String>,
250 /// Gas price in wei (LEGACY only).
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub gas_price: Option<String>,
253 /// Maximum fee per gas in wei (EIP-1559 only).
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub max_fee_per_gas: Option<String>,
256 /// Priority fee per gas in wei (EIP-1559 only, when set).
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub max_priority_fee_per_gas: Option<String>,
259 /// Base fee per gas in wei (EIP-1559 only, when set).
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub base_fee_per_gas: Option<String>,
262 /// Tron fee limit in SUN (Tron only, when set).
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub tron_fee_limit: Option<String>,
265}
266
267/// Options for [`crate::compile`].
268#[derive(Debug, Clone, Copy, Default)]
269#[non_exhaustive]
270pub struct CompileOptions {
271 /// Override wall-clock time (milliseconds since epoch) for Tron
272 /// transactions.
273 ///
274 /// **Callers that require byte-exact reproducibility MUST supply a
275 /// value here.** When `None`, Tron `timestamp` and `expiration` fall
276 /// back to `SystemTime::now()` and the compiled `unsigned_tx` /
277 /// `tx_hash` outputs will vary across calls. EVM compilation is
278 /// unaffected — this field is read only on the Tron path.
279 pub now: Option<u64>,
280}
281
282impl CompileOptions {
283 /// Construct a new `CompileOptions` with default values. Preferred
284 /// over struct-literal construction so future fields can be added
285 /// without breaking callers.
286 #[must_use]
287 pub const fn new() -> Self {
288 Self { now: None }
289 }
290
291 /// Set the wall-clock override for Tron compilation. See the `now`
292 /// field documentation for reproducibility implications.
293 #[must_use]
294 pub const fn with_now(mut self, now_ms: u64) -> Self {
295 self.now = Some(now_ms);
296 self
297 }
298}