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