tx3_resolver/inputs/
mod.rs

1//! Tx input selection algorithms
2
3use std::collections::{BTreeMap, HashSet};
4
5use tx3_tir::encoding::AnyTir;
6use tx3_tir::model::core::UtxoSet;
7use tx3_tir::model::v1beta0 as tir;
8use tx3_tir::model::{assets::CanonicalAssets, core::UtxoRef};
9
10pub use crate::inputs::narrow::SearchSpace;
11use crate::{Error, UtxoStore};
12
13mod narrow;
14mod select;
15
16macro_rules! data_or_bail {
17    ($expr:expr, bytes) => {
18        $expr
19            .as_bytes()
20            .ok_or(Error::ExpectedData("bytes".to_string(), $expr.clone()))
21    };
22
23    ($expr:expr, number) => {
24        $expr
25            .as_number()
26            .ok_or(Error::ExpectedData("number".to_string(), $expr.clone()))?
27    };
28
29    ($expr:expr, assets) => {
30        $expr
31            .as_assets()
32            .ok_or(Error::ExpectedData("assets".to_string(), $expr.clone()))
33    };
34
35    ($expr:expr, utxo_refs) => {
36        $expr
37            .as_utxo_refs()
38            .ok_or(Error::ExpectedData("utxo refs".to_string(), $expr.clone()))
39    };
40}
41
42pub struct Diagnostic {
43    pub query: tir::InputQuery,
44    pub utxos: UtxoSet,
45    pub selected: UtxoSet,
46}
47
48const MAX_SEARCH_SPACE_SIZE: usize = 50;
49
50#[derive(Debug, Clone)]
51pub struct CanonicalQuery {
52    pub address: Option<Vec<u8>>,
53    pub min_amount: Option<CanonicalAssets>,
54    pub refs: HashSet<UtxoRef>,
55    pub support_many: bool,
56    pub collateral: bool,
57}
58
59impl std::fmt::Display for CanonicalQuery {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "CanonicalQuery {{")?;
62
63        if let Some(address) = &self.address {
64            write!(f, "address: {}", hex::encode(address))?;
65        }
66
67        if let Some(min_amount) = &self.min_amount {
68            write!(f, "min_amount: {}", min_amount)?;
69        }
70
71        for (i, ref_) in self.refs.iter().enumerate() {
72            write!(f, "ref[{}]:{}#{}", i, hex::encode(&ref_.txid), ref_.index)?;
73        }
74
75        write!(f, "support_many: {:?}", self.support_many)?;
76        write!(f, "for_collateral: {:?}", self.collateral)?;
77        write!(f, "}}")
78    }
79}
80
81impl TryFrom<tir::InputQuery> for CanonicalQuery {
82    type Error = Error;
83
84    fn try_from(query: tir::InputQuery) -> Result<Self, Self::Error> {
85        let address = query
86            .address
87            .as_option()
88            .map(|x| data_or_bail!(x, bytes))
89            .transpose()?
90            .map(Vec::from);
91
92        let min_amount = query
93            .min_amount
94            .as_option()
95            .map(|x| data_or_bail!(x, assets))
96            .transpose()?
97            .map(|x| CanonicalAssets::from(Vec::from(x)));
98
99        let refs = query
100            .r#ref
101            .as_option()
102            .map(|x| data_or_bail!(x, utxo_refs))
103            .transpose()?
104            .map(|x| HashSet::from_iter(x.iter().cloned()))
105            .unwrap_or_default();
106
107        Ok(Self {
108            address,
109            min_amount,
110            refs,
111            support_many: query.many,
112            collateral: query.collateral,
113        })
114    }
115}
116
117pub async fn resolve<T: UtxoStore>(tx: AnyTir, utxos: &T) -> Result<AnyTir, Error> {
118    let mut all_inputs = BTreeMap::new();
119
120    let mut selector = select::InputSelector::new(utxos);
121
122    for (name, query) in tx3_tir::reduce::find_queries(&tx) {
123        let query = CanonicalQuery::try_from(query)?;
124
125        let space = narrow::narrow_search_space(utxos, &query).await?;
126
127        let utxos = selector.select(&space, &query).await?;
128
129        if utxos.is_empty() {
130            return Err(Error::InputNotResolved(name.to_string(), query, space));
131        }
132
133        all_inputs.insert(name, utxos);
134    }
135
136    let out = tx3_tir::reduce::apply_inputs(tx, &all_inputs)?;
137
138    Ok(out)
139}