Skip to main content

miden_protocol/account/
mod.rs

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