use alloc::boxed::Box;
use alloc::string::String;
use core::error::Error;
use miden_objects::account::AccountId;
use miden_objects::block::BlockNumber;
use miden_objects::note::{Note, NoteScript};
use miden_objects::utils::Deserializable;
use miden_objects::utils::sync::LazyLock;
use miden_objects::vm::Program;
use miden_objects::{Felt, Word};
use crate::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet};
use crate::account::interface::{AccountComponentInterface, AccountInterface};
use crate::account::wallets::BasicWallet;
static P2ID_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2ID.masb"));
let program = Program::read_from_bytes(bytes).expect("Shipped P2ID script is well-formed");
NoteScript::new(program)
});
static P2IDE_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/P2IDE.masb"));
let program = Program::read_from_bytes(bytes).expect("Shipped P2IDE script is well-formed");
NoteScript::new(program)
});
static SWAP_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/SWAP.masb"));
let program = Program::read_from_bytes(bytes).expect("Shipped SWAP script is well-formed");
NoteScript::new(program)
});
static MINT_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/MINT.masb"));
let program = Program::read_from_bytes(bytes).expect("Shipped MINT script is well-formed");
NoteScript::new(program)
});
static BURN_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/note_scripts/BURN.masb"));
let program = Program::read_from_bytes(bytes).expect("Shipped BURN script is well-formed");
NoteScript::new(program)
});
fn p2id() -> NoteScript {
P2ID_SCRIPT.clone()
}
fn p2id_root() -> Word {
P2ID_SCRIPT.root()
}
fn p2ide() -> NoteScript {
P2IDE_SCRIPT.clone()
}
fn p2ide_root() -> Word {
P2IDE_SCRIPT.root()
}
fn swap() -> NoteScript {
SWAP_SCRIPT.clone()
}
fn swap_root() -> Word {
SWAP_SCRIPT.root()
}
fn mint() -> NoteScript {
MINT_SCRIPT.clone()
}
fn mint_root() -> Word {
MINT_SCRIPT.root()
}
fn burn() -> NoteScript {
BURN_SCRIPT.clone()
}
fn burn_root() -> Word {
BURN_SCRIPT.root()
}
pub enum WellKnownNote {
P2ID,
P2IDE,
SWAP,
MINT,
BURN,
}
impl WellKnownNote {
const P2ID_NUM_INPUTS: usize = 2;
const P2IDE_NUM_INPUTS: usize = 4;
const SWAP_NUM_INPUTS: usize = 10;
const MINT_NUM_INPUTS: usize = 9;
const BURN_NUM_INPUTS: usize = 0;
pub fn from_note(note: &Note) -> Option<Self> {
let note_script_root = note.script().root();
if note_script_root == p2id_root() {
return Some(Self::P2ID);
}
if note_script_root == p2ide_root() {
return Some(Self::P2IDE);
}
if note_script_root == swap_root() {
return Some(Self::SWAP);
}
if note_script_root == mint_root() {
return Some(Self::MINT);
}
if note_script_root == burn_root() {
return Some(Self::BURN);
}
None
}
pub fn num_expected_inputs(&self) -> usize {
match self {
Self::P2ID => Self::P2ID_NUM_INPUTS,
Self::P2IDE => Self::P2IDE_NUM_INPUTS,
Self::SWAP => Self::SWAP_NUM_INPUTS,
Self::MINT => Self::MINT_NUM_INPUTS,
Self::BURN => Self::BURN_NUM_INPUTS,
}
}
pub fn script(&self) -> NoteScript {
match self {
Self::P2ID => p2id(),
Self::P2IDE => p2ide(),
Self::SWAP => swap(),
Self::MINT => mint(),
Self::BURN => burn(),
}
}
pub fn script_root(&self) -> Word {
match self {
Self::P2ID => p2id_root(),
Self::P2IDE => p2ide_root(),
Self::SWAP => swap_root(),
Self::MINT => mint_root(),
Self::BURN => burn_root(),
}
}
pub fn is_compatible_with(&self, account_interface: &AccountInterface) -> bool {
if account_interface.components().contains(&AccountComponentInterface::BasicWallet) {
return true;
}
let interface_proc_digests = account_interface.get_procedure_digests();
match self {
Self::P2ID | &Self::P2IDE => {
interface_proc_digests.contains(&BasicWallet::receive_asset_digest())
},
Self::SWAP => {
interface_proc_digests.contains(&BasicWallet::receive_asset_digest())
&& interface_proc_digests.contains(&BasicWallet::move_asset_to_note_digest())
},
Self::MINT => {
interface_proc_digests.contains(&NetworkFungibleFaucet::distribute_digest())
},
Self::BURN => {
interface_proc_digests.contains(&BasicFungibleFaucet::burn_digest())
|| interface_proc_digests.contains(&NetworkFungibleFaucet::burn_digest())
},
}
}
pub fn is_consumable(
&self,
note: &Note,
target_account_id: AccountId,
block_ref: BlockNumber,
) -> Option<NoteConsumptionStatus> {
match self.is_consumable_inner(note, target_account_id, block_ref) {
Ok(status) => status,
Err(err) => {
let err: Box<dyn Error + Send + Sync + 'static> = Box::from(err);
Some(NoteConsumptionStatus::NeverConsumable(err))
},
}
}
fn is_consumable_inner(
&self,
note: &Note,
target_account_id: AccountId,
block_ref: BlockNumber,
) -> Result<Option<NoteConsumptionStatus>, StaticAnalysisError> {
match self {
WellKnownNote::P2ID => {
let input_account_id = parse_p2id_inputs(note.inputs().values())?;
if input_account_id == target_account_id {
Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
} else {
Ok(Some(NoteConsumptionStatus::NeverConsumable("account ID provided to the P2ID note inputs doesn't match the target account ID".into())))
}
},
WellKnownNote::P2IDE => {
let (receiver_account_id, reclaim_height, timelock_height) =
parse_p2ide_inputs(note.inputs().values())?;
let current_block_height = block_ref.as_u32();
let consumable_after = reclaim_height.max(timelock_height);
if target_account_id == note.metadata().sender() {
if current_block_height >= consumable_after {
Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
} else {
Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
consumable_after,
))))
}
} else if target_account_id == receiver_account_id {
if current_block_height >= timelock_height {
Ok(Some(NoteConsumptionStatus::ConsumableWithAuthorization))
} else {
Ok(Some(NoteConsumptionStatus::ConsumableAfter(BlockNumber::from(
timelock_height,
))))
}
} else {
Ok(Some(NoteConsumptionStatus::NeverConsumable(
"target account of the transaction does not match neither the receiver account specified by the P2IDE inputs, nor the sender account".into()
)))
}
},
_ => Ok(None),
}
}
}
fn parse_p2id_inputs(note_inputs: &[Felt]) -> Result<AccountId, StaticAnalysisError> {
if note_inputs.len() != WellKnownNote::P2ID.num_expected_inputs() {
return Err(StaticAnalysisError::new(format!(
"P2ID note should have {} inputs, but {} was provided",
WellKnownNote::P2ID.num_expected_inputs(),
note_inputs.len()
)));
}
try_read_account_id_from_inputs(note_inputs)
}
fn parse_p2ide_inputs(note_inputs: &[Felt]) -> Result<(AccountId, u32, u32), StaticAnalysisError> {
if note_inputs.len() != WellKnownNote::P2IDE.num_expected_inputs() {
return Err(StaticAnalysisError::new(format!(
"P2IDE note should have {} inputs, but {} was provided",
WellKnownNote::P2IDE.num_expected_inputs(),
note_inputs.len()
)));
}
let receiver_account_id = try_read_account_id_from_inputs(note_inputs)?;
let reclaim_height = u32::try_from(note_inputs[2])
.map_err(|_err| StaticAnalysisError::new("reclaim block height should be a u32"))?;
let timelock_height = u32::try_from(note_inputs[3])
.map_err(|_err| StaticAnalysisError::new("timelock block height should be a u32"))?;
Ok((receiver_account_id, reclaim_height, timelock_height))
}
fn try_read_account_id_from_inputs(note_inputs: &[Felt]) -> Result<AccountId, StaticAnalysisError> {
if note_inputs.len() < 2 {
return Err(StaticAnalysisError::new(format!(
"P2ID and P2IDE notes should have at least 2 note inputs, but {} was provided",
note_inputs.len()
)));
}
AccountId::try_from([note_inputs[1], note_inputs[0]]).map_err(|source| {
StaticAnalysisError::with_source(
"failed to create an account ID from the first two note inputs",
source,
)
})
}
#[derive(Debug)]
pub enum NoteConsumptionStatus {
Consumable,
ConsumableAfter(BlockNumber),
ConsumableWithAuthorization,
UnconsumableConditions,
NeverConsumable(Box<dyn Error + Send + Sync + 'static>),
}
#[derive(thiserror::Error, Debug)]
#[error("{message}")]
struct StaticAnalysisError {
message: Box<str>,
source: Option<Box<dyn Error + Send + Sync + 'static>>,
}
impl StaticAnalysisError {
pub fn new(message: impl Into<String>) -> Self {
let message: String = message.into();
Self { message: message.into(), source: None }
}
pub fn with_source(
message: impl Into<String>,
source: impl Error + Send + Sync + 'static,
) -> Self {
let message: String = message.into();
Self {
message: message.into(),
source: Some(Box::new(source)),
}
}
}