use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::errors::{AccountIdError, NoteError};
use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments, NoteType};
use crate::note::{NoteExecutionHint, StandardNoteAttachment};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NetworkAccountTarget {
target_id: AccountId,
exec_hint: NoteExecutionHint,
}
impl NetworkAccountTarget {
pub const ATTACHMENT_SCHEME: NoteAttachmentScheme =
StandardNoteAttachment::NetworkAccountTarget.attachment_scheme();
pub fn new(
target_id: AccountId,
exec_hint: NoteExecutionHint,
) -> Result<Self, NetworkAccountTargetError> {
if !target_id.is_public() {
return Err(NetworkAccountTargetError::TargetNotPublic(target_id));
}
Ok(Self { target_id, exec_hint })
}
pub(crate) fn ensure_presence(
attachments: &mut Vec<NoteAttachment>,
target_id: AccountId,
) -> Result<(), NetworkAccountTargetError> {
let mut is_present = false;
for attachment in attachments
.iter()
.filter(|attachment| attachment.attachment_scheme() == Self::ATTACHMENT_SCHEME)
{
let attached_target_id = Self::try_from(attachment)?.target_id();
if attached_target_id != target_id {
return Err(NetworkAccountTargetError::TargetMismatch {
expected: target_id,
actual: attached_target_id,
});
}
is_present = true;
}
if !is_present {
let target = Self::new(target_id, NoteExecutionHint::Always)?;
attachments.push(NoteAttachment::from(target));
}
Ok(())
}
pub fn target_id(&self) -> AccountId {
self.target_id
}
pub fn execution_hint(&self) -> NoteExecutionHint {
self.exec_hint
}
}
impl From<NetworkAccountTarget> for NoteAttachment {
fn from(network_attachment: NetworkAccountTarget) -> Self {
let mut word = Word::empty();
word[0] = network_attachment.target_id.suffix();
word[1] = network_attachment.target_id.prefix().as_felt();
word[2] = network_attachment.exec_hint.into();
NoteAttachment::with_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word)
}
}
impl TryFrom<&NoteAttachments> for NetworkAccountTarget {
type Error = NetworkAccountTargetError;
fn try_from(attachments: &NoteAttachments) -> Result<Self, Self::Error> {
let attachment = attachments
.find(NetworkAccountTarget::ATTACHMENT_SCHEME)
.ok_or_else(|| NetworkAccountTargetError::MissingAttachmentScheme)?;
Self::try_from(attachment)
}
}
impl TryFrom<&NoteAttachment> for NetworkAccountTarget {
type Error = NetworkAccountTargetError;
fn try_from(attachment: &NoteAttachment) -> Result<Self, Self::Error> {
if attachment.attachment_scheme() != Self::ATTACHMENT_SCHEME {
return Err(NetworkAccountTargetError::AttachmentSchemeMismatch(
attachment.attachment_scheme(),
));
}
let words = attachment.content().as_words();
if words.len() != 1 {
return Err(NetworkAccountTargetError::AttachmentContentNumWordsMismatch(
attachment.content().num_words(),
));
}
let word = words[0];
let id_suffix = word[0];
let id_prefix = word[1];
let exec_hint = word[2];
let target_id = AccountId::try_from_elements(id_suffix, id_prefix)
.map_err(NetworkAccountTargetError::DecodeTargetId)?;
let exec_hint = NoteExecutionHint::try_from(exec_hint.as_canonical_u64())
.map_err(NetworkAccountTargetError::DecodeExecutionHint)?;
NetworkAccountTarget::new(target_id, exec_hint)
}
}
#[derive(Debug, thiserror::Error)]
pub enum NetworkAccountTargetError {
#[error("note attachments do not contain a network account target scheme")]
MissingAttachmentScheme,
#[error("target account ID must have public account type")]
TargetNotPublic(AccountId),
#[error("attached network account target {actual} does not match expected target {expected}")]
TargetMismatch { expected: AccountId, actual: AccountId },
#[error(
"attachment scheme {0} did not match expected type {expected}",
expected = NetworkAccountTarget::ATTACHMENT_SCHEME
)]
AttachmentSchemeMismatch(NoteAttachmentScheme),
#[error("network account target expects attachment content with one word, got {0}")]
AttachmentContentNumWordsMismatch(u16),
#[error("failed to decode target account ID")]
DecodeTargetId(#[source] AccountIdError),
#[error("failed to decode execution hint")]
DecodeExecutionHint(#[source] NoteError),
#[error("network note must be public, but was {0:?}")]
NoteNotPublic(NoteType),
}
#[cfg(test)]
mod tests {
use alloc::vec;
use assert_matches::assert_matches;
use miden_protocol::account::AccountType;
use miden_protocol::testing::account_id::AccountIdBuilder;
use super::*;
fn public_account_id() -> AccountId {
AccountIdBuilder::new()
.account_type(AccountType::Public)
.build_with_rng(&mut rand::rng())
}
#[test]
fn network_account_target_serde() -> anyhow::Result<()> {
let id = public_account_id();
let network_account_target = NetworkAccountTarget::new(id, NoteExecutionHint::Always)?;
assert_eq!(
network_account_target,
NetworkAccountTarget::try_from(&NoteAttachment::from(network_account_target))?
);
Ok(())
}
#[test]
fn ensure_presence_keeps_matching_target() -> anyhow::Result<()> {
let target_id = public_account_id();
let supplied = NetworkAccountTarget::new(target_id, NoteExecutionHint::None)?;
let mut attachments = vec![NoteAttachment::from(supplied)];
NetworkAccountTarget::ensure_presence(&mut attachments, target_id)?;
assert_eq!(attachments.len(), 1);
assert_eq!(NetworkAccountTarget::try_from(&attachments[0])?, supplied);
Ok(())
}
#[test]
fn ensure_presence_rejects_mismatched_target() -> anyhow::Result<()> {
let target_id = public_account_id();
let other_id = public_account_id();
let supplied = NetworkAccountTarget::new(other_id, NoteExecutionHint::Always)?;
let mut attachments = vec![NoteAttachment::from(supplied)];
let err = NetworkAccountTarget::ensure_presence(&mut attachments, target_id).unwrap_err();
assert_matches!(
err,
NetworkAccountTargetError::TargetMismatch { expected, actual }
if expected == target_id && actual == other_id
);
Ok(())
}
#[test]
fn ensure_presence_appends_missing_target() -> anyhow::Result<()> {
let target_id = public_account_id();
let unrelated =
NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::from([7u32, 0, 0, 0]));
let mut attachments = vec![unrelated.clone()];
NetworkAccountTarget::ensure_presence(&mut attachments, target_id)?;
assert_eq!(
attachments,
vec![
unrelated,
NoteAttachment::from(NetworkAccountTarget::new(
target_id,
NoteExecutionHint::Always
)?)
]
);
Ok(())
}
#[test]
fn network_account_target_fails_on_private_target_account() -> anyhow::Result<()> {
let id = AccountIdBuilder::new()
.account_type(AccountType::Private)
.build_with_rng(&mut rand::rng());
let err = NetworkAccountTarget::new(id, NoteExecutionHint::Always).unwrap_err();
assert_matches!(
err,
NetworkAccountTargetError::TargetNotPublic(account_id) if account_id == id
);
Ok(())
}
}