use std::collections::HashSet;
use crate::job::{QueryResolution, ResolveJob};
use crate::Error;
use tx3_tir::model::{
assets::CanonicalAssets,
core::{Utxo, UtxoRef, UtxoSet},
};
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Specificity {
SingleCollateral = 0,
Single = 1,
ManyCollateral = 2,
Many = 3,
}
impl QueryResolution {
fn specificity(&self) -> Specificity {
match (self.query.support_many, self.query.collateral) {
(false, true) => Specificity::SingleCollateral,
(false, false) => Specificity::Single,
(true, true) => Specificity::ManyCollateral,
(true, false) => Specificity::Many,
}
}
fn constraint_tightness(&self) -> (usize, Specificity, &str) {
(self.candidates.len(), self.specificity(), &self.name)
}
fn pick(&mut self, used: &HashSet<UtxoRef>) {
let available: Vec<Utxo> = self
.candidates
.drain(..)
.filter(|utxo| !used.contains(&utxo.r#ref))
.collect();
let target = self
.query
.min_amount
.clone()
.unwrap_or(CanonicalAssets::empty());
let selection = if self.query.support_many {
pick_many(available, &target)
} else {
pick_single(available, &target)
};
self.selection = Some(selection);
}
}
fn pick_single(candidates: Vec<Utxo>, target: &CanonicalAssets) -> UtxoSet {
candidates
.into_iter()
.find(|utxo| utxo.assets.contains_total(target))
.into_iter()
.collect()
}
fn pick_many(candidates: Vec<Utxo>, target: &CanonicalAssets) -> UtxoSet {
let mut matched = UtxoSet::new();
let mut pending = target.clone();
for candidate in candidates {
if candidate.assets.contains_some(&pending) {
matched.insert(candidate.clone());
let to_include = candidate.assets.clone();
pending = pending - to_include;
}
if pending.is_empty_or_negative() {
break;
}
}
if !pending.is_empty_or_negative() {
return UtxoSet::new();
}
while let Some(utxo) = find_first_excess_utxo(&matched, target) {
matched.remove(&utxo);
}
matched
}
fn find_first_excess_utxo(utxos: &UtxoSet, target: &CanonicalAssets) -> Option<Utxo> {
if utxos.len() == 1 {
return None;
}
let available = utxos.total_assets();
let excess = available - target.clone();
if excess.is_empty_or_negative() {
return None;
}
for utxo in utxos.iter_sorted_by_ref() {
if excess.contains_total(&utxo.assets) {
return Some(utxo.clone());
}
}
None
}
impl ResolveJob {
pub fn assign_all(&mut self) -> Result<(), Error> {
let mut indices: Vec<usize> = (0..self.input_queries.len()).collect();
indices.sort_by(|&a, &b| {
self.input_queries[a]
.constraint_tightness()
.cmp(&self.input_queries[b].constraint_tightness())
});
let mut used: HashSet<UtxoRef> = HashSet::new();
for idx in indices {
self.input_queries[idx].pick(&used);
if let Some(selection) = &self.input_queries[idx].selection {
for utxo in selection.iter() {
used.insert(utxo.r#ref.clone());
}
}
}
let pool_refs = self.pool_refs();
for qr in &self.input_queries {
qr.ensure_resolved(&pool_refs)?;
}
Ok(())
}
}