1use std::collections::{BTreeMap, HashSet};
4use tx3_lang::{
5 applying,
6 backend::{self, UtxoStore},
7 ir, CanonicalAssets, Utxo, UtxoRef, UtxoSet,
8};
9
10pub use crate::inputs::searching::SearchSpace;
11
12mod searching;
13
14macro_rules! data_or_bail {
15 ($expr:expr, bytes) => {
16 $expr
17 .as_bytes()
18 .ok_or(Error::ExpectedData("bytes".to_string(), $expr.clone()))
19 };
20
21 ($expr:expr, number) => {
22 $expr
23 .as_number()
24 .ok_or(Error::ExpectedData("number".to_string(), $expr.clone()))?
25 };
26
27 ($expr:expr, assets) => {
28 $expr
29 .as_assets()
30 .ok_or(Error::ExpectedData("assets".to_string(), $expr.clone()))
31 };
32
33 ($expr:expr, utxo_refs) => {
34 $expr
35 .as_utxo_refs()
36 .ok_or(Error::ExpectedData("utxo refs".to_string(), $expr.clone()))
37 };
38}
39
40pub struct Diagnostic {
41 pub query: ir::InputQuery,
42 pub utxos: UtxoSet,
43 pub selected: UtxoSet,
44}
45
46#[derive(thiserror::Error, Debug)]
47pub enum Error {
48 #[error("expected {0}, got {1:?}")]
49 ExpectedData(String, ir::Expression),
50
51 #[error("input query too broad")]
52 InputQueryTooBroad,
53
54 #[error("input not resolved: {0} {1} {2}")]
55 InputNotResolved(String, CanonicalQuery, SearchSpace),
56
57 #[error("store error: {0}")]
58 StoreError(#[from] backend::Error),
59
60 #[error("apply error: {0}")]
61 ApplyError(#[from] applying::Error),
62}
63
64pub trait CoinSelection {
65 fn pick(search_space: UtxoSet, target: &CanonicalAssets) -> HashSet<Utxo>;
66}
67
68struct FirstFullMatch;
69
70impl CoinSelection for FirstFullMatch {
71 fn pick(search_space: UtxoSet, target: &CanonicalAssets) -> HashSet<Utxo> {
72 for utxo in search_space.iter() {
73 if utxo.assets.contains_total(target) {
74 return HashSet::from_iter(vec![utxo.clone()]);
75 }
76 }
77
78 HashSet::new()
79 }
80}
81
82fn find_first_excess_utxo(utxos: &HashSet<Utxo>, target: &CanonicalAssets) -> Option<Utxo> {
83 if utxos.len() == 1 {
86 return None;
87 }
88
89 let available = utxos
90 .iter()
91 .fold(CanonicalAssets::empty(), |acc, x| acc + x.assets.clone());
92
93 let excess = available - target.clone();
94
95 if excess.is_empty_or_negative() {
96 return None;
97 }
98
99 for utxo in utxos.iter() {
100 if excess.contains_total(&utxo.assets) {
101 return Some(utxo.clone());
102 }
103 }
104
105 None
106}
107
108struct NaiveAccumulator;
109
110impl CoinSelection for NaiveAccumulator {
111 fn pick(search_space: UtxoSet, target: &CanonicalAssets) -> HashSet<Utxo> {
112 let mut matched = HashSet::new();
113 let mut pending = target.clone();
114
115 for utxo in search_space.iter() {
116 if utxo.assets.contains_some(target) {
117 matched.insert(utxo.clone());
118 pending = pending - utxo.assets.clone();
119 }
120
121 if pending.is_empty_or_negative() {
122 break;
124 }
125 }
126
127 if !pending.is_empty_or_negative() {
128 return HashSet::new();
131 }
132
133 while let Some(utxo) = find_first_excess_utxo(&matched, target) {
134 matched.remove(&utxo);
135 }
136
137 matched
138 }
139}
140
141const MAX_SEARCH_SPACE_SIZE: usize = 50;
142
143struct InputSelector<'a, S: UtxoStore> {
144 store: &'a S,
145 ignore: HashSet<UtxoRef>,
146 ignore_collateral: HashSet<UtxoRef>,
147}
148
149impl<'a, S: UtxoStore> InputSelector<'a, S> {
150 pub fn new(store: &'a S) -> Self {
151 Self {
152 store,
153 ignore: HashSet::new(),
154 ignore_collateral: HashSet::new(),
155 }
156 }
157
158 fn pick_from_set(utxos: UtxoSet, criteria: &CanonicalQuery) -> UtxoSet {
159 let target = criteria
160 .min_amount
161 .clone()
162 .unwrap_or(CanonicalAssets::empty());
163
164 if criteria.support_many {
165 NaiveAccumulator::pick(utxos, &target)
166 } else {
167 FirstFullMatch::pick(utxos, &target)
168 }
169 }
170
171 pub async fn select_collateral(
172 &mut self,
173 search_space: &SearchSpace,
174 criteria: &CanonicalQuery,
175 ) -> Result<UtxoSet, Error> {
176 let refs = search_space
177 .matched
178 .clone()
179 .into_iter()
180 .filter(|x| !self.ignore_collateral.contains(x))
181 .collect();
182
183 let utxos = self.store.fetch_utxos(refs).await?;
184
185 let utxos = utxos
191 .into_iter()
192 .filter(|x| x.assets.is_only_naked())
193 .collect();
194
195 let matched = Self::pick_from_set(utxos, criteria);
196
197 self.ignore_collateral
198 .extend(matched.iter().map(|x| x.r#ref.clone()));
199
200 Ok(matched)
201 }
202
203 pub async fn select_input(
204 &mut self,
205 search_space: &SearchSpace,
206 criteria: &CanonicalQuery,
207 ) -> Result<UtxoSet, Error> {
208 let refs = search_space
209 .matched
210 .clone()
211 .into_iter()
212 .filter(|x| !self.ignore.contains(x))
213 .collect();
214
215 let utxos = self.store.fetch_utxos(refs).await?;
216
217 let matched = Self::pick_from_set(utxos, criteria);
218
219 self.ignore.extend(matched.iter().map(|x| x.r#ref.clone()));
220
221 Ok(matched)
222 }
223
224 pub async fn select(
225 &mut self,
226 search_space: &SearchSpace,
227 criteria: &CanonicalQuery,
228 ) -> Result<UtxoSet, Error> {
229 if criteria.collateral {
230 self.select_collateral(search_space, criteria).await
231 } else {
232 self.select_input(search_space, criteria).await
233 }
234 }
235}
236
237#[derive(Debug, Clone)]
238pub struct CanonicalQuery {
239 pub address: Option<Vec<u8>>,
240 pub min_amount: Option<CanonicalAssets>,
241 pub refs: HashSet<UtxoRef>,
242 pub support_many: bool,
243 pub collateral: bool,
244}
245
246impl std::fmt::Display for CanonicalQuery {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 write!(f, "CanonicalQuery {{")?;
249
250 if let Some(address) = &self.address {
251 write!(f, "address: {}", hex::encode(address))?;
252 }
253
254 if let Some(min_amount) = &self.min_amount {
255 write!(f, "min_amount: {}", min_amount)?;
256 }
257
258 for (i, ref_) in self.refs.iter().enumerate() {
259 write!(f, "ref[{}]:{}#{}", i, hex::encode(&ref_.txid), ref_.index)?;
260 }
261
262 write!(f, "support_many: {:?}", self.support_many)?;
263 write!(f, "for_collateral: {:?}", self.collateral)?;
264 write!(f, "}}")
265 }
266}
267
268impl TryFrom<ir::InputQuery> for CanonicalQuery {
269 type Error = Error;
270
271 fn try_from(query: ir::InputQuery) -> Result<Self, Self::Error> {
272 let address = query
273 .address
274 .as_option()
275 .map(|x| data_or_bail!(x, bytes))
276 .transpose()?
277 .map(|x| Vec::from(x));
278
279 let min_amount = query
280 .min_amount
281 .as_option()
282 .map(|x| data_or_bail!(x, assets))
283 .transpose()?
284 .map(|x| CanonicalAssets::from(Vec::from(x)));
285
286 let refs = query
287 .r#ref
288 .as_option()
289 .map(|x| data_or_bail!(x, utxo_refs))
290 .transpose()?
291 .map(|x| HashSet::from_iter(x.iter().cloned()))
292 .unwrap_or_default();
293
294 Ok(Self {
295 address,
296 min_amount,
297 refs,
298 support_many: query.many,
299 collateral: query.collateral,
300 })
301 }
302}
303
304pub async fn resolve<T: UtxoStore>(tx: ir::Tx, utxos: &T) -> Result<ir::Tx, Error> {
305 let mut all_inputs = BTreeMap::new();
306
307 let mut selector = InputSelector::new(utxos);
308
309 for (name, query) in applying::find_queries(&tx) {
310 let query = CanonicalQuery::try_from(query)?;
311
312 let space = searching::narrow_search_space(utxos, &query, MAX_SEARCH_SPACE_SIZE).await?;
313
314 let utxos = selector.select(&space, &query).await?;
315
316 if utxos.is_empty() {
317 return Err(Error::InputNotResolved(name.to_string(), query, space));
318 }
319
320 all_inputs.insert(name, utxos);
321 }
322
323 let out = applying::apply_inputs(tx, &all_inputs)?;
324
325 Ok(out)
326}
327
328#[cfg(test)]
329mod tests {
330 use crate::mock;
331
332 use super::*;
333
334 pub fn new_input_query(
335 address: &mock::KnownAddress,
336 naked_amount: Option<u64>,
337 other_assets: Vec<(mock::KnownAsset, u64)>,
338 many: bool,
339 collateral: bool,
340 ) -> CanonicalQuery {
341 let naked_asset = naked_amount.map(|x| ir::AssetExpr {
342 policy: ir::Expression::None,
343 asset_name: ir::Expression::None,
344 amount: ir::Expression::Number(x as i128),
345 });
346
347 let other_assets: Vec<ir::AssetExpr> = other_assets
348 .into_iter()
349 .map(|(asset, amount)| ir::AssetExpr {
350 policy: ir::Expression::Bytes(asset.policy().as_slice().to_vec()),
351 asset_name: ir::Expression::Bytes(asset.name().to_vec()),
352 amount: ir::Expression::Number(amount as i128),
353 })
354 .collect();
355
356 let all_assets = naked_asset.into_iter().chain(other_assets).collect();
357
358 ir::InputQuery {
359 address: ir::Expression::Address(address.to_bytes()),
360 min_amount: ir::Expression::Assets(all_assets),
361 r#ref: ir::Expression::None,
362 many,
363 collateral,
364 }
365 .try_into()
366 .unwrap()
367 }
368
369 #[pollster::test]
370 async fn test_select_by_address() {
371 let store = mock::seed_random_memory_store(
372 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, _: u64| {
373 mock::utxo_with_random_amount(x, 4_000_000..5_000_000)
374 },
375 2..4,
376 );
377
378 let mut selector = InputSelector::new(&store);
379
380 for subject in mock::KnownAddress::everyone() {
381 let criteria = new_input_query(&subject, None, vec![], false, false);
382
383 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
384 .await
385 .unwrap();
386
387 let utxos = selector.select(&space, &criteria).await.unwrap();
388
389 assert_eq!(utxos.len(), 1);
390
391 for utxo in utxos {
392 assert_eq!(utxo.address, subject.to_bytes());
393 }
394 }
395 }
396
397 #[pollster::test]
398 async fn test_input_query_too_broad() {
399 let store = mock::seed_random_memory_store(
400 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, _: u64| {
401 mock::utxo_with_random_amount(x, 4_000_000..5_000_000)
402 },
403 2..4,
404 );
405
406 let empty_criteria = ir::InputQuery {
407 address: ir::Expression::None,
408 min_amount: ir::Expression::None,
409 r#ref: ir::Expression::None,
410 many: false,
411 collateral: false,
412 }
413 .try_into()
414 .unwrap();
415
416 let space = searching::narrow_search_space(&store, &empty_criteria, MAX_SEARCH_SPACE_SIZE)
417 .await
418 .unwrap_err();
419
420 assert!(matches!(space, Error::InputQueryTooBroad));
421 }
422
423 #[pollster::test]
424 async fn test_select_anything() {
425 let store = mock::seed_random_memory_store(
426 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, seq: u64| {
427 if seq % 2 == 0 {
428 mock::utxo_with_random_amount(x, 4_000_000..5_000_000)
429 } else {
430 mock::utxo_with_random_asset(x, mock::KnownAsset::Hosky, 500..1000)
431 }
432 },
433 2..3, );
435
436 let mut selector = InputSelector::new(&store);
437
438 let criteria = new_input_query(&mock::KnownAddress::Alice, None, vec![], true, false);
439
440 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
441 .await
442 .unwrap();
443
444 let utxos = selector.select(&space, &criteria).await.unwrap();
445 assert_eq!(utxos.len(), 1);
446 }
447
448 #[pollster::test]
449 async fn test_select_by_naked_amount() {
450 let store = mock::seed_random_memory_store(
451 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, _: u64| {
452 mock::utxo_with_random_amount(x, 4_000_000..5_000_000)
453 },
454 2..4,
455 );
456
457 let mut selector = InputSelector::new(&store);
458
459 let criteria = new_input_query(
460 &mock::KnownAddress::Alice,
461 Some(6_000_000),
462 vec![],
463 false,
464 false,
465 );
466
467 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
468 .await
469 .unwrap();
470
471 let utxos = selector.select(&space, &criteria).await.unwrap();
472 assert!(utxos.is_empty());
473
474 let criteria = new_input_query(
475 &mock::KnownAddress::Alice,
476 Some(4_000_000),
477 vec![],
478 false,
479 false,
480 );
481
482 let utxos = selector.select(&space, &criteria).await.unwrap();
483
484 let match_count = dbg!(utxos.len());
485 assert_eq!(match_count, 1);
486 }
487
488 #[pollster::test]
489 async fn test_select_by_asset_amount() {
490 let store = mock::seed_random_memory_store(
491 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, _: u64| {
492 mock::utxo_with_random_asset(x, mock::KnownAsset::Hosky, 500..1000)
493 },
494 2..4,
495 );
496
497 for address in mock::KnownAddress::everyone() {
498 let criteria = new_input_query(
502 &address,
503 None,
504 vec![(mock::KnownAsset::Hosky, 1001)],
505 false,
506 false,
507 );
508
509 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
510 .await
511 .unwrap();
512
513 let mut selector = InputSelector::new(&store);
514 let utxos = selector.select(&space, &criteria).await.unwrap();
515 assert!(utxos.is_empty());
516
517 let criteria = new_input_query(
521 &address,
522 None,
523 vec![(mock::KnownAsset::Hosky, 1001)],
524 true,
525 false,
526 );
527
528 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
529 .await
530 .unwrap();
531
532 let mut selector = InputSelector::new(&store);
533 let utxos = selector.select(&space, &criteria).await.unwrap();
534 assert!(utxos.len() > 1);
535
536 let criteria = new_input_query(
540 &address,
541 None,
542 vec![(mock::KnownAsset::Hosky, 4001)],
543 true,
544 false,
545 );
546
547 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
548 .await
549 .unwrap();
550
551 let mut selector = InputSelector::new(&store);
552 let utxos = selector.select(&space, &criteria).await.unwrap();
553 assert!(utxos.is_empty());
554
555 let criteria = new_input_query(
558 &address,
559 None,
560 vec![(mock::KnownAsset::Snek, 500)],
561 false,
562 false,
563 );
564
565 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
566 .await
567 .unwrap();
568
569 let mut selector = InputSelector::new(&store);
570 let utxos = selector.select(&space, &criteria).await.unwrap();
571 assert!(utxos.is_empty());
572
573 let criteria = new_input_query(
576 &address,
577 None,
578 vec![(mock::KnownAsset::Hosky, 500)],
579 false,
580 false,
581 );
582
583 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
584 .await
585 .unwrap();
586
587 let mut selector = InputSelector::new(&store);
588 let utxos = selector.select(&space, &criteria).await.unwrap();
589 assert_eq!(utxos.len(), 1);
590 }
591 }
592
593 #[pollster::test]
594 async fn test_select_by_collateral() {
595 let store = mock::seed_random_memory_store(
596 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, sequence: u64| {
597 if sequence % 2 == 0 {
598 mock::utxo_with_random_amount(x, 4_000_000..5_000_000)
599 } else {
600 mock::utxo_with_random_asset(x, mock::KnownAsset::Hosky, 500..1000)
601 }
602 },
603 2..4,
604 );
605
606 let mut selector = InputSelector::new(&store);
607
608 for address in mock::KnownAddress::everyone() {
609 let criteria = new_input_query(&address, Some(1_000_000), vec![], false, true);
610
611 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
612 .await
613 .unwrap();
614
615 let utxos = selector.select(&space, &criteria).await.unwrap();
616
617 assert_eq!(utxos.len(), 1);
618 let utxo = utxos.iter().next().unwrap();
619 assert_eq!(utxo.assets.keys().len(), 1);
620 assert_eq!(utxo.assets.keys().next().unwrap().is_naked(), true);
621 }
622 }
623
624 #[pollster::test]
625 async fn test_select_same_collateral_and_input() {
626 let store = mock::seed_random_memory_store(
627 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, _: u64| {
628 mock::utxo_with_random_amount(x, 4_000_000..5_000_000)
629 },
630 1..2, );
632
633 let mut selector = InputSelector::new(&store);
634
635 for address in mock::KnownAddress::everyone() {
636 let criteria = new_input_query(&address, Some(1_000_000), vec![], false, false);
638
639 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
640 .await
641 .unwrap();
642
643 let utxos = selector.select(&space, &criteria).await.unwrap();
644 assert_eq!(utxos.len(), 1);
645
646 let criteria = new_input_query(&address, Some(1_000_000), vec![], false, true);
648
649 let space = searching::narrow_search_space(&store, &criteria, MAX_SEARCH_SPACE_SIZE)
650 .await
651 .unwrap();
652
653 let utxos = selector.select(&space, &criteria).await.unwrap();
654 assert_eq!(utxos.len(), 1);
655 }
656 }
657
658 #[pollster::test]
659 async fn test_resolve_ignores_selected_utxos() {
660 let store = mock::seed_random_memory_store(
661 |_: &mock::FuzzTxoRef, x: &mock::KnownAddress, seq: u64| {
662 if seq % 2 == 0 {
663 mock::utxo_with_random_amount(x, 1_000..1_500)
664 } else {
665 mock::utxo_with_random_amount(x, 100..150)
666 }
667 },
668 2..3, );
670
671 for address in mock::KnownAddress::everyone() {
672 let mut selector = InputSelector::new(&store);
673
674 let query1 = new_input_query(&address, Some(1_000), vec![], true, false);
676
677 let space = searching::narrow_search_space(&store, &query1, MAX_SEARCH_SPACE_SIZE)
678 .await
679 .unwrap();
680
681 let utxos1 = selector.select(&space, &query1).await.unwrap();
682 assert_eq!(utxos1.len(), 1);
683
684 let query2 = new_input_query(&address, Some(1_000), vec![], true, false);
687
688 let space = searching::narrow_search_space(&store, &query2, MAX_SEARCH_SPACE_SIZE)
689 .await
690 .unwrap();
691
692 let utxos2 = selector.select(&space, &query2).await.unwrap();
693 assert_eq!(utxos2.len(), 0);
694
695 let query3 = new_input_query(&address, Some(100), vec![], true, false);
697
698 let space = searching::narrow_search_space(&store, &query3, MAX_SEARCH_SPACE_SIZE)
699 .await
700 .unwrap();
701
702 let utxos3 = selector.select(&space, &query3).await.unwrap();
703 assert_eq!(utxos3.len(), 1);
704
705 let query4 = new_input_query(&address, Some(100), vec![], true, false);
708
709 let space = searching::narrow_search_space(&store, &query4, MAX_SEARCH_SPACE_SIZE)
710 .await
711 .unwrap();
712
713 let utxos4 = selector.select(&space, &query4).await.unwrap();
714 assert_eq!(utxos4.len(), 0);
715
716 utxos1.is_disjoint(&utxos3);
718 }
719 }
720}