starknet_devnet_core/
account.rs

1use std::sync::Arc;
2
3use blockifier::state::state_api::StateReader;
4use starknet_api::core::calculate_contract_address;
5use starknet_api::transaction::fields::{Calldata, ContractAddressSalt};
6use starknet_api::{felt, patricia_key};
7use starknet_rs_core::types::Felt;
8use starknet_types::contract_address::ContractAddress;
9use starknet_types::contract_class::ContractClass;
10use starknet_types::error::Error;
11use starknet_types::felt::{ClassHash, Key, felt_from_prefixed_hex, join_felts, split_biguint};
12use starknet_types::rpc::state::Balance;
13
14use crate::constants::{
15    CHARGEABLE_ACCOUNT_ADDRESS, CHARGEABLE_ACCOUNT_PRIVATE_KEY, CHARGEABLE_ACCOUNT_PUBLIC_KEY,
16    ISRC6_ID_HEX, chargeable_account_initial_balance,
17};
18use crate::contract_class_choice::{AccountClassWrapper, AccountContractClassChoice};
19use crate::error::DevnetResult;
20use crate::state::state_readers::DictState;
21use crate::state::{CustomState, StarknetState};
22use crate::traits::{Accounted, Deployed};
23use crate::utils::get_storage_var_address;
24
25/// data taken from https://github.com/0xSpaceShard/starknet-devnet-deprecated/blob/fb96e0cc3c1c31fb29892ecefd2a670cf8a32b51/starknet_devnet/account.py
26const ACCOUNT_CLASS_HASH_HEX_FOR_ADDRESS_COMPUTATION: &str =
27    "0x3FCBF77B28C96F4F2FB5BD2D176AB083A12A5E123ADEB0DE955D7EE228C9854";
28
29pub enum FeeToken {
30    ETH,
31    STRK,
32}
33
34#[derive(Clone)]
35pub struct KeyPair {
36    pub public_key: Key,
37    pub private_key: Key,
38}
39
40#[derive(Clone)]
41pub struct Account {
42    pub keys: KeyPair,
43    pub account_address: ContractAddress,
44    pub initial_balance: Balance,
45    pub class_hash: ClassHash,
46    pub class_metadata: &'static str,
47    pub(crate) contract_class: ContractClass,
48    pub(crate) eth_fee_token_address: ContractAddress,
49    pub(crate) strk_fee_token_address: ContractAddress,
50}
51
52impl Account {
53    pub(crate) fn new_chargeable(
54        eth_fee_token_address: ContractAddress,
55        strk_fee_token_address: ContractAddress,
56    ) -> DevnetResult<Self> {
57        let AccountClassWrapper { contract_class, class_hash, class_metadata } =
58            AccountContractClassChoice::Cairo1.get_class_wrapper()?;
59
60        Ok(Self {
61            keys: KeyPair {
62                public_key: Key::from_hex(CHARGEABLE_ACCOUNT_PUBLIC_KEY)?,
63                private_key: Key::from_hex(CHARGEABLE_ACCOUNT_PRIVATE_KEY)?,
64            },
65            account_address: ContractAddress::new(felt_from_prefixed_hex(
66                CHARGEABLE_ACCOUNT_ADDRESS,
67            )?)?,
68            initial_balance: chargeable_account_initial_balance(),
69            class_hash,
70            class_metadata,
71            contract_class,
72            eth_fee_token_address,
73            strk_fee_token_address,
74        })
75    }
76
77    pub(crate) fn new(
78        initial_balance: Balance,
79        keys: KeyPair,
80        class_hash: ClassHash,
81        class_metadata: &'static str,
82        contract_class: ContractClass,
83        eth_fee_token_address: ContractAddress,
84        strk_fee_token_address: ContractAddress,
85    ) -> DevnetResult<Self> {
86        let account_address = Account::compute_account_address(&keys.public_key)?;
87        Ok(Self {
88            initial_balance,
89            keys,
90            class_hash,
91            class_metadata,
92            contract_class,
93            account_address,
94            eth_fee_token_address,
95            strk_fee_token_address,
96        })
97    }
98
99    fn compute_account_address(public_key: &Key) -> DevnetResult<ContractAddress> {
100        let account_address = calculate_contract_address(
101            ContractAddressSalt(felt!(20u32)),
102            starknet_api::core::ClassHash(felt_from_prefixed_hex(
103                ACCOUNT_CLASS_HASH_HEX_FOR_ADDRESS_COMPUTATION,
104            )?),
105            &Calldata(Arc::new(vec![*public_key])),
106            starknet_api::core::ContractAddress(patricia_key!(0u32)),
107        )
108        .map_err(Error::StarknetApiError)?;
109
110        Ok(ContractAddress::from(account_address))
111    }
112
113    // simulate constructor logic (register interfaces and set public key), as done in
114    // https://github.com/OpenZeppelin/cairo-contracts/blob/89a450a88628ec3b86273f261b2d8d1ca9b1522b/src/account/account.cairo#L207-L211
115    fn simulate_constructor(&self, state: &mut StarknetState) -> DevnetResult<()> {
116        let core_address = self.account_address.into();
117
118        let interface_storage_var = get_storage_var_address(
119            "SRC5_supported_interfaces",
120            &[felt_from_prefixed_hex(ISRC6_ID_HEX)?],
121        )?;
122        state.state.state.set_storage_at(core_address, interface_storage_var.into(), Felt::ONE)?;
123
124        let public_key_storage_var = get_storage_var_address("Account_public_key", &[])?;
125        state.state.state.set_storage_at(
126            core_address,
127            public_key_storage_var.into(),
128            self.keys.public_key,
129        )?;
130
131        Ok(())
132    }
133}
134
135impl Deployed for Account {
136    fn deploy(&self, state: &mut StarknetState) -> DevnetResult<()> {
137        self.declare_if_undeclared(state, self.class_hash, &self.contract_class)?;
138
139        state.predeploy_contract(self.account_address, self.class_hash)?;
140        self.set_initial_balance(&mut state.state.state)?;
141        self.simulate_constructor(state)?;
142
143        Ok(())
144    }
145
146    fn get_address(&self) -> ContractAddress {
147        self.account_address
148    }
149}
150
151impl Accounted for Account {
152    // Set balance directly in the most underlying state
153    fn set_initial_balance(&self, state: &mut DictState) -> DevnetResult<()> {
154        let storage_var_address_low: starknet_api::state::StorageKey =
155            get_storage_var_address("ERC20_balances", &[Felt::from(self.account_address)])?.into();
156
157        let storage_var_address_high = storage_var_address_low.next_storage_key()?;
158
159        let total_supply_storage_address_low: starknet_api::state::StorageKey =
160            get_storage_var_address("ERC20_total_supply", &[])?.into();
161        let total_supply_storage_address_high =
162            total_supply_storage_address_low.next_storage_key()?;
163
164        let (high, low) = split_biguint(self.initial_balance.clone());
165
166        for fee_token_address in [self.eth_fee_token_address, self.strk_fee_token_address] {
167            let token_address = fee_token_address.into();
168
169            let total_supply_low =
170                state.get_storage_at(token_address, total_supply_storage_address_low)?;
171            let total_supply_high =
172                state.get_storage_at(token_address, total_supply_storage_address_high)?;
173
174            let new_total_supply =
175                join_felts(&total_supply_high, &total_supply_low) + self.initial_balance.clone();
176
177            let (new_total_supply_high, new_total_supply_low) = split_biguint(new_total_supply);
178
179            // set balance in ERC20_balances
180            state.set_storage_at(token_address, storage_var_address_low, low)?;
181            state.set_storage_at(token_address, storage_var_address_high, high)?;
182
183            // set total supply in ERC20_total_supply
184            state.set_storage_at(
185                token_address,
186                total_supply_storage_address_low,
187                new_total_supply_low,
188            )?;
189
190            state.set_storage_at(
191                token_address,
192                total_supply_storage_address_high,
193                new_total_supply_high,
194            )?;
195        }
196
197        Ok(())
198    }
199
200    fn get_balance(&self, state: &mut impl StateReader, token: FeeToken) -> DevnetResult<Balance> {
201        let fee_token_address = match token {
202            FeeToken::ETH => self.eth_fee_token_address,
203            FeeToken::STRK => self.strk_fee_token_address,
204        };
205        let (low, high) =
206            state.get_fee_token_balance(self.account_address.into(), fee_token_address.into())?;
207        Ok(join_felts(&high, &low))
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use starknet_rs_core::types::Felt;
214    use starknet_types::contract_address::ContractAddress;
215    use starknet_types::felt::felt_from_prefixed_hex;
216    use starknet_types::rpc::state::Balance;
217
218    use super::{Account, KeyPair};
219    use crate::account::FeeToken;
220    use crate::constants::STRK_ERC20_CONTRACT_CLASS_HASH;
221    use crate::state::{CustomState, StarknetState};
222    use crate::traits::{Accounted, Deployed};
223    use crate::utils::test_utils::{
224        dummy_cairo_1_contract_class, dummy_contract_address, dummy_felt,
225    };
226
227    /// Testing if generated account address has the same value as the first account in
228    /// https://github.com/0xSpaceShard/starknet-devnet-deprecated/blob/9d867e38e6d465e568e82a47e82e40608f6d220f/test/support/schemas/predeployed_accounts_fixed_seed.json
229    #[test]
230    fn account_address_should_be_equal() {
231        let expected_result = ContractAddress::new(
232            felt_from_prefixed_hex(
233                "0x6e3205f9b7c4328f00f718fdecf56ab31acfb3cd6ffeb999dcbac41236ea502",
234            )
235            .unwrap(),
236        )
237        .unwrap();
238        let generated_result = Account::compute_account_address(
239            &felt_from_prefixed_hex(
240                "0x60dea6c1228f1db4ca1f9db11c01b6e9cce5e627f7181dcaa27d69cbdbe57b5",
241            )
242            .unwrap(),
243        )
244        .unwrap();
245
246        assert_eq!(expected_result, generated_result);
247    }
248
249    #[test]
250    fn account_address_should_not_be_equal() {
251        let expected_result = ContractAddress::new(
252            felt_from_prefixed_hex(
253                "0x6e3205f9b7c4328f00f718fdecf56ab31acfb3cd6ffeb999dcbac41236ea502",
254            )
255            .unwrap(),
256        )
257        .unwrap();
258        let generated_result = Account::compute_account_address(
259            &felt_from_prefixed_hex(
260                "0x60dea6c1228f1db4ca1f9db11c01b6e9cce5e627f7181dcaa27d69cbdbe57b6",
261            )
262            .unwrap(),
263        )
264        .unwrap();
265
266        assert_ne!(expected_result, generated_result);
267    }
268
269    #[test]
270    fn account_deployed_successfully() {
271        let (account, mut state) = setup();
272        assert!(account.deploy(&mut state).is_ok());
273    }
274
275    #[test]
276    fn account_get_balance_should_return_correct_value() {
277        let (mut account, mut state) = setup();
278        let expected_balance = Balance::from(100_u8);
279        account.initial_balance = expected_balance.clone();
280        account.deploy(&mut state).unwrap();
281
282        let generated_balance = account.get_balance(&mut state, FeeToken::ETH).unwrap();
283        assert_eq!(expected_balance, generated_balance);
284
285        let generated_balance = account.get_balance(&mut state, FeeToken::STRK).unwrap();
286        assert_eq!(expected_balance, generated_balance);
287    }
288
289    #[test]
290    fn account_changed_balance_successfully_without_deployment() {
291        let (account, mut state) = setup();
292        assert!(account.set_initial_balance(&mut state.state.state).is_ok());
293    }
294
295    #[test]
296    fn account_get_address_correct() {
297        let (mut account, _) = setup();
298        let expected_address = ContractAddress::new(Felt::from(11111)).unwrap();
299        account.account_address = expected_address;
300        assert_eq!(expected_address, account.get_address());
301    }
302
303    fn setup() -> (Account, StarknetState) {
304        let mut state = StarknetState::default();
305        let fee_token_address = dummy_contract_address();
306
307        // deploy the erc20 contract
308        state.predeploy_contract(fee_token_address, STRK_ERC20_CONTRACT_CLASS_HASH).unwrap();
309
310        (
311            Account::new(
312                Balance::from(10_u8),
313                KeyPair { public_key: Felt::from(13431515), private_key: Felt::from(11) },
314                dummy_felt(),
315                "Dummy account",
316                dummy_cairo_1_contract_class().into(),
317                fee_token_address,
318                fee_token_address,
319            )
320            .unwrap(),
321            state,
322        )
323    }
324}