miden_objects/account/
mod.rs

1use alloc::collections::BTreeMap;
2use alloc::string::ToString;
3
4use miden_core::LexicographicWord;
5
6use crate::asset::{Asset, AssetVault};
7use crate::utils::serde::{
8    ByteReader,
9    ByteWriter,
10    Deserializable,
11    DeserializationError,
12    Serializable,
13};
14use crate::{AccountError, Felt, Hasher, Word, ZERO};
15
16mod account_id;
17pub use account_id::{
18    AccountId,
19    AccountIdPrefix,
20    AccountIdPrefixV0,
21    AccountIdV0,
22    AccountIdVersion,
23    AccountStorageMode,
24    AccountType,
25};
26
27pub mod auth;
28
29mod builder;
30pub use builder::AccountBuilder;
31
32pub mod code;
33pub use code::AccountCode;
34pub use code::procedure::AccountProcedureInfo;
35
36pub mod component;
37pub use component::{
38    AccountComponent,
39    AccountComponentMetadata,
40    AccountComponentTemplate,
41    FeltRepresentation,
42    InitStorageData,
43    MapEntry,
44    MapRepresentation,
45    PlaceholderTypeRequirement,
46    StorageEntry,
47    StorageValueName,
48    StorageValueNameError,
49    TemplateType,
50    TemplateTypeError,
51    WordRepresentation,
52};
53
54pub mod delta;
55pub use delta::{
56    AccountDelta,
57    AccountStorageDelta,
58    AccountVaultDelta,
59    FungibleAssetDelta,
60    NonFungibleAssetDelta,
61    NonFungibleDeltaAction,
62    StorageMapDelta,
63};
64
65mod storage;
66pub use storage::{
67    AccountStorage,
68    AccountStorageHeader,
69    PartialStorage,
70    PartialStorageMap,
71    SlotName,
72    StorageMap,
73    StorageMapWitness,
74    StorageSlot,
75    StorageSlotType,
76};
77
78mod header;
79pub use header::AccountHeader;
80
81mod file;
82pub use file::AccountFile;
83
84mod partial;
85pub use partial::PartialAccount;
86
87// ACCOUNT
88// ================================================================================================
89
90/// An account which can store assets and define rules for manipulating them.
91///
92/// An account consists of the following components:
93/// - Account ID, which uniquely identifies the account and also defines basic properties of the
94///   account.
95/// - Account vault, which stores assets owned by the account.
96/// - Account storage, which is a key-value map (both keys and values are words) used to store
97///   arbitrary user-defined data.
98/// - Account code, which is a set of Miden VM programs defining the public interface of the
99///   account.
100/// - Account nonce, a value which is incremented whenever account state is updated.
101///
102/// Out of the above components account ID is always immutable (once defined it can never be
103/// changed). Other components may be mutated throughout the lifetime of the account. However,
104/// account state can be changed only by invoking one of account interface methods.
105///
106/// The recommended way to build an account is through an [`AccountBuilder`], which can be
107/// instantiated through [`Account::builder`]. See the type's documentation for details.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct Account {
110    id: AccountId,
111    vault: AssetVault,
112    storage: AccountStorage,
113    code: AccountCode,
114    nonce: Felt,
115    seed: Option<Word>,
116}
117
118impl Account {
119    // CONSTRUCTORS
120    // --------------------------------------------------------------------------------------------
121
122    /// Returns an [`Account`] instantiated with the provided components.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if:
127    /// - an account seed is provided but the account's nonce indicates the account already exists.
128    /// - an account seed is not provided but the account's nonce indicates the account is new.
129    /// - an account seed is provided but the account ID derived from it is invalid or does not
130    ///   match the provided account's ID.
131    pub fn new(
132        id: AccountId,
133        vault: AssetVault,
134        storage: AccountStorage,
135        code: AccountCode,
136        nonce: Felt,
137        seed: Option<Word>,
138    ) -> Result<Self, AccountError> {
139        validate_account_seed(id, code.commitment(), storage.commitment(), seed, nonce)?;
140
141        Ok(Self::new_unchecked(id, vault, storage, code, nonce, seed))
142    }
143
144    /// Returns an [`Account`] instantiated with the provided components.
145    ///
146    /// # Warning
147    ///
148    /// This does not check that the provided seed is valid with respect to the provided components.
149    /// Prefer using [`Account::new`] whenever possible.
150    pub fn new_unchecked(
151        id: AccountId,
152        vault: AssetVault,
153        storage: AccountStorage,
154        code: AccountCode,
155        nonce: Felt,
156        seed: Option<Word>,
157    ) -> Self {
158        Self { id, vault, storage, code, nonce, seed }
159    }
160
161    /// Creates an account's [`AccountCode`] and [`AccountStorage`] from the provided components.
162    ///
163    /// This merges all libraries of the components into a single
164    /// [`MastForest`](miden_processor::MastForest) to produce the [`AccountCode`]. For each
165    /// procedure in the resulting forest, the storage offset and size are set so that the
166    /// procedure can only access the storage slots of the component in which it was defined and
167    /// each component's storage offset is the total number of slots in the previous components.
168    /// To illustrate, given two components with one and two storage slots respectively:
169    ///
170    /// - RpoFalcon512 Component: Component slot 0 stores the public key.
171    /// - Custom Component: Component slot 0 stores a custom [`StorageSlot::Value`] and component
172    ///   slot 1 stores a custom [`StorageSlot::Map`].
173    ///
174    /// When combined, their assigned slots in the [`AccountStorage`] would be:
175    ///
176    /// - The RpoFalcon512 Component has offset 0 and size 1: Account slot 0 stores the public key.
177    /// - The Custom Component has offset 1 and size 2: Account slot 1 stores the value and account
178    ///   slot 2 stores the map.
179    ///
180    /// The resulting commitments from code and storage can then be used to construct an
181    /// [`AccountId`]. Finally, a new account can then be instantiated from those parts using
182    /// [`Account::new`].
183    ///
184    /// If the account type is faucet the reserved slot (slot 0) will be initialized.
185    /// - For Fungible Faucets the value is [`StorageSlot::empty_value`].
186    /// - For Non-Fungible Faucets the value is [`StorageSlot::empty_map`].
187    ///
188    /// If the storage needs to be initialized with certain values in that slot, those can be added
189    /// after construction with the standard set methods for items and maps.
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if:
194    /// - Any of the components does not support `account_type`.
195    /// - The number of procedures in all merged libraries is 0 or exceeds
196    ///   [`AccountCode::MAX_NUM_PROCEDURES`].
197    /// - Two or more libraries export a procedure with the same MAST root.
198    /// - The first component doesn't contain exactly one authentication procedure.
199    /// - Other components contain authentication procedures.
200    /// - The number of [`StorageSlot`]s of all components exceeds 255.
201    /// - [`MastForest::merge`](miden_processor::MastForest::merge) fails on all libraries.
202    pub(super) fn initialize_from_components(
203        account_type: AccountType,
204        components: &[AccountComponent],
205    ) -> Result<(AccountCode, AccountStorage), AccountError> {
206        validate_components_support_account_type(components, account_type)?;
207
208        let code = AccountCode::from_components_unchecked(components, account_type)?;
209        let storage = AccountStorage::from_components(components, account_type)?;
210
211        Ok((code, storage))
212    }
213
214    /// Creates a new [`AccountBuilder`] for an account and sets the initial seed from which the
215    /// grinding process for that account's [`AccountId`] will start.
216    ///
217    /// This initial seed should come from a cryptographic random number generator.
218    pub fn builder(init_seed: [u8; 32]) -> AccountBuilder {
219        AccountBuilder::new(init_seed)
220    }
221
222    // PUBLIC ACCESSORS
223    // --------------------------------------------------------------------------------------------
224
225    /// Returns the commitment of this account.
226    ///
227    /// The commitment of an account is computed as hash(id, nonce, vault_root, storage_commitment,
228    /// code_commitment). Computing the account commitment requires 2 permutations of the hash
229    /// function.
230    pub fn commitment(&self) -> Word {
231        hash_account(
232            self.id,
233            self.nonce,
234            self.vault.root(),
235            self.storage.commitment(),
236            self.code.commitment(),
237        )
238    }
239
240    /// Returns the commitment of this account as used for the initial account state commitment in
241    /// transaction proofs.
242    ///
243    /// For existing accounts, this is exactly the same as [Account::commitment()], however, for new
244    /// accounts this value is set to [crate::EMPTY_WORD]. This is because when a transaction is
245    /// executed against a new account, public input for the initial account state is set to
246    /// [crate::EMPTY_WORD] to distinguish new accounts from existing accounts. The actual
247    /// commitment of the initial account state (and the initial state itself), are provided to
248    /// the VM via the advice provider.
249    pub fn initial_commitment(&self) -> Word {
250        if self.is_new() {
251            Word::empty()
252        } else {
253            self.commitment()
254        }
255    }
256
257    /// Returns unique identifier of this account.
258    pub fn id(&self) -> AccountId {
259        self.id
260    }
261
262    /// Returns the account type
263    pub fn account_type(&self) -> AccountType {
264        self.id.account_type()
265    }
266
267    /// Returns a reference to the vault of this account.
268    pub fn vault(&self) -> &AssetVault {
269        &self.vault
270    }
271
272    /// Returns a reference to the storage of this account.
273    pub fn storage(&self) -> &AccountStorage {
274        &self.storage
275    }
276
277    /// Returns a reference to the code of this account.
278    pub fn code(&self) -> &AccountCode {
279        &self.code
280    }
281
282    /// Returns nonce for this account.
283    pub fn nonce(&self) -> Felt {
284        self.nonce
285    }
286
287    /// Returns the seed of the account's ID if the account is new.
288    ///
289    /// That is, if [`Account::is_new`] returns `true`, the seed will be `Some`.
290    pub fn seed(&self) -> Option<Word> {
291        self.seed
292    }
293
294    /// Returns true if this account can issue assets.
295    pub fn is_faucet(&self) -> bool {
296        self.id.is_faucet()
297    }
298
299    /// Returns true if this is a regular account.
300    pub fn is_regular_account(&self) -> bool {
301        self.id.is_regular_account()
302    }
303
304    /// Returns `true` if the full state of the account is public on chain, i.e. if the modes are
305    /// [`AccountStorageMode::Public`] or [`AccountStorageMode::Network`], `false` otherwise.
306    pub fn has_public_state(&self) -> bool {
307        self.id().has_public_state()
308    }
309
310    /// Returns `true` if the storage mode is [`AccountStorageMode::Public`], `false` otherwise.
311    pub fn is_public(&self) -> bool {
312        self.id().is_public()
313    }
314
315    /// Returns `true` if the storage mode is [`AccountStorageMode::Private`], `false` otherwise.
316    pub fn is_private(&self) -> bool {
317        self.id().is_private()
318    }
319
320    /// Returns `true` if the storage mode is [`AccountStorageMode::Network`], `false` otherwise.
321    pub fn is_network(&self) -> bool {
322        self.id().is_network()
323    }
324
325    /// Returns `true` if the account is new, `false` otherwise.
326    ///
327    /// An account is considered new if the account's nonce is zero and it hasn't been registered on
328    /// chain yet.
329    pub fn is_new(&self) -> bool {
330        self.nonce == ZERO
331    }
332
333    /// Decomposes the account into the underlying account components.
334    pub fn into_parts(
335        self,
336    ) -> (AccountId, AssetVault, AccountStorage, AccountCode, Felt, Option<Word>) {
337        (self.id, self.vault, self.storage, self.code, self.nonce, self.seed)
338    }
339
340    // DATA MUTATORS
341    // --------------------------------------------------------------------------------------------
342
343    /// Applies the provided delta to this account. This updates account vault, storage, and nonce
344    /// to the values specified by the delta.
345    ///
346    /// # Errors
347    ///
348    /// Returns an error if:
349    /// - [`AccountDelta::is_full_state`] returns `true`, i.e. represents the state of an entire
350    ///   account. Only partial state deltas can be applied to an account.
351    /// - Applying vault sub-delta to the vault of this account fails.
352    /// - Applying storage sub-delta to the storage of this account fails.
353    /// - The nonce specified in the provided delta smaller than or equal to the current account
354    ///   nonce.
355    pub fn apply_delta(&mut self, delta: &AccountDelta) -> Result<(), AccountError> {
356        if delta.is_full_state() {
357            return Err(AccountError::ApplyFullStateDeltaToAccount);
358        }
359
360        // update vault; we don't check vault delta validity here because `AccountDelta` can contain
361        // only valid vault deltas
362        self.vault
363            .apply_delta(delta.vault())
364            .map_err(AccountError::AssetVaultUpdateError)?;
365
366        // update storage
367        self.storage.apply_delta(delta.storage())?;
368
369        // update nonce
370        self.increment_nonce(delta.nonce_delta())?;
371
372        Ok(())
373    }
374
375    /// Increments the nonce of this account by the provided increment.
376    ///
377    /// # Errors
378    ///
379    /// Returns an error if:
380    /// - Incrementing the nonce overflows a [`Felt`].
381    pub fn increment_nonce(&mut self, nonce_delta: Felt) -> Result<(), AccountError> {
382        let new_nonce = self.nonce + nonce_delta;
383
384        if new_nonce.as_int() < self.nonce.as_int() {
385            return Err(AccountError::NonceOverflow {
386                current: self.nonce,
387                increment: nonce_delta,
388                new: new_nonce,
389            });
390        }
391
392        self.nonce = new_nonce;
393
394        // Maintain internal consistency of the account, i.e. the seed should not be present for
395        // existing accounts, where existing accounts are defined as having a nonce > 0.
396        // If we've incremented the nonce, then we should remove the seed (if it was present at
397        // all).
398        if !self.is_new() {
399            self.seed = None;
400        }
401
402        Ok(())
403    }
404
405    // TEST HELPERS
406    // --------------------------------------------------------------------------------------------
407
408    #[cfg(any(feature = "testing", test))]
409    /// Returns a mutable reference to the vault of this account.
410    pub fn vault_mut(&mut self) -> &mut AssetVault {
411        &mut self.vault
412    }
413
414    #[cfg(any(feature = "testing", test))]
415    /// Returns a mutable reference to the storage of this account.
416    pub fn storage_mut(&mut self) -> &mut AccountStorage {
417        &mut self.storage
418    }
419}
420
421impl TryFrom<Account> for AccountDelta {
422    type Error = AccountError;
423
424    /// Converts an [`Account`] into an [`AccountDelta`].
425    ///
426    /// # Errors
427    ///
428    /// Returns an error if:
429    /// - the account has a seed. Accounts with seeds have a nonce of 0. Representing such accounts
430    ///   as deltas is not possible because deltas with a non-empty state change need a nonce_delta
431    ///   greater than 0.
432    fn try_from(account: Account) -> Result<Self, Self::Error> {
433        let Account { id, vault, storage, code, nonce, seed } = account;
434
435        if seed.is_some() {
436            return Err(AccountError::DeltaFromAccountWithSeed);
437        }
438
439        let mut value_slots = BTreeMap::new();
440        let mut map_slots = BTreeMap::new();
441
442        for (slot_idx, slot) in (0..u8::MAX).zip(storage.into_slots().into_iter()) {
443            match slot {
444                StorageSlot::Value(word) => {
445                    value_slots.insert(slot_idx, word);
446                },
447                StorageSlot::Map(storage_map) => {
448                    let map_delta = StorageMapDelta::new(
449                        storage_map
450                            .into_entries()
451                            .into_iter()
452                            .map(|(key, value)| (LexicographicWord::from(key), value))
453                            .collect(),
454                    );
455                    map_slots.insert(slot_idx, map_delta);
456                },
457            }
458        }
459        let storage_delta = AccountStorageDelta::from_parts(value_slots, map_slots)
460            .expect("value and map slots from account storage should not overlap");
461
462        let mut fungible_delta = FungibleAssetDelta::default();
463        let mut non_fungible_delta = NonFungibleAssetDelta::default();
464        for asset in vault.assets() {
465            // SAFETY: All assets in the account vault should be representable in the delta.
466            match asset {
467                Asset::Fungible(fungible_asset) => {
468                    fungible_delta
469                        .add(fungible_asset)
470                        .expect("delta should allow representing valid fungible assets");
471                },
472                Asset::NonFungible(non_fungible_asset) => {
473                    non_fungible_delta
474                        .add(non_fungible_asset)
475                        .expect("delta should allow representing valid non-fungible assets");
476                },
477            }
478        }
479        let vault_delta = AccountVaultDelta::new(fungible_delta, non_fungible_delta);
480
481        // The nonce of the account is the nonce delta since adding the nonce_delta to 0 would
482        // result in the nonce.
483        let nonce_delta = nonce;
484
485        // SAFETY: As checked earlier, the nonce delta should be greater than 0 allowing for
486        // non-empty state changes.
487        let delta = AccountDelta::new(id, storage_delta, vault_delta, nonce_delta)
488            .expect("nonce_delta should be greater than 0")
489            .with_code(Some(code));
490
491        Ok(delta)
492    }
493}
494
495// SERIALIZATION
496// ================================================================================================
497
498impl Serializable for Account {
499    fn write_into<W: ByteWriter>(&self, target: &mut W) {
500        let Account { id, vault, storage, code, nonce, seed } = self;
501
502        id.write_into(target);
503        vault.write_into(target);
504        storage.write_into(target);
505        code.write_into(target);
506        nonce.write_into(target);
507        seed.write_into(target);
508    }
509
510    fn get_size_hint(&self) -> usize {
511        self.id.get_size_hint()
512            + self.vault.get_size_hint()
513            + self.storage.get_size_hint()
514            + self.code.get_size_hint()
515            + self.nonce.get_size_hint()
516            + self.seed.get_size_hint()
517    }
518}
519
520impl Deserializable for Account {
521    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
522        let id = AccountId::read_from(source)?;
523        let vault = AssetVault::read_from(source)?;
524        let storage = AccountStorage::read_from(source)?;
525        let code = AccountCode::read_from(source)?;
526        let nonce = Felt::read_from(source)?;
527        let seed = <Option<Word>>::read_from(source)?;
528
529        Self::new(id, vault, storage, code, nonce, seed)
530            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
531    }
532}
533
534// HELPERS
535// ================================================================================================
536
537/// Returns hash of an account with the specified ID, nonce, vault root, storage commitment, and
538/// code commitment.
539///
540/// Hash of an account is computed as hash(id, nonce, vault_root, storage_commitment,
541/// code_commitment). Computing the account commitment requires 2 permutations of the hash function.
542pub fn hash_account(
543    id: AccountId,
544    nonce: Felt,
545    vault_root: Word,
546    storage_commitment: Word,
547    code_commitment: Word,
548) -> Word {
549    let mut elements = [ZERO; 16];
550    elements[0] = id.suffix();
551    elements[1] = id.prefix().as_felt();
552    elements[3] = nonce;
553    elements[4..8].copy_from_slice(&*vault_root);
554    elements[8..12].copy_from_slice(&*storage_commitment);
555    elements[12..].copy_from_slice(&*code_commitment);
556    Hasher::hash_elements(&elements)
557}
558
559// HELPER FUNCTIONS
560// ================================================================================================
561
562/// Validates that the provided seed is valid for the provided account components.
563pub(super) fn validate_account_seed(
564    id: AccountId,
565    code_commitment: Word,
566    storage_commitment: Word,
567    seed: Option<Word>,
568    nonce: Felt,
569) -> Result<(), AccountError> {
570    let account_is_new = nonce == ZERO;
571
572    match (account_is_new, seed) {
573        (true, Some(seed)) => {
574            let account_id =
575                AccountId::new(seed, id.version(), code_commitment, storage_commitment)
576                    .map_err(AccountError::SeedConvertsToInvalidAccountId)?;
577
578            if account_id != id {
579                return Err(AccountError::AccountIdSeedMismatch {
580                    expected: id,
581                    actual: account_id,
582                });
583            }
584
585            Ok(())
586        },
587        (true, None) => Err(AccountError::NewAccountMissingSeed),
588        (false, Some(_)) => Err(AccountError::ExistingAccountWithSeed),
589        (false, None) => Ok(()),
590    }
591}
592
593/// Validates that all `components` support the given `account_type`.
594fn validate_components_support_account_type(
595    components: &[AccountComponent],
596    account_type: AccountType,
597) -> Result<(), AccountError> {
598    for (component_index, component) in components.iter().enumerate() {
599        if !component.supports_type(account_type) {
600            return Err(AccountError::UnsupportedComponentForAccountType {
601                account_type,
602                component_index,
603            });
604        }
605    }
606
607    Ok(())
608}
609
610// TESTS
611// ================================================================================================
612
613#[cfg(test)]
614mod tests {
615    use alloc::vec::Vec;
616
617    use assert_matches::assert_matches;
618    use miden_assembly::Assembler;
619    use miden_core::FieldElement;
620    use miden_crypto::utils::{Deserializable, Serializable};
621    use miden_crypto::{Felt, Word};
622
623    use super::{
624        AccountCode,
625        AccountDelta,
626        AccountId,
627        AccountStorage,
628        AccountStorageDelta,
629        AccountVaultDelta,
630    };
631    use crate::AccountError;
632    use crate::account::AccountStorageMode::Network;
633    use crate::account::{
634        Account,
635        AccountBuilder,
636        AccountComponent,
637        AccountIdVersion,
638        AccountType,
639        PartialAccount,
640        StorageMap,
641        StorageMapDelta,
642        StorageSlot,
643    };
644    use crate::asset::{Asset, AssetVault, FungibleAsset, NonFungibleAsset};
645    use crate::testing::account_id::{
646        ACCOUNT_ID_PRIVATE_SENDER,
647        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
648    };
649    use crate::testing::add_component::AddComponent;
650    use crate::testing::noop_auth_component::NoopAuthComponent;
651    use crate::testing::storage::AccountStorageDeltaBuilder;
652
653    #[test]
654    fn test_serde_account() {
655        let init_nonce = Felt::new(1);
656        let asset_0 = FungibleAsset::mock(99);
657        let word = Word::from([1, 2, 3, 4u32]);
658        let storage_slot = StorageSlot::Value(word);
659        let account = build_account(vec![asset_0], init_nonce, vec![storage_slot]);
660
661        let serialized = account.to_bytes();
662        let deserialized = Account::read_from_bytes(&serialized).unwrap();
663        assert_eq!(deserialized, account);
664    }
665
666    #[test]
667    fn test_serde_account_delta() {
668        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
669        let nonce_delta = Felt::new(2);
670        let asset_0 = FungibleAsset::mock(15);
671        let asset_1 = NonFungibleAsset::mock(&[5, 5, 5]);
672        let storage_delta = AccountStorageDeltaBuilder::new()
673            .add_cleared_items([0])
674            .add_updated_values([(1_u8, Word::from([1, 2, 3, 4u32]))])
675            .build()
676            .unwrap();
677        let account_delta = build_account_delta(
678            account_id,
679            vec![asset_1],
680            vec![asset_0],
681            nonce_delta,
682            storage_delta,
683        );
684
685        let serialized = account_delta.to_bytes();
686        let deserialized = AccountDelta::read_from_bytes(&serialized).unwrap();
687        assert_eq!(deserialized, account_delta);
688    }
689
690    #[test]
691    fn valid_account_delta_is_correctly_applied() {
692        // build account
693        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
694        let init_nonce = Felt::new(1);
695        let asset_0 = FungibleAsset::mock(100);
696        let asset_1 = NonFungibleAsset::mock(&[1, 2, 3]);
697
698        // build storage slots
699        let storage_slot_value_0 = StorageSlot::Value(Word::from([1, 2, 3, 4u32]));
700        let storage_slot_value_1 = StorageSlot::Value(Word::from([5, 6, 7, 8u32]));
701        let mut storage_map = StorageMap::with_entries([
702            (
703                Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]),
704                Word::from([
705                    Felt::new(1_u64),
706                    Felt::new(2_u64),
707                    Felt::new(3_u64),
708                    Felt::new(4_u64),
709                ]),
710            ),
711            (
712                Word::new([Felt::new(105), Felt::new(106), Felt::new(107), Felt::new(108)]),
713                Word::new([Felt::new(5_u64), Felt::new(6_u64), Felt::new(7_u64), Felt::new(8_u64)]),
714            ),
715        ])
716        .unwrap();
717        let storage_slot_map = StorageSlot::Map(storage_map.clone());
718
719        let mut account = build_account(
720            vec![asset_0],
721            init_nonce,
722            vec![storage_slot_value_0, storage_slot_value_1, storage_slot_map],
723        );
724
725        // update storage map
726        let new_map_entry = (
727            Word::new([Felt::new(101), Felt::new(102), Felt::new(103), Felt::new(104)]),
728            [Felt::new(9_u64), Felt::new(10_u64), Felt::new(11_u64), Felt::new(12_u64)],
729        );
730
731        let updated_map =
732            StorageMapDelta::from_iters([], [(new_map_entry.0, new_map_entry.1.into())]);
733        storage_map.insert(new_map_entry.0, new_map_entry.1.into()).unwrap();
734
735        // build account delta
736        let final_nonce = Felt::new(2);
737        let storage_delta = AccountStorageDeltaBuilder::new()
738            .add_cleared_items([0])
739            .add_updated_values([(1, Word::from([1, 2, 3, 4u32]))])
740            .add_updated_maps([(2, updated_map)])
741            .build()
742            .unwrap();
743        let account_delta = build_account_delta(
744            account_id,
745            vec![asset_1],
746            vec![asset_0],
747            final_nonce - init_nonce,
748            storage_delta,
749        );
750
751        // apply delta and create final_account
752        account.apply_delta(&account_delta).unwrap();
753
754        let final_account = build_account(
755            vec![asset_1],
756            final_nonce,
757            vec![
758                StorageSlot::Value(Word::empty()),
759                StorageSlot::Value(Word::from([1, 2, 3, 4u32])),
760                StorageSlot::Map(storage_map),
761            ],
762        );
763
764        // assert account is what it should be
765        assert_eq!(account, final_account);
766    }
767
768    #[test]
769    #[should_panic]
770    fn valid_account_delta_with_unchanged_nonce() {
771        // build account
772        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
773        let init_nonce = Felt::new(1);
774        let asset = FungibleAsset::mock(110);
775        let mut account =
776            build_account(vec![asset], init_nonce, vec![StorageSlot::Value(Word::empty())]);
777
778        // build account delta
779        let storage_delta = AccountStorageDeltaBuilder::new()
780            .add_cleared_items([0])
781            .add_updated_values([(1_u8, Word::from([1, 2, 3, 4u32]))])
782            .build()
783            .unwrap();
784        let account_delta =
785            build_account_delta(account_id, vec![], vec![asset], init_nonce, storage_delta);
786
787        // apply delta
788        account.apply_delta(&account_delta).unwrap()
789    }
790
791    #[test]
792    #[should_panic]
793    fn valid_account_delta_with_decremented_nonce() {
794        // build account
795        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
796        let init_nonce = Felt::new(2);
797        let asset = FungibleAsset::mock(100);
798        let mut account =
799            build_account(vec![asset], init_nonce, vec![StorageSlot::Value(Word::empty())]);
800
801        // build account delta
802        let final_nonce = Felt::new(1);
803        let storage_delta = AccountStorageDeltaBuilder::new()
804            .add_cleared_items([0])
805            .add_updated_values([(1_u8, Word::from([1, 2, 3, 4u32]))])
806            .build()
807            .unwrap();
808        let account_delta =
809            build_account_delta(account_id, vec![], vec![asset], final_nonce, storage_delta);
810
811        // apply delta
812        account.apply_delta(&account_delta).unwrap()
813    }
814
815    #[test]
816    fn empty_account_delta_with_incremented_nonce() {
817        // build account
818        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
819        let init_nonce = Felt::new(1);
820        let word = Word::from([1, 2, 3, 4u32]);
821        let storage_slot = StorageSlot::Value(word);
822        let mut account = build_account(vec![], init_nonce, vec![storage_slot]);
823
824        // build account delta
825        let nonce_delta = Felt::new(1);
826        let account_delta = AccountDelta::new(
827            account_id,
828            AccountStorageDelta::new(),
829            AccountVaultDelta::default(),
830            nonce_delta,
831        )
832        .unwrap();
833
834        // apply delta
835        account.apply_delta(&account_delta).unwrap()
836    }
837
838    pub fn build_account_delta(
839        account_id: AccountId,
840        added_assets: Vec<Asset>,
841        removed_assets: Vec<Asset>,
842        nonce_delta: Felt,
843        storage_delta: AccountStorageDelta,
844    ) -> AccountDelta {
845        let vault_delta = AccountVaultDelta::from_iters(added_assets, removed_assets);
846        AccountDelta::new(account_id, storage_delta, vault_delta, nonce_delta).unwrap()
847    }
848
849    pub fn build_account(assets: Vec<Asset>, nonce: Felt, slots: Vec<StorageSlot>) -> Account {
850        let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
851        let code = AccountCode::mock();
852
853        let vault = AssetVault::new(&assets).unwrap();
854
855        let storage = AccountStorage::new(slots).unwrap();
856
857        Account::new_existing(id, vault, storage, code, nonce)
858    }
859
860    /// Tests that initializing code and storage from a component which does not support the given
861    /// account type returns an error.
862    #[test]
863    fn test_account_unsupported_component_type() {
864        let code1 = "export.foo add end";
865        let library1 = Assembler::default().assemble_library([code1]).unwrap();
866
867        // This component support all account types except the regular account with updatable code.
868        let component1 = AccountComponent::new(library1, vec![])
869            .unwrap()
870            .with_supported_type(AccountType::FungibleFaucet)
871            .with_supported_type(AccountType::NonFungibleFaucet)
872            .with_supported_type(AccountType::RegularAccountImmutableCode);
873
874        let err = Account::initialize_from_components(
875            AccountType::RegularAccountUpdatableCode,
876            &[component1],
877        )
878        .unwrap_err();
879
880        assert!(matches!(
881            err,
882            AccountError::UnsupportedComponentForAccountType {
883                account_type: AccountType::RegularAccountUpdatableCode,
884                component_index: 0
885            }
886        ))
887    }
888
889    /// Two components who export a procedure with the same MAST root should fail to convert into
890    /// code and storage.
891    #[test]
892    fn test_account_duplicate_exported_mast_root() {
893        let code1 = "export.foo add eq.1 end";
894        let code2 = "export.bar add eq.1 end";
895
896        let library1 = Assembler::default().assemble_library([code1]).unwrap();
897        let library2 = Assembler::default().assemble_library([code2]).unwrap();
898
899        let component1 = AccountComponent::new(library1, vec![]).unwrap().with_supports_all_types();
900        let component2 = AccountComponent::new(library2, vec![]).unwrap().with_supports_all_types();
901
902        let err = Account::initialize_from_components(
903            AccountType::RegularAccountUpdatableCode,
904            &[NoopAuthComponent.into(), component1, component2],
905        )
906        .unwrap_err();
907
908        assert_matches!(err, AccountError::AccountComponentDuplicateProcedureRoot(_))
909    }
910
911    /// Tests all cases of account ID seed validation.
912    #[test]
913    fn seed_validation() -> anyhow::Result<()> {
914        let account = AccountBuilder::new([5; 32])
915            .with_auth_component(NoopAuthComponent)
916            .with_component(AddComponent)
917            .build()?;
918        let (id, vault, storage, code, _nonce, seed) = account.into_parts();
919        assert!(seed.is_some());
920
921        let other_seed = AccountId::compute_account_seed(
922            [9; 32],
923            AccountType::FungibleFaucet,
924            Network,
925            AccountIdVersion::Version0,
926            code.commitment(),
927            storage.commitment(),
928        )?;
929
930        // Set nonce to 1 so the account is considered existing and provide the seed.
931        let err = Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ONE, seed)
932            .unwrap_err();
933        assert_matches!(err, AccountError::ExistingAccountWithSeed);
934
935        // Set nonce to 0 so the account is considered new but don't provide the seed.
936        let err = Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ZERO, None)
937            .unwrap_err();
938        assert_matches!(err, AccountError::NewAccountMissingSeed);
939
940        // Set nonce to 0 so the account is considered new and provide a valid seed that results in
941        // a different ID than the provided one.
942        let err = Account::new(
943            id,
944            vault.clone(),
945            storage.clone(),
946            code.clone(),
947            Felt::ZERO,
948            Some(other_seed),
949        )
950        .unwrap_err();
951        assert_matches!(err, AccountError::AccountIdSeedMismatch { .. });
952
953        // Set nonce to 0 so the account is considered new and provide a seed that results in an
954        // invalid ID.
955        let err = Account::new(
956            id,
957            vault.clone(),
958            storage.clone(),
959            code.clone(),
960            Felt::ZERO,
961            Some(Word::from([1, 2, 3, 4u32])),
962        )
963        .unwrap_err();
964        assert_matches!(err, AccountError::SeedConvertsToInvalidAccountId(_));
965
966        // Set nonce to 1 so the account is considered existing and don't provide the seed, which
967        // should be valid.
968        Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ONE, None)?;
969
970        // Set nonce to 0 so the account is considered new and provide the original seed, which
971        // should be valid.
972        Account::new(id, vault.clone(), storage.clone(), code.clone(), Felt::ZERO, seed)?;
973
974        Ok(())
975    }
976
977    #[test]
978    fn incrementing_nonce_should_remove_seed() -> anyhow::Result<()> {
979        let mut account = AccountBuilder::new([5; 32])
980            .with_auth_component(NoopAuthComponent)
981            .with_component(AddComponent)
982            .build()?;
983        account.increment_nonce(Felt::ONE)?;
984
985        assert_matches!(account.seed(), None);
986
987        // Sanity check: We should be able to convert the account into a partial account which will
988        // re-check the internal seed - nonce consistency.
989        let _partial_account = PartialAccount::from(&account);
990
991        Ok(())
992    }
993}