miden_standards/note/
network_account_target.rs1use alloc::vec::Vec;
2
3use miden_protocol::Word;
4use miden_protocol::account::AccountId;
5use miden_protocol::errors::{AccountIdError, NoteError};
6use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments, NoteType};
7
8use crate::note::{NoteExecutionHint, StandardNoteAttachment};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct NetworkAccountTarget {
25 target_id: AccountId,
26 exec_hint: NoteExecutionHint,
27}
28
29impl NetworkAccountTarget {
30 pub const ATTACHMENT_SCHEME: NoteAttachmentScheme =
35 StandardNoteAttachment::NetworkAccountTarget.attachment_scheme();
36
37 pub fn new(
48 target_id: AccountId,
49 exec_hint: NoteExecutionHint,
50 ) -> Result<Self, NetworkAccountTargetError> {
51 if !target_id.is_public() {
52 return Err(NetworkAccountTargetError::TargetNotPublic(target_id));
53 }
54
55 Ok(Self { target_id, exec_hint })
56 }
57
58 pub(crate) fn ensure_presence(
74 attachments: &mut Vec<NoteAttachment>,
75 target_id: AccountId,
76 ) -> Result<(), NetworkAccountTargetError> {
77 let mut is_present = false;
80 for attachment in attachments
81 .iter()
82 .filter(|attachment| attachment.attachment_scheme() == Self::ATTACHMENT_SCHEME)
83 {
84 let attached_target_id = Self::try_from(attachment)?.target_id();
85 if attached_target_id != target_id {
86 return Err(NetworkAccountTargetError::TargetMismatch {
87 expected: target_id,
88 actual: attached_target_id,
89 });
90 }
91
92 is_present = true;
93 }
94
95 if !is_present {
96 let target = Self::new(target_id, NoteExecutionHint::Always)?;
97 attachments.push(NoteAttachment::from(target));
98 }
99
100 Ok(())
101 }
102
103 pub fn target_id(&self) -> AccountId {
108 self.target_id
109 }
110
111 pub fn execution_hint(&self) -> NoteExecutionHint {
113 self.exec_hint
114 }
115}
116
117impl From<NetworkAccountTarget> for NoteAttachment {
118 fn from(network_attachment: NetworkAccountTarget) -> Self {
119 let mut word = Word::empty();
120 word[0] = network_attachment.target_id.suffix();
121 word[1] = network_attachment.target_id.prefix().as_felt();
122 word[2] = network_attachment.exec_hint.into();
123
124 NoteAttachment::with_word(NetworkAccountTarget::ATTACHMENT_SCHEME, word)
125 }
126}
127
128impl TryFrom<&NoteAttachments> for NetworkAccountTarget {
129 type Error = NetworkAccountTargetError;
130
131 fn try_from(attachments: &NoteAttachments) -> Result<Self, Self::Error> {
132 let attachment = attachments
135 .find(NetworkAccountTarget::ATTACHMENT_SCHEME)
136 .ok_or_else(|| NetworkAccountTargetError::MissingAttachmentScheme)?;
137
138 Self::try_from(attachment)
139 }
140}
141impl TryFrom<&NoteAttachment> for NetworkAccountTarget {
142 type Error = NetworkAccountTargetError;
143
144 fn try_from(attachment: &NoteAttachment) -> Result<Self, Self::Error> {
145 if attachment.attachment_scheme() != Self::ATTACHMENT_SCHEME {
146 return Err(NetworkAccountTargetError::AttachmentSchemeMismatch(
147 attachment.attachment_scheme(),
148 ));
149 }
150
151 let words = attachment.content().as_words();
152 if words.len() != 1 {
153 return Err(NetworkAccountTargetError::AttachmentContentNumWordsMismatch(
154 attachment.content().num_words(),
155 ));
156 }
157 let word = words[0];
158
159 let id_suffix = word[0];
160 let id_prefix = word[1];
161 let exec_hint = word[2];
162
163 let target_id = AccountId::try_from_elements(id_suffix, id_prefix)
164 .map_err(NetworkAccountTargetError::DecodeTargetId)?;
165
166 let exec_hint = NoteExecutionHint::try_from(exec_hint.as_canonical_u64())
167 .map_err(NetworkAccountTargetError::DecodeExecutionHint)?;
168
169 NetworkAccountTarget::new(target_id, exec_hint)
170 }
171}
172
173#[derive(Debug, thiserror::Error)]
177pub enum NetworkAccountTargetError {
178 #[error("note attachments do not contain a network account target scheme")]
179 MissingAttachmentScheme,
180 #[error("target account ID must have public account type")]
181 TargetNotPublic(AccountId),
182 #[error("attached network account target {actual} does not match expected target {expected}")]
183 TargetMismatch { expected: AccountId, actual: AccountId },
184 #[error(
185 "attachment scheme {0} did not match expected type {expected}",
186 expected = NetworkAccountTarget::ATTACHMENT_SCHEME
187 )]
188 AttachmentSchemeMismatch(NoteAttachmentScheme),
189 #[error("network account target expects attachment content with one word, got {0}")]
190 AttachmentContentNumWordsMismatch(u16),
191 #[error("failed to decode target account ID")]
192 DecodeTargetId(#[source] AccountIdError),
193 #[error("failed to decode execution hint")]
194 DecodeExecutionHint(#[source] NoteError),
195 #[error("network note must be public, but was {0:?}")]
196 NoteNotPublic(NoteType),
197}
198
199#[cfg(test)]
203mod tests {
204 use alloc::vec;
205
206 use assert_matches::assert_matches;
207 use miden_protocol::account::AccountType;
208 use miden_protocol::testing::account_id::AccountIdBuilder;
209
210 use super::*;
211
212 fn public_account_id() -> AccountId {
213 AccountIdBuilder::new()
214 .account_type(AccountType::Public)
215 .build_with_rng(&mut rand::rng())
216 }
217
218 #[test]
219 fn network_account_target_serde() -> anyhow::Result<()> {
220 let id = public_account_id();
221 let network_account_target = NetworkAccountTarget::new(id, NoteExecutionHint::Always)?;
222 assert_eq!(
223 network_account_target,
224 NetworkAccountTarget::try_from(&NoteAttachment::from(network_account_target))?
225 );
226
227 Ok(())
228 }
229
230 #[test]
233 fn ensure_presence_keeps_matching_target() -> anyhow::Result<()> {
234 let target_id = public_account_id();
235 let supplied = NetworkAccountTarget::new(target_id, NoteExecutionHint::None)?;
236 let mut attachments = vec![NoteAttachment::from(supplied)];
237
238 NetworkAccountTarget::ensure_presence(&mut attachments, target_id)?;
239
240 assert_eq!(attachments.len(), 1);
241 assert_eq!(NetworkAccountTarget::try_from(&attachments[0])?, supplied);
242
243 Ok(())
244 }
245
246 #[test]
249 fn ensure_presence_rejects_mismatched_target() -> anyhow::Result<()> {
250 let target_id = public_account_id();
251 let other_id = public_account_id();
252 let supplied = NetworkAccountTarget::new(other_id, NoteExecutionHint::Always)?;
253 let mut attachments = vec![NoteAttachment::from(supplied)];
254
255 let err = NetworkAccountTarget::ensure_presence(&mut attachments, target_id).unwrap_err();
256
257 assert_matches!(
258 err,
259 NetworkAccountTargetError::TargetMismatch { expected, actual }
260 if expected == target_id && actual == other_id
261 );
262
263 Ok(())
264 }
265
266 #[test]
268 fn ensure_presence_appends_missing_target() -> anyhow::Result<()> {
269 let target_id = public_account_id();
270 let unrelated =
271 NoteAttachment::with_word(NoteAttachmentScheme::new(64)?, Word::from([7u32, 0, 0, 0]));
272 let mut attachments = vec![unrelated.clone()];
273
274 NetworkAccountTarget::ensure_presence(&mut attachments, target_id)?;
275
276 assert_eq!(
277 attachments,
278 vec![
279 unrelated,
280 NoteAttachment::from(NetworkAccountTarget::new(
281 target_id,
282 NoteExecutionHint::Always
283 )?)
284 ]
285 );
286
287 Ok(())
288 }
289
290 #[test]
291 fn network_account_target_fails_on_private_target_account() -> anyhow::Result<()> {
292 let id = AccountIdBuilder::new()
293 .account_type(AccountType::Private)
294 .build_with_rng(&mut rand::rng());
295 let err = NetworkAccountTarget::new(id, NoteExecutionHint::Always).unwrap_err();
296
297 assert_matches!(
298 err,
299 NetworkAccountTargetError::TargetNotPublic(account_id) if account_id == id
300 );
301
302 Ok(())
303 }
304}