1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum TxKind {
20 Legacy,
22 AccessList,
24 DynamicFee,
26 SetCode,
28 Deposit,
30 Unsigned,
32 Contract,
34 Retry,
36 SubmitRetryable,
38 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 #[must_use]
80 pub const fn is_nitro(self) -> bool {
81 matches!(self.byte(), 0x64..=0x6a)
82 }
83}
84
85#[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 #[serde(default)]
99 pub to: Option<Address>,
100 #[serde(default)]
102 pub contract_address: Option<Address>,
103 #[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 #[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 #[serde(default, deserialize_with = "quantity")]
118 pub gas_used_for_l1: u64,
119 #[serde(default, deserialize_with = "opt_quantity")]
121 pub l1_block_number: Option<u64>,
122}
123
124impl Receipt {
125 #[must_use]
127 pub fn succeeded(&self) -> bool {
128 self.status == Some(true)
129 }
130
131 #[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 #[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 #[must_use]
145 pub fn tx_kind(&self) -> Option<TxKind> {
146 self.kind.map(TxKind::from_byte)
147 }
148}
149
150#[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 #[serde(default, deserialize_with = "opt_quantity")]
170 pub block_timestamp: Option<u64>,
171 pub from: Address,
172 #[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 #[serde(default)]
199 pub request_id: Option<B256>,
200 #[serde(default)]
202 pub ticket_id: Option<B256>,
203 #[serde(default)]
205 pub refund_to: Option<Address>,
206 #[serde(default)]
208 pub max_refund: Option<U256>,
209 #[serde(default)]
211 pub submission_fee_refund: Option<U256>,
212 #[serde(default)]
214 pub l1_base_fee: Option<U256>,
215 #[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 #[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 #[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 #[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 #[must_use]
266 pub fn message_number(&self) -> Option<U256> {
267 self.request_id.map(U256::from)
268 }
269}
270
271#[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 #[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 pub miner: Address,
290 pub transactions: BlockTransactions,
291 #[serde(deserialize_with = "quantity")]
293 pub l1_block_number: u64,
294 #[serde(deserialize_with = "quantity")]
297 pub send_count: u64,
298 pub send_root: B256,
300 pub mix_hash: B256,
302}
303
304impl Block {
305 #[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#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
317#[serde(untagged)]
318pub enum BlockTransactions {
319 Hashes(Vec<B256>),
320 Full(Vec<Transaction>),
321}
322
323pub(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
361pub(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;