Skip to main content

miden_standards/account/interface/
mod.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3
4use miden_protocol::account::AccountId;
5use miden_protocol::note::PartialNote;
6use miden_protocol::transaction::TransactionScript;
7use thiserror::Error;
8
9use crate::AuthMethod;
10use crate::code_builder::CodeBuilder;
11use crate::errors::CodeBuilderError;
12
13#[cfg(test)]
14mod test;
15
16mod component;
17pub use component::AccountComponentInterface;
18
19mod extension;
20pub use extension::{AccountComponentInterfaceExt, AccountInterfaceExt};
21
22// ACCOUNT INTERFACE
23// ================================================================================================
24
25/// An [`AccountInterface`] describes the exported, callable procedures of an account.
26pub struct AccountInterface {
27    account_id: AccountId,
28    auth: Vec<AuthMethod>,
29    components: Vec<AccountComponentInterface>,
30}
31
32// ------------------------------------------------------------------------------------------------
33/// Constructors and public accessors
34impl AccountInterface {
35    // CONSTRUCTORS
36    // --------------------------------------------------------------------------------------------
37
38    /// Creates a new [`AccountInterface`] instance from the provided account ID, authentication
39    /// schemes and account component interfaces.
40    pub fn new(
41        account_id: AccountId,
42        auth: Vec<AuthMethod>,
43        components: Vec<AccountComponentInterface>,
44    ) -> Self {
45        Self { account_id, auth, components }
46    }
47
48    /// Returns `true` if the account installs an [`AccountComponentInterface::Ownable2Step`]
49    /// access component. Since [`AccountComponentInterface::RoleBasedAccessControl`] always
50    /// includes Ownable2Step, this also covers RBAC-controlled accounts.
51    pub fn is_owner_controlled(&self) -> bool {
52        self.components.contains(&AccountComponentInterface::Ownable2Step)
53    }
54
55    // PUBLIC ACCESSORS
56    // --------------------------------------------------------------------------------------------
57
58    /// Returns a reference to the account ID.
59    pub fn id(&self) -> &AccountId {
60        &self.account_id
61    }
62
63    /// Returns `true` if the reference account is a private account, `false` otherwise.
64    pub fn is_private(&self) -> bool {
65        self.account_id.is_private()
66    }
67
68    /// Returns true if the reference account is a public account, `false` otherwise.
69    pub fn is_public(&self) -> bool {
70        self.account_id.is_public()
71    }
72
73    /// Returns a reference to the vector of used authentication methods.
74    pub fn auth(&self) -> &Vec<AuthMethod> {
75        &self.auth
76    }
77
78    /// Returns a reference to the set of used component interfaces.
79    pub fn components(&self) -> &Vec<AccountComponentInterface> {
80        &self.components
81    }
82}
83
84// ------------------------------------------------------------------------------------------------
85/// Code generation
86impl AccountInterface {
87    /// Returns a transaction script which sends the specified notes using the procedures available
88    /// in the current interface.
89    ///
90    /// Provided `expiration_delta` parameter is used to specify how close to the transaction's
91    /// reference block the transaction must be included into the chain. For example, if the
92    /// transaction's reference block is 100 and transaction expiration delta is 10, the transaction
93    /// can be included into the chain by block 110. If this does not happen, the transaction is
94    /// considered expired and cannot be included into the chain.
95    ///
96    /// Currently only [`AccountComponentInterface::BasicWallet`] and
97    /// [`AccountComponentInterface::FungibleFaucet`] interfaces are supported for the
98    /// `send_note` script creation. Attempt to generate the script using some other interface will
99    /// lead to an error. In case both supported interfaces are available in the account, the script
100    /// will be generated for the [`AccountComponentInterface::FungibleFaucet`] interface.
101    ///
102    /// # Example
103    ///
104    /// Example of the `send_note` script with specified expiration delta and one output note:
105    ///
106    /// ```masm
107    /// begin
108    ///     push.{expiration_delta} exec.::miden::protocol::tx::update_expiration_block_delta
109    ///
110    ///     push.{note information}
111    ///
112    ///     push.{ASSET_VALUE} push.{ASSET_KEY}
113    ///     call.::miden::standards::faucets::fungible::mint_and_send
114    ///     swapdw dropw dropw swapdw dropw dropw
115    /// end
116    /// ```
117    ///
118    /// # Errors:
119    /// Returns an error if:
120    /// - the available interfaces does not support the generation of the standard `send_note`
121    ///   procedure.
122    /// - the sender of the note isn't the account for which the script is being built.
123    /// - the note created by the faucet doesn't contain exactly one asset.
124    /// - a faucet tries to mint an asset with a different faucet ID.
125    ///
126    /// [wallet]: crate::account::interface::AccountComponentInterface::BasicWallet
127    /// [faucet]: crate::account::interface::AccountComponentInterface::FungibleFaucet
128    pub fn build_send_notes_script(
129        &self,
130        output_notes: &[PartialNote],
131        expiration_delta: Option<u16>,
132    ) -> Result<TransactionScript, AccountInterfaceError> {
133        let note_creation_source = self.build_create_notes_section(output_notes)?;
134
135        let script = format!(
136            "begin\n{}\n{}\nend",
137            self.build_set_tx_expiration_section(expiration_delta),
138            note_creation_source,
139        );
140
141        // Add attachment entries to the code builder's advice map.
142        // The commitment is used as key and the elements as value.
143        let mut code_builder = CodeBuilder::new();
144        for note in output_notes {
145            for attachment in note.attachments().iter() {
146                code_builder
147                    .add_advice_map_entry(attachment.to_commitment(), attachment.to_elements());
148            }
149        }
150
151        let tx_script = code_builder
152            .compile_tx_script(script)
153            .map_err(AccountInterfaceError::InvalidTransactionScript)?;
154
155        Ok(tx_script)
156    }
157
158    /// Generates a note creation code required for the `send_note` transaction script.
159    ///
160    /// For the example of the resulting code see [AccountComponentInterface::send_note_body]
161    /// description.
162    ///
163    /// # Errors:
164    /// Returns an error if:
165    /// - the available interfaces does not support the generation of the standard `send_note`
166    ///   procedure.
167    /// - the sender of the note isn't the account for which the script is being built.
168    /// - the note created by the faucet doesn't contain exactly one asset.
169    /// - a faucet tries to mint an asset with a different faucet ID.
170    fn build_create_notes_section(
171        &self,
172        output_notes: &[PartialNote],
173    ) -> Result<String, AccountInterfaceError> {
174        if let Some(fungible_faucet) = self.components().iter().find(|component_interface| {
175            matches!(component_interface, AccountComponentInterface::FungibleFaucet)
176        }) {
177            // Owner-controlled faucets (network-style) mint exclusively via MINT notes; refuse to
178            // generate a tx-script `send_note` flow that would fail at runtime under the
179            // OwnerOnly mint policy.
180            if self.is_owner_controlled() {
181                return Err(AccountInterfaceError::UnsupportedAccountInterface);
182            }
183            fungible_faucet.send_note_body(*self.id(), output_notes)
184        } else if self.components().contains(&AccountComponentInterface::BasicWallet) {
185            AccountComponentInterface::BasicWallet.send_note_body(*self.id(), output_notes)
186        } else {
187            Err(AccountInterfaceError::UnsupportedAccountInterface)
188        }
189    }
190
191    /// Returns a string with the expiration delta update procedure call for the script.
192    fn build_set_tx_expiration_section(&self, expiration_delta: Option<u16>) -> String {
193        if let Some(expiration_delta) = expiration_delta {
194            format!(
195                "push.{expiration_delta} exec.::miden::protocol::tx::update_expiration_block_delta\n"
196            )
197        } else {
198            String::new()
199        }
200    }
201}
202
203// ACCOUNT INTERFACE ERROR
204// ============================================================================================
205
206/// Account interface related errors.
207#[derive(Debug, Error)]
208pub enum AccountInterfaceError {
209    #[error("note asset is not issued by faucet {0}")]
210    IssuanceFaucetMismatch(AccountId),
211    #[error("note created by the basic fungible faucet doesn't contain exactly one asset")]
212    FaucetNoteWithoutAsset,
213    #[error("invalid transaction script")]
214    InvalidTransactionScript(#[source] CodeBuilderError),
215    #[error("invalid sender account: {0}")]
216    InvalidSenderAccount(AccountId),
217    #[error("{} interface does not support the generation of the standard send_note script", interface.name())]
218    UnsupportedInterface { interface: AccountComponentInterface },
219    #[error(
220        "account does not contain the basic fungible faucet or basic wallet interfaces which are needed to support the send_note script generation"
221    )]
222    UnsupportedAccountInterface,
223}