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