Skip to main content

miden_standards/tx_script/
send_notes_script.rs

1use 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// SEND NOTES TRANSACTION SCRIPT
17// ================================================================================================
18
19/// A [`TransactionScript`] that sends the specified notes from an account whose code interface
20/// exposes either the [`BasicWallet`] or [`FungibleFaucet`] procedures.
21///
22/// Construction is fallible (see [`SendNotesTransactionScriptError`]); converting the wrapper into
23/// the underlying [`TransactionScript`] via the [`From`] impl is infallible.
24///
25/// Provided `expiration_delta` specifies how close to the transaction's reference block the
26/// transaction must be included into the chain. For example, with a reference block of 100 and a
27/// delta of 10, the transaction must be included by block 110.
28///
29/// When the account exposes both [`BasicWallet`] and [`FungibleFaucet`] procedures, the faucet
30/// branch is preferred. Owner-controlled faucets (those exposing [`Ownable2Step`]) mint
31/// exclusively via MINT notes, so the standard `send_note` flow is rejected at script-build time
32/// to avoid runtime failures under the OwnerOnly mint policy.
33///
34/// # Example
35///
36/// Example of the generated script with one output note and an expiration delta against a
37/// [`FungibleFaucet`]:
38///
39/// ```masm
40/// @transaction_script
41/// pub proc main
42///     push.{expiration_delta} exec.::miden::protocol::tx::update_expiration_block_delta
43///
44///     push.{note information}
45///
46///     push.{ASSET_VALUE} push.{ASSET_ID}
47///     call.::miden::standards::faucets::fungible::mint_and_send
48///     swapdw dropw dropw swapdw dropw dropw
49/// end
50/// ```
51#[derive(Debug, Clone)]
52pub struct SendNotesTransactionScript(TransactionScript);
53
54impl SendNotesTransactionScript {
55    /// Builds a `send_notes` transaction script for the account described by `interface`,
56    /// without an expiration delta.
57    ///
58    /// See [`Self::with_expiration_delta`] for the variant that pins the transaction to a
59    /// reference-block delta. See the [type-level docs](Self) for the full list of error
60    /// conditions.
61    pub fn new(
62        interface: &AccountCodeInterface,
63        output_notes: &[PartialNote],
64    ) -> Result<Self, SendNotesTransactionScriptError> {
65        Self::build(interface, output_notes, "")
66    }
67
68    /// Builds a `send_notes` transaction script for the account described by `interface`,
69    /// with the given non-zero expiration delta.
70    ///
71    /// See the [type-level docs](Self) for the full list of error conditions.
72    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// SEND NOTES SCRIPT ERROR
132// ================================================================================================
133
134/// Errors that can occur while building a [`SendNotesTransactionScript`].
135#[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
152// HELPER FUNCTIONS
153// ================================================================================================
154
155fn 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        // Note creation is only accessible from the account context, so it is created through the
164        // wallet's `create_note` procedure rather than the kernel procedure directly
165        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}