Skip to main content

df_hyperliquid_core/client/evm/
tx.rs

1//! EIP-1559 transactions, type 2, for HyperEVM and for Arbitrum: a call, or
2//! a contract creation when there is no `to`.
3//!
4//! ```text
5//! unsigned = 0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas,
6//!                         gas, to, value, data, accessList])
7//! signed   = 0x02 || rlp([... the same nine, yParity, r, s])
8//! ```
9//!
10//! Without the `sign` feature the unsigned transaction, its signing hash and
11//! the raw bytes from a signature made elsewhere still build.
12
13#[cfg(feature = "sign")]
14use crate::account::{LocalKey, recover};
15use crate::account::{SignError, Signature};
16use crate::client::evm::rlp;
17use crate::client::evm::u256::{B256, U256};
18use crate::utils::Address;
19use crate::utils::sign::keccak256;
20use serde::Deserialize;
21
22/// The type byte that leads an EIP-1559 transaction, before and after signing.
23pub const EIP1559: u8 = 0x02;
24
25/// Gas asked for over the node's estimate, in percent, so a busier block does
26/// not run the transaction out.
27pub const GAS_HEADROOM: u64 = 130;
28
29/// The fee cap over the current base fee: room for two blocks of the largest rise.
30const BASE_FEE_TIMES: u64 = 2;
31
32/// A contract and the storage slots the transaction declares it will touch.
33#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct AccessItem {
36    pub address: Address,
37    pub storage_keys: Vec<B256>,
38}
39
40/// The fields of a type 2 transaction, in the order they are encoded. Every
41/// amount is in wei of the chain's coin; `data` is the calldata, or the init
42/// code of a creation.
43#[derive(Debug, Clone, PartialEq, Eq, Default)]
44pub struct Eip1559 {
45    pub chain_id: u64,
46    pub nonce: u64,
47    pub max_priority_fee: U256,
48    pub max_fee: U256,
49    pub gas: u64,
50    /// `None` creates a contract from `data`.
51    pub to: Option<Address>,
52    pub value: U256,
53    pub data: Vec<u8>,
54    pub access_list: Vec<AccessItem>,
55}
56
57impl Eip1559 {
58    fn fields(&self) -> Vec<Vec<u8>> {
59        let to = match &self.to {
60            Some(to) => rlp::bytes(to.bytes()),
61            None => rlp::bytes(&[]),
62        };
63        let access: Vec<Vec<u8>> = self
64            .access_list
65            .iter()
66            .map(|item| {
67                let keys: Vec<Vec<u8>> = item.storage_keys.iter().map(|key| rlp::bytes(&key.0)).collect();
68                rlp::list(&[rlp::bytes(item.address.bytes()), rlp::list(&keys)])
69            })
70            .collect();
71        vec![
72            rlp::uint(u128::from(self.chain_id)),
73            rlp::uint(u128::from(self.nonce)),
74            rlp::u256(self.max_priority_fee),
75            rlp::u256(self.max_fee),
76            rlp::uint(u128::from(self.gas)),
77            to,
78            rlp::u256(self.value),
79            rlp::bytes(&self.data),
80            rlp::list(&access),
81        ]
82    }
83
84    /// `0x02 || rlp(fields)`: what the signature commits to.
85    #[must_use]
86    pub fn unsigned(&self) -> Vec<u8> {
87        typed(&self.fields())
88    }
89
90    #[must_use]
91    pub fn signing_hash(&self) -> B256 {
92        B256(keccak256(&self.unsigned()))
93    }
94
95    /// `0x02 || rlp(fields, yParity, r, s)`: the bytes a node is sent. The
96    /// signature is over [`Eip1559::signing_hash`], from a key here or a
97    /// signer elsewhere; its `v` must be 27 or 28.
98    pub fn signed(&self, signature: &Signature) -> Result<Vec<u8>, SignError> {
99        let parity = match signature.recovery_id() {
100            Some(parity @ (0 | 1)) => parity,
101            _ => return Err(SignError::BadV(signature.v())),
102        };
103        let mut fields = self.fields();
104        fields.push(rlp::uint(u128::from(parity)));
105        fields.push(rlp::word(signature.r()));
106        fields.push(rlp::word(signature.s()));
107        Ok(typed(&fields))
108    }
109
110    /// Signed with a key held in memory: RFC 6979, low-s.
111    #[cfg(feature = "sign")]
112    pub fn sign(&self, key: &LocalKey) -> Result<Vec<u8>, SignError> {
113        self.signed(&key.sign_digest(&self.signing_hash().0)?)
114    }
115
116    /// The address that signed it, to check a wallet's signature before sending.
117    #[cfg(feature = "sign")]
118    pub fn signer(&self, signature: &Signature) -> Result<Address, SignError> {
119        recover(&self.signing_hash().0, signature)
120    }
121
122    /// The contract a creation makes when `from` sends it; `None` for a call.
123    #[must_use]
124    pub fn created(&self, from: Address) -> Option<Address> {
125        self.to.is_none().then(|| create_address(from, self.nonce))
126    }
127
128    /// The most the gas can cost: every unit at the fee cap.
129    #[must_use]
130    pub fn gas_ceiling(&self) -> U256 {
131        self.max_fee.saturating_mul(U256::from_u64(self.gas))
132    }
133
134    /// The most the sender can spend: the gas ceiling and the value.
135    #[must_use]
136    pub fn most_spent(&self) -> Option<U256> {
137        self.max_fee.checked_mul(U256::from_u64(self.gas))?.checked_add(self.value)
138    }
139}
140
141fn typed(fields: &[Vec<u8>]) -> Vec<u8> {
142    let mut out = vec![EIP1559];
143    out.extend(rlp::list(fields));
144    out
145}
146
147/// The hash a node gives a signed transaction: `keccak256` of its raw bytes.
148#[must_use]
149pub fn tx_hash(raw: &[u8]) -> B256 {
150    B256(keccak256(raw))
151}
152
153/// Where a creation sent by `from` at `nonce` puts its contract:
154/// `keccak256(rlp([from, nonce]))`, last twenty bytes.
155#[must_use]
156pub fn create_address(from: Address, nonce: u64) -> Address {
157    let hash = keccak256(&rlp::list(&[rlp::bytes(from.bytes()), rlp::uint(u128::from(nonce))]));
158    let mut bytes = [0u8; 20];
159    bytes.copy_from_slice(&hash[12..]);
160    Address::from_bytes(bytes)
161}
162
163/// What a transaction does: where it goes, the value it carries, its calldata.
164#[derive(Debug, Clone, PartialEq, Eq, Default)]
165pub struct Call {
166    /// `None` creates a contract from `data`.
167    pub to: Option<Address>,
168    pub value: U256,
169    pub data: Vec<u8>,
170}
171
172impl Call {
173    /// The transaction that makes this call on `chain_id`.
174    #[must_use]
175    pub fn transaction(self, chain_id: u64, nonce: u64, gas: u64, fees: Fees) -> Eip1559 {
176        Eip1559 {
177            chain_id,
178            nonce,
179            max_priority_fee: fees.max_priority_fee,
180            max_fee: fees.max_fee,
181            gas,
182            to: self.to,
183            value: self.value,
184            data: self.data,
185            access_list: Vec::new(),
186        }
187    }
188}
189
190/// The two fees a type 2 transaction names, in wei per unit of gas.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
192pub struct Fees {
193    pub max_priority_fee: U256,
194    pub max_fee: U256,
195}
196
197impl Fees {
198    /// The tip, and a cap of twice the base fee plus the tip: what a block or
199    /// two of rising fees can take. Saturates rather than wraps.
200    #[must_use]
201    pub fn over_base(base: U256, tip: U256) -> Fees {
202        let max_fee = base.saturating_mul(U256::from_u64(BASE_FEE_TIMES)).saturating_add(tip);
203        Fees { max_priority_fee: tip, max_fee }
204    }
205}
206
207/// The node's gas estimate with [`GAS_HEADROOM`].
208#[must_use]
209pub fn with_headroom(gas: u64) -> u64 {
210    let wide = u128::from(gas) * u128::from(GAS_HEADROOM) / 100;
211    u64::try_from(wide).unwrap_or(u64::MAX)
212}
213
214#[cfg(test)]
215#[path = "tests/tx.rs"]
216mod tests;