Skip to main content

miden_standards/note/
network_account_target.rs

1use miden_protocol::Word;
2use miden_protocol::account::AccountId;
3use miden_protocol::errors::{AccountIdError, NoteError};
4use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments, NoteType};
5
6use crate::note::{NoteExecutionHint, StandardNoteAttachment};
7
8// NETWORK ACCOUNT TARGET
9// ================================================================================================
10
11/// A [`NoteAttachment`] for notes targeted at network accounts.
12///
13/// It can be encoded to and from a single-word attachment content with the following layout:
14///
15/// ```text
16/// - 0th felt: [target_id_suffix (56 bits) | 8 zero bits]
17/// - 1st felt: [target_id_prefix (64 bits)]
18/// - 2nd felt: [24 zero bits | exec_hint_payload (32 bits) | exec_hint_tag (8 bits)]
19/// - 3rd felt: [64 zero bits]
20/// ```
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct NetworkAccountTarget {
23    target_id: AccountId,
24    exec_hint: NoteExecutionHint,
25}
26
27impl NetworkAccountTarget {
28    // CONSTANTS
29    // --------------------------------------------------------------------------------------------
30
31    /// The standardized scheme of [`NetworkAccountTarget`] attachments.
32    pub const ATTACHMENT_SCHEME: NoteAttachmentScheme =
33        StandardNoteAttachment::NetworkAccountTarget.attachment_scheme();
34
35    // CONSTRUCTORS
36    // --------------------------------------------------------------------------------------------
37
38    /// Creates a new [`NetworkAccountTarget`] from the provided parts.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if:
43    /// - the provided `target_id` does not have
44    ///   [`AccountType::Public`](miden_protocol::account::AccountType::Public).
45    pub fn new(
46        target_id: AccountId,
47        exec_hint: NoteExecutionHint,
48    ) -> Result<Self, NetworkAccountTargetError> {
49        if !target_id.is_public() {
50            return Err(NetworkAccountTargetError::TargetNotPublic(target_id));
51        }
52
53        Ok(Self { target_id, exec_hint })
54    }
55
56    // ACCESSORS
57    // --------------------------------------------------------------------------------------------
58
59    /// Returns the [`AccountId`] at which the note is targeted.
60    pub fn target_id(&self) -> AccountId {
61        self.target_id
62    }
63
64    /// Returns the [`NoteExecutionHint`] of the note.
65    pub fn execution_hint(&self) -> NoteExecutionHint {
66        self.exec_hint
67    }
68}
69
70impl From<NetworkAccountTarget> for NoteAttachment {
71    fn from(network_attachment: NetworkAccountTarget) -> Self {
72        let mut word = Word::empty();
73        word[0] = network_attachment.target_id.suffix();
74        word[1] = network_attachment.target_id.prefix().as_felt();
75        word[2] = network_attachment.exec_hint.into();
76
77        NoteAttachment::with_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word)
78    }
79}
80
81impl TryFrom<&NoteAttachments> for NetworkAccountTarget {
82    type Error = NetworkAccountTargetError;
83
84    fn try_from(attachments: &NoteAttachments) -> Result<Self, Self::Error> {
85        // Find the first matching attachment. In case of multiple network account target
86        // attachments, we pick the first one as the canonical one.
87        let attachment = attachments
88            .find(NetworkAccountTarget::ATTACHMENT_SCHEME)
89            .ok_or_else(|| NetworkAccountTargetError::MissingAttachmentScheme)?;
90
91        Self::try_from(attachment)
92    }
93}
94impl TryFrom<&NoteAttachment> for NetworkAccountTarget {
95    type Error = NetworkAccountTargetError;
96
97    fn try_from(attachment: &NoteAttachment) -> Result<Self, Self::Error> {
98        if attachment.attachment_scheme() != Self::ATTACHMENT_SCHEME {
99            return Err(NetworkAccountTargetError::AttachmentSchemeMismatch(
100                attachment.attachment_scheme(),
101            ));
102        }
103
104        let words = attachment.content().as_words();
105        if words.len() != 1 {
106            return Err(NetworkAccountTargetError::AttachmentContentNumWordsMismatch(
107                attachment.content().num_words(),
108            ));
109        }
110        let word = words[0];
111
112        let id_suffix = word[0];
113        let id_prefix = word[1];
114        let exec_hint = word[2];
115
116        let target_id = AccountId::try_from_elements(id_suffix, id_prefix)
117            .map_err(NetworkAccountTargetError::DecodeTargetId)?;
118
119        let exec_hint = NoteExecutionHint::try_from(exec_hint.as_canonical_u64())
120            .map_err(NetworkAccountTargetError::DecodeExecutionHint)?;
121
122        NetworkAccountTarget::new(target_id, exec_hint)
123    }
124}
125
126// NETWORK ACCOUNT TARGET ERROR
127// ================================================================================================
128
129#[derive(Debug, thiserror::Error)]
130pub enum NetworkAccountTargetError {
131    #[error("note attachments do not contain a network account target scheme")]
132    MissingAttachmentScheme,
133    #[error("target account ID must have public account type")]
134    TargetNotPublic(AccountId),
135    #[error(
136        "attachment scheme {0} did not match expected type {expected}",
137        expected = NetworkAccountTarget::ATTACHMENT_SCHEME
138    )]
139    AttachmentSchemeMismatch(NoteAttachmentScheme),
140    #[error("network account target expects attachment content with one word, got {0}")]
141    AttachmentContentNumWordsMismatch(u16),
142    #[error("failed to decode target account ID")]
143    DecodeTargetId(#[source] AccountIdError),
144    #[error("failed to decode execution hint")]
145    DecodeExecutionHint(#[source] NoteError),
146    #[error("network note must be public, but was {0:?}")]
147    NoteNotPublic(NoteType),
148}
149
150// TESTS
151// ================================================================================================
152
153#[cfg(test)]
154mod tests {
155    use assert_matches::assert_matches;
156    use miden_protocol::account::AccountType;
157    use miden_protocol::testing::account_id::AccountIdBuilder;
158
159    use super::*;
160
161    #[test]
162    fn network_account_target_serde() -> anyhow::Result<()> {
163        let id = AccountIdBuilder::new()
164            .account_type(AccountType::Public)
165            .build_with_rng(&mut rand::rng());
166        let network_account_target = NetworkAccountTarget::new(id, NoteExecutionHint::Always)?;
167        assert_eq!(
168            network_account_target,
169            NetworkAccountTarget::try_from(&NoteAttachment::from(network_account_target))?
170        );
171
172        Ok(())
173    }
174
175    #[test]
176    fn network_account_target_fails_on_private_target_account() -> anyhow::Result<()> {
177        let id = AccountIdBuilder::new()
178            .account_type(AccountType::Private)
179            .build_with_rng(&mut rand::rng());
180        let err = NetworkAccountTarget::new(id, NoteExecutionHint::Always).unwrap_err();
181
182        assert_matches!(
183            err,
184            NetworkAccountTargetError::TargetNotPublic(account_id) if account_id == id
185        );
186
187        Ok(())
188    }
189}