miden_standards/tx_script/
send_notes_script.rs1use alloc::string::String;
2use core::num::NonZeroU16;
3
4use miden_protocol::Felt;
5use miden_protocol::account::{AccountCodeInterface, AccountId};
6use miden_protocol::note::PartialNote;
7use miden_protocol::transaction::{TRANSACTION_SCRIPT_ATTRIBUTE, TransactionScript};
8use thiserror::Error;
9
10use crate::account::access::Ownable2Step;
11use crate::account::faucets::FungibleFaucet;
12use crate::account::wallets::BasicWallet;
13use crate::code_builder::CodeBuilder;
14use crate::errors::CodeBuilderError;
15
16#[derive(Debug, Clone)]
52pub struct SendNotesTransactionScript(TransactionScript);
53
54impl SendNotesTransactionScript {
55 pub fn new(
62 interface: &AccountCodeInterface,
63 output_notes: &[PartialNote],
64 ) -> Result<Self, SendNotesTransactionScriptError> {
65 Self::build(interface, output_notes, "")
66 }
67
68 pub fn with_expiration_delta(
73 interface: &AccountCodeInterface,
74 output_notes: &[PartialNote],
75 expiration_delta: NonZeroU16,
76 ) -> Result<Self, SendNotesTransactionScriptError> {
77 let prelude = format!(
78 "push.{expiration_delta} exec.::miden::protocol::tx::update_expiration_block_delta\n"
79 );
80 Self::build(interface, output_notes, &prelude)
81 }
82
83 fn build(
84 interface: &AccountCodeInterface,
85 output_notes: &[PartialNote],
86 expiration_prelude: &str,
87 ) -> Result<Self, SendNotesTransactionScriptError> {
88 let sender = interface.id();
89
90 let has_mint_and_send = interface.contains([FungibleFaucet::mint_and_send_root()]);
91 let has_move_asset_to_note = interface.contains([BasicWallet::move_asset_to_note_root()]);
92 let is_owner_controlled = interface.contains(Ownable2Step::code().procedure_roots());
93
94 let body = if has_mint_and_send {
95 if is_owner_controlled {
96 return Err(SendNotesTransactionScriptError::UnsupportedAccountInterface);
97 }
98 mint_and_send_note_body(sender, output_notes)?
99 } else if has_move_asset_to_note {
100 move_asset_to_note_body(sender, output_notes)?
101 } else {
102 return Err(SendNotesTransactionScriptError::UnsupportedAccountInterface);
103 };
104
105 let script = format!(
106 "@{TRANSACTION_SCRIPT_ATTRIBUTE}\npub proc main\n{expiration_prelude}\n{body}\nend"
107 );
108
109 let mut code_builder = CodeBuilder::new();
110 for note in output_notes {
111 for attachment in note.attachments().iter() {
112 code_builder
113 .add_advice_map_entry(attachment.to_commitment(), attachment.to_elements());
114 }
115 }
116
117 let tx_script = code_builder
118 .compile_tx_script(script)
119 .map_err(SendNotesTransactionScriptError::InvalidTransactionScript)?;
120
121 Ok(Self(tx_script))
122 }
123}
124
125impl From<SendNotesTransactionScript> for TransactionScript {
126 fn from(value: SendNotesTransactionScript) -> Self {
127 value.0
128 }
129}
130
131#[derive(Debug, Error)]
136pub enum SendNotesTransactionScriptError {
137 #[error("note asset is not issued by faucet {0}")]
138 IssuanceFaucetMismatch(AccountId),
139 #[error("note created by the basic fungible faucet doesn't contain exactly one asset")]
140 FaucetNoteWithoutAsset,
141 #[error("invalid transaction script")]
142 InvalidTransactionScript(#[source] CodeBuilderError),
143 #[error("invalid sender account: {0}")]
144 InvalidSenderAccount(AccountId),
145 #[error(
146 "account does not contain the basic fungible faucet or basic wallet interfaces \
147 which are needed to support the send_notes script generation"
148 )]
149 UnsupportedAccountInterface,
150}
151
152fn move_asset_to_note_body(
156 sender: AccountId,
157 notes: &[PartialNote],
158) -> Result<String, SendNotesTransactionScriptError> {
159 let mut body = String::new();
160 for note in notes {
161 push_note_header(&mut body, sender, note)?;
162
163 body.push_str(
166 "
167 call.::miden::standards::note::note_creator::create_note
168 # => [note_idx, pad(21)]
169
170 movdn.5 dropw drop
171 # => [note_idx, pad(16)]\n
172 ",
173 );
174
175 for asset in note.assets().iter() {
176 body.push_str(&format!(
177 "
178 padw push.0 push.0 push.0 dup.7
179 # => [note_idx, pad(7), note_idx, pad(16)]
180
181 push.{ASSET_VALUE}
182 push.{ASSET_ID}
183 # => [ASSET_ID, ASSET_VALUE, note_idx, pad(7), note_idx, pad(16)]
184
185 call.::miden::standards::wallets::basic::move_asset_to_note
186 # => [pad(16), note_idx, pad(16)]
187
188 dropw dropw dropw dropw
189 # => [note_idx, pad(16)]\n
190 ",
191 ASSET_ID = asset.to_id_word(),
192 ASSET_VALUE = asset.to_value_word(),
193 ));
194 }
195
196 push_attachments(&mut body, note);
197 finalize_note(&mut body);
198 }
199 Ok(body)
200}
201
202fn mint_and_send_note_body(
203 sender: AccountId,
204 notes: &[PartialNote],
205) -> Result<String, SendNotesTransactionScriptError> {
206 let mut body = String::new();
207 for note in notes {
208 push_note_header(&mut body, sender, note)?;
209
210 if note.assets().num_assets() != 1 {
211 return Err(SendNotesTransactionScriptError::FaucetNoteWithoutAsset);
212 }
213 let asset = note.assets().iter().next().expect("note should contain an asset");
214 if asset.faucet_id() != sender {
215 return Err(SendNotesTransactionScriptError::IssuanceFaucetMismatch(asset.faucet_id()));
216 }
217
218 body.push_str(&format!(
219 "
220 push.{ASSET_VALUE}
221 push.{ASSET_ID}
222 # => [ASSET_ID, ASSET_VALUE, tag, note_type, RECIPIENT, pad(16)]
223
224 call.::miden::standards::faucets::fungible::mint_and_send
225 # => [note_idx, pad(29)]
226
227 swapdw dropw dropw swapdw dropw dropw
228 # => [note_idx, pad(13)]\n
229 ",
230 ASSET_ID = asset.to_id_word(),
231 ASSET_VALUE = asset.to_value_word(),
232 ));
233
234 push_attachments(&mut body, note);
235 finalize_note(&mut body);
236 }
237 Ok(body)
238}
239
240fn push_note_header(
241 body: &mut String,
242 sender: AccountId,
243 note: &PartialNote,
244) -> Result<(), SendNotesTransactionScriptError> {
245 if note.metadata().sender() != sender {
246 return Err(SendNotesTransactionScriptError::InvalidSenderAccount(
247 note.metadata().sender(),
248 ));
249 }
250
251 body.push_str(&format!(
252 "
253 push.{recipient}
254 push.{note_type}
255 push.{tag}
256 # => [tag, note_type, RECIPIENT, pad(16)]
257 ",
258 recipient = note.recipient_digest(),
259 note_type = Felt::from(note.metadata().note_type()),
260 tag = Felt::from(note.metadata().tag()),
261 ));
262
263 Ok(())
264}
265
266fn push_attachments(body: &mut String, note: &PartialNote) {
267 for attachment in note.attachments().iter() {
268 let attachment_scheme = attachment.attachment_scheme().as_u16();
269 let attachment_commitment = attachment.content().to_commitment();
270
271 body.push_str(&format!(
272 "
273 dup
274 push.{attachment_commitment}
275 push.{attachment_scheme}
276 # => [attachment_scheme, ATTACHMENT_COMMITMENT, note_idx, note_idx, pad(16)]
277 exec.::miden::protocol::output_note::add_attachment
278 # => [note_idx, pad(16)]
279 ",
280 ));
281 }
282}
283
284fn finalize_note(body: &mut String) {
285 body.push_str(
286 "
287 # drop the note idx
288 drop
289 # => [pad(16)]
290 ",
291 );
292}