use miden_client::note::NoteRelevance;
use miden_objects::account::AccountId;
use miden_objects::asset::Asset;
use miden_objects::note::NoteId;
use super::MultisigClient;
use crate::error::{MultisigError, Result};
#[derive(Debug, Clone)]
pub struct ConsumableNote {
pub id: NoteId,
pub assets: Vec<Asset>,
}
impl ConsumableNote {
pub fn amount_for_faucet(&self, faucet_id: AccountId) -> u64 {
self.assets
.iter()
.filter_map(|asset| match asset {
Asset::Fungible(fungible) if fungible.faucet_id() == faucet_id => {
Some(fungible.amount())
}
_ => None,
})
.sum()
}
pub fn has_faucet(&self, faucet_id: AccountId) -> bool {
self.assets.iter().any(|asset| match asset {
Asset::Fungible(fungible) => fungible.faucet_id() == faucet_id,
Asset::NonFungible(_) => false,
})
}
}
#[derive(Debug, Clone, Default)]
pub struct NoteFilter {
pub faucet_id: Option<AccountId>,
pub min_amount: Option<u64>,
}
impl NoteFilter {
pub fn by_faucet(faucet_id: AccountId) -> Self {
Self {
faucet_id: Some(faucet_id),
min_amount: None,
}
}
pub fn by_faucet_min_amount(faucet_id: AccountId, min_amount: u64) -> Self {
Self {
faucet_id: Some(faucet_id),
min_amount: Some(min_amount),
}
}
pub fn validate(&self) -> Result<()> {
if self.min_amount.is_some() && self.faucet_id.is_none() {
return Err(MultisigError::InvalidFilter(
"min_amount requires faucet_id to be set".to_string(),
));
}
Ok(())
}
}
impl MultisigClient {
pub async fn list_consumable_notes(&mut self) -> Result<Vec<ConsumableNote>> {
let account_id = self.require_account()?.id();
let consumable = self
.miden_client
.get_consumable_notes(Some(account_id))
.await
.map_err(|e| {
MultisigError::MidenClient(format!("failed to get consumable notes: {}", e))
})?;
let notes = consumable
.into_iter()
.filter_map(|(record, relevances)| {
let can_consume_now = relevances
.iter()
.any(|(id, rel)| *id == account_id && matches!(rel, NoteRelevance::Now));
if can_consume_now {
Some(ConsumableNote {
id: record.id(),
assets: record.assets().iter().cloned().collect(),
})
} else {
None
}
})
.collect();
Ok(notes)
}
pub async fn list_committed_notes(&mut self) -> Result<Vec<ConsumableNote>> {
let account_id = self.require_account()?.id();
let notes = self
.miden_client
.get_consumable_notes(Some(account_id))
.await
.map_err(|e| MultisigError::MidenClient(format!("failed to get notes: {}", e)))?;
let result = notes
.into_iter()
.filter(|(_, relevances)| relevances.iter().any(|(id, _)| *id == account_id))
.map(|(record, _)| ConsumableNote {
id: record.id(),
assets: record.assets().iter().cloned().collect(),
})
.collect();
Ok(result)
}
pub async fn list_consumable_notes_filtered(
&mut self,
filter: NoteFilter,
) -> Result<Vec<ConsumableNote>> {
filter.validate()?;
let notes = self.list_consumable_notes().await?;
let filtered = notes
.into_iter()
.filter(|note| {
if let Some(faucet_id) = filter.faucet_id {
if !note.has_faucet(faucet_id) {
return false;
}
if let Some(min) = filter.min_amount
&& note.amount_for_faucet(faucet_id) < min
{
return false;
}
}
true
})
.collect();
Ok(filtered)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_account_id() -> AccountId {
AccountId::from_hex("0x7bfb0f38b0fafa103f86a805594170").unwrap()
}
#[test]
fn test_note_filter_validate_min_amount_without_faucet() {
let filter = NoteFilter {
faucet_id: None,
min_amount: Some(1000),
};
assert!(filter.validate().is_err());
}
#[test]
fn test_note_filter_validate_valid() {
let filter = NoteFilter::default();
assert!(filter.validate().is_ok());
let filter = NoteFilter::by_faucet(test_account_id());
assert!(filter.validate().is_ok());
let filter = NoteFilter::by_faucet_min_amount(test_account_id(), 1000);
assert!(filter.validate().is_ok());
}
#[test]
fn test_consumable_note_empty_assets() {
use miden_objects::Word;
use miden_objects::note::NoteId;
let note = ConsumableNote {
id: NoteId::from(Word::default()),
assets: vec![],
};
assert_eq!(note.amount_for_faucet(test_account_id()), 0);
assert!(!note.has_faucet(test_account_id()));
}
}