1use std::collections::{BTreeMap, HashSet};
4use tx3_lang::{
5 applying,
6 backend::{UtxoPattern, UtxoStore},
7 ir, CanonicalAssets, Utxo, UtxoRef, UtxoSet,
8};
9
10use crate::Error;
11
12macro_rules! data_or_bail {
13 ($expr:expr, bytes) => {
14 $expr
15 .as_bytes()
16 .ok_or(Error::ExpectedData("bytes".to_string(), $expr.clone()))?
17 };
18
19 ($expr:expr, number) => {
20 $expr
21 .as_number()
22 .ok_or(Error::ExpectedData("number".to_string(), $expr.clone()))?
23 };
24
25 ($expr:expr, assets) => {
26 $expr
27 .as_assets()
28 .ok_or(Error::ExpectedData("assets".to_string(), $expr.clone()))?
29 };
30
31 ($expr:expr, utxo_refs) => {
32 $expr
33 .as_utxo_refs()
34 .ok_or(Error::ExpectedData("utxo refs".to_string(), $expr.clone()))?
35 };
36}
37
38enum Subset {
39 All,
40 Specific(HashSet<UtxoRef>),
41}
42
43impl Subset {
44 #[allow(dead_code)]
45 fn union(a: Self, b: Self) -> Self {
46 match (a, b) {
47 (Self::All, _) => Self::All,
48 (_, Self::All) => Self::All,
49 (Self::Specific(s1), Self::Specific(s2)) => {
50 Self::Specific(s1.union(&s2).cloned().collect())
51 }
52 }
53 }
54
55 fn intersection(a: Self, b: Self) -> Self {
56 match (a, b) {
57 (Self::All, x) => x,
58 (x, Self::All) => x,
59 (Self::Specific(s1), Self::Specific(s2)) => {
60 Self::Specific(s1.intersection(&s2).cloned().collect())
61 }
62 }
63 }
64
65 fn intersection_of_all<const N: usize>(subsets: [Self; N]) -> Self {
66 let mut result = Subset::All;
67
68 for subset in subsets {
69 result = Self::intersection(result, subset);
70 }
71
72 result
73 }
74
75 fn is_empty(&self) -> bool {
76 match self {
77 Self::All => false,
78 Self::Specific(s) => s.is_empty(),
79 }
80 }
81}
82
83impl From<HashSet<UtxoRef>> for Subset {
84 fn from(value: HashSet<UtxoRef>) -> Self {
85 Self::Specific(value)
86 }
87}
88
89pub trait CoinSelection {
90 fn pick(search_space: UtxoSet, target: &CanonicalAssets) -> HashSet<Utxo>;
91}
92
93struct FirstFullMatch;
94
95impl CoinSelection for FirstFullMatch {
96 fn pick(search_space: UtxoSet, target: &CanonicalAssets) -> HashSet<Utxo> {
97 for utxo in search_space.iter() {
98 if utxo.assets.contains_total(target) {
99 return HashSet::from_iter(vec![utxo.clone()]);
100 }
101 }
102
103 HashSet::new()
104 }
105}
106
107struct NaiveAccumulator;
108
109impl CoinSelection for NaiveAccumulator {
110 fn pick(search_space: UtxoSet, target: &CanonicalAssets) -> HashSet<Utxo> {
111 let mut matched = HashSet::new();
112 let mut required = target.clone();
113
114 for utxo in search_space.iter() {
115 if utxo.assets.contains_some(target) {
116 matched.insert(utxo.clone());
117 required = required - utxo.assets.clone();
118 }
119
120 if required.is_empty_or_negative() {
121 return matched;
122 }
123 }
124
125 HashSet::new()
128 }
129}
130
131struct CollateralMatch;
132
133impl CoinSelection for CollateralMatch {
134 fn pick(search_space: UtxoSet, target: &CanonicalAssets) -> HashSet<Utxo> {
135 for utxo in search_space.iter() {
136 let only_naked = utxo.assets.keys().all(|x| x.is_naked());
137 let has_enough = utxo.assets.contains_total(target);
138
139 if only_naked && has_enough {
140 return HashSet::from_iter(vec![utxo.clone()]);
141 }
142 }
143
144 HashSet::new()
145 }
146}
147
148const MAX_SEARCH_SPACE_SIZE: usize = 50;
149
150struct InputSelector<'a, S: UtxoStore> {
151 store: &'a S,
152}
153
154impl<'a, S: UtxoStore> InputSelector<'a, S> {
155 pub fn new(store: &'a S) -> Self {
156 Self { store }
157 }
158
159 async fn narrow_by_address(&self, expr: &ir::Expression) -> Result<Subset, Error> {
160 let address = data_or_bail!(expr, bytes);
161
162 let utxos = self
163 .store
164 .narrow_refs(UtxoPattern::by_address(address))
165 .await?;
166
167 Ok(Subset::Specific(utxos.into_iter().collect()))
168 }
169
170 async fn narrow_by_asset_presence(&self, expr: &ir::AssetExpr) -> Result<Subset, Error> {
171 let amount = data_or_bail!(&expr.amount, number);
172
173 if amount == 0 {
175 return Ok(Subset::All);
176 }
177
178 if expr.policy.is_none() {
180 return Ok(Subset::All);
181 }
182
183 let policy_bytes = data_or_bail!(&expr.policy, bytes);
184 let name_bytes = data_or_bail!(&expr.asset_name, bytes);
185
186 let utxos = self
187 .store
188 .narrow_refs(UtxoPattern::by_asset(policy_bytes, name_bytes))
189 .await?;
190
191 Ok(Subset::Specific(utxos.into_iter().collect()))
192 }
193
194 async fn narrow_by_multi_asset_presence(&self, expr: &ir::Expression) -> Result<Subset, Error> {
195 let assets = data_or_bail!(expr, assets);
196
197 let mut matches = Subset::All;
198
199 for asset in assets {
200 let next = self.narrow_by_asset_presence(asset).await?;
201 matches = Subset::intersection(matches, next);
202 }
203
204 Ok(matches)
205 }
206
207 fn narrow_by_ref(&self, expr: &ir::Expression) -> Result<Subset, Error> {
208 let refs = data_or_bail!(expr, utxo_refs);
209
210 let refs = HashSet::from_iter(refs.iter().cloned());
211
212 Ok(Subset::Specific(refs))
213 }
214
215 async fn narrow_search_space(&self, criteria: &ir::InputQuery) -> Result<Subset, Error> {
216 let matching_address = if let Some(address) = &criteria.address.as_option() {
217 self.narrow_by_address(address).await?
218 } else {
219 Subset::All
220 };
221
222 if matching_address.is_empty() {
223 }
225
226 let matching_assets = if let Some(min_amount) = &criteria.min_amount.as_option() {
227 self.narrow_by_multi_asset_presence(min_amount).await?
228 } else {
229 Subset::All
230 };
231
232 if matching_assets.is_empty() {
233 }
235
236 let matching_refs = if let Some(refs) = &criteria.r#ref.as_option() {
237 self.narrow_by_ref(refs)?
238 } else {
239 Subset::All
240 };
241
242 if matching_refs.is_empty() {
243 }
245
246 Ok(Subset::intersection_of_all([
247 matching_address,
248 matching_assets,
249 matching_refs,
250 ]))
251 }
252
253 pub async fn select(&self, criteria: &ir::InputQuery) -> Result<UtxoSet, Error> {
254 let search_space = self.narrow_search_space(criteria).await?;
255
256 let refs = match search_space {
257 Subset::Specific(refs) if refs.len() <= MAX_SEARCH_SPACE_SIZE => refs,
258 Subset::Specific(_) => return Err(Error::InputQueryTooBroad),
259 Subset::All => return Err(Error::InputQueryTooBroad),
260 };
261
262 let refs = refs.into_iter().collect();
263
264 let utxos = self.store.fetch_utxos(refs).await?;
265
266 let target = data_or_bail!(&criteria.min_amount, assets);
267 let target = CanonicalAssets::from(Vec::from(target));
268
269 let matched = if criteria.collateral {
270 CollateralMatch::pick(utxos, &target)
271 } else if criteria.many {
272 NaiveAccumulator::pick(utxos, &target)
273 } else {
274 FirstFullMatch::pick(utxos, &target)
275 };
276
277 Ok(matched)
278 }
279}
280
281pub async fn resolve<T: UtxoStore>(tx: ir::Tx, utxos: &T) -> Result<ir::Tx, Error> {
282 let mut all_inputs = BTreeMap::new();
283
284 for (name, query) in applying::find_queries(&tx) {
285 let selector = InputSelector::new(utxos);
286
287 let utxos = selector.select(&query).await?;
288
289 if utxos.is_empty() {
290 return Err(Error::InputNotResolved(name, query));
291 }
292
293 all_inputs.insert(name, utxos);
294 }
295
296 let out = applying::apply_inputs(tx, &all_inputs)?;
297
298 Ok(out)
299}
300
301#[cfg(test)]
302mod tests {
303 use crate::mock;
304
305 use super::*;
306
307 pub fn new_input_query(
308 address: &mock::KnownAddress,
309 naked_amount: Option<u64>,
310 other_assets: Vec<(mock::KnownAsset, u64)>,
311 many: bool,
312 collateral: bool,
313 ) -> ir::InputQuery {
314 let naked_asset = naked_amount.map(|x| ir::AssetExpr {
315 policy: ir::Expression::None,
316 asset_name: ir::Expression::None,
317 amount: ir::Expression::Number(x as i128),
318 });
319
320 let other_assets: Vec<ir::AssetExpr> = other_assets
321 .into_iter()
322 .map(|(asset, amount)| ir::AssetExpr {
323 policy: ir::Expression::Bytes(asset.policy().as_slice().to_vec()),
324 asset_name: ir::Expression::Bytes(asset.name().to_vec()),
325 amount: ir::Expression::Number(amount as i128),
326 })
327 .collect();
328
329 let all_assets = naked_asset.into_iter().chain(other_assets).collect();
330
331 ir::InputQuery {
332 address: ir::Expression::Address(address.to_bytes()),
333 min_amount: ir::Expression::Assets(all_assets),
334 r#ref: ir::Expression::None,
335 many,
336 collateral,
337 }
338 }
339
340 #[pollster::test]
341 async fn test_select_by_address() {
342 let store = mock::seed_random_memory_store(
343 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, _: u64| {
344 mock::utxo_with_random_amount(x, 4_000_000..5_000_000)
345 },
346 );
347
348 let selector = InputSelector::new(&store);
349
350 for subject in mock::KnownAddress::everyone() {
351 let criteria = new_input_query(&subject, None, vec![], false, false);
352
353 let utxos = selector.select(&criteria).await.unwrap();
354
355 assert_eq!(utxos.len(), 1);
356
357 for utxo in utxos {
358 assert_eq!(utxo.address, subject.to_bytes());
359 }
360 }
361 }
362
363 #[pollster::test]
364 async fn test_select_by_naked_amount() {
365 let store = mock::seed_random_memory_store(
366 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, _: u64| {
367 mock::utxo_with_random_amount(x, 4_000_000..5_000_000)
368 },
369 );
370
371 let selector = InputSelector::new(&store);
372
373 let criteria = new_input_query(
374 &mock::KnownAddress::Alice,
375 Some(6_000_000),
376 vec![],
377 false,
378 false,
379 );
380
381 let utxos = selector.select(&criteria).await.unwrap();
382 assert!(utxos.is_empty());
383
384 let criteria = new_input_query(
385 &mock::KnownAddress::Alice,
386 Some(4_000_000),
387 vec![],
388 false,
389 false,
390 );
391
392 let utxos = selector.select(&criteria).await.unwrap();
393
394 let match_count = dbg!(utxos.len());
395 assert_eq!(match_count, 1);
396 }
397
398 #[pollster::test]
399 async fn test_select_by_asset_amount() {
400 let store = mock::seed_random_memory_store(
401 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, _: u64| {
402 mock::utxo_with_random_asset(x, mock::KnownAsset::Hosky, 500..1000)
403 },
404 );
405
406 let selector = InputSelector::new(&store);
407
408 for address in mock::KnownAddress::everyone() {
409 let criteria = new_input_query(
413 &address,
414 None,
415 vec![(mock::KnownAsset::Hosky, 1001)],
416 false,
417 false,
418 );
419
420 let utxos = selector.select(&criteria).await.unwrap();
421 assert!(utxos.is_empty());
422
423 let criteria = new_input_query(
427 &address,
428 None,
429 vec![(mock::KnownAsset::Hosky, 1001)],
430 true,
431 false,
432 );
433
434 let utxos = selector.select(&criteria).await.unwrap();
435 assert!(utxos.len() > 1);
436
437 let criteria = new_input_query(
441 &address,
442 None,
443 vec![(mock::KnownAsset::Hosky, 4001)],
444 true,
445 false,
446 );
447
448 let utxos = selector.select(&criteria).await.unwrap();
449 assert!(utxos.is_empty());
450
451 let criteria = new_input_query(
454 &address,
455 None,
456 vec![(mock::KnownAsset::Snek, 500)],
457 false,
458 false,
459 );
460
461 let utxos = selector.select(&criteria).await.unwrap();
462 assert!(utxos.is_empty());
463
464 let criteria = new_input_query(
467 &address,
468 None,
469 vec![(mock::KnownAsset::Hosky, 500)],
470 false,
471 false,
472 );
473
474 let utxos = selector.select(&criteria).await.unwrap();
475 assert_eq!(utxos.len(), 1);
476 }
477 }
478
479 #[pollster::test]
480 async fn test_select_by_collateral() {
481 let store = mock::seed_random_memory_store(
482 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, sequence: u64| {
483 if sequence % 2 == 0 {
484 mock::utxo_with_random_amount(x, 4_000_000..5_000_000)
485 } else {
486 mock::utxo_with_random_asset(x, mock::KnownAsset::Hosky, 500..1000)
487 }
488 },
489 );
490
491 let selector = InputSelector::new(&store);
492
493 for address in mock::KnownAddress::everyone() {
494 let criteria = new_input_query(&address, Some(1_000_000), vec![], false, true);
495
496 let utxos = selector.select(&criteria).await.unwrap();
497
498 assert_eq!(utxos.len(), 1);
499 let utxo = utxos.iter().next().unwrap();
500 assert_eq!(utxo.assets.keys().len(), 1);
501 assert_eq!(utxo.assets.keys().next().unwrap().is_naked(), true);
502 }
503 }
504}