Skip to main content

df_hyperliquid_core/client/evm/
records.rs

1//! Receipts, transactions, blocks, logs and fee history as JSON-RPC answers
2//! them. Quantities are exact integers; a block's gas used ratio, the one
3//! float a node writes, is kept as the digits it wrote.
4
5use crate::account::Signature;
6use crate::client::evm::tx::{AccessItem, EIP1559, Eip1559};
7use crate::client::evm::u256::{B256, U256};
8use crate::utils::{Address, Figure};
9use serde::{Deserialize, Deserializer, de};
10
11/// What `eth_getTransactionReceipt` answers once a transaction is mined.
12#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct Receipt {
15    pub transaction_hash: B256,
16    #[serde(default, deserialize_with = "opt_quantity")]
17    pub transaction_index: Option<u64>,
18    #[serde(default)]
19    pub block_hash: Option<B256>,
20    #[serde(default, deserialize_with = "opt_quantity")]
21    pub block_number: Option<u64>,
22    pub from: Address,
23    /// `None` for a creation.
24    #[serde(default)]
25    pub to: Option<Address>,
26    /// The contract a creation made; `None` for a call.
27    #[serde(default)]
28    pub contract_address: Option<Address>,
29    #[serde(deserialize_with = "quantity")]
30    pub gas_used: u64,
31    #[serde(deserialize_with = "quantity")]
32    pub cumulative_gas_used: u64,
33    #[serde(default)]
34    pub effective_gas_price: Option<U256>,
35    /// `true` when it succeeded, `false` when it reverted.
36    #[serde(default, deserialize_with = "opt_status")]
37    pub status: Option<bool>,
38    pub logs: Vec<Log>,
39    /// The transaction type: 2 for EIP-1559.
40    #[serde(default, rename = "type", deserialize_with = "opt_byte")]
41    pub kind: Option<u8>,
42}
43
44impl Receipt {
45    /// Mined and not reverted.
46    #[must_use]
47    pub fn succeeded(&self) -> bool {
48        self.status == Some(true)
49    }
50}
51
52/// What `eth_getTransactionByHash` answers, mined or still pending.
53#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct Transaction {
56    pub hash: B256,
57    /// 2 for EIP-1559; 0 for legacy.
58    #[serde(default, rename = "type", deserialize_with = "byte")]
59    pub kind: u8,
60    #[serde(default, deserialize_with = "opt_quantity")]
61    pub chain_id: Option<u64>,
62    #[serde(deserialize_with = "quantity")]
63    pub nonce: u64,
64    #[serde(default)]
65    pub block_hash: Option<B256>,
66    #[serde(default, deserialize_with = "opt_quantity")]
67    pub block_number: Option<u64>,
68    #[serde(default, deserialize_with = "opt_quantity")]
69    pub transaction_index: Option<u64>,
70    pub from: Address,
71    /// `None` for a creation.
72    #[serde(default)]
73    pub to: Option<Address>,
74    pub value: U256,
75    #[serde(deserialize_with = "quantity")]
76    pub gas: u64,
77    #[serde(default)]
78    pub gas_price: Option<U256>,
79    #[serde(default)]
80    pub max_fee_per_gas: Option<U256>,
81    #[serde(default)]
82    pub max_priority_fee_per_gas: Option<U256>,
83    #[serde(default)]
84    pub access_list: Vec<AccessItem>,
85    #[serde(deserialize_with = "bytes")]
86    pub input: Vec<u8>,
87    #[serde(default)]
88    pub r: Option<U256>,
89    #[serde(default)]
90    pub s: Option<U256>,
91    #[serde(default, deserialize_with = "opt_quantity")]
92    pub y_parity: Option<u64>,
93}
94
95impl Transaction {
96    /// The type 2 transaction this record is of: rebuilt, it hashes to
97    /// [`Transaction::hash`] once signed with [`Transaction::signature`].
98    #[must_use]
99    pub fn to_eip1559(&self) -> Option<Eip1559> {
100        if self.kind != EIP1559 {
101            return None;
102        }
103        Some(Eip1559 {
104            chain_id: self.chain_id?,
105            nonce: self.nonce,
106            max_priority_fee: self.max_priority_fee_per_gas?,
107            max_fee: self.max_fee_per_gas?,
108            gas: self.gas,
109            to: self.to,
110            value: self.value,
111            data: self.input.clone(),
112            access_list: self.access_list.clone(),
113        })
114    }
115
116    /// The signature the record carries, `v` as 27 or 28.
117    #[must_use]
118    pub fn signature(&self) -> Option<Signature> {
119        let parity = u8::try_from(self.y_parity?).ok().filter(|parity| *parity <= 1)?;
120        Some(Signature::from_parts(self.r?.to_be_bytes(), self.s?.to_be_bytes(), 27 + parity))
121    }
122}
123
124/// What `eth_getBlockByNumber` answers.
125#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
126#[serde(rename_all = "camelCase")]
127pub struct Block {
128    #[serde(deserialize_with = "quantity")]
129    pub number: u64,
130    pub hash: B256,
131    pub parent_hash: B256,
132    /// Unix seconds.
133    #[serde(deserialize_with = "quantity")]
134    pub timestamp: u64,
135    #[serde(deserialize_with = "quantity")]
136    pub gas_limit: u64,
137    #[serde(deserialize_with = "quantity")]
138    pub gas_used: u64,
139    #[serde(default)]
140    pub base_fee_per_gas: Option<U256>,
141    pub miner: Address,
142    pub transactions: BlockTransactions,
143}
144
145/// A block's transactions: their hashes, or the whole records when asked for.
146#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
147#[serde(untagged)]
148pub enum BlockTransactions {
149    Hashes(Vec<B256>),
150    Full(Vec<Transaction>),
151}
152
153/// One log, from a receipt or `eth_getLogs`.
154#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct Log {
157    pub address: Address,
158    pub topics: Vec<B256>,
159    #[serde(deserialize_with = "bytes")]
160    pub data: Vec<u8>,
161    #[serde(default, deserialize_with = "opt_quantity")]
162    pub block_number: Option<u64>,
163    #[serde(default)]
164    pub block_hash: Option<B256>,
165    #[serde(default)]
166    pub transaction_hash: Option<B256>,
167    #[serde(default, deserialize_with = "opt_quantity")]
168    pub transaction_index: Option<u64>,
169    #[serde(default, deserialize_with = "opt_quantity")]
170    pub log_index: Option<u64>,
171    /// Dropped by a reorganisation.
172    #[serde(default)]
173    pub removed: bool,
174}
175
176/// What `eth_feeHistory` answers.
177#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct FeeHistory {
180    #[serde(deserialize_with = "quantity")]
181    pub oldest_block: u64,
182    /// One per block asked for, and one more: the next block's.
183    pub base_fee_per_gas: Vec<U256>,
184    /// Each block's gas used over its limit, as the node wrote it.
185    pub gas_used_ratio: Vec<Figure>,
186    /// Per block, the tip at each percentile asked for.
187    #[serde(default)]
188    pub reward: Vec<Vec<U256>>,
189}
190
191impl FeeHistory {
192    /// The base fee of the block after the newest one asked for.
193    #[must_use]
194    pub fn next_base_fee(&self) -> Option<U256> {
195        self.base_fee_per_gas.last().copied()
196    }
197}
198
199/// A quantity that fits a `u64`: block numbers, nonces, gas.
200pub(crate) fn quantity<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
201    let value = U256::deserialize(deserializer)?;
202    value.to_u64().ok_or_else(|| de::Error::custom(format!("{value} does not fit 64 bits")))
203}
204
205fn opt_quantity<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<u64>, D::Error> {
206    match Option::<U256>::deserialize(deserializer)? {
207        Some(value) => {
208            value.to_u64().map(Some).ok_or_else(|| de::Error::custom(format!("{value} does not fit 64 bits")))
209        }
210        None => Ok(None),
211    }
212}
213
214fn byte<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u8, D::Error> {
215    let value = quantity(deserializer)?;
216    u8::try_from(value).map_err(|_| de::Error::custom(format!("{value} is not a transaction type")))
217}
218
219fn opt_byte<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<u8>, D::Error> {
220    match opt_quantity(deserializer)? {
221        Some(value) => {
222            u8::try_from(value).map(Some).map_err(|_| de::Error::custom(format!("{value} is not a transaction type")))
223        }
224        None => Ok(None),
225    }
226}
227
228fn opt_status<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<bool>, D::Error> {
229    match opt_quantity(deserializer)? {
230        Some(0) => Ok(Some(false)),
231        Some(1) => Ok(Some(true)),
232        Some(other) => Err(de::Error::custom(format!("{other} is not a receipt status"))),
233        None => Ok(None),
234    }
235}
236
237/// `0x` and hex bytes.
238pub(crate) fn bytes<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
239    let text = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
240    let digits = text.strip_prefix("0x").ok_or_else(|| de::Error::custom(format!("`{text}` is not 0x and hex")))?;
241    crate::utils::hex::decode(digits).map_err(de::Error::custom)
242}
243
244#[cfg(test)]
245#[path = "tests/records.rs"]
246mod tests;