|
| 1 | +use alloy::{ |
| 2 | + eips::{eip2718::Encodable2718, BlockNumberOrTag}, |
| 3 | + rpc::types::mev::{EthCallBundle, EthCallBundleResponse, EthSendBundle}, |
| 4 | +}; |
| 5 | +use alloy_primitives::{keccak256, Address, Bytes, B256, U256}; |
| 6 | +use serde::{Deserialize, Serialize}; |
| 7 | +use std::collections::BTreeMap; |
| 8 | + |
| 9 | +use 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")] |
| 14 | +pub 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 | + |
| 23 | +impl 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")] |
| 58 | +pub 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")] |
| 68 | +pub 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 | + |
| 78 | +impl 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")] |
| 274 | +pub struct ZenithCallBundleResponse { |
| 275 | + /// The flattened "vanilla" response which comes from `eth_callBundle` |
| 276 | + #[serde(flatten)] |
| 277 | + pub response: EthCallBundleResponse, |
| 278 | +} |
0 commit comments