miden_standards/account/interface/mod.rs
1use alloc::string::String;
2use alloc::vec::Vec;
3
4use miden_protocol::account::{AccountId, AccountIdPrefix, AccountType};
5use miden_protocol::note::{NoteAttachmentContent, 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.
26///
27/// A note script's compatibility with this interface can be inspected to check whether the note may
28/// result in a successful execution against this account.
29pub struct AccountInterface {
30 account_id: AccountId,
31 auth: Vec<AuthMethod>,
32 components: Vec<AccountComponentInterface>,
33}
34
35// ------------------------------------------------------------------------------------------------
36/// Constructors and public accessors
37impl AccountInterface {
38 // CONSTRUCTORS
39 // --------------------------------------------------------------------------------------------
40
41 /// Creates a new [`AccountInterface`] instance from the provided account ID, authentication
42 /// schemes and account component interfaces.
43 pub fn new(
44 account_id: AccountId,
45 auth: Vec<AuthMethod>,
46 components: Vec<AccountComponentInterface>,
47 ) -> Self {
48 Self { account_id, auth, components }
49 }
50
51 // PUBLIC ACCESSORS
52 // --------------------------------------------------------------------------------------------
53
54 /// Returns a reference to the account ID.
55 pub fn id(&self) -> &AccountId {
56 &self.account_id
57 }
58
59 /// Returns the type of the reference account.
60 pub fn account_type(&self) -> AccountType {
61 self.account_id.account_type()
62 }
63
64 /// Returns true if the reference account can issue assets.
65 pub fn is_faucet(&self) -> bool {
66 self.account_id.is_faucet()
67 }
68
69 /// Returns true if the reference account is a regular.
70 pub fn is_regular_account(&self) -> bool {
71 self.account_id.is_regular_account()
72 }
73
74 /// Returns `true` if the full state of the account is public on chain, i.e. if the modes are
75 /// [`AccountStorageMode::Public`](miden_protocol::account::AccountStorageMode::Public) or
76 /// [`AccountStorageMode::Network`](miden_protocol::account::AccountStorageMode::Network),
77 /// `false` otherwise.
78 pub fn has_public_state(&self) -> bool {
79 self.account_id.has_public_state()
80 }
81
82 /// Returns `true` if the reference account is a private account, `false` otherwise.
83 pub fn is_private(&self) -> bool {
84 self.account_id.is_private()
85 }
86
87 /// Returns true if the reference account is a public account, `false` otherwise.
88 pub fn is_public(&self) -> bool {
89 self.account_id.is_public()
90 }
91
92 /// Returns true if the reference account is a network account, `false` otherwise.
93 pub fn is_network(&self) -> bool {
94 self.account_id.is_network()
95 }
96
97 /// Returns a reference to the vector of used authentication methods.
98 pub fn auth(&self) -> &Vec<AuthMethod> {
99 &self.auth
100 }
101
102 /// Returns a reference to the set of used component interfaces.
103 pub fn components(&self) -> &Vec<AccountComponentInterface> {
104 &self.components
105 }
106}
107
108// ------------------------------------------------------------------------------------------------
109/// Code generation
110impl AccountInterface {
111 /// Returns a transaction script which sends the specified notes using the procedures available
112 /// in the current interface.
113 ///
114 /// Provided `expiration_delta` parameter is used to specify how close to the transaction's
115 /// reference block the transaction must be included into the chain. For example, if the
116 /// transaction's reference block is 100 and transaction expiration delta is 10, the transaction
117 /// can be included into the chain by block 110. If this does not happen, the transaction is
118 /// considered expired and cannot be included into the chain.
119 ///
120 /// Currently only [`AccountComponentInterface::BasicWallet`] and
121 /// [`AccountComponentInterface::BasicFungibleFaucet`] interfaces are supported for the
122 /// `send_note` script creation. Attempt to generate the script using some other interface will
123 /// lead to an error. In case both supported interfaces are available in the account, the script
124 /// will be generated for the [`AccountComponentInterface::BasicFungibleFaucet`] interface.
125 ///
126 /// # Example
127 ///
128 /// Example of the `send_note` script with specified expiration delta and one output note:
129 ///
130 /// ```masm
131 /// begin
132 /// push.{expiration_delta} exec.::miden::protocol::tx::update_expiration_block_delta
133 ///
134 /// push.{note information}
135 ///
136 /// push.{asset amount}
137 /// call.::miden::standards::faucets::basic_fungible::distribute dropw dropw drop
138 /// end
139 /// ```
140 ///
141 /// # Errors:
142 /// Returns an error if:
143 /// - the available interfaces does not support the generation of the standard `send_note`
144 /// procedure.
145 /// - the sender of the note isn't the account for which the script is being built.
146 /// - the note created by the faucet doesn't contain exactly one asset.
147 /// - a faucet tries to distribute an asset with a different faucet ID.
148 ///
149 /// [wallet]: crate::account::interface::AccountComponentInterface::BasicWallet
150 /// [faucet]: crate::account::interface::AccountComponentInterface::BasicFungibleFaucet
151 pub fn build_send_notes_script(
152 &self,
153 output_notes: &[PartialNote],
154 expiration_delta: Option<u16>,
155 ) -> Result<TransactionScript, AccountInterfaceError> {
156 let note_creation_source = self.build_create_notes_section(output_notes)?;
157
158 let script = format!(
159 "begin\n{}\n{}\nend",
160 self.build_set_tx_expiration_section(expiration_delta),
161 note_creation_source,
162 );
163
164 // Add attachment array entries to the code builder's advice map.
165 // For NoteAttachmentContent::Array, the commitment (to_word) is used as key
166 // and the array elements as value.
167 let mut code_builder = CodeBuilder::new();
168 for note in output_notes {
169 if let NoteAttachmentContent::Array(array) = note.metadata().attachment().content() {
170 code_builder.add_advice_map_entry(array.commitment(), array.as_slice().to_vec());
171 }
172 }
173
174 let tx_script = code_builder
175 .compile_tx_script(script)
176 .map_err(AccountInterfaceError::InvalidTransactionScript)?;
177
178 Ok(tx_script)
179 }
180
181 /// Generates a note creation code required for the `send_note` transaction script.
182 ///
183 /// For the example of the resulting code see [AccountComponentInterface::send_note_body]
184 /// description.
185 ///
186 /// # Errors:
187 /// Returns an error if:
188 /// - the available interfaces does not support the generation of the standard `send_note`
189 /// procedure.
190 /// - the sender of the note isn't the account for which the script is being built.
191 /// - the note created by the faucet doesn't contain exactly one asset.
192 /// - a faucet tries to distribute an asset with a different faucet ID.
193 fn build_create_notes_section(
194 &self,
195 output_notes: &[PartialNote],
196 ) -> Result<String, AccountInterfaceError> {
197 if let Some(basic_fungible_faucet) = self.components().iter().find(|component_interface| {
198 matches!(component_interface, AccountComponentInterface::BasicFungibleFaucet)
199 }) {
200 basic_fungible_faucet.send_note_body(*self.id(), output_notes)
201 } else if let Some(_network_fungible_faucet) =
202 self.components().iter().find(|component_interface| {
203 matches!(component_interface, AccountComponentInterface::NetworkFungibleFaucet)
204 })
205 {
206 // Network fungible faucet doesn't support send_note_body, because minting
207 // is done via a MINT note.
208 Err(AccountInterfaceError::UnsupportedAccountInterface)
209 } else if self.components().contains(&AccountComponentInterface::BasicWallet) {
210 AccountComponentInterface::BasicWallet.send_note_body(*self.id(), output_notes)
211 } else {
212 Err(AccountInterfaceError::UnsupportedAccountInterface)
213 }
214 }
215
216 /// Returns a string with the expiration delta update procedure call for the script.
217 fn build_set_tx_expiration_section(&self, expiration_delta: Option<u16>) -> String {
218 if let Some(expiration_delta) = expiration_delta {
219 format!(
220 "push.{expiration_delta} exec.::miden::protocol::tx::update_expiration_block_delta\n"
221 )
222 } else {
223 String::new()
224 }
225 }
226}
227
228// NOTE ACCOUNT COMPATIBILITY
229// ================================================================================================
230
231/// Describes whether a note is compatible with a specific account.
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum NoteAccountCompatibility {
234 /// A note is incompatible with an account.
235 ///
236 /// The account interface does not have procedures for being able to execute at least one of
237 /// the program execution branches.
238 No,
239 /// The account has all necessary procedures of one execution branch of the note script. This
240 /// means the note may be able to be consumed by the account if that branch is executed.
241 Maybe,
242 /// A note could be successfully executed and consumed by the account.
243 Yes,
244}
245
246// ACCOUNT INTERFACE ERROR
247// ============================================================================================
248
249/// Account interface related errors.
250#[derive(Debug, Error)]
251pub enum AccountInterfaceError {
252 #[error("note asset is not issued by this faucet: {0}")]
253 IssuanceFaucetMismatch(AccountIdPrefix),
254 #[error("note created by the basic fungible faucet doesn't contain exactly one asset")]
255 FaucetNoteWithoutAsset,
256 #[error("invalid transaction script")]
257 InvalidTransactionScript(#[source] CodeBuilderError),
258 #[error("invalid sender account: {0}")]
259 InvalidSenderAccount(AccountId),
260 #[error("{} interface does not support the generation of the standard send_note script", interface.name())]
261 UnsupportedInterface { interface: AccountComponentInterface },
262 #[error(
263 "account does not contain the basic fungible faucet or basic wallet interfaces which are needed to support the send_note script generation"
264 )]
265 UnsupportedAccountInterface,
266}