use alloc::vec::Vec;
use miden_protocol::account::AccountId;
use miden_protocol::assembly::Path;
use miden_protocol::asset::Asset;
use miden_protocol::crypto::rand::FeltRng;
use miden_protocol::errors::NoteError;
use miden_protocol::note::{
Note,
NoteAssets,
NoteAttachment,
NoteAttachments,
NoteRecipient,
NoteScript,
NoteScriptRoot,
NoteStorage,
NoteTag,
NoteType,
PartialNoteMetadata,
};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use crate::StandardsLib;
const P2ID_SCRIPT_PATH: &str = "::miden::standards::notes::p2id::main";
static P2ID_SCRIPT: LazyLock<NoteScript> = LazyLock::new(|| {
let standards_lib = StandardsLib::default();
let path = Path::new(P2ID_SCRIPT_PATH);
NoteScript::from_library_reference(standards_lib.as_ref(), path)
.expect("Standards library contains P2ID note script procedure")
});
#[derive(Debug, Clone)]
pub struct P2idNote {
sender: AccountId,
storage: P2idNoteStorage,
serial_number: Word,
note_type: NoteType,
assets: NoteAssets,
attachments: NoteAttachments,
}
#[bon::bon]
impl P2idNote {
#[builder]
pub fn new(
#[builder(field)] assets: Vec<Asset>,
#[builder(field)] attachments: Vec<NoteAttachment>,
sender: AccountId,
#[builder(name = target, with = |target: AccountId| P2idNoteStorage::new(target))]
storage: P2idNoteStorage,
serial_number: Word,
#[builder(default)] note_type: NoteType,
) -> Result<Self, NoteError> {
if assets.is_empty() {
return Err(NoteError::other("a P2ID note must contain at least one asset"));
}
let assets = NoteAssets::new(assets)?;
let attachments = NoteAttachments::new(attachments)?;
Ok(Self {
sender,
storage,
serial_number,
note_type,
assets,
attachments,
})
}
}
impl P2idNote {
pub const NUM_STORAGE_ITEMS: usize = P2idNoteStorage::NUM_ITEMS;
pub fn script() -> NoteScript {
P2ID_SCRIPT.clone()
}
pub fn script_root() -> NoteScriptRoot {
P2ID_SCRIPT.root()
}
pub fn sender(&self) -> AccountId {
self.sender
}
pub fn storage(&self) -> P2idNoteStorage {
self.storage
}
pub fn target(&self) -> AccountId {
self.storage.target()
}
pub fn serial_number(&self) -> Word {
self.serial_number
}
pub fn note_type(&self) -> NoteType {
self.note_type
}
pub fn assets(&self) -> &NoteAssets {
&self.assets
}
pub fn attachments(&self) -> &NoteAttachments {
&self.attachments
}
}
impl<S: p2id_note_builder::State> P2idNoteBuilder<S> {
pub fn asset(mut self, asset: impl Into<Asset>) -> Self {
self.assets.push(asset.into());
self
}
pub fn assets(mut self, assets: impl IntoIterator<Item = impl Into<Asset>>) -> Self {
self.assets.extend(assets.into_iter().map(Into::into));
self
}
pub fn attachment(mut self, attachment: impl Into<NoteAttachment>) -> Self {
self.attachments.push(attachment.into());
self
}
pub fn attachments(
mut self,
attachments: impl IntoIterator<Item = impl Into<NoteAttachment>>,
) -> Self {
self.attachments.extend(attachments.into_iter().map(Into::into));
self
}
}
impl<S: p2id_note_builder::State> P2idNoteBuilder<S>
where
S::SerialNumber: p2id_note_builder::IsUnset,
{
pub fn generate_serial_number(
self,
rng: &mut impl FeltRng,
) -> P2idNoteBuilder<p2id_note_builder::SetSerialNumber<S>> {
self.serial_number(rng.draw_word())
}
}
impl From<P2idNote> for Note {
fn from(note: P2idNote) -> Self {
let recipient = note.storage.into_recipient(note.serial_number);
let tag = NoteTag::with_account_target(note.storage.target());
let metadata = PartialNoteMetadata::new(note.sender, note.note_type).with_tag(tag);
Note::with_attachments(note.assets, metadata, recipient, note.attachments)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct P2idNoteStorage {
target: AccountId,
}
impl P2idNoteStorage {
pub const NUM_ITEMS: usize = 2;
pub fn new(target: AccountId) -> Self {
Self { target }
}
pub fn into_recipient(self, serial_num: Word) -> NoteRecipient {
NoteRecipient::new(serial_num, P2idNote::script(), NoteStorage::from(self))
}
pub fn target(&self) -> AccountId {
self.target
}
}
impl From<P2idNoteStorage> for NoteStorage {
fn from(storage: P2idNoteStorage) -> Self {
NoteStorage::new(vec![storage.target.suffix(), storage.target.prefix().as_felt()])
.expect("number of storage items should not exceed max storage items")
}
}
impl TryFrom<&[Felt]> for P2idNoteStorage {
type Error = NoteError;
fn try_from(note_storage: &[Felt]) -> Result<Self, Self::Error> {
if note_storage.len() != P2idNote::NUM_STORAGE_ITEMS {
return Err(NoteError::InvalidNoteStorageLength {
expected: P2idNote::NUM_STORAGE_ITEMS,
actual: note_storage.len(),
});
}
let target = AccountId::try_from_elements(note_storage[0], note_storage[1])
.map_err(|err| NoteError::other_with_source("failed to create account id", err))?;
Ok(Self { target })
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use miden_protocol::account::{AccountId, AccountType};
use miden_protocol::asset::FungibleAsset;
use miden_protocol::crypto::rand::RandomCoin;
use miden_protocol::errors::NoteError;
use miden_protocol::{Felt, Word};
use super::*;
#[test]
fn try_from_valid_storage_succeeds() {
let target = AccountId::builder()
.account_type(AccountType::Private)
.build_with_seed([1u8; 32]);
let storage = vec![target.suffix(), target.prefix().as_felt()];
let parsed =
P2idNoteStorage::try_from(storage.as_slice()).expect("storage should be valid");
assert_eq!(parsed.target(), target);
}
#[test]
fn try_from_invalid_length_returns_error() {
let storage = vec![Felt::ZERO];
let err = P2idNoteStorage::try_from(storage.as_slice())
.expect_err("should fail due to invalid length");
assert!(matches!(
err,
NoteError::InvalidNoteStorageLength {
expected: P2idNote::NUM_STORAGE_ITEMS,
actual: 1
}
));
}
#[test]
fn try_from_invalid_storage_contents_returns_error() {
let storage = vec![Felt::new_unchecked(999_u64), Felt::new_unchecked(888_u64)];
let err = P2idNoteStorage::try_from(storage.as_slice())
.expect_err("should fail due to invalid account id encoding");
assert!(matches!(err, NoteError::Other { source: Some(_), .. }));
}
fn sender() -> AccountId {
AccountId::builder()
.account_type(AccountType::Private)
.build_with_seed([1u8; 32])
}
fn target() -> AccountId {
AccountId::builder()
.account_type(AccountType::Private)
.build_with_seed([2u8; 32])
}
fn faucet_a() -> AccountId {
AccountId::builder()
.account_type(AccountType::Public)
.build_with_seed([3u8; 32])
}
fn faucet_b() -> AccountId {
AccountId::builder()
.account_type(AccountType::Public)
.build_with_seed([4u8; 32])
}
#[test]
fn builder_minimal_uses_defaults() {
let note = P2idNote::builder()
.sender(sender())
.target(target())
.serial_number(Word::empty())
.asset(FungibleAsset::new(faucet_a(), 1).unwrap())
.build()
.unwrap();
assert_eq!(note.sender(), sender());
assert_eq!(note.target(), target());
assert_eq!(note.note_type(), NoteType::default());
assert_eq!(note.assets().num_assets(), 1);
assert_eq!(note.attachments().num_attachments(), 0);
}
#[test]
fn builder_accumulates_assets() {
let mut rng = RandomCoin::new(Word::empty());
let note = P2idNote::builder()
.sender(sender())
.target(target())
.asset(FungibleAsset::new(faucet_a(), 100).unwrap())
.assets([Asset::from(FungibleAsset::new(faucet_b(), 200).unwrap())])
.generate_serial_number(&mut rng)
.build()
.unwrap();
assert_eq!(note.assets().num_assets(), 2);
assert_ne!(note.serial_number(), Word::empty());
}
#[test]
fn builder_rejects_empty_assets() {
let err = P2idNote::builder()
.sender(sender())
.target(target())
.serial_number(Word::empty())
.build()
.expect_err("a note without assets must be rejected");
assert_matches!(err, NoteError::Other { error_msg, .. } => {
assert!(error_msg.contains("note must contain at least one asset"))
});
}
}