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
18pub 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 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}