use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use apollo_infra_utils::path::current_dir;
use cairo_lang_starknet_classes::casm_contract_class::CasmContractClass;
use cairo_lang_utils::bigint::BigUintAsHex;
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use starknet_types_core::felt::Felt;
use starknet_types_core::hash::{Pedersen, StarkHash as StarkHashTrait};
use crate::block::{
BlockHash,
BlockInfo,
BlockNumber,
BlockTimestamp,
GasPrice,
GasPriceVector,
GasPrices,
NonzeroGasPrice,
StarknetVersion,
};
use crate::contract_class::{ContractClass, SierraVersion};
use crate::core::{ChainId, ContractAddress, Nonce, STARKNET_OS_CONFIG_HASH_VERSION};
use crate::deprecated_contract_class::{ContractClass as DeprecatedContractClass, Program};
use crate::executable_transaction::AccountTransaction;
use crate::execution_resources::GasAmount;
use crate::rpc_transaction::{InternalRpcTransaction, RpcTransaction};
use crate::transaction::fields::{
AllResourceBounds,
Fee,
Proof,
ProofFacts,
ResourceBounds,
ValidResourceBounds,
PROOF_VERSION,
VIRTUAL_OS_OUTPUT_VERSION,
VIRTUAL_SNOS,
};
use crate::transaction::{Transaction, TransactionHash};
pub static TEST_OS_CONFIG_HASH: LazyLock<Felt> = LazyLock::new(|| {
let chain_id_felt: Felt = (&*CHAIN_ID_FOR_TESTS).try_into().unwrap();
let strk_fee_token_address: Felt = contract_address!(TEST_ERC20_CONTRACT_ADDRESS2).into();
Pedersen::hash_array(&[STARKNET_OS_CONFIG_HASH_VERSION, chain_id_felt, strk_fee_token_address])
});
use crate::{contract_address, felt};
pub mod declare;
pub mod deploy_account;
pub mod invoke;
pub mod l1_handler;
pub const TEST_SEQUENCER_ADDRESS: &str = "0x1000";
pub const TEST_ERC20_CONTRACT_ADDRESS: &str = "0x1001";
pub const TEST_ERC20_CONTRACT_ADDRESS2: &str = "0x1002";
pub const CURRENT_BLOCK_NUMBER: u64 = 2001;
pub const CURRENT_BLOCK_NUMBER_FOR_VALIDATE: u64 = 2000;
pub const BLOCK_HASH_HISTORY_RANGE: u64 = 51;
pub const CURRENT_BLOCK_TIMESTAMP: u64 = 1072023;
pub const CURRENT_BLOCK_TIMESTAMP_FOR_VALIDATE: u64 = 1069200;
pub static CHAIN_ID_FOR_TESTS: LazyLock<ChainId> =
LazyLock::new(|| ChainId::Other("CHAIN_ID_SUBDIR".to_owned()));
pub fn path_in_resources<P: AsRef<Path>>(file_path: P) -> PathBuf {
current_dir().unwrap().join("resources").join(file_path)
}
pub fn read_json_file<P: AsRef<Path>, T>(path_in_resource_dir: P) -> T
where
T: for<'a> serde::de::Deserialize<'a>,
{
let path = path_in_resources(path_in_resource_dir);
let file =
File::open(&path).unwrap_or_else(|_| panic!("Failed to open file at path: {path:?}"));
serde_json::from_reader(file)
.unwrap_or_else(|_| panic!("Failed to parse JSON from file at path: {path:?}"))
}
#[derive(Deserialize, Serialize, Debug)]
pub struct TransactionTestData {
pub transaction: Transaction,
pub transaction_hash: TransactionHash,
pub only_query_transaction_hash: Option<TransactionHash>,
pub chain_id: ChainId,
pub block_number: BlockNumber,
}
#[derive(Debug, Default, Clone)]
pub struct NonceManager {
next_nonce: HashMap<ContractAddress, Felt>,
}
impl NonceManager {
pub fn get(&self, account_address: ContractAddress) -> Nonce {
Nonce(*self.next_nonce.get(&account_address).unwrap_or(&Felt::default()))
}
pub fn next(&mut self, account_address: ContractAddress) -> Nonce {
let next = self.next_nonce.remove(&account_address).unwrap_or_default();
self.next_nonce.insert(account_address, next + 1);
Nonce(next)
}
pub fn rollback(&mut self, account_address: ContractAddress) {
let current_nonce = *self.next_nonce.get(&account_address).unwrap_or(&Felt::default());
if current_nonce != Felt::ZERO {
self.next_nonce.insert(account_address, current_nonce - 1);
}
}
}
#[macro_export]
macro_rules! nonce {
($s:expr) => {
$crate::core::Nonce(starknet_types_core::felt::Felt::from($s))
};
}
#[macro_export]
macro_rules! storage_key {
($s:expr) => {
$crate::state::StorageKey(starknet_api::patricia_key!($s))
};
}
#[macro_export]
macro_rules! compiled_class_hash {
($s:expr) => {
$crate::core::CompiledClassHash(starknet_types_core::felt::Felt::from($s))
};
}
#[macro_export]
macro_rules! proof_facts {
( $( $x:expr ),* ) => {
$crate::transaction::fields::ProofFacts(vec![$($x),*].into())
};
}
#[macro_export]
macro_rules! proof {
( $( $x:expr ),* ) => {
$crate::transaction::fields::Proof(vec![$($x),*].into())
};
}
pub const VALID_L1_GAS_MAX_AMOUNT: u64 = 203484;
pub const VALID_L1_GAS_MAX_PRICE_PER_UNIT: u128 = 100000000000000;
pub const VALID_L2_GAS_MAX_AMOUNT: u64 = 1_100_000_000;
pub const VALID_L2_GAS_MAX_PRICE_PER_UNIT: u128 = 100000000000000;
pub const VALID_L1_DATA_GAS_MAX_AMOUNT: u64 = 203484;
pub const VALID_L1_DATA_GAS_MAX_PRICE_PER_UNIT: u128 = 100000000000000;
#[allow(clippy::as_conversions)]
pub const VALID_ACCOUNT_BALANCE: Fee =
Fee(VALID_L2_GAS_MAX_AMOUNT as u128 * VALID_L2_GAS_MAX_PRICE_PER_UNIT * 1000);
pub const DEFAULT_L1_GAS_AMOUNT: GasAmount = GasAmount(u64::pow(10, 6));
pub const DEFAULT_L1_DATA_GAS_MAX_AMOUNT: GasAmount = GasAmount(u64::pow(10, 6));
pub const DEFAULT_L2_GAS_MAX_AMOUNT: GasAmount = GasAmount(u64::pow(10, 9));
pub const MAX_L1_GAS_PRICE: NonzeroGasPrice = DEFAULT_STRK_L1_GAS_PRICE;
pub const MAX_L2_GAS_PRICE: NonzeroGasPrice = DEFAULT_STRK_L2_GAS_PRICE;
pub const MAX_L1_DATA_GAS_PRICE: NonzeroGasPrice = DEFAULT_STRK_L1_DATA_GAS_PRICE;
pub const DEFAULT_ETH_L1_GAS_PRICE: NonzeroGasPrice =
NonzeroGasPrice::new_unchecked(GasPrice(100 * u128::pow(10, 9))); pub const DEFAULT_STRK_L1_GAS_PRICE: NonzeroGasPrice =
NonzeroGasPrice::new_unchecked(GasPrice(100 * u128::pow(10, 9))); pub const DEFAULT_ETH_L1_DATA_GAS_PRICE: NonzeroGasPrice =
NonzeroGasPrice::new_unchecked(GasPrice(u128::pow(10, 6))); pub const DEFAULT_STRK_L1_DATA_GAS_PRICE: NonzeroGasPrice =
NonzeroGasPrice::new_unchecked(GasPrice(u128::pow(10, 9))); pub const DEFAULT_ETH_L2_GAS_PRICE: NonzeroGasPrice =
NonzeroGasPrice::new_unchecked(GasPrice(u128::pow(10, 6)));
pub const DEFAULT_STRK_L2_GAS_PRICE: NonzeroGasPrice =
NonzeroGasPrice::new_unchecked(GasPrice(u128::pow(10, 9)));
pub const DEFAULT_GAS_PRICES: GasPrices = GasPrices {
eth_gas_prices: GasPriceVector {
l1_gas_price: DEFAULT_ETH_L1_GAS_PRICE,
l2_gas_price: DEFAULT_ETH_L2_GAS_PRICE,
l1_data_gas_price: DEFAULT_ETH_L1_DATA_GAS_PRICE,
},
strk_gas_prices: GasPriceVector {
l1_gas_price: DEFAULT_STRK_L1_GAS_PRICE,
l2_gas_price: DEFAULT_STRK_L2_GAS_PRICE,
l1_data_gas_price: DEFAULT_STRK_L1_DATA_GAS_PRICE,
},
};
pub const MAX_FEE: Fee = DEFAULT_L1_GAS_AMOUNT.nonzero_saturating_mul(DEFAULT_ETH_L1_GAS_PRICE);
pub const VIRTUAL_OS_PROGRAM_HASH: Felt = Felt::from_hex_unchecked("0x1");
pub fn test_block_hash(block_number: u64) -> BlockHash {
BlockHash(Felt::from(block_number * 100))
}
impl BlockInfo {
pub fn create_for_testing() -> Self {
Self {
block_number: BlockNumber(CURRENT_BLOCK_NUMBER),
block_timestamp: BlockTimestamp(CURRENT_BLOCK_TIMESTAMP),
sequencer_address: contract_address!(TEST_SEQUENCER_ADDRESS),
gas_prices: DEFAULT_GAS_PRICES,
use_kzg_da: false,
starknet_version: StarknetVersion::LATEST,
}
}
pub fn create_for_testing_with_kzg(use_kzg_da: bool) -> Self {
Self { use_kzg_da, ..Self::create_for_testing() }
}
}
pub fn resource_bounds_for_testing() -> AllResourceBounds {
AllResourceBounds {
l1_gas: ResourceBounds {
max_amount: GasAmount(VALID_L1_GAS_MAX_AMOUNT),
max_price_per_unit: GasPrice(VALID_L1_GAS_MAX_PRICE_PER_UNIT),
},
l2_gas: ResourceBounds {
max_amount: GasAmount(VALID_L2_GAS_MAX_AMOUNT),
max_price_per_unit: GasPrice(VALID_L2_GAS_MAX_PRICE_PER_UNIT),
},
l1_data_gas: ResourceBounds {
max_amount: GasAmount(VALID_L1_DATA_GAS_MAX_AMOUNT),
max_price_per_unit: GasPrice(VALID_L1_DATA_GAS_MAX_PRICE_PER_UNIT),
},
}
}
pub fn valid_resource_bounds_for_testing() -> ValidResourceBounds {
ValidResourceBounds::AllResources(resource_bounds_for_testing())
}
pub trait TestingTxArgs {
fn get_rpc_tx(&self) -> RpcTransaction;
fn get_internal_tx(&self) -> InternalRpcTransaction;
fn get_executable_tx(&self) -> AccountTransaction;
}
static TEST_CASM_CONTRACT_CLASS: LazyLock<ContractClass> = LazyLock::new(|| {
let default_casm = CasmContractClass {
prime: Default::default(),
compiler_version: Default::default(),
bytecode: vec![
BigUintAsHex { value: BigUint::from(1_u8) },
BigUintAsHex { value: BigUint::from(1_u8) },
BigUintAsHex { value: BigUint::from(1_u8) },
],
bytecode_segment_lengths: Default::default(),
hints: Default::default(),
pythonic_hints: Default::default(),
entry_points_by_type: Default::default(),
};
ContractClass::V1((default_casm, SierraVersion::default()))
});
static TEST_DEPRECATED_CASM_CONTRACT_CLASS: LazyLock<ContractClass> = LazyLock::new(|| {
let default_deprecated_casm = DeprecatedContractClass {
abi: None,
program: Program {
attributes: serde_json::Value::Null,
builtins: serde_json::Value::Array(vec![]),
compiler_version: serde_json::Value::Null,
data: serde_json::Value::Array(vec![]),
debug_info: serde_json::Value::Null,
hints: serde_json::Value::Object(serde_json::Map::new()),
identifiers: serde_json::Value::Object(serde_json::Map::new()),
main_scope: serde_json::Value::String("__main__".to_string()),
prime: serde_json::Value::String(
"0x800000000000011000000000000000000000000000000000000000000000001".to_string(),
),
reference_manager: serde_json::Value::Object({
let mut map = serde_json::Map::new();
map.insert("references".to_string(), serde_json::Value::Array(vec![]));
map
}),
},
entry_points_by_type: Default::default(),
};
ContractClass::V0(default_deprecated_casm)
});
impl ContractClass {
pub fn test_casm_contract_class() -> Self {
TEST_CASM_CONTRACT_CLASS.clone()
}
pub fn test_deprecated_casm_contract_class() -> Self {
TEST_DEPRECATED_CASM_CONTRACT_CLASS.clone()
}
}
pub(crate) struct PyJsonFormatter;
impl PyJsonFormatter {
pub(crate) fn comma() -> &'static [u8; 2] {
b", "
}
pub(crate) fn colon() -> &'static [u8; 2] {
b": "
}
}
impl serde_json::ser::Formatter for PyJsonFormatter {
fn begin_array_value<W: ?Sized + std::io::Write>(
&mut self,
writer: &mut W,
first: bool,
) -> std::io::Result<()> {
if !first {
writer.write_all(Self::comma())?;
}
Ok(())
}
fn begin_object_key<W: ?Sized + std::io::Write>(
&mut self,
writer: &mut W,
first: bool,
) -> std::io::Result<()> {
if !first {
writer.write_all(Self::comma())?;
}
Ok(())
}
fn begin_object_value<W: ?Sized + std::io::Write>(
&mut self,
writer: &mut W,
) -> std::io::Result<()> {
writer.write_all(Self::colon())
}
}
pub(crate) fn py_json_dumps<T: ?Sized + Serialize>(value: &T) -> Result<String, serde_json::Error> {
let mut string_buffer = vec![];
let mut ser = serde_json::Serializer::with_formatter(&mut string_buffer, PyJsonFormatter);
value.serialize(&mut ser)?;
Ok(String::from_utf8(string_buffer).expect("serialized JSON should be valid UTF-8"))
}
impl ProofFacts {
pub fn snos_proof_facts_for_testing() -> Self {
Self::custom_proof_facts_for_testing(VIRTUAL_OS_PROGRAM_HASH, *TEST_OS_CONFIG_HASH)
}
pub fn custom_proof_facts_for_testing(program_hash: Felt, config_hash: Felt) -> Self {
let block_hash_history_start = CURRENT_BLOCK_NUMBER - BLOCK_HASH_HISTORY_RANGE;
let block_number_u64 = block_hash_history_start + 2;
let block_number = felt!(block_number_u64);
let block_hash = test_block_hash(block_number_u64).0;
assert!(
block_number < felt!(CURRENT_BLOCK_NUMBER)
&& block_number >= felt!(block_hash_history_start),
"To have a matching block hash stored in tests, the block number must be one whose \
block hash is populated in the block-hash contract. block_number: {block_number}, \
block_hash_history_start: {block_hash_history_start}"
);
let l2_to_l1_messages_segment_size = Felt::ZERO;
proof_facts![
PROOF_VERSION,
VIRTUAL_SNOS,
program_hash,
VIRTUAL_OS_OUTPUT_VERSION,
block_number,
block_hash,
config_hash,
l2_to_l1_messages_segment_size
]
}
}
impl Proof {
pub fn proof_for_testing() -> Self {
proof!(1, 2, 3, 4, 5, 6)
}
}