Skip to main content

miden_client/store/
account.rs

1// ACCOUNT RECORD
2// ================================================================================================
3use alloc::vec::Vec;
4use core::fmt::Display;
5
6use miden_protocol::account::{Account, AccountId, PartialAccount};
7use miden_protocol::{Felt, Word};
8
9use crate::ClientError;
10use crate::sync::PublicAccountUpdate;
11
12// ACCOUNT RECORD DATA
13// ================================================================================================
14
15/// Represents types of records retrieved from the store
16#[derive(Debug)]
17pub enum AccountRecordData {
18    Full(Account),
19    Partial(PartialAccount),
20}
21
22impl AccountRecordData {
23    pub fn nonce(&self) -> Felt {
24        match self {
25            AccountRecordData::Full(account) => account.nonce(),
26            AccountRecordData::Partial(partial_account) => partial_account.nonce(),
27        }
28    }
29}
30
31// CLIENT ACCOUNT TYPE
32// ================================================================================================
33
34/// How the client tracks a given account.
35///
36/// This drives two pieces of behavior:
37///
38/// - **Note sync:** native accounts have their derived note tag registered so `sync_state` pulls
39///   notes targeted at them. Watched accounts do not.
40/// - **Transaction execution:** native accounts can be used as the source of a transaction; watched
41///   accounts cannot, because the client doesn't hold the keys / authority for them.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum ClientAccountType {
44    /// Account is fully owned by this client: notes are synced and transactions can be executed.
45    Native,
46    /// Account state is mirrored from the network for observability only: no note sync, no
47    /// transaction execution.
48    Watched,
49}
50
51// ACCOUNT RECORD
52// ================================================================================================
53
54/// Represents a stored account state along with its status.
55///
56/// The account should be stored in the database with its parts normalized. Meaning that the
57/// account header, vault, storage and code are stored separately. This is done to avoid data
58/// duplication as the header can reference the same elements if they have equal roots.
59#[derive(Debug)]
60pub struct AccountRecord {
61    /// Full account object.
62    account_data: AccountRecordData,
63    /// Status of the tracked account.
64    status: AccountStatus,
65    /// How the client tracks this account.
66    client_account_type: ClientAccountType,
67}
68
69impl AccountRecord {
70    pub fn new(
71        account_data: AccountRecordData,
72        status: AccountStatus,
73        client_account_type: ClientAccountType,
74    ) -> Self {
75        // TODO: remove this?
76        #[cfg(debug_assertions)]
77        {
78            let account_seed = match &account_data {
79                AccountRecordData::Full(acc) => acc.seed(),
80                AccountRecordData::Partial(acc) => acc.seed(),
81            };
82            debug_assert_eq!(account_seed, status.seed().copied(), "account seed mismatch");
83        }
84
85        Self {
86            account_data,
87            status,
88            client_account_type,
89        }
90    }
91
92    pub fn is_locked(&self) -> bool {
93        self.status.is_locked()
94    }
95
96    pub fn client_account_type(&self) -> ClientAccountType {
97        self.client_account_type
98    }
99
100    pub fn is_watched(&self) -> bool {
101        self.client_account_type == ClientAccountType::Watched
102    }
103
104    pub fn nonce(&self) -> Felt {
105        self.account_data.nonce()
106    }
107}
108
109impl TryFrom<AccountRecord> for Account {
110    type Error = ClientError;
111
112    fn try_from(value: AccountRecord) -> Result<Self, Self::Error> {
113        match value.account_data {
114            AccountRecordData::Full(acc) => Ok(acc),
115            AccountRecordData::Partial(acc) => Err(ClientError::AccountRecordNotFull(acc.id())),
116        }
117    }
118}
119
120impl TryFrom<AccountRecord> for PartialAccount {
121    type Error = ClientError;
122
123    fn try_from(value: AccountRecord) -> Result<Self, Self::Error> {
124        match value.account_data {
125            AccountRecordData::Partial(acc) => Ok(acc),
126            AccountRecordData::Full(acc) => Err(ClientError::AccountRecordNotPartial(acc.id())),
127        }
128    }
129}
130
131// ACCOUNT STATUS
132// ================================================================================================
133
134/// Represents the status of an account tracked by the client.
135///
136/// The status of an account may change by local or external factors.
137#[derive(Debug, Clone)]
138pub enum AccountStatus {
139    /// The account is new and hasn't been used yet. The seed used to create the account is
140    /// stored in this state.
141    New { seed: Word },
142    /// The account is tracked by the node and was used at least once.
143    Tracked,
144    /// The local account state doesn't match the node's state, rendering it unusable.
145    /// Only used for private accounts.
146    /// The seed is preserved for private accounts with nonce=0 that need reconstruction via
147    /// `Account::new()`.
148    Locked { seed: Option<Word> },
149}
150
151impl AccountStatus {
152    pub fn is_new(&self) -> bool {
153        matches!(self, AccountStatus::New { .. })
154    }
155
156    pub fn is_locked(&self) -> bool {
157        matches!(self, AccountStatus::Locked { .. })
158    }
159
160    pub fn seed(&self) -> Option<&Word> {
161        match self {
162            AccountStatus::New { seed } => Some(seed),
163            AccountStatus::Locked { seed } => seed.as_ref(),
164            AccountStatus::Tracked => None,
165        }
166    }
167}
168
169impl Display for AccountStatus {
170    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
171        match self {
172            AccountStatus::New { .. } => write!(f, "New"),
173            AccountStatus::Tracked => write!(f, "Tracked"),
174            AccountStatus::Locked { .. } => write!(f, "Locked"),
175        }
176    }
177}
178
179// ACCOUNT UPDATES
180// ================================================================================================
181
182/// Contains account changes to apply to the store.
183pub struct AccountUpdates {
184    /// Updated public accounts, either as full state replacements or incremental deltas.
185    updated_public_accounts: Vec<PublicAccountUpdate>,
186    /// Network account commitments that don't match the current tracked state for private
187    /// accounts.
188    mismatched_private_accounts: Vec<(AccountId, Word)>,
189}
190
191impl AccountUpdates {
192    /// Creates a new instance of `AccountUpdates`.
193    pub fn new(
194        updated_public_accounts: Vec<PublicAccountUpdate>,
195        mismatched_private_accounts: Vec<(AccountId, Word)>,
196    ) -> Self {
197        Self {
198            updated_public_accounts,
199            mismatched_private_accounts,
200        }
201    }
202
203    /// Returns the updated public accounts.
204    pub fn updated_public_accounts(&self) -> &[PublicAccountUpdate] {
205        &self.updated_public_accounts
206    }
207
208    /// Returns the mismatched private accounts.
209    pub fn mismatched_private_accounts(&self) -> &[(AccountId, Word)] {
210        &self.mismatched_private_accounts
211    }
212}