miden_client/pswap/
observer.rs1use alloc::boxed::Box;
5use alloc::sync::Arc;
6use alloc::vec::Vec;
7
8use async_trait::async_trait;
9use miden_protocol::asset::AssetAmount;
10use miden_protocol::note::NoteAttachments;
11use miden_standards::note::{PswapNote, PswapNoteAttachment};
12use tracing::warn;
13
14use crate::ClientError;
15use crate::pswap::discovery::discover_pswap_rounds;
16use crate::pswap::lineage::ObservedPswapNote;
17use crate::rpc::domain::note::CommittedNote;
18use crate::store::Store;
19use crate::sync::NoteObserver;
20use crate::utils::RwLock;
21
22pub struct PswapChainObserver {
33 store: Arc<dyn Store>,
34 chain_note_updates: RwLock<Vec<ObservedPswapNote>>,
39}
40
41impl PswapChainObserver {
42 pub fn new(store: Arc<dyn Store>) -> Self {
43 Self {
44 store,
45 chain_note_updates: RwLock::new(Vec::new()),
46 }
47 }
48}
49
50#[async_trait(?Send)]
51impl NoteObserver for PswapChainObserver {
52 fn name(&self) -> &'static str {
53 "PswapChainObserver"
54 }
55
56 async fn observe(
57 &self,
58 committed_note: &CommittedNote,
59 attachments: Option<&NoteAttachments>,
60 ) -> Result<bool, ClientError> {
61 let Some(attachments) = attachments else {
64 return Ok(false);
65 };
66 let Some(attachment) = extract_pswap_attachment(attachments) else {
67 return Ok(false);
68 };
69
70 let inclusion_proof = committed_note.inclusion_proof().clone();
71 self.chain_note_updates.write().push(ObservedPswapNote {
72 note_id: *committed_note.note_id(),
73 attachment,
74 sender: committed_note.sender(),
75 tag: committed_note.metadata().tag(),
76 block_num: inclusion_proof.location().block_num(),
77 inclusion_proof,
78 });
79 Ok(true)
80 }
81
82 async fn apply(&self, sync_update: &crate::sync::StateSyncUpdate) -> Result<(), ClientError> {
85 let chain_note_updates = core::mem::take(&mut *self.chain_note_updates.write());
86
87 if chain_note_updates.is_empty()
89 && sync_update.note_updates.consumed_note_ids().next().is_none()
90 {
91 return Ok(());
92 }
93
94 let round_updates =
95 discover_pswap_rounds(self.store.clone(), sync_update, &chain_note_updates).await?;
96
97 for round_update in round_updates {
98 if let Err(err) = crate::pswap::store::apply_round(&self.store, &round_update).await {
99 warn!(
100 order_id = round_update.order_id.as_canonical_u64(),
101 round_depth = round_update.round_depth,
102 error = ?err,
103 "apply_round failed; lineage left at previous tip",
104 );
105 }
106 }
107 Ok(())
108 }
109}
110
111fn extract_pswap_attachment(attachments: &NoteAttachments) -> Option<PswapNoteAttachment> {
119 let pswap_attach = attachments.find(PswapNote::PSWAP_ATTACHMENT_SCHEME)?;
120 let word = pswap_attach.content().as_words().first()?;
121
122 let amount = AssetAmount::new(word[0].as_canonical_u64()).ok()?;
123 let order_id = word[1];
124 let depth = u32::try_from(word[2].as_canonical_u64()).ok()?;
125 Some(PswapNoteAttachment::new(amount, order_id, depth))
126}
127
128#[cfg(test)]
133mod tests {
134 use alloc::vec::Vec;
138
139 use miden_protocol::note::{NoteAttachment, NoteAttachmentScheme, NoteAttachments};
140 use miden_protocol::{Felt, Word};
141 use miden_standards::note::PswapNote;
142
143 use super::*;
144
145 fn pswap_word(amount: u64, order_id: u64, depth: u64) -> Word {
147 Word::from([
148 Felt::new(amount).unwrap(),
149 Felt::new(order_id).unwrap(),
150 Felt::new(depth).unwrap(),
151 Felt::new(0).unwrap(),
152 ])
153 }
154
155 fn pswap_attachments(word: Word) -> NoteAttachments {
157 NoteAttachments::from(NoteAttachment::with_word(PswapNote::PSWAP_ATTACHMENT_SCHEME, word))
158 }
159
160 #[test]
162 fn extract_pswap_attachment_reads_wellformed_word() {
163 let parsed = extract_pswap_attachment(&pswap_attachments(pswap_word(25, 0xabcd, 3)))
164 .expect("valid PSWAP word must parse");
165 assert_eq!(u64::from(parsed.amount()), 25);
166 assert_eq!(parsed.order_id().as_canonical_u64(), 0xabcd);
167 assert_eq!(parsed.depth(), 3);
168 }
169
170 #[test]
174 fn extract_pswap_attachment_rejects_missing_scheme() {
175 let empty = NoteAttachments::new(Vec::new()).unwrap();
176 assert!(extract_pswap_attachment(&empty).is_none());
177
178 let other = NoteAttachments::from(NoteAttachment::with_word(
180 NoteAttachmentScheme::new(1).unwrap(),
181 pswap_word(1, 2, 3),
182 ));
183 assert!(extract_pswap_attachment(&other).is_none());
184 }
185
186 #[test]
188 fn extract_pswap_attachment_rejects_oversized_amount() {
189 let word = pswap_word(AssetAmount::MAX.as_u64() + 1, 7, 1);
190 assert!(extract_pswap_attachment(&pswap_attachments(word)).is_none());
191 }
192
193 #[test]
196 fn extract_pswap_attachment_rejects_oversized_depth() {
197 let word = pswap_word(10, 7, u64::from(u32::MAX) + 1);
198 assert!(extract_pswap_attachment(&pswap_attachments(word)).is_none());
199 }
200}