Skip to main content

tx3_resolver/
lib.rs

1use std::collections::HashSet;
2
3use tx3_tir::compile::{CompiledTx, Compiler};
4use tx3_tir::encoding::AnyTir;
5use tx3_tir::model::v1beta0 as tir;
6use tx3_tir::reduce::{Apply as _, ArgMap};
7use tx3_tir::Node as _;
8
9use crate::inputs::CanonicalQuery;
10
11pub mod inputs;
12pub mod interop;
13pub mod trp;
14
15pub use tx3_tir::model::assets::CanonicalAssets;
16pub use tx3_tir::model::core::{Type, Utxo, UtxoRef, UtxoSet};
17
18// TODO: we need to re-export this because some of the UtxoStore interface depends ond them, but this is tech debt. We should remove any dependency to versioned IR artifacts.
19pub use tx3_tir::model::v1beta0::{Expression, StructExpr};
20
21#[cfg(test)]
22pub mod mock;
23
24#[derive(Debug, thiserror::Error)]
25pub enum Error {
26    #[error("can't compile non-constant tir")]
27    CantCompileNonConstantTir,
28
29    #[error(transparent)]
30    CompileError(#[from] tx3_tir::compile::Error),
31
32    #[error(transparent)]
33    InteropError(#[from] interop::Error),
34
35    #[error(transparent)]
36    ReduceError(#[from] tx3_tir::reduce::Error),
37
38    #[error("expected {0}, got {1:?}")]
39    ExpectedData(String, tir::Expression),
40
41    #[error("input query too broad")]
42    InputQueryTooBroad,
43
44    #[error("input not resolved: {0}")]
45    InputNotResolved(String, CanonicalQuery, Vec<UtxoRef>),
46
47    #[error("missing argument `{key}` of type {ty:?}")]
48    MissingTxArg {
49        key: String,
50        ty: tx3_tir::model::core::Type,
51    },
52
53    #[error("transient error: {0}")]
54    TransientError(String),
55
56    #[error("store error: {0}")]
57    StoreError(String),
58
59    #[error("TIR encode / decode error: {0}")]
60    TirEncodingError(#[from] tx3_tir::encoding::Error),
61
62    #[error("tx was not accepted: {0}")]
63    TxNotAccepted(String),
64
65    #[error("tx script returned failure")]
66    TxScriptFailure(Vec<String>),
67}
68
69pub enum UtxoPattern<'a> {
70    ByAddress(&'a [u8]),
71    ByAssetPolicy(&'a [u8]),
72    ByAsset(&'a [u8], &'a [u8]),
73}
74
75impl<'a> UtxoPattern<'a> {
76    pub fn by_address(address: &'a [u8]) -> Self {
77        Self::ByAddress(address)
78    }
79
80    pub fn by_asset_policy(policy: &'a [u8]) -> Self {
81        Self::ByAssetPolicy(policy)
82    }
83
84    pub fn by_asset(policy: &'a [u8], name: &'a [u8]) -> Self {
85        Self::ByAsset(policy, name)
86    }
87}
88
89#[trait_variant::make(Send)]
90pub trait UtxoStore {
91    async fn narrow_refs(&self, pattern: UtxoPattern<'_>) -> Result<HashSet<UtxoRef>, Error>;
92    async fn fetch_utxos(&self, refs: HashSet<UtxoRef>) -> Result<UtxoSet, Error>;
93}
94
95async fn eval_pass<C, S>(
96    tx: &AnyTir,
97    compiler: &mut C,
98    utxos: &S,
99    last_eval: Option<&CompiledTx>,
100) -> Result<Option<CompiledTx>, Error>
101where
102    C: Compiler<Expression = tir::Expression, CompilerOp = tir::CompilerOp>,
103    S: UtxoStore,
104{
105    let attempt = tx.clone();
106
107    let fees = last_eval.as_ref().map(|e| e.fee).unwrap_or(0);
108
109    let attempt = tx3_tir::reduce::apply_fees(attempt, fees)?;
110
111    let attempt = attempt.apply(compiler)?;
112
113    let attempt = tx3_tir::reduce::reduce(attempt)?;
114
115    let attempt = crate::inputs::resolve(attempt, utxos).await?;
116
117    let attempt = tx3_tir::reduce::reduce(attempt)?;
118
119    if !attempt.is_constant() {
120        return Err(Error::CantCompileNonConstantTir);
121    }
122
123    let eval = compiler.compile(&attempt)?;
124
125    let Some(last_eval) = last_eval else {
126        return Ok(Some(eval));
127    };
128
129    if eval != *last_eval {
130        return Ok(Some(eval));
131    }
132
133    Ok(None)
134}
135
136fn safe_apply_args(tir: AnyTir, args: &ArgMap) -> Result<AnyTir, Error> {
137    let params = tx3_tir::reduce::find_params(&tir);
138
139    // ensure all required arguments are provided
140    for (key, ty) in params.iter() {
141        if !args.contains_key(key) {
142            return Err(Error::MissingTxArg {
143                key: key.to_string(),
144                ty: ty.clone(),
145            });
146        };
147    }
148
149    let tir = tx3_tir::reduce::apply_args(tir, args)?;
150
151    Ok(tir)
152}
153
154pub async fn resolve_tx<C, S>(
155    tx: AnyTir,
156    args: &ArgMap,
157    compiler: &mut C,
158    utxos: &S,
159    max_optimize_rounds: usize,
160) -> Result<CompiledTx, Error>
161where
162    C: Compiler<Expression = tir::Expression, CompilerOp = tir::CompilerOp>,
163    S: UtxoStore,
164{
165    let tx = safe_apply_args(tx, args)?;
166
167    let max_optimize_rounds = max_optimize_rounds.max(3);
168
169    let mut last_eval = None;
170    let mut rounds = 0;
171
172    while let Some(better) = eval_pass(&tx, compiler, utxos, last_eval.as_ref()).await? {
173        last_eval = Some(better);
174
175        if rounds > max_optimize_rounds {
176            break;
177        }
178
179        rounds += 1;
180    }
181
182    Ok(last_eval.unwrap())
183}