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