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