miden_multisig_client/client/
notes.rs1use miden_client::note::NoteConsumptionStatus;
7use miden_protocol::account::AccountId;
8use miden_protocol::asset::Asset;
9use miden_protocol::note::NoteId;
10
11use super::MultisigClient;
12use crate::error::{MultisigError, Result};
13
14#[derive(Debug, Clone)]
16pub struct ConsumableNote {
17 pub id: NoteId,
19 pub assets: Vec<Asset>,
21}
22
23impl ConsumableNote {
24 pub fn amount_for_faucet(&self, faucet_id: AccountId) -> u64 {
26 self.assets
27 .iter()
28 .filter_map(|asset| match asset {
29 Asset::Fungible(fungible) if fungible.faucet_id() == faucet_id => {
30 Some(fungible.amount().as_u64())
31 }
32 _ => None,
33 })
34 .sum()
35 }
36
37 pub fn has_faucet(&self, faucet_id: AccountId) -> bool {
39 self.assets.iter().any(|asset| match asset {
40 Asset::Fungible(fungible) => fungible.faucet_id() == faucet_id,
41 Asset::NonFungible(_) => false,
42 })
43 }
44}
45
46#[derive(Debug, Clone, Default)]
53pub struct NoteFilter {
54 pub faucet_id: Option<AccountId>,
56 pub min_amount: Option<u64>,
59}
60
61impl NoteFilter {
62 pub fn by_faucet(faucet_id: AccountId) -> Self {
64 Self {
65 faucet_id: Some(faucet_id),
66 min_amount: None,
67 }
68 }
69
70 pub fn by_faucet_min_amount(faucet_id: AccountId, min_amount: u64) -> Self {
72 Self {
73 faucet_id: Some(faucet_id),
74 min_amount: Some(min_amount),
75 }
76 }
77
78 pub fn validate(&self) -> Result<()> {
83 if self.min_amount.is_some() && self.faucet_id.is_none() {
84 return Err(MultisigError::InvalidFilter(
85 "min_amount requires faucet_id to be set".to_string(),
86 ));
87 }
88 Ok(())
89 }
90}
91
92impl MultisigClient {
93 pub async fn list_consumable_notes(&mut self) -> Result<Vec<ConsumableNote>> {
98 let account_id = self.require_account()?.id();
99
100 let consumable = self
101 .miden_client
102 .get_consumable_notes(Some(account_id))
103 .await
104 .map_err(|e| {
105 MultisigError::MidenClient(format!("failed to get consumable notes: {}", e))
106 })?;
107
108 let notes = consumable
110 .into_iter()
111 .filter_map(|(record, relevances)| {
112 let can_consume_now = relevances.iter().any(|(id, status)| {
114 *id == account_id
115 && matches!(
116 status,
117 NoteConsumptionStatus::Consumable
118 | NoteConsumptionStatus::ConsumableWithAuthorization
119 )
120 });
121 if can_consume_now {
122 record.id().map(|id| ConsumableNote {
123 id,
124 assets: record.assets().iter().cloned().collect(),
125 })
126 } else {
127 None
128 }
129 })
130 .collect();
131
132 Ok(notes)
133 }
134
135 pub async fn list_notes_with_status(
137 &mut self,
138 ) -> Result<Vec<(ConsumableNote, Vec<(AccountId, String)>)>> {
139 let account_id = self.require_account()?.id();
140
141 let notes = self
142 .miden_client
143 .get_consumable_notes(Some(account_id))
144 .await
145 .map_err(|e| MultisigError::MidenClient(format!("failed to get notes: {}", e)))?;
146
147 let result = notes
148 .into_iter()
149 .filter(|(_, relevances)| relevances.iter().any(|(id, _)| *id == account_id))
150 .filter_map(|(record, relevances)| {
151 let note = ConsumableNote {
152 id: record.id()?,
153 assets: record.assets().iter().cloned().collect(),
154 };
155 let statuses: Vec<(AccountId, String)> = relevances
156 .into_iter()
157 .map(|(id, status)| (id, format!("{:?}", status)))
158 .collect();
159 Some((note, statuses))
160 })
161 .collect();
162
163 Ok(result)
164 }
165
166 pub async fn list_committed_notes(&mut self) -> Result<Vec<ConsumableNote>> {
168 let account_id = self.require_account()?.id();
169
170 let notes = self
171 .miden_client
172 .get_consumable_notes(Some(account_id))
173 .await
174 .map_err(|e| MultisigError::MidenClient(format!("failed to get notes: {}", e)))?;
175
176 let result = notes
177 .into_iter()
178 .filter(|(_, relevances)| relevances.iter().any(|(id, _)| *id == account_id))
179 .filter_map(|(record, _)| {
180 Some(ConsumableNote {
181 id: record.id()?,
182 assets: record.assets().iter().cloned().collect(),
183 })
184 })
185 .collect();
186
187 Ok(result)
188 }
189
190 pub async fn list_consumable_notes_filtered(
209 &mut self,
210 filter: NoteFilter,
211 ) -> Result<Vec<ConsumableNote>> {
212 filter.validate()?;
214
215 let notes = self.list_consumable_notes().await?;
216
217 let filtered = notes
218 .into_iter()
219 .filter(|note| {
220 if let Some(faucet_id) = filter.faucet_id {
222 if !note.has_faucet(faucet_id) {
223 return false;
224 }
225 if let Some(min) = filter.min_amount
227 && note.amount_for_faucet(faucet_id) < min
228 {
229 return false;
230 }
231 }
232 true
233 })
234 .collect();
235
236 Ok(filtered)
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 fn test_account_id() -> AccountId {
246 AccountId::from_hex("0x7b7b7b7a7b7b7b017b7b7b7b7b7b7b").unwrap()
247 }
248
249 #[test]
250 fn test_note_filter_validate_min_amount_without_faucet() {
251 let filter = NoteFilter {
252 faucet_id: None,
253 min_amount: Some(1000),
254 };
255 assert!(filter.validate().is_err());
256 }
257
258 #[test]
259 fn test_note_filter_validate_valid() {
260 let filter = NoteFilter::default();
262 assert!(filter.validate().is_ok());
263
264 let filter = NoteFilter::by_faucet(test_account_id());
266 assert!(filter.validate().is_ok());
267
268 let filter = NoteFilter::by_faucet_min_amount(test_account_id(), 1000);
270 assert!(filter.validate().is_ok());
271 }
272
273 #[test]
274 fn test_consumable_note_empty_assets() {
275 use miden_protocol::Word;
277 use miden_protocol::note::NoteId;
278
279 let note = ConsumableNote {
280 id: NoteId::from_raw(Word::default()),
281 assets: vec![],
282 };
283
284 assert_eq!(note.amount_for_faucet(test_account_id()), 0);
285 assert!(!note.has_faucet(test_account_id()));
286 }
287}