diff --git a/crates/node/src/chain_spec.rs b/crates/node/src/chain_spec.rs index c0819eb7d2..2f1b63ed13 100644 --- a/crates/node/src/chain_spec.rs +++ b/crates/node/src/chain_spec.rs @@ -155,6 +155,10 @@ fn testnet_genesis( }, transaction_payment: Default::default(), /// Starknet Genesis configuration. - starknet: madara_runtime::pallet_starknet::GenesisConfig { contracts: vec![], _phantom: Default::default() }, + starknet: madara_runtime::pallet_starknet::GenesisConfig { + contracts: vec![], + contract_classes: vec![], + _phantom: Default::default(), + }, } } diff --git a/crates/pallets/starknet/src/lib.rs b/crates/pallets/starknet/src/lib.rs index 7c704fc3bf..50f15e3b93 100644 --- a/crates/pallets/starknet/src/lib.rs +++ b/crates/pallets/starknet/src/lib.rs @@ -58,13 +58,12 @@ pub mod pallet { // use blockifier::execution::contract_class::ContractClass; use blockifier::state::cached_state::{CachedState, ContractClassMapping, ContractStorageKey}; - use blockifier::test_utils::{get_contract_class, get_test_contract_class, ACCOUNT_CONTRACT_PATH}; use frame_support::pallet_prelude::*; use frame_support::traits::{OriginTrait, Randomness, Time}; use frame_system::pallet_prelude::*; use mp_starknet::crypto::commitment; use mp_starknet::crypto::hash::pedersen::PedersenHasher; - use mp_starknet::execution::{ClassHashWrapper, ContractAddressWrapper}; + use mp_starknet::execution::{ClassHashWrapper, ContractAddressWrapper, ContractClassWrapper}; use mp_starknet::starknet_block::block::Block; use mp_starknet::starknet_block::header::Header; use mp_starknet::state::DictStateReader; @@ -79,6 +78,7 @@ pub mod pallet { use starknet_api::hash::StarkFelt; use starknet_api::state::StorageKey; use starknet_api::stdlib::collections::HashMap; + use starknet_api::StarknetApiError; use types::{EthBlockNumber, OffchainWorkerError}; use super::*; @@ -87,6 +87,7 @@ pub mod pallet { use crate::types::{ContractStorageKeyWrapper, EthLogs, NonceWrapper, StarkFeltWrapper}; #[pallet::pallet] + #[pallet::without_storage_info] // TODO: Remove this when we have bounded every storage item. pub struct Pallet(_); /// Configure the pallet by specifying the parameters and types on which it depends. @@ -161,10 +162,16 @@ pub mod pallet { /// Mapping from Starknet contract address to the contract's class hash. #[pallet::storage] - #[pallet::getter(fn contract_class)] + #[pallet::getter(fn contract_class_hash_by_address)] pub(super) type ContractClassHashes = StorageMap<_, Twox64Concat, ContractAddressWrapper, ClassHashWrapper, ValueQuery>; + /// Mapping from Starknet class hash to contract class. + #[pallet::storage] + #[pallet::getter(fn contract_class_by_class_hash)] + pub(super) type ContractClasses = + StorageMap<_, Twox64Concat, ClassHashWrapper, ContractClassWrapper, ValueQuery>; + /// Mapping from Starknet contract address to its nonce. #[pallet::storage] #[pallet::getter(fn nonce)] @@ -184,13 +191,14 @@ pub mod pallet { #[pallet::genesis_config] pub struct GenesisConfig { pub contracts: Vec<(ContractAddressWrapper, ClassHashWrapper)>, + pub contract_classes: Vec<(ClassHashWrapper, ContractClassWrapper)>, pub _phantom: PhantomData, } #[cfg(feature = "std")] impl Default for GenesisConfig { fn default() -> Self { - Self { contracts: vec![], _phantom: PhantomData } + Self { contracts: vec![], contract_classes: vec![], _phantom: PhantomData } } } @@ -207,6 +215,10 @@ pub mod pallet { ContractClassHashes::::insert(address, class_hash); } + for (class_hash, contract_class) in self.contract_classes.iter() { + ContractClasses::::insert(class_hash, contract_class); + } + LastKnownEthBlock::::set(None); } } @@ -228,8 +240,17 @@ pub mod pallet { pub enum Error { AccountNotDeployed, TransactionExecutionFailed, - ContractClassHashAlreadyAssociated, + ClassHashAlreadyDeclared, ContractClassHashUnknown, + ContractClassAlreadyAssociated, + ContractClassMustBeSpecified, + AccountAlreadyDeployed, + ContractAddressAlreadyAssociated, + InvalidContractClass, + ClassHashMustBeSpecified, + TooManyPendingTransactions, + InvalidCurrentBlock, + StateReaderError, } /// The Starknet pallet external functions. @@ -272,8 +293,8 @@ pub mod pallet { ensure!(ContractClassHashes::::contains_key(transaction.sender_address), Error::::AccountNotDeployed); let block = Self::current_block().unwrap(); - let state = &mut Self::create_state_reader(); - match transaction.execute(state, block, TxType::InvokeTx) { + let state = &mut Self::create_state_reader()?; + match transaction.execute(state, block, TxType::InvokeTx, None) { Ok(v) => { log!(info, "Transaction executed successfully: {:?}", v.unwrap()); } @@ -291,6 +312,136 @@ pub mod pallet { Ok(()) } + // Submit a Starknet declare transaction. + /// # Arguments + /// + /// * `origin` - The origin of the transaction. + /// * `transaction` - The Starknet transaction. + /// + /// # Returns + /// + /// * `DispatchResult` - The result of the transaction. + /// + /// # TODO + /// * Compute weight + #[pallet::call_index(3)] + #[pallet::weight(0)] + pub fn add_declare_transaction(_origin: OriginFor, transaction: Transaction) -> DispatchResult { + // TODO: add origin check when proxy pallet added + + // Check if contract is deployed + ensure!(ContractClassHashes::::contains_key(transaction.sender_address), Error::::AccountNotDeployed); + + // Check that class hash is not None + ensure!(transaction.call_entrypoint.class_hash.is_some(), Error::::ClassHashMustBeSpecified); + + let class_hash = transaction.call_entrypoint.class_hash.unwrap(); + + // Check class hash is not already declared + ensure!(!ContractClasses::::contains_key(class_hash), Error::::ClassHashAlreadyDeclared); + + // Check that contract class is not None + ensure!(transaction.contract_class.is_some(), Error::::ContractClassMustBeSpecified); + + // Get current block + let block = match Self::current_block() { + Some(b) => b, + None => { + log!(error, "Current block is None"); + return Err(Error::::InvalidCurrentBlock.into()); + } + }; + + // Create state reader from substrate storage + let state = &mut Self::create_state_reader()?; + + // Parse contract class + let contract_class = transaction + .clone() + .contract_class + .unwrap() + .to_starknet_contract_class() + .or(Err(Error::::InvalidContractClass))?; + + // Execute transaction + match transaction.execute(state, block, TxType::DeclareTx, Some(contract_class.clone())) { + Ok(v) => { + log!(info, "Transaction executed successfully: {:?}", v.unwrap_or_default()); + } + Err(e) => { + log!(error, "Transaction execution failed: {:?}", e); + return Err(Error::::TransactionExecutionFailed.into()); + } + } + + // Append the transaction to the pending transactions. + Pending::::try_append(transaction.clone()).or(Err(Error::::TooManyPendingTransactions))?; + + // Associate contract class to class hash + Self::associate_class_hash(class_hash, contract_class.into())?; + + // TODO: Update class hashes root + + Ok(()) + } + + // Submit a Starknet deploy transaction. + /// # Arguments + /// + /// * `origin` - The origin of the transaction. + /// * `transaction` - The Starknet transaction. + /// + /// # Returns + /// + /// * `DispatchResult` - The result of the transaction. + /// + /// # TODO + /// * Compute weight + #[pallet::call_index(4)] + #[pallet::weight(0)] + pub fn add_deploy_account_transaction(_origin: OriginFor, transaction: Transaction) -> DispatchResult { + // TODO: add origin check when proxy pallet added + + // Check if contract is deployed + ensure!( + !ContractClassHashes::::contains_key(transaction.sender_address), + Error::::AccountAlreadyDeployed + ); + + // Get current block + let block = match Self::current_block() { + Some(b) => b, + None => { + log!(error, "Current block is None"); + return Err(Error::::InvalidCurrentBlock.into()); + } + }; + + let state = &mut Self::create_state_reader()?; + match transaction.execute(state, block, TxType::DeployTx, None) { + Ok(v) => { + log!(info, "Transaction executed successfully: {:?}", v.unwrap()); + } + Err(e) => { + log!(error, "Transaction execution failed: {:?}", e); + return Err(Error::::TransactionExecutionFailed.into()); + } + } + + // Append the transaction to the pending transactions. + Pending::::try_append(transaction.clone()).unwrap(); + + // Associate contract class to class hash + Self::associate_contract_class( + transaction.clone().sender_address, + transaction.clone().call_entrypoint.class_hash.unwrap(), + )?; + + // TODO: Apply state diff and update state root + + Ok(()) + } + /// Consume a message from L1. /// /// # Arguments @@ -312,8 +463,8 @@ pub mod pallet { ensure!(ContractClassHashes::::contains_key(transaction.sender_address), Error::::AccountNotDeployed); let block = Self::current_block().unwrap(); - let state = &mut Self::create_state_reader(); - match transaction.execute(state, block, TxType::L1HandlerTx) { + let state = &mut Self::create_state_reader()?; + match transaction.execute(state, block, TxType::L1HandlerTx, None) { Ok(v) => { log!(info, "Transaction executed successfully: {:?}", v.unwrap()); } @@ -324,7 +475,7 @@ pub mod pallet { } // Append the transaction to the pending transactions. - Pending::::try_append(transaction).unwrap(); + Pending::::try_append(transaction.clone()).or(Err(Error::::TooManyPendingTransactions))?; // TODO: Apply state diff and update state root @@ -423,25 +574,43 @@ pub mod pallet { Pending::::kill(); } + /// Associate a contract class hash with a contract class info + /// + /// # Arguments + /// + /// * `contract_class_hash` - The contract class hash. + /// * `class_info` - The contract class info. + fn associate_class_hash( + contract_class_hash: ClassHashWrapper, + class_info: ContractClassWrapper, + ) -> Result<(), DispatchError> { + // Check if the contract address is already associated with a contract class hash. + ensure!( + !ContractClasses::::contains_key(contract_class_hash), + Error::::ContractClassAlreadyAssociated + ); + + ContractClasses::::insert(contract_class_hash, class_info); + + Ok(()) + } + /// Associate a contract address with a contract class hash. /// /// # Arguments /// /// * `contract_address` - The contract address. /// * `contract_class_hash` - The contract class hash. - fn _associate_contract_class( + fn associate_contract_class( contract_address: ContractAddressWrapper, contract_class_hash: ClassHashWrapper, ) -> Result<(), DispatchError> { // Check if the contract address is already associated with a contract class hash. ensure!( !ContractClassHashes::::contains_key(contract_address), - Error::::ContractClassHashAlreadyAssociated + Error::::ContractAddressAlreadyAssociated ); - // Check if the contract class hash is known. - ensure!(ContractClassHashes::::contains_key(contract_class_hash), Error::::ContractClassHashUnknown); - ContractClassHashes::::insert(contract_address, contract_class_hash); Ok(()) @@ -452,7 +621,9 @@ pub mod pallet { /// # Returns /// /// The state reader. - fn create_state_reader() -> CachedState { + fn create_state_reader() -> Result, DispatchError> { + // TODO: Handle errors and propagate them to the caller. + let address_to_class_hash: HashMap = ContractClassHashes::::iter() .map(|(key, value)| { ( @@ -483,54 +654,23 @@ pub mod pallet { }) .collect(); - // let class_hash_to_class: ContractClassMapping = ContractClasses::::iter().map(|(key, value)| { - // let class_hash = ClassHash(StarkFelt::new(key).unwrap()); - // let contract_class = ContractClass::try_from(value).unwrap(); - // (class_hash, contract_class) - // }).collect(); - // TODO: remove this when declare is implemented - let class_hash_to_class: ContractClassMapping = HashMap::from([ - ( - ClassHash( - StarkFelt::try_from( - "0x025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918", /* TEST ACCOUNT - * CONTRACT CLASS - * HASH */ - ) - .unwrap(), - ), - get_contract_class(ACCOUNT_CONTRACT_PATH), - ), - ( - ClassHash( - StarkFelt::try_from( - "0x025ec026985a3bf9d0cc1fe17326b245bfdc3ff89b8fde106242a3ea56c5a918", /* TEST FEATURES - * CONTRACT CLASS - * HASH */ - ) - .unwrap(), - ), - get_test_contract_class(), - ), - ( - ClassHash( - StarkFelt::try_from( - "0x1cb5d0b5b5146e1aab92eb9fc9883a32a33a604858bb0275ac0ee65d885bba8", /* TEST EMPTY L1 - * HANDLER CONTRACT - * CLASS HASH */ - ) - .unwrap(), - ), - get_contract_class(include_bytes!("../../../../ressources/l1_handler.json")), - ), - ]); - - CachedState::new(DictStateReader { + let class_hash_to_class: ContractClassMapping = ContractClasses::::iter() + .map(|(key, value)| { + let class_hash = ClassHash(StarkFelt::new(key)?); + let contract_class = value.to_starknet_contract_class().unwrap(); + Ok((class_hash, contract_class)) + }) + .collect::>() + .map_err(|_| Error::::StateReaderError)? + .into_iter() + .collect(); + + Ok(CachedState::new(DictStateReader { address_to_class_hash, address_to_nonce, storage_view, class_hash_to_class, - }) + })) } /// Queries an Eth json rpc node. diff --git a/crates/pallets/starknet/src/message.rs b/crates/pallets/starknet/src/message.rs index 810e40a741..18cb28fde8 100644 --- a/crates/pallets/starknet/src/message.rs +++ b/crates/pallets/starknet/src/message.rs @@ -95,6 +95,7 @@ impl Message { sender_address, nonce, call_entrypoint, + contract_class: None, }) } } @@ -146,6 +147,7 @@ mod test { storage_address: H256::from_low_u64_be(1).to_fixed_bytes(), caller_address: ContractAddressWrapper::default(), }, + contract_class: None, }; pretty_assertions::assert_eq!(test_message.try_into_transaction().unwrap(), expected_tx); } diff --git a/crates/pallets/starknet/src/mock.rs b/crates/pallets/starknet/src/mock.rs index b7ceed8567..298b804049 100644 --- a/crates/pallets/starknet/src/mock.rs +++ b/crates/pallets/starknet/src/mock.rs @@ -1,5 +1,7 @@ +use blockifier::test_utils::{get_contract_class, get_test_contract_class, ACCOUNT_CONTRACT_PATH}; use frame_support::traits::{ConstU16, ConstU64, GenesisBuild, Hooks}; use hex::FromHex; +use mp_starknet::execution::ContractClassWrapper; use sp_core::H256; use sp_runtime::testing::Header; use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; @@ -71,6 +73,10 @@ impl pallet_starknet::Config for Test { pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + let account_class = get_contract_class(ACCOUNT_CONTRACT_PATH); + let test_class = get_test_contract_class(); + let l1_handler_class = get_contract_class(include_bytes!("../../../../ressources/l1_handler.json")); + // ACCOUNT CONTRACT let contract_address_str = "02356b628D108863BAf8644c945d97bAD70190AF5957031f4852d00D0F690a77"; let contract_address_bytes = <[u8; 32]>::from_hex(contract_address_str).unwrap(); @@ -98,6 +104,11 @@ pub fn new_test_ext() -> sp_io::TestExternalities { (other_contract_address_bytes, other_class_hash_bytes), (l1_handler_contract_address_bytes, l1_handler_class_hash_bytes), ], + contract_classes: vec![ + (class_hash_bytes, ContractClassWrapper::from(account_class)), + (other_class_hash_bytes, ContractClassWrapper::from(test_class)), + (l1_handler_class_hash_bytes, ContractClassWrapper::from(l1_handler_class)), + ], ..Default::default() } .assimilate_storage(&mut t) diff --git a/crates/pallets/starknet/src/tests.rs b/crates/pallets/starknet/src/tests.rs index fc17ec648d..67a751a5d1 100644 --- a/crates/pallets/starknet/src/tests.rs +++ b/crates/pallets/starknet/src/tests.rs @@ -1,8 +1,9 @@ use core::str::FromStr; +use blockifier::test_utils::{get_contract_class, ACCOUNT_CONTRACT_PATH}; use frame_support::{assert_err, assert_ok, bounded_vec}; use hex::FromHex; -use mp_starknet::execution::{CallEntryPointWrapper, EntryPointTypeWrapper}; +use mp_starknet::execution::{CallEntryPointWrapper, ContractClassWrapper, EntryPointTypeWrapper}; use mp_starknet::starknet_block::header::Header; use mp_starknet::transaction::types::Transaction; use sp_core::{H256, U256}; @@ -111,6 +112,7 @@ fn given_hardcoded_contract_run_invoke_tx_then_it_works() { contract_address_bytes, contract_address_bytes ), + None, ); let tx = Message { @@ -121,3 +123,268 @@ fn given_hardcoded_contract_run_invoke_tx_then_it_works() { assert_ok!(Starknet::consume_l1_message(none_origin, tx)); }); } + +#[test] +fn given_hardcoded_contract_run_deploy_account_tx_then_it_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + run_to_block(2); + + let none_origin = RuntimeOrigin::none(); + + // TODO: Compute address from salt/hash/calldata/deployer + let contract_address_str = "02356b628D108863BAf8644c125d97bAD70190AF5957031f4852d00D0F690a77"; + let contract_address_bytes = <[u8; 32]>::from_hex(contract_address_str).unwrap(); + + let class_hash_str = "025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918"; + let class_hash_bytes = <[u8; 32]>::from_hex(class_hash_str).unwrap(); + + // Example tx : https://testnet.starkscan.co/tx/0x6fc3466f58b5c6aaa6633d48702e1f2048fb96b7de25f2bde0bce64dca1d212 + let transaction = Transaction::new( + U256::from(1), + H256::default(), + bounded_vec!(), + bounded_vec!(), + contract_address_bytes, + U256::from(0), + CallEntryPointWrapper::new( + Some(class_hash_bytes), + EntryPointTypeWrapper::External, + None, + bounded_vec![ + // Constructor calldata + ], + contract_address_bytes, + contract_address_bytes, + ), + None, + ); + + assert_ok!(Starknet::add_deploy_account_transaction(none_origin, transaction)); + + // Check that the account was created + assert_eq!(Starknet::contract_class_hash_by_address(contract_address_bytes), class_hash_bytes); + }); +} + +#[test] +fn given_hardcoded_contract_run_deploy_account_tx_twice_then_it_fails() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + run_to_block(2); + + let none_origin = RuntimeOrigin::none(); + + // TODO: Compute address from salt/hash/calldata/deployer + let contract_address_str = "02356b628D108863BAf8644c125d97bAD70190AF5957031f4852d00D0F690a77"; + let contract_address_bytes = <[u8; 32]>::from_hex(contract_address_str).unwrap(); + + let class_hash_str = "025ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918"; + let class_hash_bytes = <[u8; 32]>::from_hex(class_hash_str).unwrap(); + + // Example tx : https://testnet.starkscan.co/tx/0x6fc3466f58b5c6aaa6633d48702e1f2048fb96b7de25f2bde0bce64dca1d212 + let transaction = Transaction::new( + U256::from(1), + H256::default(), + bounded_vec!(), + bounded_vec!(), + contract_address_bytes, + U256::from(0), + CallEntryPointWrapper::new( + Some(class_hash_bytes), + EntryPointTypeWrapper::External, + None, + bounded_vec![ + // Constructor calldata + ], + contract_address_bytes, + contract_address_bytes, + ), + None, + ); + + assert_ok!(Starknet::add_deploy_account_transaction(none_origin.clone(), transaction.clone())); + + // Check that the account was created + assert_eq!(Starknet::contract_class_hash_by_address(contract_address_bytes), class_hash_bytes); + + assert_err!( + Starknet::add_deploy_account_transaction(none_origin, transaction), + Error::::AccountAlreadyDeployed + ); + }); +} + +#[test] +fn given_hardcoded_contract_run_deploy_account_tx_undeclared_then_it_fails() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + run_to_block(2); + + let none_origin = RuntimeOrigin::none(); + + // TODO: Compute address from salt/hash/calldata/deployer + let contract_address_str = "02356b628D108863BAf8644c125d97bAD70190AF5957031f4852d00D0F690a77"; + let contract_address_bytes = <[u8; 32]>::from_hex(contract_address_str).unwrap(); + + let class_hash_str = "0334e1e4d148a789fb44367eff869a6330693037983ba6fd2291b2be1249e15a"; + let class_hash_bytes = <[u8; 32]>::from_hex(class_hash_str).unwrap(); + + // Example tx : https://testnet.starkscan.co/tx/0x6fc3466f58b5c6aaa6633d48702e1f2048fb96b7de25f2bde0bce64dca1d212 + let transaction = Transaction::new( + U256::from(1), + H256::default(), + bounded_vec!(), + bounded_vec!(), + contract_address_bytes, + U256::from(0), + CallEntryPointWrapper::new( + Some(class_hash_bytes), + EntryPointTypeWrapper::External, + None, + bounded_vec![ + // Constructor calldata + ], + contract_address_bytes, + contract_address_bytes, + ), + None, + ); + + assert_err!( + Starknet::add_deploy_account_transaction(none_origin.clone(), transaction), + Error::::TransactionExecutionFailed + ); + }); +} + +#[test] +fn given_hardcoded_contract_run_declare_tx_then_it_works() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + run_to_block(2); + + let none_origin = RuntimeOrigin::none(); + + let contract_address_str = "02356b628D108863BAf8644c945d97bAD70190AF5957031f4852d00D0F690a77"; + let contract_address_bytes = <[u8; 32]>::from_hex(contract_address_str).unwrap(); + + let class_hash_str = "025ec026985a3bf8a0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918"; + let class_hash_bytes = <[u8; 32]>::from_hex(class_hash_str).unwrap(); + + let account_class = ContractClassWrapper::from(get_contract_class(ACCOUNT_CONTRACT_PATH)); + + // Example tx : https://testnet.starkscan.co/tx/0x6fc3466f58b5c6aaa6633d48702e1f2048fb96b7de25f2bde0bce64dca1d212 + let transaction = Transaction::new( + U256::from(1), + H256::default(), + bounded_vec!(), + bounded_vec!(), + contract_address_bytes, + U256::from(0), + CallEntryPointWrapper::new( + Some(class_hash_bytes), + EntryPointTypeWrapper::External, + None, + bounded_vec![], + contract_address_bytes, + contract_address_bytes, + ), + Some(account_class.clone()), + ); + + assert_ok!(Starknet::add_declare_transaction(none_origin, transaction)); + + // Check that the class hash was declared + assert_eq!(Starknet::contract_class_by_class_hash(class_hash_bytes), account_class); + }); +} + +#[test] +fn given_hardcoded_contract_run_declare_twice_then_it_fails() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + run_to_block(2); + + let none_origin = RuntimeOrigin::none(); + + let contract_address_str = "02356b628D108863BAf8644c945d97bAD70190AF5957031f4852d00D0F690a77"; + let contract_address_bytes = <[u8; 32]>::from_hex(contract_address_str).unwrap(); + + let class_hash_str = "025ec026985a3bf8a0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918"; + let class_hash_bytes = <[u8; 32]>::from_hex(class_hash_str).unwrap(); + + let account_class = ContractClassWrapper::from(get_contract_class(ACCOUNT_CONTRACT_PATH)); + + // Example tx : https://testnet.starkscan.co/tx/0x6fc3466f58b5c6aaa6633d48702e1f2048fb96b7de25f2bde0bce64dca1d212 + let transaction = Transaction::new( + U256::from(1), + H256::default(), + bounded_vec!(), + bounded_vec!(), + contract_address_bytes, + U256::from(0), + CallEntryPointWrapper::new( + Some(class_hash_bytes), + EntryPointTypeWrapper::External, + None, + bounded_vec![], + contract_address_bytes, + contract_address_bytes, + ), + Some(account_class.clone()), + ); + + assert_ok!(Starknet::add_declare_transaction(none_origin.clone(), transaction.clone())); + + // Check that the class hash was declared + assert_eq!(Starknet::contract_class_by_class_hash(class_hash_bytes), account_class); + + // Second declare should fail + assert_err!( + Starknet::add_declare_transaction(none_origin, transaction), + Error::::ClassHashAlreadyDeclared + ); + }); +} + +#[test] +fn given_hardcoded_contract_run_declare_none_then_it_fails() { + new_test_ext().execute_with(|| { + System::set_block_number(0); + run_to_block(2); + + let none_origin = RuntimeOrigin::none(); + + let contract_address_str = "02356b628D108863BAf8644c945d97bAD70190AF5957031f4852d00D0F690a77"; + let contract_address_bytes = <[u8; 32]>::from_hex(contract_address_str).unwrap(); + + let class_hash_str = "025ec026985a3bf8a0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918"; + let class_hash_bytes = <[u8; 32]>::from_hex(class_hash_str).unwrap(); + + // Example tx : https://testnet.starkscan.co/tx/0x6fc3466f58b5c6aaa6633d48702e1f2048fb96b7de25f2bde0bce64dca1d212 + let transaction = Transaction::new( + U256::from(1), + H256::default(), + bounded_vec!(), + bounded_vec!(), + contract_address_bytes, + U256::from(0), + CallEntryPointWrapper::new( + Some(class_hash_bytes), + EntryPointTypeWrapper::External, + None, + bounded_vec![], + contract_address_bytes, + contract_address_bytes, + ), + None, + ); + + // Cannot declare a class with None + assert_err!( + Starknet::add_declare_transaction(none_origin, transaction), + Error::::ContractClassMustBeSpecified + ); + }); +} diff --git a/crates/primitives/starknet/Cargo.toml b/crates/primitives/starknet/Cargo.toml index 50a34570ff..9c33acfaf6 100644 --- a/crates/primitives/starknet/Cargo.toml +++ b/crates/primitives/starknet/Cargo.toml @@ -16,6 +16,7 @@ frame-support = { workspace = true } sp-core = { workspace = true } sp-std = { workspace = true } sp-runtime = { workspace = true } +# sp-serializer = { workspace = true } # Starknet starknet-crypto = { workspace = true, default-features = false, features = [ @@ -36,12 +37,12 @@ codec = { package = "parity-scale-codec", version = "3.2.2", default-features = scale-info = { version = "2.1.1", default-features = false, features = [ "derive", ] } -serde = { version = "1.0.136", features = ["derive"], optional = true } +serde = { version = "1.0.136", features = ["derive"], optional = true, default-features = false } +serde_json = { version = "1.0.81", default-features = false } bitvec = { version = "0.17.4", features = ["alloc"], default-features = false } [dev-dependencies] rand = "0.8.5" -serde_json = "1.0.85" zstd = { version = "0.11.2", default-features = false } [features] diff --git a/crates/primitives/starknet/src/execution/mod.rs b/crates/primitives/starknet/src/execution/mod.rs index e3df35ce0e..8655962e4f 100644 --- a/crates/primitives/starknet/src/execution/mod.rs +++ b/crates/primitives/starknet/src/execution/mod.rs @@ -1,25 +1,115 @@ //! Starknet execution functionality. +use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec; +use blockifier::execution::contract_class::ContractClass; use blockifier::execution::entry_point::CallEntryPoint; use frame_support::BoundedVec; -use sp_core::{ConstU32, H256}; +use serde_json::{from_slice, to_string}; +use sp_core::{ConstU32, H256, U256}; use starknet_api::api_core::{ClassHash, ContractAddress, EntryPointSelector}; use starknet_api::hash::StarkFelt; -use starknet_api::state::EntryPointType; +use starknet_api::state::{EntryPoint, EntryPointOffset, EntryPointType, Program}; use starknet_api::transaction::Calldata; /// The address of a contract. pub type ContractAddressWrapper = [u8; 32]; +/// Maximum vector sizes. type MaxCalldataSize = ConstU32<4294967295>; +// type MaxAbiSize = ConstU32<4294967295>; +type MaxProgramSize = ConstU32<4294967295>; +type MaxEntryPoints = ConstU32<4294967295>; + /// Wrapper type for class hash field. pub type ClassHashWrapper = [u8; 32]; +/// Contract Class +#[derive(Clone, Debug, PartialEq, Eq, codec::Encode, codec::Decode, scale_info::TypeInfo)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub struct ContractClassWrapper { + /// Contract class program json. + pub program: BoundedVec, + // /// Contract class abi. + // pub abi: BoundedVec, + /// Contract class entrypoints. + pub entry_points_by_type: BTreeMap>, /* TODO: Switch to BoundedBTreeMap */ +} + +impl ContractClassWrapper { + /// Creates a new instance of a contract class. + pub fn new( + program: BoundedVec, + entry_points_by_type: BTreeMap>, + ) -> Self { + Self { program, entry_points_by_type } + } + + /// Convert to starknet contract class. + pub fn to_starknet_contract_class(&self) -> Result { + let program = from_slice::(self.program.as_ref()); + match program { + Ok(program) => Ok(ContractClass { + program, + abi: None, + entry_points_by_type: self + .entry_points_by_type + .iter() + .map(|(k, v)| { + (k.to_starknet_entry_point_type(), v.iter().map(|e| e.to_starknet_entry_point()).collect()) + }) + .collect(), + }), + Err(e) => Err(e), + } + } +} + +impl From for ContractClassWrapper { + fn from(contract_class: ContractClass) -> Self { + let program_string = to_string(&contract_class.program).unwrap(); + Self { + program: BoundedVec::try_from(program_string.as_bytes().to_vec()).unwrap(), + entry_points_by_type: contract_class + .entry_points_by_type + .into_iter() + .map(|(k, v)| { + ( + EntryPointTypeWrapper::from(k), + BoundedVec::try_from( + v.iter() + .map(|e| EntryPointWrapper::from(e.clone())) + .collect::>(), + ) + .unwrap(), + ) + }) + .collect(), + } + } +} + +impl Default for ContractClassWrapper { + fn default() -> Self { + Self { program: BoundedVec::try_from(vec![]).unwrap(), entry_points_by_type: BTreeMap::default() } + } +} + /// Enum that represents all the entrypoints types. -#[derive(Clone, Debug, PartialEq, Eq, codec::Encode, codec::Decode, scale_info::TypeInfo, codec::MaxEncodedLen)] +#[derive( + Clone, + Debug, + PartialEq, + Eq, + codec::Encode, + codec::Decode, + scale_info::TypeInfo, + codec::MaxEncodedLen, + PartialOrd, + Ord, +)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub enum EntryPointTypeWrapper { /// Constructor. @@ -30,7 +120,82 @@ pub enum EntryPointTypeWrapper { L1Handler, } -/// Representation of a Starknet transaction. +impl From for EntryPointTypeWrapper { + fn from(entry_point_type: EntryPointType) -> Self { + match entry_point_type { + EntryPointType::Constructor => EntryPointTypeWrapper::Constructor, + EntryPointType::External => EntryPointTypeWrapper::External, + EntryPointType::L1Handler => EntryPointTypeWrapper::L1Handler, + } + } +} + +impl EntryPointTypeWrapper { + /// Convert to starknet entrypoint type. + pub fn to_starknet_entry_point_type(&self) -> EntryPointType { + match self { + EntryPointTypeWrapper::Constructor => EntryPointType::Constructor, + EntryPointTypeWrapper::External => EntryPointType::External, + EntryPointTypeWrapper::L1Handler => EntryPointType::L1Handler, + } + } +} + +// pub enum ContractClassAbiEntryWrapper { +// /// An event abi entry. +// Event(EventAbiEntry), +// /// A function abi entry. +// Function(FunctionAbiEntryWithType), +// /// A struct abi entry. +// Struct(StructAbiEntry), +// } + +/// Representation of a Starknet Entry Point. +#[derive( + Clone, + Debug, + PartialEq, + Eq, + codec::Encode, + codec::Decode, + scale_info::TypeInfo, + codec::MaxEncodedLen, + PartialOrd, + Ord, +)] +#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] +pub struct EntryPointWrapper { + /// The entrypoint selector + pub entrypoint_selector: H256, + /// The entrypoint offset + pub entrypoint_offset: U256, +} + +impl EntryPointWrapper { + /// Creates a new instance of an entrypoint. + pub fn new(entrypoint_selector: H256, entrypoint_offset: U256) -> Self { + Self { entrypoint_selector, entrypoint_offset } + } + + /// Convert to Starknet EntryPoint + pub fn to_starknet_entry_point(&self) -> EntryPoint { + EntryPoint { + selector: EntryPointSelector(StarkFelt::new(self.entrypoint_selector.0).unwrap()), + offset: EntryPointOffset(self.entrypoint_offset.as_usize()), + } + } +} + +impl From for EntryPointWrapper { + fn from(entry_point: EntryPoint) -> Self { + Self { + entrypoint_selector: H256::from_slice(entry_point.selector.0.bytes()), + entrypoint_offset: U256::from(entry_point.offset.0), + } + } +} + +/// Representation of a Starknet Call Entry Point. #[derive(Clone, Debug, PartialEq, Eq, codec::Encode, codec::Decode, scale_info::TypeInfo, codec::MaxEncodedLen)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub struct CallEntryPointWrapper { diff --git a/crates/primitives/starknet/src/transaction/mod.rs b/crates/primitives/starknet/src/transaction/mod.rs index 32a8b52269..938e0f9f76 100644 --- a/crates/primitives/starknet/src/transaction/mod.rs +++ b/crates/primitives/starknet/src/transaction/mod.rs @@ -5,23 +5,27 @@ pub mod types; use alloc::vec; use blockifier::block_context::BlockContext; +use blockifier::execution::contract_class::ContractClass; use blockifier::execution::entry_point::CallInfo; use blockifier::state::cached_state::CachedState; use blockifier::state::state_api::StateReader; -use blockifier::transaction::account_transaction::AccountTransaction; use blockifier::transaction::errors::InvokeTransactionError; -use blockifier::transaction::objects::{AccountTransactionContext, TransactionExecutionResult}; +use blockifier::transaction::objects::AccountTransactionContext; use blockifier::transaction::transactions::Executable; use frame_support::BoundedVec; use sp_core::{H256, U256}; use starknet_api::api_core::{ContractAddress as StarknetContractAddress, EntryPointSelector, Nonce}; use starknet_api::hash::{StarkFelt, StarkHash}; use starknet_api::transaction::{ - Fee, InvokeTransaction, L1HandlerTransaction, TransactionHash, TransactionSignature, TransactionVersion, + ContractAddressSalt, DeclareTransaction, DeployAccountTransaction, Fee, InvokeTransaction, L1HandlerTransaction, + TransactionHash, TransactionSignature, TransactionVersion, }; +use starknet_api::StarknetApiError; -use self::types::{Event, MaxArraySize, Transaction, TxType}; -use crate::execution::{CallEntryPointWrapper, ContractAddressWrapper}; +use self::types::{ + Event, MaxArraySize, Transaction, TransactionExecutionErrorWrapper, TransactionExecutionResultWrapper, TxType, +}; +use crate::execution::{CallEntryPointWrapper, ContractAddressWrapper, ContractClassWrapper}; use crate::starknet_block::block::Block; use crate::starknet_block::serialize::SerializeBlockContext; @@ -53,6 +57,81 @@ impl Default for Event { } } +impl TryInto for &Transaction { + type Error = StarknetApiError; + + fn try_into(self) -> Result { + Ok(DeployAccountTransaction { + transaction_hash: TransactionHash(StarkFelt::new(self.hash.0)?), + max_fee: Fee(2), + version: TransactionVersion(StarkFelt::new(self.version.into())?), + signature: TransactionSignature( + self.signature.clone().into_inner().iter().map(|x| StarkFelt::new(x.0).unwrap()).collect(), + ), + nonce: Nonce(StarkFelt::new(self.nonce.into())?), + contract_address: StarknetContractAddress::try_from(StarkFelt::new(self.sender_address)?)?, + class_hash: self.call_entrypoint.to_starknet_call_entry_point().class_hash.unwrap_or_default(), + constructor_calldata: self.call_entrypoint.to_starknet_call_entry_point().calldata, + // TODO: add salt + contract_address_salt: ContractAddressSalt(StarkFelt::new([0; 32])?), + }) + } +} + +impl TryInto for &Transaction { + type Error = StarknetApiError; + + fn try_into(self) -> Result { + Ok(L1HandlerTransaction { + transaction_hash: TransactionHash(StarkFelt::new(self.hash.0)?), + version: TransactionVersion(StarkFelt::new(self.version.into())?), + nonce: Nonce(StarkFelt::new(self.nonce.into())?), + contract_address: StarknetContractAddress::try_from(StarkFelt::new(self.sender_address)?)?, + calldata: self.call_entrypoint.to_starknet_call_entry_point().calldata, + entry_point_selector: EntryPointSelector(StarkHash::new( + *self.call_entrypoint.entrypoint_selector.unwrap_or_default().as_fixed_bytes(), + )?), + }) + } +} + +impl TryInto for &Transaction { + type Error = StarknetApiError; + + fn try_into(self) -> Result { + Ok(InvokeTransaction { + transaction_hash: TransactionHash(StarkFelt::new(self.hash.0)?), + max_fee: Fee(2), + version: TransactionVersion(StarkFelt::new(self.version.into())?), + signature: TransactionSignature( + self.signature.clone().into_inner().iter().map(|x| StarkFelt::new(x.0).unwrap()).collect(), + ), + nonce: Nonce(StarkFelt::new(self.nonce.into())?), + sender_address: StarknetContractAddress::try_from(StarkFelt::new(self.sender_address)?)?, + calldata: self.call_entrypoint.to_starknet_call_entry_point().calldata, + entry_point_selector: None, + }) + } +} + +impl TryInto for &Transaction { + type Error = StarknetApiError; + + fn try_into(self) -> Result { + Ok(DeclareTransaction { + transaction_hash: TransactionHash(StarkFelt::new(self.hash.0)?), + max_fee: Fee(2), + version: TransactionVersion(StarkFelt::new(self.version.into())?), + signature: TransactionSignature( + self.signature.clone().into_inner().iter().map(|x| StarkFelt::new(x.0).unwrap()).collect(), + ), + nonce: Nonce(StarkFelt::new(self.nonce.into())?), + sender_address: StarknetContractAddress::try_from(StarkFelt::new(self.sender_address)?)?, + class_hash: self.call_entrypoint.to_starknet_call_entry_point().class_hash.unwrap_or_default(), + }) + } +} + impl Transaction { /// Creates a new instance of a transaction. #[allow(clippy::too_many_arguments)] @@ -64,8 +143,9 @@ impl Transaction { sender_address: ContractAddressWrapper, nonce: U256, call_entrypoint: CallEntryPointWrapper, + contract_class: Option, ) -> Self { - Self { version, hash, signature, events, sender_address, nonce, call_entrypoint } + Self { version, hash, signature, events, sender_address, nonce, call_entrypoint, contract_class } } /// Creates a new instance of a transaction without signature. @@ -73,52 +153,6 @@ impl Transaction { Self { hash, ..Self::default() } } - /// Converts a transaction to a blockifier transaction - /// - /// # Arguments - /// - /// * `self` - The transaction to convert - /// - /// # Returns - /// - /// * `AccountTransaction` - The converted transaction - pub fn to_invoke_tx(&self) -> AccountTransaction { - AccountTransaction::Invoke(InvokeTransaction { - transaction_hash: TransactionHash(StarkFelt::new(self.hash.0).unwrap()), - max_fee: Fee(2), - version: TransactionVersion(StarkFelt::new(self.version.into()).unwrap()), - signature: TransactionSignature( - self.signature.clone().into_inner().iter().map(|x| StarkFelt::new(x.0).unwrap()).collect(), - ), - nonce: Nonce(StarkFelt::new(self.nonce.into()).unwrap()), - sender_address: StarknetContractAddress::try_from(StarkFelt::new(self.sender_address).unwrap()).unwrap(), - calldata: self.call_entrypoint.to_starknet_call_entry_point().calldata, - entry_point_selector: None, - }) - } - - /// Converts a transaction to a blockifier l1 handler transaction - /// - /// # Arguments - /// - /// * `self` - The transaction to convert - /// - /// # Returns - /// - /// * `L1HandlerTransaction` - The converted transaction - pub fn to_l1_handler_tx(&self) -> L1HandlerTransaction { - L1HandlerTransaction { - transaction_hash: TransactionHash(StarkFelt::new(self.hash.0).unwrap()), - version: TransactionVersion(StarkFelt::new(self.version.into()).unwrap()), - nonce: Nonce(StarkFelt::new(self.nonce.into()).unwrap()), - contract_address: StarknetContractAddress::try_from(StarkFelt::new(self.sender_address).unwrap()).unwrap(), - calldata: self.call_entrypoint.to_starknet_call_entry_point().calldata, - entry_point_selector: EntryPointSelector( - StarkHash::new(*self.call_entrypoint.entrypoint_selector.unwrap().as_fixed_bytes()).unwrap(), - ), - } - } - /// Executes a transaction /// /// # Arguments @@ -136,27 +170,44 @@ impl Transaction { state: &mut CachedState, block: Block, tx_type: TxType, - ) -> TransactionExecutionResult> { + contract_class: Option, + ) -> TransactionExecutionResultWrapper> { let block_context = BlockContext::serialize(block.header); match tx_type { - TxType::InvokeTx => match self.to_invoke_tx() { - AccountTransaction::Invoke(ref tx) => { - let account_context = self.get_invoke_transaction_context(tx); - // Specifying an entry point selector is not allowed; `__execute__` is called, and - // the inner selector appears in the calldata. - if tx.entry_point_selector.is_some() { - return Err(InvokeTransactionError::SpecifiedEntryPoint)?; - } - - tx.run_execute(state, &block_context, &account_context, None) - } - _ => { - panic!("Only invoke transactions are supported"); + TxType::InvokeTx => { + let tx = self.try_into().map_err(TransactionExecutionErrorWrapper::StarknetApi)?; + let account_context = self.get_invoke_transaction_context(&tx); + // Specifying an entry point selector is not allowed; `__execute__` is called, and + // the inner selector appears in the calldata. + if tx.entry_point_selector.is_some() { + return Err(TransactionExecutionErrorWrapper::TransactionExecution( + InvokeTransactionError::SpecifiedEntryPoint.into(), + ))?; } - }, + + tx.run_execute(state, &block_context, &account_context, contract_class) + .map_err( TransactionExecutionErrorWrapper::TransactionExecution) + } TxType::L1HandlerTx => { - let tx = self.to_l1_handler_tx(); - tx.run_execute(state, &block_context, &self.get_l1_handler_transaction_context(&tx), None) + let tx = self.try_into().map_err(TransactionExecutionErrorWrapper::StarknetApi)?; + let account_context = self.get_l1_handler_transaction_context(&tx); + tx.run_execute(state, &block_context, &account_context, contract_class) + .map_err(TransactionExecutionErrorWrapper::TransactionExecution) + } + TxType::DeclareTx => { + let tx = self.try_into().map_err(TransactionExecutionErrorWrapper::StarknetApi)?; + let account_context = self.get_declare_transaction_context(&tx); + // Execute. + tx.run_execute(state, &block_context, &account_context, contract_class) + .map_err(TransactionExecutionErrorWrapper::TransactionExecution) + } + TxType::DeployTx => { + let tx = self.try_into().map_err(TransactionExecutionErrorWrapper::StarknetApi)?; + let account_context = self.get_deploy_transaction_context(&tx); + + // Execute. + tx.run_execute(state, &block_context, &account_context, contract_class) + .map_err(TransactionExecutionErrorWrapper::TransactionExecution) } } @@ -166,6 +217,16 @@ impl Transaction { // However it also means we need to copy/paste internal code from the tx.execute() method. } + /// Get the transaction context for a l1 handler transaction + /// + /// # Arguments + /// + /// * `self` - The transaction to get the context for + /// * `tx` - The l1 handler transaction to get the context for + /// + /// # Returns + /// + /// * `AccountTransactionContext` - The context of the transaction fn get_l1_handler_transaction_context(&self, tx: &L1HandlerTransaction) -> AccountTransactionContext { AccountTransactionContext { transaction_hash: tx.transaction_hash, @@ -176,6 +237,17 @@ impl Transaction { sender_address: tx.contract_address, } } + + /// Get the transaction context for an invoke transaction + /// + /// # Arguments + /// + /// * `self` - The transaction to get the context for + /// * `tx` - The invoke transaction to get the context for + /// + /// # Returns + /// + /// * `AccountTransactionContext` - The context of the transaction fn get_invoke_transaction_context(&self, tx: &InvokeTransaction) -> AccountTransactionContext { AccountTransactionContext { transaction_hash: tx.transaction_hash, @@ -186,6 +258,48 @@ impl Transaction { sender_address: tx.sender_address, } } + + /// Get the transaction context for a deploy transaction + /// + /// # Arguments + /// + /// * `self` - The transaction to get the context for + /// * `tx` - The deploy transaction to get the context for + /// + /// # Returns + /// + /// * `AccountTransactionContext` - The context of the transaction + fn get_deploy_transaction_context(&self, tx: &DeployAccountTransaction) -> AccountTransactionContext { + AccountTransactionContext { + transaction_hash: tx.transaction_hash, + max_fee: tx.max_fee, + version: tx.version, + signature: tx.signature.clone(), + nonce: tx.nonce, + sender_address: tx.contract_address, + } + } + + /// Get the transaction context for a declare transaction + /// + /// # Arguments + /// + /// * `self` - The transaction to get the context for + /// * `tx` - The declare transaction to get the context for + /// + /// # Returns + /// + /// * `AccountTransactionContext` - The context of the transaction + fn get_declare_transaction_context(&self, tx: &DeclareTransaction) -> AccountTransactionContext { + AccountTransactionContext { + transaction_hash: tx.transaction_hash, + max_fee: tx.max_fee, + version: tx.version, + signature: tx.signature.clone(), + nonce: tx.nonce, + sender_address: tx.sender_address, + } + } } impl Default for Transaction { @@ -201,6 +315,7 @@ impl Default for Transaction { nonce: U256::default(), sender_address: ContractAddressWrapper::default(), call_entrypoint: CallEntryPointWrapper::default(), + contract_class: None, } } } diff --git a/crates/primitives/starknet/src/transaction/types.rs b/crates/primitives/starknet/src/transaction/types.rs index cbadb46860..5faa0dccc8 100644 --- a/crates/primitives/starknet/src/transaction/types.rs +++ b/crates/primitives/starknet/src/transaction/types.rs @@ -1,20 +1,38 @@ +use blockifier::transaction::errors::TransactionExecutionError; use frame_support::BoundedVec; use sp_core::{ConstU32, H256, U256}; +use starknet_api::StarknetApiError; -use crate::execution::{CallEntryPointWrapper, ContractAddressWrapper}; +use crate::execution::{CallEntryPointWrapper, ContractAddressWrapper, ContractClassWrapper}; /// Max size of the event array. pub type MaxArraySize = ConstU32<4294967295>; +/// Wrapper type for transaction execution result. +pub type TransactionExecutionResultWrapper = Result; + +/// Wrapper type for transaction execution error. +#[derive(Debug)] +pub enum TransactionExecutionErrorWrapper { + /// Transaction execution error. + TransactionExecution(TransactionExecutionError), + /// Starknet API error. + StarknetApi(StarknetApiError), +} + /// Different tx types. pub enum TxType { /// Regular invoke transaction. InvokeTx, + /// Declare transaction. + DeclareTx, + /// Deploy transaction. + DeployTx, /// Message sent from ethereum. L1HandlerTx, } /// Representation of a Starknet transaction. -#[derive(Clone, Debug, PartialEq, Eq, codec::Encode, codec::Decode, scale_info::TypeInfo, codec::MaxEncodedLen)] +#[derive(Clone, Debug, PartialEq, Eq, codec::Encode, codec::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] pub struct Transaction { /// The version of the transaction. @@ -31,6 +49,8 @@ pub struct Transaction { pub nonce: U256, /// Call entrypoint pub call_entrypoint: CallEntryPointWrapper, + /// Contract Class + pub contract_class: Option, } /// Representation of a Starknet event. diff --git a/crates/primitives/starknet/tests/crypto.rs b/crates/primitives/starknet/tests/crypto.rs index 605cade705..f297be4b86 100644 --- a/crates/primitives/starknet/tests/crypto.rs +++ b/crates/primitives/starknet/tests/crypto.rs @@ -21,6 +21,7 @@ fn test_merkle_tree() { sender_address: ContractAddressWrapper::from([0; 32]), nonce: U256::zero(), call_entrypoint: CallEntryPointWrapper::default(), + contract_class: None, }, Transaction { version: U256::zero(), @@ -30,6 +31,7 @@ fn test_merkle_tree() { sender_address: ContractAddressWrapper::from([1; 32]), nonce: U256::zero(), call_entrypoint: CallEntryPointWrapper::default(), + contract_class: None, }, ]; let tx_com = calculate_transaction_commitment::(&txs);