tx3_resolver/inputs/
searching.rs1use std::collections::HashSet;
2
3use tx3_lang::{
4 backend::{UtxoPattern, UtxoStore},
5 ir, AssetClass, CanonicalAssets, UtxoRef,
6};
7
8use crate::inputs::CanonicalQuery;
9
10use super::Error;
11
12pub enum Subset {
13 All,
14 Specific(HashSet<UtxoRef>),
15}
16
17impl Subset {
18 fn count(&self) -> Option<usize> {
19 match self {
20 Self::All => None,
21 Self::Specific(s) => Some(s.len()),
22 }
23 }
24
25 #[allow(dead_code)]
26 fn union(a: Self, b: Self) -> Self {
27 match (a, b) {
28 (Self::All, _) => Self::All,
29 (_, Self::All) => Self::All,
30 (Self::Specific(s1), Self::Specific(s2)) => {
31 Self::Specific(s1.union(&s2).cloned().collect())
32 }
33 }
34 }
35
36 fn intersection(a: Self, b: Self) -> Self {
37 match (a, b) {
38 (Self::All, x) => x,
39 (x, Self::All) => x,
40 (Self::Specific(s1), Self::Specific(s2)) => {
41 Self::Specific(s1.intersection(&s2).cloned().collect())
42 }
43 }
44 }
45
46 fn intersection_of_all<const N: usize>(subsets: [Self; N]) -> Self {
47 let mut result = Subset::All;
48
49 for subset in subsets {
50 result = Self::intersection(result, subset);
51 }
52
53 result
54 }
55
56 fn is_empty(&self) -> bool {
57 match self {
58 Self::All => false,
59 Self::Specific(s) => s.is_empty(),
60 }
61 }
62}
63
64impl From<HashSet<UtxoRef>> for Subset {
65 fn from(value: HashSet<UtxoRef>) -> Self {
66 Self::Specific(value)
67 }
68}
69
70#[derive(Debug, Clone)]
71pub struct SearchSpace {
72 pub matched: HashSet<UtxoRef>,
73 pub by_address_count: Option<usize>,
74 pub by_asset_class_count: Option<usize>,
75 pub by_ref_count: Option<usize>,
76}
77
78impl std::fmt::Display for SearchSpace {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 write!(f, "SearchSpace {{")?;
81 write!(f, "matched:{}, ", self.matched.len())?;
82
83 for (i, ref_) in self.matched.iter().enumerate().take(10) {
84 write!(f, "match[{}]: {}, ", i, ref_)?;
85 }
86
87 if self.matched.len() > 10 {
88 write!(f, "... {} more, ", self.matched.len() - 10)?;
89 }
90
91 write!(f, "by_address_count: {:?}, ", self.by_address_count)?;
92 write!(f, "by_asset_class_count: {:?}, ", self.by_asset_class_count)?;
93 write!(f, "by_ref_count: {:?}, ", self.by_ref_count)?;
94 write!(f, "}}")?;
95 Ok(())
96 }
97}
98
99async fn narrow_by_address<T: UtxoStore>(store: &T, address: &[u8]) -> Result<Subset, Error> {
100 let utxos = store.narrow_refs(UtxoPattern::by_address(address)).await?;
101
102 Ok(Subset::Specific(utxos.into_iter().collect()))
103}
104
105async fn narrow_by_asset_class<T: UtxoStore>(
106 store: &T,
107 assets: &AssetClass,
108) -> Result<Subset, Error> {
109 if matches!(assets, AssetClass::Naked) {
111 return Ok(Subset::All);
112 }
113
114 let AssetClass::Defined(policy, name) = assets else {
115 return Ok(Subset::All);
116 };
117
118 let utxos = store
119 .narrow_refs(UtxoPattern::by_asset(policy, name))
120 .await?;
121
122 Ok(Subset::Specific(utxos.into_iter().collect()))
123}
124
125async fn narrow_by_multi_asset_presence<T: UtxoStore>(
126 store: &T,
127 assets: &CanonicalAssets,
128) -> Result<Subset, Error> {
129 let mut matches = Subset::All;
130
131 for (class, amount) in assets.iter() {
132 if *amount > 0 {
133 let next = narrow_by_asset_class(store, class).await?;
134 matches = Subset::intersection(matches, next);
135 }
136 }
137
138 Ok(matches)
139}
140
141pub async fn narrow_search_space<T: UtxoStore>(
142 store: &T,
143 criteria: &CanonicalQuery,
144 max_size: usize,
145) -> Result<SearchSpace, Error> {
146 let matching_address = if let Some(address) = &criteria.address {
147 narrow_by_address(store, address).await?
148 } else {
149 Subset::All
150 };
151
152 let matching_assets = if let Some(min_amount) = &criteria.min_amount {
153 narrow_by_multi_asset_presence(store, min_amount).await?
154 } else {
155 Subset::All
156 };
157
158 let matching_refs = if !criteria.refs.is_empty() {
159 Subset::Specific(criteria.refs.clone())
160 } else {
161 Subset::All
162 };
163
164 let by_address_count = matching_address.count();
165 let by_asset_class_count = matching_assets.count();
166 let by_ref_count = matching_refs.count();
167
168 let subset = Subset::intersection_of_all([matching_address, matching_assets, matching_refs]);
169
170 let mut refs = match subset {
171 Subset::Specific(refs) => refs,
172 Subset::All => return Err(Error::InputQueryTooBroad),
173 };
174
175 if refs.len() > max_size {
176 let first_n = refs.into_iter().take(max_size);
177 refs = HashSet::from_iter(first_n);
178 }
179
180 Ok(SearchSpace {
181 matched: refs,
182 by_address_count,
183 by_asset_class_count,
184 by_ref_count,
185 })
186}