use alloy::{
consensus::{transaction::SignerRecoverable, Transaction, TxEnvelope},
eips::{eip2718::Encodable2718, BlockNumberOrTag, Decodable2718},
primitives::{keccak256, Bytes, B256, U256},
rlp::Buf,
rpc::types::mev::{EthCallBundle, EthCallBundleResponse, EthCallBundleTransactionResult},
};
use serde::{Deserialize, Serialize};
use signet_types::{AggregateFills, AggregateOrders};
use trevm::{
revm::{context::result::ExecutionResult, Database},
BundleError,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SignetCallBundle {
#[serde(flatten)]
pub bundle: EthCallBundle,
}
impl SignetCallBundle {
#[allow(clippy::missing_const_for_fn)] pub fn txs(&self) -> &[Bytes] {
&self.bundle.txs
}
pub const fn block_number(&self) -> u64 {
self.bundle.block_number
}
pub const fn state_block_number(&self) -> BlockNumberOrTag {
self.bundle.state_block_number
}
pub const fn timestamp(&self) -> Option<u64> {
self.bundle.timestamp
}
pub const fn gas_limit(&self) -> Option<u64> {
self.bundle.gas_limit
}
pub const fn difficulty(&self) -> Option<U256> {
self.bundle.difficulty
}
pub const fn base_fee(&self) -> Option<u128> {
self.bundle.base_fee
}
pub fn append_2718_tx(self, tx: impl Encodable2718) -> Self {
self.append_raw_tx(tx.encoded_2718())
}
pub fn append_raw_tx(mut self, tx: impl Into<Bytes>) -> Self {
self.bundle.txs.push(tx.into());
self
}
pub fn extend_2718_txs<I, T>(self, tx: I) -> Self
where
I: IntoIterator<Item = T>,
T: Encodable2718,
{
self.extend_raw_txs(tx.into_iter().map(|tx| tx.encoded_2718()))
}
pub fn extend_raw_txs<I, T>(mut self, txs: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Bytes>,
{
self.bundle.txs.extend(txs.into_iter().map(Into::into));
self
}
pub const fn with_block_number(mut self, block_number: u64) -> Self {
self.bundle.block_number = block_number;
self
}
pub fn with_state_block_number(
mut self,
state_block_number: impl Into<BlockNumberOrTag>,
) -> Self {
self.bundle.state_block_number = state_block_number.into();
self
}
pub const fn with_timestamp(mut self, timestamp: u64) -> Self {
self.bundle.timestamp = Some(timestamp);
self
}
pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self {
self.bundle.gas_limit = Some(gas_limit);
self
}
pub const fn with_difficulty(mut self, difficulty: U256) -> Self {
self.bundle.difficulty = Some(difficulty);
self
}
pub const fn with_base_fee(mut self, base_fee: u128) -> Self {
self.bundle.base_fee = Some(base_fee);
self
}
pub fn bundle_hash(&self) -> B256 {
let mut hasher = alloy::primitives::Keccak256::new();
for tx in self.bundle.txs.iter() {
hasher.update(keccak256(tx).as_slice());
}
hasher.finalize()
}
pub fn decode_and_validate_txs<Db: Database>(
&self,
) -> Result<Vec<TxEnvelope>, BundleError<Db>> {
let txs = self
.txs()
.iter()
.map(|tx| TxEnvelope::decode_2718(&mut tx.chunk()))
.collect::<Result<Vec<_>, _>>()
.map_err(|err| BundleError::TransactionDecodingError(err))?;
if txs.iter().any(|tx| tx.is_eip4844()) {
return Err(BundleError::UnsupportedTransactionType);
}
Ok(txs)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct SignetCallBundleResponse {
#[serde(flatten)]
inner: EthCallBundleResponse,
pub orders: AggregateOrders,
pub fills: AggregateFills,
}
impl core::ops::Deref for SignetCallBundleResponse {
type Target = EthCallBundleResponse;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl core::ops::DerefMut for SignetCallBundleResponse {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl AsRef<EthCallBundleResponse> for SignetCallBundleResponse {
fn as_ref(&self) -> &EthCallBundleResponse {
&self.inner
}
}
impl AsMut<EthCallBundleResponse> for SignetCallBundleResponse {
fn as_mut(&mut self) -> &mut EthCallBundleResponse {
&mut self.inner
}
}
impl From<EthCallBundleResponse> for SignetCallBundleResponse {
fn from(inner: EthCallBundleResponse) -> Self {
Self { inner, orders: Default::default(), fills: Default::default() }
}
}
impl From<SignetCallBundleResponse> for EthCallBundleResponse {
fn from(this: SignetCallBundleResponse) -> Self {
this.inner
}
}
impl SignetCallBundleResponse {
fn accumulate_tx_result(&mut self, tx_result: EthCallBundleTransactionResult) {
self.inner.total_gas_used += tx_result.gas_used;
self.inner.gas_fees += tx_result.gas_fees;
self.inner.results.push(tx_result);
}
pub fn accumulate_tx<Db: Database>(
&mut self,
tx: &TxEnvelope,
coinbase_diff: U256,
base_fee: u64,
execution_result: ExecutionResult,
) -> Result<(), BundleError<Db>> {
if let TxEnvelope::Eip4844(_) = tx {
return Err(BundleError::UnsupportedTransactionType);
}
let mut result = EthCallBundleTransactionResult::default();
result.from_address =
tx.recover_signer().map_err(|e| BundleError::TransactionSenderRecoveryError(e))?;
result.gas_price = U256::from(tx.effective_gas_price(Some(base_fee)));
result.gas_used = execution_result.gas_used();
result.gas_fees = result.gas_price * U256::from(result.gas_used);
if execution_result.is_success() {
result.value = Some(execution_result.into_output().unwrap_or_default());
} else {
result.revert = Some(execution_result.into_output().unwrap_or_default());
};
result.coinbase_diff = coinbase_diff;
result.eth_sent_to_coinbase = result.coinbase_diff.saturating_sub(result.gas_fees);
self.accumulate_tx_result(result);
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use alloy::{
eips::BlockNumberOrTag,
primitives::{Address, U256},
rpc::types::mev::{EthCallBundle, EthCallBundleTransactionResult},
};
#[test]
fn call_bundle_ser_roundtrip() {
let bundle = SignetCallBundle {
bundle: EthCallBundle {
txs: vec![b"tx1".into(), b"tx2".into()],
block_number: 1,
state_block_number: BlockNumberOrTag::Number(2),
timestamp: Some(3),
gas_limit: Some(4),
difficulty: Some(alloy::primitives::U256::from(5)),
base_fee: Some(6),
transaction_index: Some(7.into()),
coinbase: Some(Address::repeat_byte(8)),
timeout: Some(9),
},
};
let serialized = serde_json::to_string(&bundle).unwrap();
let deserialized: SignetCallBundle = serde_json::from_str(&serialized).unwrap();
assert_eq!(bundle, deserialized);
}
#[test]
fn call_bundle_resp_ser_roundtrip() {
let resp: SignetCallBundleResponse = EthCallBundleResponse {
bundle_hash: B256::repeat_byte(1),
bundle_gas_price: U256::from(2),
coinbase_diff: U256::from(3),
eth_sent_to_coinbase: U256::from(4),
gas_fees: U256::from(5),
results: vec![EthCallBundleTransactionResult {
coinbase_diff: U256::from(6),
eth_sent_to_coinbase: U256::from(7),
from_address: Address::repeat_byte(8),
gas_fees: U256::from(9),
gas_price: U256::from(10),
gas_used: 11,
to_address: Some(Address::repeat_byte(12)),
tx_hash: B256::repeat_byte(13),
value: Some(Bytes::from(b"value")),
revert: Some(Bytes::from(b"revert")),
}],
state_block_number: 14,
total_gas_used: 15,
}
.into();
let serialized = serde_json::to_string(&resp).unwrap();
let deserialized: SignetCallBundleResponse = serde_json::from_str(&serialized).unwrap();
assert_eq!(resp, deserialized);
}
#[test]
#[ignore]
fn generate_call_bundle_vectors() {
let vectors = vec![
(
"minimal",
SignetCallBundle {
bundle: EthCallBundle {
txs: vec![b"\x02\xf8test_tx_1".into()],
block_number: 12345678,
state_block_number: BlockNumberOrTag::Number(12345677),
..Default::default()
},
},
),
(
"with_overrides",
SignetCallBundle {
bundle: EthCallBundle {
txs: vec![b"\x02\xf8test_tx_1".into()],
block_number: 12345678,
state_block_number: BlockNumberOrTag::Number(12345677),
timestamp: Some(1700000000),
gas_limit: Some(30000000),
base_fee: Some(1000000000),
..Default::default()
},
},
),
(
"with_coinbase",
SignetCallBundle {
bundle: EthCallBundle {
txs: vec![b"\x02\xf8test_tx_1".into()],
block_number: 12345678,
state_block_number: BlockNumberOrTag::Latest,
coinbase: Some(Address::repeat_byte(0x42)),
timeout: Some(5),
..Default::default()
},
},
),
];
let output: Vec<_> = vectors
.into_iter()
.map(|(name, bundle)| {
serde_json::json!({
"name": name,
"bundle": bundle,
})
})
.collect();
println!("// SignetCallBundle vectors\n{}", serde_json::to_string_pretty(&output).unwrap());
let response_vectors = vec![
(
"minimal_response",
SignetCallBundleResponse::from(EthCallBundleResponse {
bundle_hash: B256::repeat_byte(0xaa),
bundle_gas_price: U256::from(1000000000u64),
coinbase_diff: U256::from(100000000000000u64),
eth_sent_to_coinbase: U256::from(50000000000000u64),
gas_fees: U256::from(50000000000000u64),
results: vec![EthCallBundleTransactionResult {
coinbase_diff: U256::from(100000000000000u64),
eth_sent_to_coinbase: U256::from(50000000000000u64),
from_address: Address::repeat_byte(0x11),
gas_fees: U256::from(50000000000000u64),
gas_price: U256::from(1000000000u64),
gas_used: 21000,
to_address: Some(Address::repeat_byte(0x22)),
tx_hash: B256::repeat_byte(0xbb),
value: Some(Bytes::from(b"result_data")),
revert: None,
}],
state_block_number: 12345677,
total_gas_used: 21000,
}),
),
(
"reverted_response",
SignetCallBundleResponse::from(EthCallBundleResponse {
bundle_hash: B256::repeat_byte(0xcc),
bundle_gas_price: U256::from(1000000000u64),
coinbase_diff: U256::from(0u64),
eth_sent_to_coinbase: U256::from(0u64),
gas_fees: U256::from(21000000000000u64),
results: vec![EthCallBundleTransactionResult {
coinbase_diff: U256::from(0u64),
eth_sent_to_coinbase: U256::from(0u64),
from_address: Address::repeat_byte(0x33),
gas_fees: U256::from(21000000000000u64),
gas_price: U256::from(1000000000u64),
gas_used: 21000,
to_address: Some(Address::repeat_byte(0x44)),
tx_hash: B256::repeat_byte(0xdd),
value: None,
revert: Some(Bytes::from(b"execution reverted")),
}],
state_block_number: 12345677,
total_gas_used: 21000,
}),
),
];
let response_output: Vec<_> = response_vectors
.into_iter()
.map(|(name, resp)| {
serde_json::json!({
"name": name,
"response": resp,
})
})
.collect();
println!(
"\n// SignetCallBundleResponse vectors\n{}",
serde_json::to_string_pretty(&response_output).unwrap()
);
}
}