tx3_resolver/inputs/assign/
mod.rs1use std::collections::HashSet;
5
6use crate::job::{QueryResolution, ResolveJob};
7use crate::Error;
8use tx3_tir::model::{
9 assets::CanonicalAssets,
10 core::{Utxo, UtxoRef, UtxoSet},
11};
12
13#[cfg(test)]
14mod tests;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
20enum Specificity {
21 SingleCollateral = 0,
22 Single = 1,
23 ManyCollateral = 2,
24 Many = 3,
25}
26
27impl QueryResolution {
28 fn specificity(&self) -> Specificity {
29 match (self.query.support_many, self.query.collateral) {
30 (false, true) => Specificity::SingleCollateral,
31 (false, false) => Specificity::Single,
32 (true, true) => Specificity::ManyCollateral,
33 (true, false) => Specificity::Many,
34 }
35 }
36
37 fn constraint_tightness(&self) -> (usize, Specificity, &str) {
38 (self.candidates.len(), self.specificity(), &self.name)
39 }
40
41 fn pick(&mut self, used: &HashSet<UtxoRef>) {
42 let available: Vec<Utxo> = self
43 .candidates
44 .drain(..)
45 .filter(|utxo| !used.contains(&utxo.r#ref))
46 .collect();
47
48 let target = self
49 .query
50 .min_amount
51 .clone()
52 .unwrap_or(CanonicalAssets::empty());
53
54 let selection = if self.query.support_many {
55 pick_many(available, &target)
56 } else {
57 pick_single(available, &target)
58 };
59
60 self.selection = Some(selection);
61 }
62}
63
64fn pick_single(candidates: Vec<Utxo>, target: &CanonicalAssets) -> UtxoSet {
66 candidates
67 .into_iter()
68 .find(|utxo| utxo.assets.contains_total(target))
69 .into_iter()
70 .collect()
71}
72
73fn pick_many(candidates: Vec<Utxo>, target: &CanonicalAssets) -> UtxoSet {
76 let mut matched = UtxoSet::new();
77 let mut pending = target.clone();
78
79 for candidate in candidates {
80 if candidate.assets.contains_some(&pending) {
81 matched.insert(candidate.clone());
82 let to_include = candidate.assets.clone();
83 pending = pending - to_include;
84 }
85
86 if pending.is_empty_or_negative() {
87 break;
88 }
89 }
90
91 if !pending.is_empty_or_negative() {
92 return UtxoSet::new();
93 }
94
95 while let Some(utxo) = find_first_excess_utxo(&matched, target) {
96 matched.remove(&utxo);
97 }
98
99 matched
100}
101
102fn find_first_excess_utxo(utxos: &UtxoSet, target: &CanonicalAssets) -> Option<Utxo> {
103 if utxos.len() == 1 {
104 return None;
105 }
106
107 let available = utxos.total_assets();
108
109 let excess = available - target.clone();
110
111 if excess.is_empty_or_negative() {
112 return None;
113 }
114
115 for utxo in utxos.iter_sorted_by_ref() {
116 if excess.contains_total(&utxo.assets) {
117 return Some(utxo.clone());
118 }
119 }
120
121 None
122}
123
124impl ResolveJob {
125 pub fn assign_all(&mut self) -> Result<(), Error> {
129 let mut indices: Vec<usize> = (0..self.input_queries.len()).collect();
131 indices.sort_by(|&a, &b| {
132 self.input_queries[a]
133 .constraint_tightness()
134 .cmp(&self.input_queries[b].constraint_tightness())
135 });
136
137 let mut used: HashSet<UtxoRef> = HashSet::new();
138
139 for idx in indices {
140 self.input_queries[idx].pick(&used);
141
142 if let Some(selection) = &self.input_queries[idx].selection {
143 for utxo in selection.iter() {
144 used.insert(utxo.r#ref.clone());
145 }
146 }
147 }
148
149 let pool_refs = self.pool_refs();
150 for qr in &self.input_queries {
151 qr.ensure_resolved(&pool_refs)?;
152 }
153
154 Ok(())
155 }
156}