use alloc::vec::Vec;
use core::num::NonZeroU16;
use miden_protocol::account::{AccountCodeInterface, AccountId, AccountProcedureRoot};
use miden_protocol::asset::AssetComposition;
use miden_protocol::note::PartialNote;
use miden_protocol::transaction::{TransactionScript, TransactionScriptRoot};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::vm::AdviceMap;
use miden_protocol::{Felt, Hasher, Word, ZERO};
use thiserror::Error;
use crate::account::access::{Ownable2Step, RoleBasedAccessControl};
use crate::account::faucets::{FungibleFaucet, NonFungibleFaucet};
use crate::account::wallets::BasicWallet;
use crate::tx_script::transaction_script;
const SEND_NOTES_WALLET_TX_SCRIPT_PATH: &str =
"::miden::standards::tx_scripts::send_notes::wallet::main";
const SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT_PATH: &str =
"::miden::standards::tx_scripts::send_notes::fungible_faucet::main";
const SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT_PATH: &str =
"::miden::standards::tx_scripts::send_notes::non_fungible_faucet::main";
static SEND_NOTES_WALLET_TX_SCRIPT: LazyLock<TransactionScript> =
LazyLock::new(|| transaction_script(SEND_NOTES_WALLET_TX_SCRIPT_PATH));
static SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT: LazyLock<TransactionScript> =
LazyLock::new(|| transaction_script(SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT_PATH));
static SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT: LazyLock<TransactionScript> =
LazyLock::new(|| transaction_script(SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT_PATH));
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum SendNotesTransactionScript {
Wallet(SendWalletNotesTransactionScript),
Fungible(SendFungibleFaucetNotesTransactionScript),
NonFungible(SendNonFungibleFaucetNotesTransactionScript),
}
impl SendNotesTransactionScript {
pub const PAYLOAD_HEADER_NUM_ELEMENTS: usize = 4;
pub const NOTE_RECORD_NUM_ASSETS_OFFSET: usize = 6;
pub const NOTE_RECORD_ITEMS_OFFSET: usize = 8;
pub const ITEM_NUM_ELEMENTS: usize = 8;
pub fn new(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
) -> Result<Self, SendNotesTransactionScriptError> {
Self::build(interface, output_notes, 0)
}
pub fn with_expiration_delta(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
expiration_delta: NonZeroU16,
) -> Result<Self, SendNotesTransactionScriptError> {
Self::build(interface, output_notes, expiration_delta.get())
}
pub fn tx_script(&self) -> &TransactionScript {
match self {
Self::Wallet(script) => script.tx_script(),
Self::Fungible(script) => script.tx_script(),
Self::NonFungible(script) => script.tx_script(),
}
}
pub fn tx_script_args(&self) -> Word {
match self {
Self::Wallet(script) => script.tx_script_args(),
Self::Fungible(script) => script.tx_script_args(),
Self::NonFungible(script) => script.tx_script_args(),
}
}
pub fn script_roots() -> [TransactionScriptRoot; 3] {
[
SendWalletNotesTransactionScript::script_root(),
SendFungibleFaucetNotesTransactionScript::script_root(),
SendNonFungibleFaucetNotesTransactionScript::script_root(),
]
}
fn build(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
expiration_delta: u16,
) -> Result<Self, SendNotesTransactionScriptError> {
let fungible_faucet = interface.contains([FungibleFaucet::mint_and_send_root()]);
let non_fungible_faucet = interface.contains([NonFungibleFaucet::mint_and_send_root()]);
let basic_wallet = interface
.contains([BasicWallet::move_asset_to_note_root(), BasicWallet::create_note_root()]);
let one_asset_per_note = output_notes.iter().all(|note| note.assets().num_assets() == 1);
let mints_composition = |composition| {
one_asset_per_note
&& output_notes
.iter()
.flat_map(|note| note.assets().iter())
.all(|asset| asset.id().composition() == composition)
};
if fungible_faucet && mints_composition(AssetComposition::Fungible) {
SendFungibleFaucetNotesTransactionScript::build(
interface,
output_notes,
expiration_delta,
)
.map(Self::Fungible)
} else if non_fungible_faucet && mints_composition(AssetComposition::None) {
SendNonFungibleFaucetNotesTransactionScript::build(
interface,
output_notes,
expiration_delta,
)
.map(Self::NonFungible)
} else if basic_wallet {
SendWalletNotesTransactionScript::build(interface, output_notes, expiration_delta)
.map(Self::Wallet)
} else if !(fungible_faucet || non_fungible_faucet) {
Err(SendNotesTransactionScriptError::UnsupportedAccountInterface)
} else if !one_asset_per_note {
Err(SendNotesTransactionScriptError::FaucetNoteUnexpectedNumAssets)
} else {
let expected = if fungible_faucet {
AssetComposition::Fungible
} else {
AssetComposition::None
};
let actual = output_notes
.iter()
.flat_map(|note| note.assets().iter())
.map(|asset| asset.id().composition())
.find(|composition| *composition != expected)
.unwrap_or(expected);
Err(SendNotesTransactionScriptError::AssetCompositionMismatch { expected, actual })
}
}
}
#[derive(Debug, Clone)]
pub struct SendWalletNotesTransactionScript(SendNotesScript);
impl SendWalletNotesTransactionScript {
pub fn new(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
) -> Result<Self, SendNotesTransactionScriptError> {
Self::build(interface, output_notes, 0)
}
pub fn with_expiration_delta(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
expiration_delta: NonZeroU16,
) -> Result<Self, SendNotesTransactionScriptError> {
Self::build(interface, output_notes, expiration_delta.get())
}
pub fn tx_script(&self) -> &TransactionScript {
self.0.tx_script()
}
pub fn tx_script_args(&self) -> Word {
self.0.tx_script_args()
}
pub fn script_root() -> TransactionScriptRoot {
SEND_NOTES_WALLET_TX_SCRIPT.root()
}
fn build(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
expiration_delta: u16,
) -> Result<Self, SendNotesTransactionScriptError> {
let can_send_own_assets = interface
.contains([BasicWallet::move_asset_to_note_root(), BasicWallet::create_note_root()]);
if !can_send_own_assets {
return Err(SendNotesTransactionScriptError::UnsupportedAccountInterface);
}
for note in output_notes {
validate_note_sender(interface.id(), note)?;
}
Ok(Self(SendNotesScript::new(
SEND_NOTES_WALLET_TX_SCRIPT.clone(),
output_notes,
expiration_delta,
)))
}
}
#[derive(Debug, Clone)]
pub struct SendFungibleFaucetNotesTransactionScript(SendNotesScript);
impl SendFungibleFaucetNotesTransactionScript {
pub fn new(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
) -> Result<Self, SendNotesTransactionScriptError> {
Self::build(interface, output_notes, 0)
}
pub fn with_expiration_delta(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
expiration_delta: NonZeroU16,
) -> Result<Self, SendNotesTransactionScriptError> {
Self::build(interface, output_notes, expiration_delta.get())
}
pub fn tx_script(&self) -> &TransactionScript {
self.0.tx_script()
}
pub fn tx_script_args(&self) -> Word {
self.0.tx_script_args()
}
pub fn script_root() -> TransactionScriptRoot {
SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT.root()
}
fn build(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
expiration_delta: u16,
) -> Result<Self, SendNotesTransactionScriptError> {
validate_faucet(interface, FungibleFaucet::mint_and_send_root())?;
validate_minted_notes(interface.id(), output_notes, AssetComposition::Fungible)?;
Ok(Self(SendNotesScript::new(
SEND_NOTES_FUNGIBLE_FAUCET_TX_SCRIPT.clone(),
output_notes,
expiration_delta,
)))
}
}
#[derive(Debug, Clone)]
pub struct SendNonFungibleFaucetNotesTransactionScript(SendNotesScript);
impl SendNonFungibleFaucetNotesTransactionScript {
pub fn new(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
) -> Result<Self, SendNotesTransactionScriptError> {
Self::build(interface, output_notes, 0)
}
pub fn with_expiration_delta(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
expiration_delta: NonZeroU16,
) -> Result<Self, SendNotesTransactionScriptError> {
Self::build(interface, output_notes, expiration_delta.get())
}
pub fn tx_script(&self) -> &TransactionScript {
self.0.tx_script()
}
pub fn tx_script_args(&self) -> Word {
self.0.tx_script_args()
}
pub fn script_root() -> TransactionScriptRoot {
SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT.root()
}
fn build(
interface: &AccountCodeInterface,
output_notes: &[PartialNote],
expiration_delta: u16,
) -> Result<Self, SendNotesTransactionScriptError> {
validate_faucet(interface, NonFungibleFaucet::mint_and_send_root())?;
validate_minted_notes(interface.id(), output_notes, AssetComposition::None)?;
Ok(Self(SendNotesScript::new(
SEND_NOTES_NON_FUNGIBLE_FAUCET_TX_SCRIPT.clone(),
output_notes,
expiration_delta,
)))
}
}
#[derive(Debug, Clone)]
struct SendNotesScript {
script: TransactionScript,
tx_script_args: Word,
}
impl SendNotesScript {
fn new(script: TransactionScript, output_notes: &[PartialNote], expiration_delta: u16) -> Self {
let payload = encode_payload(output_notes, expiration_delta);
let tx_script_args = Hasher::hash_elements(&payload);
let mut advice_map = AdviceMap::default();
advice_map.insert(tx_script_args, payload);
for note in output_notes {
for attachment in note.attachments().iter() {
advice_map.insert(attachment.to_commitment(), attachment.to_elements());
}
}
Self {
script: script.with_advice_map(advice_map),
tx_script_args,
}
}
fn tx_script(&self) -> &TransactionScript {
&self.script
}
fn tx_script_args(&self) -> Word {
self.tx_script_args
}
}
#[derive(Debug, Error)]
pub enum SendNotesTransactionScriptError {
#[error("note asset is not issued by faucet {0}")]
IssuanceFaucetMismatch(AccountId),
#[error("note created by the faucet doesn't contain exactly one asset")]
FaucetNoteUnexpectedNumAssets,
#[error(
"note asset has the {actual} composition but the faucet mints assets with the {expected} \
composition"
)]
AssetCompositionMismatch {
expected: AssetComposition,
actual: AssetComposition,
},
#[error("invalid sender account: {0}")]
InvalidSenderAccount(AccountId),
#[error(
"account does not contain the basic wallet, fungible faucet or non-fungible faucet \
interfaces which are needed to support the send_notes script generation"
)]
UnsupportedAccountInterface,
}
fn validate_faucet(
interface: &AccountCodeInterface,
mint_and_send_root: AccountProcedureRoot,
) -> Result<(), SendNotesTransactionScriptError> {
if !interface.contains([mint_and_send_root]) {
return Err(SendNotesTransactionScriptError::UnsupportedAccountInterface);
}
let is_authority_controlled = interface.contains(Ownable2Step::code().procedure_roots())
|| interface.contains(RoleBasedAccessControl::code().procedure_roots());
if is_authority_controlled {
return Err(SendNotesTransactionScriptError::UnsupportedAccountInterface);
}
Ok(())
}
fn validate_minted_notes(
sender: AccountId,
notes: &[PartialNote],
expected_composition: AssetComposition,
) -> Result<(), SendNotesTransactionScriptError> {
for note in notes {
validate_note_sender(sender, note)?;
if note.assets().num_assets() != 1 {
return Err(SendNotesTransactionScriptError::FaucetNoteUnexpectedNumAssets);
}
let asset = note.assets().iter().next().expect("note should contain an asset");
if asset.faucet_id() != sender {
return Err(SendNotesTransactionScriptError::IssuanceFaucetMismatch(asset.faucet_id()));
}
let composition = asset.id().composition();
if composition != expected_composition {
return Err(SendNotesTransactionScriptError::AssetCompositionMismatch {
expected: expected_composition,
actual: composition,
});
}
}
Ok(())
}
fn validate_note_sender(
sender: AccountId,
note: &PartialNote,
) -> Result<(), SendNotesTransactionScriptError> {
if note.metadata().sender() != sender {
return Err(SendNotesTransactionScriptError::InvalidSenderAccount(
note.metadata().sender(),
));
}
Ok(())
}
fn encode_payload(notes: &[PartialNote], expiration_delta: u16) -> Vec<Felt> {
let num_notes = u32::try_from(notes.len()).expect("note count should fit in a u32");
let mut payload = alloc::vec![Felt::from(num_notes), Felt::from(expiration_delta), ZERO, ZERO];
debug_assert_eq!(
payload.len(),
SendNotesTransactionScript::PAYLOAD_HEADER_NUM_ELEMENTS,
"header size should match the advertised constant"
);
for note in notes {
let num_assets =
u32::try_from(note.assets().num_assets()).expect("asset count should fit in a u32");
let record_start = payload.len();
payload.extend(note.recipient_digest().iter());
payload.push(Felt::from(note.metadata().tag()));
payload.push(Felt::from(note.metadata().note_type()));
debug_assert_eq!(
payload.len() - record_start,
SendNotesTransactionScript::NOTE_RECORD_NUM_ASSETS_OFFSET,
"asset count should sit at the advertised record offset"
);
payload.push(Felt::from(num_assets));
payload.push(Felt::from(note.attachments().num_attachments()));
debug_assert_eq!(
payload.len() - record_start,
SendNotesTransactionScript::NOTE_RECORD_ITEMS_OFFSET,
"items should start at the advertised record offset"
);
for asset in note.assets().iter() {
let item_start = payload.len();
payload.extend(asset.to_id_word().iter());
payload.extend(asset.to_value_word().iter());
debug_assert_eq!(
payload.len() - item_start,
SendNotesTransactionScript::ITEM_NUM_ELEMENTS,
"an asset item should occupy the advertised number of elements"
);
}
for attachment in note.attachments().iter() {
payload.push(Felt::from(attachment.attachment_scheme().as_u16()));
payload.extend([ZERO; 3]);
payload.extend(attachment.to_commitment().iter());
}
}
payload
}