zenith_types/
bundle.rs

1use alloy::primitives::{keccak256, Address, Bytes, B256, U256};
2use alloy::{
3    eips::{eip2718::Encodable2718, BlockNumberOrTag},
4    rpc::types::mev::{EthCallBundle, EthCallBundleResponse, EthSendBundle},
5};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8
9use crate::SignedOrder;
10
11/// Wraps a flashbots style EthSendBundle with host fills to make a Zenith compatible bundle
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ZenithEthBundle {
15    /// The bundle of transactions to simulate. Same structure as a Flashbots [EthSendBundle] bundle.
16    /// see <https://github.com/alloy-rs/alloy/blob/main/crates/rpc-types-mev/src/eth_calls.rs#L121-L139>
17    #[serde(flatten)]
18    pub bundle: EthSendBundle,
19    /// Host fills to be applied with the bundle, represented as a signed permit2 order.
20    pub host_fills: Option<SignedOrder>,
21}
22
23impl ZenithEthBundle {
24    /// Returns the transactions in this bundle.
25    pub fn txs(&self) -> &[Bytes] {
26        &self.bundle.txs
27    }
28
29    /// Returns the block number for this bundle.
30    pub const fn block_number(&self) -> u64 {
31        self.bundle.block_number
32    }
33
34    /// Returns the minimum timestamp for this bundle.
35    pub const fn min_timestamp(&self) -> Option<u64> {
36        self.bundle.min_timestamp
37    }
38
39    /// Returns the maximum timestamp for this bundle.
40    pub const fn max_timestamp(&self) -> Option<u64> {
41        self.bundle.max_timestamp
42    }
43
44    /// Returns the reverting tx hashes for this bundle.
45    pub fn reverting_tx_hashes(&self) -> &[B256] {
46        self.bundle.reverting_tx_hashes.as_slice()
47    }
48
49    /// Returns the replacement uuid for this bundle.
50    pub fn replacement_uuid(&self) -> Option<&str> {
51        self.bundle.replacement_uuid.as_deref()
52    }
53}
54
55/// Response for `zenith_sendBundle`
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct ZenithEthBundleResponse {
59    /// The bundle hash of the sent bundle.
60    ///
61    /// This is calculated as keccak256(tx_hashes) where tx_hashes are the concatenated transaction hashes.
62    pub bundle_hash: B256,
63}
64
65/// Bundle of transactions for `zenith_callBundle`
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct ZenithCallBundle {
69    /// The bundle of transactions to simulate. Same structure as a Flashbots [EthCallBundle] bundle.
70    /// see <https://github.com/alloy-rs/alloy/blob/main/crates/rpc-types-mev/src/eth_calls.rs#L13-L33>
71    #[serde(flatten)]
72    pub bundle: EthCallBundle,
73    /// Host fills to be applied to the bundle for simulation. The mapping corresponds
74    /// to asset => user => amount.
75    pub host_fills: BTreeMap<Address, BTreeMap<Address, U256>>,
76}
77
78impl ZenithCallBundle {
79    /// Returns the host fills for this bundle.
80    pub const fn host_fills(&self) -> &BTreeMap<Address, BTreeMap<Address, U256>> {
81        &self.host_fills
82    }
83
84    /// Returns the transactions in this bundle.
85    pub fn txs(&self) -> &[Bytes] {
86        &self.bundle.txs
87    }
88
89    /// Returns the block number for this bundle.
90    pub const fn block_number(&self) -> u64 {
91        self.bundle.block_number
92    }
93
94    /// Returns the state block number for this bundle.
95    pub const fn state_block_number(&self) -> BlockNumberOrTag {
96        self.bundle.state_block_number
97    }
98
99    /// Returns the timestamp for this bundle.
100    pub const fn timestamp(&self) -> Option<u64> {
101        self.bundle.timestamp
102    }
103
104    /// Returns the gas limit for this bundle.
105    pub const fn gas_limit(&self) -> Option<u64> {
106        self.bundle.gas_limit
107    }
108
109    /// Returns the difficulty for this bundle.
110    pub const fn difficulty(&self) -> Option<U256> {
111        self.bundle.difficulty
112    }
113
114    /// Returns the base fee for this bundle.
115    pub const fn base_fee(&self) -> Option<u128> {
116        self.bundle.base_fee
117    }
118
119    /// Creates a new bundle from the given [`Encodable2718`] transactions.
120    pub fn from_2718_and_host_fills<I, T>(
121        txs: I,
122        host_fills: BTreeMap<Address, BTreeMap<Address, U256>>,
123    ) -> Self
124    where
125        I: IntoIterator<Item = T>,
126        T: Encodable2718,
127    {
128        Self::from_raw_txs_and_host_fills(txs.into_iter().map(|tx| tx.encoded_2718()), host_fills)
129    }
130
131    /// Creates a new bundle with the given transactions and host fills.
132    pub fn from_raw_txs_and_host_fills<I, T>(
133        txs: I,
134        host_fills: BTreeMap<Address, BTreeMap<Address, U256>>,
135    ) -> Self
136    where
137        I: IntoIterator<Item = T>,
138        T: Into<Bytes>,
139    {
140        Self {
141            bundle: EthCallBundle {
142                txs: txs.into_iter().map(Into::into).collect(),
143                ..Default::default()
144            },
145            host_fills,
146        }
147    }
148
149    /// Adds an [`Encodable2718`] transaction to the bundle.
150    pub fn append_2718_tx(self, tx: impl Encodable2718) -> Self {
151        self.append_raw_tx(tx.encoded_2718())
152    }
153
154    /// Adds an EIP-2718 envelope to the bundle.
155    pub fn append_raw_tx(mut self, tx: impl Into<Bytes>) -> Self {
156        self.bundle.txs.push(tx.into());
157        self
158    }
159
160    /// Adds multiple [`Encodable2718`] transactions to the bundle.
161    pub fn extend_2718_txs<I, T>(self, tx: I) -> Self
162    where
163        I: IntoIterator<Item = T>,
164        T: Encodable2718,
165    {
166        self.extend_raw_txs(tx.into_iter().map(|tx| tx.encoded_2718()))
167    }
168
169    /// Adds multiple calls to the block.
170    pub fn extend_raw_txs<I, T>(mut self, txs: I) -> Self
171    where
172        I: IntoIterator<Item = T>,
173        T: Into<Bytes>,
174    {
175        self.bundle.txs.extend(txs.into_iter().map(Into::into));
176        self
177    }
178
179    /// Sets the block number for the bundle.
180    pub const fn with_block_number(mut self, block_number: u64) -> Self {
181        self.bundle.block_number = block_number;
182        self
183    }
184
185    /// Sets the state block number for the bundle.
186    pub fn with_state_block_number(
187        mut self,
188        state_block_number: impl Into<BlockNumberOrTag>,
189    ) -> Self {
190        self.bundle.state_block_number = state_block_number.into();
191        self
192    }
193
194    /// Sets the timestamp for the bundle.
195    pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
196        self.bundle.timestamp = Some(timestamp);
197        self
198    }
199
200    /// Sets the gas limit for the bundle.
201    pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
202        self.bundle.gas_limit = Some(gas_limit);
203        self
204    }
205
206    /// Sets the difficulty for the bundle.
207    pub const fn with_difficulty(mut self, difficulty: U256) -> Self {
208        self.bundle.difficulty = Some(difficulty);
209        self
210    }
211
212    /// Sets the base fee for the bundle.
213    pub const fn with_base_fee(mut self, base_fee: u128) -> Self {
214        self.bundle.base_fee = Some(base_fee);
215        self
216    }
217
218    /// Make a bundle hash from the given deserialized transaction array and host fills from this bundle.
219    /// The hash is calculated as keccak256(tx_preimage + host_preimage).
220    /// The tx_preimage is calculated as `keccak(tx_hash1 + tx_hash2 + ... + tx_hashn)`.
221    /// The host_preimage is calculated as
222    /// `keccak(NUM_OF_ASSETS_LE + asset1 + NUM_OF_FILLS_LE + asset1_user1 + user1_amount2 + ... + asset1_usern + asset1_amountn + ...)`.
223    /// For the number of users/fills and amounts in the host_preimage, the amounts are serialized as little-endian U256 slice.
224    pub fn bundle_hash(&self) -> B256 {
225        let mut hasher = alloy::primitives::Keccak256::new();
226
227        // Concatenate the transaction hashes, to then hash them. This is the tx_preimage.
228        for tx in self.bundle.txs.iter() {
229            // Calculate the tx hash (keccak256(encoded_signed_tx)) and append it to the tx_bytes.
230            hasher.update(keccak256(tx).as_slice());
231        }
232        let tx_preimage = hasher.finalize();
233
234        // Now, let's build the host_preimage. We do it in steps:
235        // 1. Prefix the number of assets, encoded as a little-endian U256 slice.
236        // 2. For each asset:
237        // 3. Concatenate the asset address.
238        // 4. Prefix the number of fills.
239        // 5. For each fill, concatenate the user and amount, the latter encoded as a little-endian U256 slice.
240        let mut hasher = alloy::primitives::Keccak256::new();
241
242        // Prefix the list of users with the number of assets.
243        hasher.update(U256::from(self.host_fills.len()).as_le_slice());
244
245        for (asset, fills) in self.host_fills.iter() {
246            // Concatenate the asset address.
247            hasher.update(asset.as_slice());
248
249            // Prefix the list of fills with the number of fills
250            hasher.update(U256::from(fills.len()).as_le_slice());
251
252            for (user, amount) in fills.iter() {
253                // Concatenate the user address and amount for each fill.
254                hasher.update(user.as_slice());
255                hasher.update(amount.as_le_slice());
256            }
257        }
258
259        // Hash the host pre-image.
260        let host_preimage = hasher.finalize();
261
262        let mut pre_image = alloy::primitives::Keccak256::new();
263        pre_image.update(tx_preimage.as_slice());
264        pre_image.update(host_preimage.as_slice());
265
266        // Hash both tx and host hashes to get the final bundle hash.
267        pre_image.finalize()
268    }
269}
270
271/// Response for `zenith_callBundle`
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
273#[serde(rename_all = "camelCase")]
274pub struct ZenithCallBundleResponse {
275    /// The flattened "vanilla" response which comes from `eth_callBundle`
276    #[serde(flatten)]
277    pub response: EthCallBundleResponse,
278}