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