Skip to main content

miden_protocol/account/
mod.rs

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