1use alloc::boxed::Box;
2use alloc::string::{String, ToString};
3use core::error::Error;
4
5use miden_protocol::account::AccountId;
6use miden_protocol::block::BlockNumber;
7use miden_protocol::note::{Note, NoteScript};
8use miden_protocol::utils::Deserializable;
9use miden_protocol::utils::sync::LazyLock;
10use miden_protocol::vm::Program;
11use miden_protocol::{Felt, Word};
12
13use crate::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
14use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt};
15use crate::account::wallets::BasicWallet;
16
17static P2ID_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
22 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2ID.masb"));
23 let program = Program::read_from_bytes(bytes).expect("Shipped P2ID script is well-formed");
24 NoteScript::new(program)
25});
26
27static P2IDE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
29 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2IDE.masb"));
30 let program = Program::read_from_bytes(bytes).expect("Shipped P2IDE script is well-formed");
31 NoteScript::new(program)
32});
33
34static SWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
36 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/SWAP.masb"));
37 let program = Program::read_from_bytes(bytes).expect("Shipped SWAP script is well-formed");
38 NoteScript::new(program)
39});
40
41static MINT_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
43 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/MINT.masb"));
44 let program = Program::read_from_bytes(bytes).expect("Shipped MINT script is well-formed");
45 NoteScript::new(program)
46});
47
48static BURN_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
50 let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/BURN.masb"));
51 let program = Program::read_from_bytes(bytes).expect("Shipped BURN script is well-formed");
52 NoteScript::new(program)
53});
54
55fn p2id() -> NoteScript {
57 P2ID_SCRIPT.clone()
58}
59
60fn p2id_root() -> Word {
62 P2ID_SCRIPT.root()
63}
64
65fn p2ide() -> NoteScript {
67 P2IDE_SCRIPT.clone()
68}
69
70fn p2ide_root() -> Word {
72 P2IDE_SCRIPT.root()
73}
74
75fn swap() -> NoteScript {
77 SWAP_SCRIPT.clone()
78}
79
80fn swap_root() -> Word {
82 SWAP_SCRIPT.root()
83}
84
85fn mint() -> NoteScript {
87 MINT_SCRIPT.clone()
88}
89
90fn mint_root() -> Word {
92 MINT_SCRIPT.root()
93}
94
95fn burn() -> NoteScript {
97 BURN_SCRIPT.clone()
98}
99
100fn burn_root() -> Word {
102 BURN_SCRIPT.root()
103}
104
105pub enum WellKnownNote {
110 P2ID,
111 P2IDE,
112 SWAP,
113 MINT,
114 BURN,
115}
116
117impl WellKnownNote {
118 const P2ID_NUM_INPUTS: usize = 2;
123
124 const P2IDE_NUM_INPUTS: usize = 4;
126
127 const SWAP_NUM_INPUTS: usize = 16;
129
130 const MINT_NUM_INPUTS_PRIVATE: usize = 8;
132
133 const BURN_NUM_INPUTS: usize = 0;
135
136 pub fn from_note(note: &Note) -> Option<Self> {
142 let note_script_root = note.script().root();
143
144 if note_script_root == p2id_root() {
145 return Some(Self::P2ID);
146 }
147 if note_script_root == p2ide_root() {
148 return Some(Self::P2IDE);
149 }
150 if note_script_root == swap_root() {
151 return Some(Self::SWAP);
152 }
153 if note_script_root == mint_root() {
154 return Some(Self::MINT);
155 }
156 if note_script_root == burn_root() {
157 return Some(Self::BURN);
158 }
159
160 None
161 }
162
163 pub fn num_expected_inputs(&self) -> usize {
168 match self {
169 Self::P2ID => Self::P2ID_NUM_INPUTS,
170 Self::P2IDE => Self::P2IDE_NUM_INPUTS,
171 Self::SWAP => Self::SWAP_NUM_INPUTS,
172 Self::MINT => Self::MINT_NUM_INPUTS_PRIVATE,
173 Self::BURN => Self::BURN_NUM_INPUTS,
174 }
175 }
176
177 pub fn script(&self) -> NoteScript {
179 match self {
180 Self::P2ID => p2id(),
181 Self::P2IDE => p2ide(),
182 Self::SWAP => swap(),
183 Self::MINT => mint(),
184 Self::BURN => burn(),
185 }
186 }
187
188 pub fn script_root(&self) -> Word {
190 match self {
191 Self::P2ID => p2id_root(),
192 Self::P2IDE => p2ide_root(),
193 Self::SWAP => swap_root(),
194 Self::MINT => mint_root(),
195 Self::BURN => burn_root(),
196 }
197 }
198
199 pub fn is_compatible_with(&self, account_interface: &AccountInterface) -> bool {
202 if account_interface.components().contains(&AccountComponentInterface::BasicWallet) {
203 return true;
204 }
205
206 let interface_proc_digests = account_interface.get_procedure_digests();
207 match self {
208 Self::P2ID | &Self::P2IDE => {
209 interface_proc_digests.contains(&BasicWallet::receive_asset_digest())
212 },
213 Self::SWAP => {
214 interface_proc_digests.contains(&BasicWallet::receive_asset_digest())
217 && interface_proc_digests.contains(&BasicWallet::move_asset_to_note_digest())
218 },
219 Self::MINT => {
220 interface_proc_digests.contains(&NetworkFungibleFaucet::distribute_digest())
225 },
226 Self::BURN => {
227 interface_proc_digests.contains(&BasicFungibleFaucet::burn_digest())
231 || interface_proc_digests.contains(&NetworkFungibleFaucet::burn_digest())
232 },
233 }
234 }
235
236 pub fn is_consumable(
245 &self,
246 note: &Note,
247 target_account_id: AccountId,
248 block_ref: BlockNumber,
249 ) -> Option<NoteConsumptionStatus> {
250 match self.is_consumable_inner(note, target_account_id, block_ref) {
251 Ok(status) => status,
252 Err(err) => {
253 let err: Box<dyn Error + Send + Sync + 'static> = Box::from(err);
254 Some(NoteConsumptionStatus::NeverConsumable(err))
255 },
256 }
257 }
258
259 fn is_consumable_inner(
273 &self,
274 note: &Note,
275 target_account_id: AccountId,
276 block_ref: BlockNumber,
277 ) -> Result<Option<NoteConsumptionStatus>, StaticAnalysisError> {
278 match self {
279 WellKnownNote::P2ID => {
280 let input_account_id = parse_p2id_inputs(note.inputs().values())?;
281
282 if input_account_id == target_account_id {
283 Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
284 } else {
285 Ok(Some(NoteConsumptionStatus::NeverConsumable("account ID provided to the P2ID note inputs doesn't match the target account ID".into())))
286 }
287 },
288 WellKnownNote::P2IDE => {
289 let (receiver_account_id, reclaim_height, timelock_height) =
290 parse_p2ide_inputs(note.inputs().values())?;
291
292 let current_block_height = block_ref.as_u32();
293
294 let consumable_after = reclaim_height.max(timelock_height);
296
297 if target_account_id == note.metadata().sender() {
299 if current_block_height >= consumable_after {
302 Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
303 } else {
304 Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
305 consumable_after,
306 ))))
307 }
308 } else if target_account_id == receiver_account_id {
310 if current_block_height >= timelock_height {
314 Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
315 } else {
316 Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
317 timelock_height,
318 ))))
319 }
320 } else {
323 Ok(Some(NoteConsumptionStatus::NeverConsumable(
324 "target account of the transaction does not match neither the receiver account specified by the P2IDE inputs, nor the sender account".into()
325 )))
326 }
327 },
328
329 _ => Ok(None),
332 }
333 }
334}
335
336fn parse_p2id_inputs(note_inputs: &[Felt]) -> Result<AccountId, StaticAnalysisError> {
348 if note_inputs.len() != WellKnownNote::P2ID.num_expected_inputs() {
349 return Err(StaticAnalysisError::new(format!(
350 "P2ID note should have {} inputs, but {} was provided",
351 WellKnownNote::P2ID.num_expected_inputs(),
352 note_inputs.len()
353 )));
354 }
355
356 try_read_account_id_from_inputs(note_inputs)
357}
358
359fn parse_p2ide_inputs(note_inputs: &[Felt]) -> Result<(AccountId, u32, u32), StaticAnalysisError> {
371 if note_inputs.len() != WellKnownNote::P2IDE.num_expected_inputs() {
372 return Err(StaticAnalysisError::new(format!(
373 "P2IDE note should have {} inputs, but {} was provided",
374 WellKnownNote::P2IDE.num_expected_inputs(),
375 note_inputs.len()
376 )));
377 }
378
379 let receiver_account_id = try_read_account_id_from_inputs(note_inputs)?;
380
381 let reclaim_height = u32::try_from(note_inputs[2])
382 .map_err(|_err| StaticAnalysisError::new("reclaim block height should be a u32"))?;
383
384 let timelock_height = u32::try_from(note_inputs[3])
385 .map_err(|_err| StaticAnalysisError::new("timelock block height should be a u32"))?;
386
387 Ok((receiver_account_id, reclaim_height, timelock_height))
388}
389
390fn try_read_account_id_from_inputs(note_inputs: &[Felt]) -> Result<AccountId, StaticAnalysisError> {
394 if note_inputs.len() < 2 {
395 return Err(StaticAnalysisError::new(format!(
396 "P2ID and P2IDE notes should have at least 2 note inputs, but {} was provided",
397 note_inputs.len()
398 )));
399 }
400
401 AccountId::try_from([note_inputs[1], note_inputs[0]]).map_err(|source| {
402 StaticAnalysisError::with_source(
403 "failed to create an account ID from the first two note inputs",
404 source,
405 )
406 })
407}
408
409#[derive(Debug)]
418pub enum NoteConsumptionStatus {
419 Consumable,
421 ConsumableAfter(BlockNumber),
423 ConsumableWithAuthorization,
425 UnconsumableConditions,
428 NeverConsumable(Box<dyn Error + Send + Sync + 'static>),
430}
431
432impl Clone for NoteConsumptionStatus {
433 fn clone(&self) -> Self {
434 match self {
435 NoteConsumptionStatus::Consumable => NoteConsumptionStatus::Consumable,
436 NoteConsumptionStatus::ConsumableAfter(block_height) => {
437 NoteConsumptionStatus::ConsumableAfter(*block_height)
438 },
439 NoteConsumptionStatus::ConsumableWithAuthorization => {
440 NoteConsumptionStatus::ConsumableWithAuthorization
441 },
442 NoteConsumptionStatus::UnconsumableConditions => {
443 NoteConsumptionStatus::UnconsumableConditions
444 },
445 NoteConsumptionStatus::NeverConsumable(error) => {
446 let err = error.to_string();
447 NoteConsumptionStatus::NeverConsumable(err.into())
448 },
449 }
450 }
451}
452
453#[derive(thiserror::Error, Debug)]
454#[error("{message}")]
455struct StaticAnalysisError {
456 message: Box<str>,
458 source: Option<Box<dyn Error + Send + Sync + 'static>>,
460}
461
462impl StaticAnalysisError {
463 pub fn new(message: impl Into<String>) -> Self {
465 let message: String = message.into();
466 Self { message: message.into(), source: None }
467 }
468
469 pub fn with_source(
471 message: impl Into<String>,
472 source: impl Error + Send + Sync + 'static,
473 ) -> Self {
474 let message: String = message.into();
475 Self {
476 message: message.into(),
477 source: Some(Box::new(source)),
478 }
479 }
480}