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}