Skip to main content

df_elysium_core/client/
records.rs

1//! Receipts, transactions and blocks as Elysium's Nitro node answers them: Ethereum's fields and
2//! Nitro's own. A receipt says how much of its gas paid for posting it to HyperEVM; a block names
3//! the parent block it was sequenced at and the outbox's send count and root; a transaction may
4//! be one of Nitro's own types (a deposit, a retryable and its redeem, ArbOS's start of block).
5//! Logs, access lists and fee history are df-hyperliquid-core's.
6
7use serde::{Deserialize, Deserializer, de};
8
9use df_hyperliquid_core::account::Signature;
10use df_hyperliquid_core::client::evm::tx::{AccessItem, EIP1559, Eip1559};
11
12pub use df_hyperliquid_core::client::evm::records::{FeeHistory, Log};
13
14use crate::utils::{Address, hex};
15use crate::{B256, U256};
16
17/// A transaction's type byte, Ethereum's and Nitro's.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum TxKind {
20    /// 0x00: legacy, EIP-155 on Elysium.
21    Legacy,
22    /// 0x01: EIP-2930.
23    AccessList,
24    /// 0x02: EIP-1559.
25    DynamicFee,
26    /// 0x04: EIP-7702.
27    SetCode,
28    /// 0x64: HYPE deposited from HyperEVM (`Inbox.depositEth`).
29    Deposit,
30    /// 0x65: an unsigned transaction from the parent chain.
31    Unsigned,
32    /// 0x66: a contract's transaction from the parent chain.
33    Contract,
34    /// 0x68: a redeem of a retryable ticket, scheduled by its submission or by `redeem`.
35    Retry,
36    /// 0x69: a retryable ticket submitted on the parent chain.
37    SubmitRetryable,
38    /// 0x6a: ArbOS's own, at the start of every block.
39    Internal,
40    Other(u8),
41}
42
43impl TxKind {
44    #[must_use]
45    pub const fn from_byte(byte: u8) -> TxKind {
46        match byte {
47            0x00 => TxKind::Legacy,
48            0x01 => TxKind::AccessList,
49            0x02 => TxKind::DynamicFee,
50            0x04 => TxKind::SetCode,
51            0x64 => TxKind::Deposit,
52            0x65 => TxKind::Unsigned,
53            0x66 => TxKind::Contract,
54            0x68 => TxKind::Retry,
55            0x69 => TxKind::SubmitRetryable,
56            0x6a => TxKind::Internal,
57            other => TxKind::Other(other),
58        }
59    }
60
61    #[must_use]
62    pub const fn byte(self) -> u8 {
63        match self {
64            TxKind::Legacy => 0x00,
65            TxKind::AccessList => 0x01,
66            TxKind::DynamicFee => 0x02,
67            TxKind::SetCode => 0x04,
68            TxKind::Deposit => 0x64,
69            TxKind::Unsigned => 0x65,
70            TxKind::Contract => 0x66,
71            TxKind::Retry => 0x68,
72            TxKind::SubmitRetryable => 0x69,
73            TxKind::Internal => 0x6a,
74            TxKind::Other(byte) => byte,
75        }
76    }
77
78    /// One of Nitro's own: it came from the parent chain or from ArbOS, never from a signer here.
79    #[must_use]
80    pub const fn is_nitro(self) -> bool {
81        matches!(self.byte(), 0x64..=0x6a)
82    }
83}
84
85/// What `eth_getTransactionReceipt` answers once a transaction is in a block.
86#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct Receipt {
89    pub transaction_hash: B256,
90    #[serde(default, deserialize_with = "opt_quantity")]
91    pub transaction_index: Option<u64>,
92    #[serde(default)]
93    pub block_hash: Option<B256>,
94    #[serde(default, deserialize_with = "opt_quantity")]
95    pub block_number: Option<u64>,
96    pub from: Address,
97    /// `None` for a creation.
98    #[serde(default)]
99    pub to: Option<Address>,
100    /// The contract a creation made; `None` for a call.
101    #[serde(default)]
102    pub contract_address: Option<Address>,
103    /// Everything the transaction paid for, [`Receipt::gas_used_for_l1`] included.
104    #[serde(deserialize_with = "quantity")]
105    pub gas_used: u64,
106    #[serde(deserialize_with = "quantity")]
107    pub cumulative_gas_used: u64,
108    #[serde(default)]
109    pub effective_gas_price: Option<U256>,
110    /// `true` when it succeeded, `false` when it reverted.
111    #[serde(default, deserialize_with = "opt_status")]
112    pub status: Option<bool>,
113    pub logs: Vec<Log>,
114    #[serde(default, rename = "type", deserialize_with = "opt_byte")]
115    pub kind: Option<u8>,
116    /// Of [`Receipt::gas_used`], the units that paid for posting the transaction to HyperEVM.
117    #[serde(default, deserialize_with = "quantity")]
118    pub gas_used_for_l1: u64,
119    /// The parent chain's block when it was sequenced: what `block.number` reads inside a contract.
120    #[serde(default, deserialize_with = "opt_quantity")]
121    pub l1_block_number: Option<u64>,
122}
123
124impl Receipt {
125    /// In a block and not reverted.
126    #[must_use]
127    pub fn succeeded(&self) -> bool {
128        self.status == Some(true)
129    }
130
131    /// What it cost the sender, in wei: every unit of gas at the price it paid.
132    #[must_use]
133    pub fn fee(&self) -> Option<U256> {
134        self.effective_gas_price?.checked_mul(U256::from_u64(self.gas_used))
135    }
136
137    /// The gas Elysium's execution took, without HyperEVM's data.
138    #[must_use]
139    pub const fn gas_used_for_l2(&self) -> u64 {
140        self.gas_used.saturating_sub(self.gas_used_for_l1)
141    }
142
143    /// Its type, when the node names one.
144    #[must_use]
145    pub fn tx_kind(&self) -> Option<TxKind> {
146        self.kind.map(TxKind::from_byte)
147    }
148}
149
150/// What `eth_getTransactionByHash` answers. Nitro's own types carry the fields that tie them to the
151/// parent chain; each is `None` on the types that lack it.
152#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct Transaction {
155    pub hash: B256,
156    #[serde(default, rename = "type", deserialize_with = "byte")]
157    pub kind: u8,
158    #[serde(default, deserialize_with = "opt_quantity")]
159    pub chain_id: Option<u64>,
160    #[serde(default, deserialize_with = "quantity")]
161    pub nonce: u64,
162    #[serde(default)]
163    pub block_hash: Option<B256>,
164    #[serde(default, deserialize_with = "opt_quantity")]
165    pub block_number: Option<u64>,
166    #[serde(default, deserialize_with = "opt_quantity")]
167    pub transaction_index: Option<u64>,
168    /// Unix seconds of its block.
169    #[serde(default, deserialize_with = "opt_quantity")]
170    pub block_timestamp: Option<u64>,
171    pub from: Address,
172    /// `None` for a creation.
173    #[serde(default)]
174    pub to: Option<Address>,
175    #[serde(default)]
176    pub value: U256,
177    #[serde(default, deserialize_with = "quantity")]
178    pub gas: u64,
179    #[serde(default)]
180    pub gas_price: Option<U256>,
181    #[serde(default)]
182    pub max_fee_per_gas: Option<U256>,
183    #[serde(default)]
184    pub max_priority_fee_per_gas: Option<U256>,
185    #[serde(default)]
186    pub access_list: Vec<AccessItem>,
187    #[serde(default, deserialize_with = "bytes")]
188    pub input: Vec<u8>,
189    #[serde(default)]
190    pub r: Option<U256>,
191    #[serde(default)]
192    pub s: Option<U256>,
193    #[serde(default)]
194    pub v: Option<U256>,
195    #[serde(default, deserialize_with = "opt_quantity")]
196    pub y_parity: Option<u64>,
197    /// A deposit's or a retryable's message number in the parent's inbox, as a word.
198    #[serde(default)]
199    pub request_id: Option<B256>,
200    /// A redeem's ticket: the hash of the retryable it runs.
201    #[serde(default)]
202    pub ticket_id: Option<B256>,
203    /// Where a retryable's or a redeem's unspent gas is refunded.
204    #[serde(default)]
205    pub refund_to: Option<Address>,
206    /// A redeem's most refunded gas, in wei.
207    #[serde(default)]
208    pub max_refund: Option<U256>,
209    /// A redeem's refund of the submission fee, in wei.
210    #[serde(default)]
211    pub submission_fee_refund: Option<U256>,
212    /// A retryable's parent base fee when submitted.
213    #[serde(default)]
214    pub l1_base_fee: Option<U256>,
215    /// What a retryable brought from the parent, in wei.
216    #[serde(default)]
217    pub deposit_value: Option<U256>,
218    #[serde(default)]
219    pub retry_to: Option<Address>,
220    #[serde(default)]
221    pub retry_value: Option<U256>,
222    #[serde(default, deserialize_with = "opt_bytes")]
223    pub retry_data: Option<Vec<u8>>,
224    /// Who may cancel a retryable, and receives its call value if it never runs.
225    #[serde(default)]
226    pub beneficiary: Option<Address>,
227    #[serde(default)]
228    pub max_submission_fee: Option<U256>,
229}
230
231impl Transaction {
232    #[must_use]
233    pub const fn tx_kind(&self) -> TxKind {
234        TxKind::from_byte(self.kind)
235    }
236
237    /// The type 2 transaction this record is of: rebuilt, it hashes to [`Transaction::hash`] once
238    /// signed with [`Transaction::signature`].
239    #[must_use]
240    pub fn to_eip1559(&self) -> Option<Eip1559> {
241        if self.kind != EIP1559 {
242            return None;
243        }
244        Some(Eip1559 {
245            chain_id: self.chain_id?,
246            nonce: self.nonce,
247            max_priority_fee: self.max_priority_fee_per_gas?,
248            max_fee: self.max_fee_per_gas?,
249            gas: self.gas,
250            to: self.to,
251            value: self.value,
252            data: self.input.clone(),
253            access_list: self.access_list.clone(),
254        })
255    }
256
257    /// A type 2 transaction's signature, `v` as 27 or 28.
258    #[must_use]
259    pub fn signature(&self) -> Option<Signature> {
260        let parity = u8::try_from(self.y_parity?).ok().filter(|parity| *parity <= 1)?;
261        Some(Signature::from_parts(self.r?.to_be_bytes(), self.s?.to_be_bytes(), 27 + parity))
262    }
263
264    /// The parent inbox's message number a deposit or a retryable came in as.
265    #[must_use]
266    pub fn message_number(&self) -> Option<U256> {
267        self.request_id.map(U256::from)
268    }
269}
270
271/// What `eth_getBlockByNumber` and `eth_getBlockByHash` answer on Nitro.
272#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
273#[serde(rename_all = "camelCase")]
274pub struct Block {
275    #[serde(deserialize_with = "quantity")]
276    pub number: u64,
277    pub hash: B256,
278    pub parent_hash: B256,
279    /// Unix seconds.
280    #[serde(deserialize_with = "quantity")]
281    pub timestamp: u64,
282    #[serde(deserialize_with = "quantity")]
283    pub gas_limit: u64,
284    #[serde(deserialize_with = "quantity")]
285    pub gas_used: u64,
286    #[serde(default)]
287    pub base_fee_per_gas: Option<U256>,
288    /// The sequencer's fee account, not a miner.
289    pub miner: Address,
290    pub transactions: BlockTransactions,
291    /// The parent chain's block this one was sequenced at.
292    #[serde(deserialize_with = "quantity")]
293    pub l1_block_number: u64,
294    /// How many messages to the parent chain had been sent by the end of this block: the size of
295    /// the outbox's tree.
296    #[serde(deserialize_with = "quantity")]
297    pub send_count: u64,
298    /// The root of the outbox's tree after this block, what a confirmed assertion commits to.
299    pub send_root: B256,
300    /// Nitro packs the send count, the parent block and ArbOS's version into it.
301    pub mix_hash: B256,
302}
303
304impl Block {
305    /// ArbOS's version when the block was made: the third eight bytes of its mix hash.
306    /// `ArbSys.arbOSVersion()` answers it plus 55.
307    #[must_use]
308    pub fn arbos_version(&self) -> u64 {
309        let mut eight = [0u8; 8];
310        eight.copy_from_slice(&self.mix_hash.0[16..24]);
311        u64::from_be_bytes(eight)
312    }
313}
314
315/// A block's transactions: their hashes, or the whole records when asked for.
316#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
317#[serde(untagged)]
318pub enum BlockTransactions {
319    Hashes(Vec<B256>),
320    Full(Vec<Transaction>),
321}
322
323/// A quantity that fits a `u64`: block numbers, nonces, gas.
324pub(crate) fn quantity<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
325    let value = U256::deserialize(deserializer)?;
326    value.to_u64().ok_or_else(|| de::Error::custom(format!("{value} does not fit 64 bits")))
327}
328
329fn opt_quantity<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<u64>, D::Error> {
330    match Option::<U256>::deserialize(deserializer)? {
331        Some(value) => {
332            value.to_u64().map(Some).ok_or_else(|| de::Error::custom(format!("{value} does not fit 64 bits")))
333        }
334        None => Ok(None),
335    }
336}
337
338fn byte<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u8, D::Error> {
339    let value = quantity(deserializer)?;
340    u8::try_from(value).map_err(|_| de::Error::custom(format!("{value} is not a transaction type")))
341}
342
343fn opt_byte<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<u8>, D::Error> {
344    match opt_quantity(deserializer)? {
345        Some(value) => {
346            u8::try_from(value).map(Some).map_err(|_| de::Error::custom(format!("{value} is not a transaction type")))
347        }
348        None => Ok(None),
349    }
350}
351
352fn opt_status<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<bool>, D::Error> {
353    match opt_quantity(deserializer)? {
354        Some(0) => Ok(Some(false)),
355        Some(1) => Ok(Some(true)),
356        Some(other) => Err(de::Error::custom(format!("{other} is not a receipt status"))),
357        None => Ok(None),
358    }
359}
360
361/// `0x` and hex bytes.
362pub(crate) fn bytes<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
363    let text = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
364    let digits = text.strip_prefix("0x").ok_or_else(|| de::Error::custom(format!("`{text}` is not 0x and hex")))?;
365    hex::decode(digits).map_err(de::Error::custom)
366}
367
368fn opt_bytes<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error> {
369    match Option::<std::borrow::Cow<'de, str>>::deserialize(deserializer)? {
370        Some(text) => {
371            let digits =
372                text.strip_prefix("0x").ok_or_else(|| de::Error::custom(format!("`{text}` is not 0x and hex")))?;
373            hex::decode(digits).map(Some).map_err(de::Error::custom)
374        }
375        None => Ok(None),
376    }
377}
378
379#[cfg(test)]
380#[path = "tests/records.rs"]
381mod tests;