use std::cmp::Reverse;
use std::collections::HashMap;
use std::sync::Arc;
use miden_protocol::Word;
use miden_protocol::account::Account;
use miden_protocol::asset::{Asset, AssetAmount};
use miden_protocol::block::BlockHeader;
use miden_protocol::note::{Note, NoteId, Nullifier};
use miden_protocol::transaction::PartialBlockchain;
use miden_standards::note::AccountTargetNetworkNote;
#[derive(Clone, Debug)]
pub struct SponsoredFeatureNote {
pub feature: AccountTargetNetworkNote,
pub sponsorships: Vec<Note>,
}
impl SponsoredFeatureNote {
pub fn num_notes(&self) -> usize {
1 + self.sponsorships.len()
}
pub fn retain_sponsorships_for_fee_asset(&mut self, fee_asset_id: Word) {
self.sponsorships.retain(|note| {
note.assets()
.as_slice()
.first()
.is_some_and(|asset| asset.id().to_word() == fee_asset_id)
});
}
pub fn sort_sponsorships_by_amount(&mut self) {
self.sponsorships.sort_by_key(|note| Reverse(sponsorship_amount(note)));
}
}
fn sponsorship_amount(note: &Note) -> AssetAmount {
match note.assets().as_slice().first() {
Some(Asset::Fungible(asset)) => asset.amount(),
_ => AssetAmount::ZERO,
}
}
#[derive(Clone, Debug)]
pub struct TransactionCandidate {
pub account: Arc<Account>,
pub notes: Vec<SponsoredFeatureNote>,
pub chain_tip_header: BlockHeader,
pub chain_mmr: Arc<PartialBlockchain>,
}
impl TransactionCandidate {
pub fn num_notes(&self) -> usize {
self.notes.iter().map(SponsoredFeatureNote::num_notes).sum()
}
pub fn sponsor_to_feature_nullifier(&self) -> HashMap<NoteId, Nullifier> {
self.notes
.iter()
.flat_map(|sponsored| {
let feature_nullifier = sponsored.feature.as_note().nullifier();
sponsored
.sponsorships
.iter()
.map(move |sponsorship| (sponsorship.id(), feature_nullifier))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{
mock_network_account_id,
mock_single_target_note,
mock_sponsorship_note,
mock_sponsorship_note_with_amount,
};
fn sponsored(feature_seed: u8, num_sponsorships: u8) -> SponsoredFeatureNote {
let account_id = mock_network_account_id();
let feature = mock_single_target_note(account_id, feature_seed);
let sponsorships = (0..num_sponsorships)
.map(|i| {
mock_sponsorship_note(account_id, feature.as_note().id(), feature_seed + 100 + i)
})
.collect();
SponsoredFeatureNote { feature, sponsorships }
}
#[test]
fn sponsor_to_feature_nullifier_covers_sponsorships_only() {
let sponsored_notes = [sponsored(1, 2), sponsored(2, 0)];
let chain_mmr = PartialBlockchain::new(
miden_protocol::crypto::merkle::mmr::PartialMmr::from_peaks(
miden_protocol::crypto::merkle::mmr::MmrPeaks::new(
miden_protocol::crypto::merkle::mmr::Forest::new(0).unwrap(),
vec![],
)
.unwrap(),
),
[],
)
.unwrap();
let candidate = TransactionCandidate {
account: Arc::new(crate::test_utils::mock_account(mock_network_account_id())),
notes: sponsored_notes.to_vec(),
chain_tip_header: crate::test_utils::mock_block_header(0_u32.into()),
chain_mmr: Arc::new(chain_mmr),
};
let map = candidate.sponsor_to_feature_nullifier();
assert_eq!(map.len(), 2);
let feature_nullifier = sponsored_notes[0].feature.as_note().nullifier();
for sponsorship in &sponsored_notes[0].sponsorships {
assert_eq!(map[&sponsorship.id()], feature_nullifier);
}
assert_eq!(candidate.num_notes(), 4);
}
#[test]
fn sort_sponsorships_by_amount_orders_largest_first() {
let account_id = mock_network_account_id();
let feature = mock_single_target_note(account_id, 1);
let feature_id = feature.as_note().id();
let sponsorships = vec![
mock_sponsorship_note_with_amount(account_id, feature_id, 2, 5),
mock_sponsorship_note_with_amount(account_id, feature_id, 3, 500),
mock_sponsorship_note_with_amount(account_id, feature_id, 4, 50),
];
let mut sponsored = SponsoredFeatureNote { feature, sponsorships };
sponsored.sort_sponsorships_by_amount();
let amounts = sponsored
.sponsorships
.iter()
.map(|note| sponsorship_amount(note).as_u64())
.collect::<Vec<_>>();
assert_eq!(amounts, [500, 50, 5]);
}
}