Skip to main content

miden_standards/account/interface/
component.rs

1use alloc::string::{String, ToString};
2use alloc::vec::Vec;
3
4use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
5use miden_protocol::account::{AccountId, AccountProcedureRoot, AccountStorage, StorageSlotName};
6use miden_protocol::note::PartialNote;
7use miden_protocol::{Felt, Word};
8
9use crate::AuthMethod;
10use crate::account::auth::{AuthMultisig, AuthSingleSig, AuthSingleSigAcl};
11use crate::account::interface::AccountInterfaceError;
12
13// ACCOUNT COMPONENT INTERFACE
14// ================================================================================================
15
16/// The enum holding all possible account interfaces which could be loaded to some account.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum AccountComponentInterface {
19    /// Exposes procedures from the [`BasicWallet`][crate::account::wallets::BasicWallet] module.
20    BasicWallet,
21    /// Exposes procedures from the
22    /// [`BasicFungibleFaucet`][crate::account::faucets::BasicFungibleFaucet] module.
23    BasicFungibleFaucet,
24    /// Exposes procedures from the
25    /// [`NetworkFungibleFaucet`][crate::account::faucets::NetworkFungibleFaucet] module.
26    NetworkFungibleFaucet,
27    /// Exposes procedures from the
28    /// [`AuthSingleSig`][crate::account::auth::AuthSingleSig] module.
29    AuthSingleSig,
30    /// Exposes procedures from the
31    /// [`AuthSingleSigAcl`][crate::account::auth::AuthSingleSigAcl] module.
32    AuthSingleSigAcl,
33    /// Exposes procedures from the
34    /// [`AuthMultisig`][crate::account::auth::AuthMultisig] module.
35    AuthMultisig,
36    /// Exposes procedures from the [`NoAuth`][crate::account::auth::NoAuth] module.
37    ///
38    /// This authentication scheme provides no cryptographic authentication and only increments
39    /// the nonce if the account state has actually changed during transaction execution.
40    AuthNoAuth,
41    /// A non-standard, custom interface which exposes the contained procedures.
42    ///
43    /// Custom interface holds all procedures which are not part of some standard interface which is
44    /// used by this account.
45    Custom(Vec<AccountProcedureRoot>),
46}
47
48impl AccountComponentInterface {
49    /// Returns a string line with the name of the [AccountComponentInterface] enum variant.
50    ///
51    /// In case of a [AccountComponentInterface::Custom] along with the name of the enum variant
52    /// the vector of shortened hex representations of the used procedures is returned, e.g.
53    /// `Custom([0x6d93447, 0x0bf23d8])`.
54    pub fn name(&self) -> String {
55        match self {
56            AccountComponentInterface::BasicWallet => "Basic Wallet".to_string(),
57            AccountComponentInterface::BasicFungibleFaucet => "Basic Fungible Faucet".to_string(),
58            AccountComponentInterface::NetworkFungibleFaucet => {
59                "Network Fungible Faucet".to_string()
60            },
61            AccountComponentInterface::AuthSingleSig => "SingleSig".to_string(),
62            AccountComponentInterface::AuthSingleSigAcl => "SingleSig ACL".to_string(),
63            AccountComponentInterface::AuthMultisig => "Multisig".to_string(),
64            AccountComponentInterface::AuthNoAuth => "No Auth".to_string(),
65            AccountComponentInterface::Custom(proc_root_vec) => {
66                let result = proc_root_vec
67                    .iter()
68                    .map(|proc_root| proc_root.mast_root().to_hex()[..9].to_string())
69                    .collect::<Vec<_>>()
70                    .join(", ");
71                format!("Custom([{result}])")
72            },
73        }
74    }
75
76    /// Returns true if this component interface is an authentication component.
77    ///
78    /// TODO: currently this can identify only standard auth components
79    pub fn is_auth_component(&self) -> bool {
80        matches!(
81            self,
82            AccountComponentInterface::AuthSingleSig
83                | AccountComponentInterface::AuthSingleSigAcl
84                | AccountComponentInterface::AuthMultisig
85                | AccountComponentInterface::AuthNoAuth
86        )
87    }
88
89    /// Returns the authentication schemes associated with this component interface.
90    pub fn get_auth_methods(&self, storage: &AccountStorage) -> Vec<AuthMethod> {
91        match self {
92            AccountComponentInterface::AuthSingleSig => vec![extract_singlesig_auth_method(
93                storage,
94                AuthSingleSig::public_key_slot(),
95                AuthSingleSig::scheme_id_slot(),
96            )],
97            AccountComponentInterface::AuthSingleSigAcl => vec![extract_singlesig_auth_method(
98                storage,
99                AuthSingleSigAcl::public_key_slot(),
100                AuthSingleSigAcl::scheme_id_slot(),
101            )],
102            AccountComponentInterface::AuthMultisig => {
103                vec![extract_multisig_auth_method(
104                    storage,
105                    AuthMultisig::threshold_config_slot(),
106                    AuthMultisig::approver_public_keys_slot(),
107                    AuthMultisig::approver_scheme_ids_slot(),
108                )]
109            },
110            AccountComponentInterface::AuthNoAuth => vec![AuthMethod::NoAuth],
111            _ => vec![], // Non-auth components return empty vector
112        }
113    }
114
115    /// Generates a body for the note creation of the `send_note` transaction script. The resulting
116    /// code could use different procedures for note creation, which depends on the used interface.
117    ///
118    /// The body consists of two sections:
119    /// - Pushing the note information on the stack.
120    /// - Creating a note:
121    ///   - For basic fungible faucet: pushing the amount of assets and distributing them.
122    ///   - For basic wallet: creating a note, pushing the assets on the stack and moving them to
123    ///     the created note.
124    ///
125    /// # Examples
126    ///
127    /// Example script for the [`AccountComponentInterface::BasicWallet`] with one note:
128    ///
129    /// ```masm
130    ///     push.{note_information}
131    ///     call.::miden::protocol::output_note::create
132    ///
133    ///     push.{note asset}
134    ///     call.::miden::standards::wallets::basic::move_asset_to_note dropw
135    ///     dropw dropw dropw drop
136    /// ```
137    ///
138    /// Example script for the [`AccountComponentInterface::BasicFungibleFaucet`] with one note:
139    ///
140    /// ```masm
141    ///     push.{note information}
142    ///
143    ///     push.{asset amount}
144    ///     call.::miden::standards::faucets::basic_fungible::distribute dropw dropw drop
145    /// ```
146    ///
147    /// # Errors:
148    /// Returns an error if:
149    /// - the interface does not support the generation of the standard `send_note` procedure.
150    /// - the sender of the note isn't the account for which the script is being built.
151    /// - the note created by the faucet doesn't contain exactly one asset.
152    /// - a faucet tries to distribute an asset with a different faucet ID.
153    pub(crate) fn send_note_body(
154        &self,
155        sender_account_id: AccountId,
156        notes: &[PartialNote],
157    ) -> Result<String, AccountInterfaceError> {
158        let mut body = String::new();
159
160        for partial_note in notes {
161            if partial_note.metadata().sender() != sender_account_id {
162                return Err(AccountInterfaceError::InvalidSenderAccount(
163                    partial_note.metadata().sender(),
164                ));
165            }
166
167            body.push_str(&format!(
168                "
169                push.{recipient}
170                push.{note_type}
171                push.{tag}
172                # => [tag, note_type, RECIPIENT, pad(16)]
173                ",
174                recipient = partial_note.recipient_digest(),
175                note_type = Felt::from(partial_note.metadata().note_type()),
176                tag = Felt::from(partial_note.metadata().tag()),
177            ));
178
179            match self {
180                AccountComponentInterface::BasicFungibleFaucet => {
181                    if partial_note.assets().num_assets() != 1 {
182                        return Err(AccountInterfaceError::FaucetNoteWithoutAsset);
183                    }
184
185                    // SAFETY: We checked that the note contains exactly one asset
186                    let asset =
187                        partial_note.assets().iter().next().expect("note should contain an asset");
188
189                    if asset.faucet_id_prefix() != sender_account_id.prefix() {
190                        return Err(AccountInterfaceError::IssuanceFaucetMismatch(
191                            asset.faucet_id_prefix(),
192                        ));
193                    }
194
195                    body.push_str(&format!(
196                        "
197                        push.{amount}
198                        call.::miden::standards::faucets::basic_fungible::distribute
199                        # => [note_idx, pad(25)]
200                        swapdw dropw dropw swap drop
201                        # => [note_idx, pad(16)]\n
202                        ",
203                        amount = asset.unwrap_fungible().amount()
204                    ));
205                },
206                AccountComponentInterface::BasicWallet => {
207                    body.push_str(
208                        "
209                    exec.::miden::protocol::output_note::create
210                    # => [note_idx, pad(16)]\n
211                    ",
212                    );
213
214                    for asset in partial_note.assets().iter() {
215                        body.push_str(&format!(
216                            "
217                            push.{asset}
218                            # => [ASSET, note_idx, pad(16)]
219                            call.::miden::standards::wallets::basic::move_asset_to_note
220                            dropw
221                            # => [note_idx, pad(16)]\n
222                            ",
223                            asset = Word::from(*asset)
224                        ));
225                    }
226                },
227                _ => {
228                    return Err(AccountInterfaceError::UnsupportedInterface {
229                        interface: self.clone(),
230                    });
231                },
232            }
233
234            body.push_str(&format!(
235                "
236                push.{ATTACHMENT}
237                push.{attachment_kind}
238                push.{attachment_scheme}
239                movup.6
240                # => [note_idx, attachment_scheme, attachment_kind, ATTACHMENT, pad(16)]
241                exec.::miden::protocol::output_note::set_attachment
242                # => [pad(16)]
243            ",
244                ATTACHMENT = partial_note.metadata().to_attachment_word(),
245                attachment_scheme =
246                    partial_note.metadata().attachment().attachment_scheme().as_u32(),
247                attachment_kind = partial_note.metadata().attachment().attachment_kind().as_u8(),
248            ));
249        }
250
251        Ok(body)
252    }
253}
254
255// HELPER FUNCTIONS
256// ================================================================================================
257
258/// Extracts authentication method from a single-signature component.
259fn extract_singlesig_auth_method(
260    storage: &AccountStorage,
261    public_key_slot: &StorageSlotName,
262    scheme_id_slot: &StorageSlotName,
263) -> AuthMethod {
264    let pub_key = PublicKeyCommitment::from(
265        storage
266            .get_item(public_key_slot)
267            .expect("invalid storage index of the public key"),
268    );
269
270    let scheme_id = storage
271        .get_item(scheme_id_slot)
272        .expect("invalid storage index of the scheme id")[0]
273        .as_int() as u8;
274
275    let auth_scheme =
276        AuthScheme::try_from(scheme_id).expect("invalid auth scheme id in the scheme id slot");
277
278    AuthMethod::SingleSig { approver: (pub_key, auth_scheme) }
279}
280
281/// Extracts authentication method from a multisig component.
282fn extract_multisig_auth_method(
283    storage: &AccountStorage,
284    config_slot: &StorageSlotName,
285    approver_public_keys_slot: &StorageSlotName,
286    approver_scheme_ids_slot: &StorageSlotName,
287) -> AuthMethod {
288    // Read the multisig configuration from the config slot
289    // Format: [threshold, num_approvers, 0, 0]
290    let config = storage
291        .get_item(config_slot)
292        .expect("invalid slot name of the multisig configuration");
293
294    let threshold = config[0].as_int() as u32;
295    let num_approvers = config[1].as_int() as u8;
296
297    let mut approvers = Vec::new();
298
299    // Read each public key from the map
300    for key_index in 0..num_approvers {
301        // The multisig component stores keys and scheme IDs using pattern [index, 0, 0, 0]
302        let map_key = Word::from([key_index as u32, 0, 0, 0]);
303
304        let pub_key_word =
305            storage.get_map_item(approver_public_keys_slot, map_key).unwrap_or_else(|_| {
306                panic!(
307                    "Failed to read public key {} from multisig configuration at storage slot {}. \
308                     Expected key pattern [index, 0, 0, 0].",
309                    key_index, approver_public_keys_slot
310                )
311            });
312
313        let pub_key = PublicKeyCommitment::from(pub_key_word);
314
315        let scheme_word = storage
316            .get_map_item(approver_scheme_ids_slot, map_key)
317            .unwrap_or_else(|_| {
318                panic!(
319                    "Failed to read scheme id for approver {} from multisig configuration at storage slot {}. \
320                     Expected key pattern [index, 0, 0, 0].",
321                    key_index, approver_scheme_ids_slot
322                )
323            });
324
325        let scheme_id = scheme_word[0].as_int() as u8;
326        let auth_scheme =
327            AuthScheme::try_from(scheme_id).expect("invalid auth scheme id in the scheme id slot");
328        approvers.push((pub_key, auth_scheme));
329    }
330
331    AuthMethod::Multisig { threshold, approvers }
332}