use mega_evm::{
revm::{
context::tx::TxEnv,
primitives::{Address, Bytes, HashMap, TxKind, B256},
},
Either,
};
use serde::{Deserialize, Serialize};
use super::{error::TestError, transaction::TxPartIndices, AccountInfo, TestUnit};
use crate::utils::recover_address;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Test {
#[serde(skip_serializing_if = "Option::is_none")]
pub expect_exception: Option<String>,
pub indexes: TxPartIndices,
pub hash: B256,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub post_state: HashMap<Address, AccountInfo>,
pub logs: B256,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
state: HashMap<Address, AccountInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub txbytes: Option<Bytes>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mega_gas_used: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mega_status: Option<String>,
}
impl Test {
pub fn for_dump(hash: B256, logs: B256, mega_gas_used: u64, mega_status: String) -> Self {
Self {
expect_exception: None,
indexes: TxPartIndices { data: 0, gas: 0, value: 0 },
hash,
post_state: HashMap::default(),
logs,
state: HashMap::default(),
txbytes: None,
mega_gas_used: Some(mega_gas_used),
mega_status: Some(mega_status),
}
}
pub fn tx_env(&self, unit: &TestUnit) -> Result<TxEnv, TestError> {
tx_env_at(unit, self.indexes).map_err(|e| match e {
TestError::InvalidTransactionType if self.expect_exception.is_some() => {
TestError::UnexpectedException {
expected_exception: self.expect_exception.clone(),
got_exception: Some("Invalid transaction type".to_string()),
}
}
other => other,
})
}
}
pub fn tx_env_at(unit: &TestUnit, indexes: TxPartIndices) -> Result<TxEnv, TestError> {
let caller = if let Some(address) = unit.transaction.sender {
address
} else {
recover_address(unit.transaction.secret_key.as_slice())
.ok_or(TestError::UnknownPrivateKey(unit.transaction.secret_key))?
};
let tx_type =
unit.transaction.tx_type(indexes.data).ok_or(TestError::InvalidTransactionType)?;
let tx = TxEnv {
caller,
gas_price: unit
.transaction
.gas_price
.or(unit.transaction.max_fee_per_gas)
.unwrap_or_default()
.try_into()
.unwrap_or(u128::MAX),
gas_priority_fee: unit
.transaction
.max_priority_fee_per_gas
.map(|b| {
u128::try_from(b).map_err(|_| TestError::ValueOutOfRange {
field: "maxPriorityFeePerGas",
value: b,
})
})
.transpose()?,
blob_hashes: unit.transaction.blob_versioned_hashes.clone(),
max_fee_per_blob_gas: unit
.transaction
.max_fee_per_blob_gas
.map(|b| {
u128::try_from(b)
.map_err(|_| TestError::ValueOutOfRange { field: "maxFeePerBlobGas", value: b })
})
.transpose()?
.unwrap_or(u128::MAX),
tx_type: tx_type as u8,
gas_limit: unit
.transaction
.gas_limit
.get(indexes.gas)
.ok_or(TestError::PartIndexOutOfBounds {
part: "gasLimit",
index: indexes.gas,
len: unit.transaction.gas_limit.len(),
})?
.saturating_to(),
data: unit
.transaction
.data
.get(indexes.data)
.ok_or(TestError::PartIndexOutOfBounds {
part: "data",
index: indexes.data,
len: unit.transaction.data.len(),
})?
.clone(),
nonce: u64::try_from(unit.transaction.nonce).map_err(|_| TestError::ValueOutOfRange {
field: "nonce",
value: unit.transaction.nonce,
})?,
value: *unit.transaction.value.get(indexes.value).ok_or(
TestError::PartIndexOutOfBounds {
part: "value",
index: indexes.value,
len: unit.transaction.value.len(),
},
)?,
access_list: unit
.transaction
.access_lists
.get(indexes.data)
.cloned()
.flatten()
.unwrap_or_default(),
authorization_list: unit
.transaction
.authorization_list
.clone()
.map(|auth_list| {
auth_list.into_iter().map(|i| Either::Left(i.into())).collect::<Vec<_>>()
})
.unwrap_or_default(),
kind: match unit.transaction.to {
Some(add) => TxKind::Call(add),
None => TxKind::Create,
},
..TxEnv::default()
};
Ok(tx)
}
#[cfg(test)]
mod tests {
use super::*;
use mega_evm::revm::primitives::U256;
fn unit_json() -> serde_json::Value {
serde_json::json!({
"env": {
"currentCoinbase": "0x3000000000000000000000000000000000000003",
"currentDifficulty": "0x0",
"currentGasLimit": "0x1c9c380",
"currentNumber": "0x10",
"currentTimestamp": "0x3e8"
},
"pre": {},
"post": {},
"transaction": {
"type": 0,
"data": ["0x"],
"gasLimit": ["0x30d40"],
"gasPrice": "0x0",
"nonce": "0x0",
"secretKey": "0x0000000000000000000000000000000000000000000000000000000000000000",
"sender": "0x1000000000000000000000000000000000000001",
"to": "0x2000000000000000000000000000000000000002",
"value": ["0x0"]
}
})
}
fn unit_with(patch: impl FnOnce(&mut serde_json::Value)) -> TestUnit {
let mut json = unit_json();
patch(&mut json["transaction"]);
serde_json::from_value(json).expect("parse unit")
}
const ZERO_INDEXES: TxPartIndices = TxPartIndices { data: 0, gas: 0, value: 0 };
fn assert_out_of_range(unit: &TestUnit, field: &'static str) {
match tx_env_at(unit, ZERO_INDEXES) {
Err(TestError::ValueOutOfRange { field: got, .. }) => assert_eq!(got, field),
other => panic!("expected ValueOutOfRange for `{field}`, got {other:?}"),
}
}
#[test]
fn tx_env_at_in_range_values_succeed() {
let unit = unit_with(|tx| {
tx["nonce"] = serde_json::json!(format!("{:#x}", u64::MAX));
tx["maxPriorityFeePerGas"] = serde_json::json!(format!("{:#x}", u128::MAX));
tx["maxFeePerGas"] = serde_json::json!(format!("{:#x}", u128::MAX));
});
let tx = tx_env_at(&unit, ZERO_INDEXES).expect("in-range values must build");
assert_eq!(tx.nonce, u64::MAX);
assert_eq!(tx.gas_priority_fee, Some(u128::MAX));
}
#[test]
fn tx_env_at_nonce_overflow_is_structured_error() {
let unit =
unit_with(|tx| tx["nonce"] = serde_json::json!(format!("{:#x}", u64::MAX as u128 + 1)));
assert_out_of_range(&unit, "nonce");
}
#[test]
fn tx_env_at_priority_fee_overflow_is_structured_error() {
let unit = unit_with(|tx| {
tx["maxPriorityFeePerGas"] = serde_json::json!(format!("{:#x}", U256::MAX));
});
assert_out_of_range(&unit, "maxPriorityFeePerGas");
}
#[test]
fn tx_env_at_blob_fee_overflow_is_structured_error() {
let unit = unit_with(|tx| {
tx["type"] = serde_json::json!(3);
tx["maxFeePerBlobGas"] = serde_json::json!(format!("{:#x}", U256::MAX));
});
assert_out_of_range(&unit, "maxFeePerBlobGas");
}
#[test]
fn tx_env_at_gas_price_and_gas_limit_keep_saturating() {
let unit = unit_with(|tx| {
tx["gasPrice"] = serde_json::json!(format!("{:#x}", U256::MAX));
tx["gasLimit"] = serde_json::json!([format!("{:#x}", U256::MAX)]);
});
let tx = tx_env_at(&unit, ZERO_INDEXES).expect("saturating fields must not error");
assert_eq!(tx.gas_price, u128::MAX);
assert_eq!(tx.gas_limit, u64::MAX);
}
}