1use std::collections::{BTreeMap, HashMap};
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use tx3_tir::compile::{CompiledTx, Compiler};
7use tx3_tir::encoding::AnyTir;
8use tx3_tir::model::core::{Utxo, UtxoRef, UtxoSet};
9use tx3_tir::model::v1beta0 as tir;
10use tx3_tir::reduce::{Apply as _, ArgMap};
11use tx3_tir::Node as _;
12
13use crate::inputs::CanonicalQuery;
14use crate::{Error, UtxoStore};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub enum ResolveLog {
18 ArgsApplied(AnyTir),
19 FeesApplied(AnyTir),
20 CompilerApplied(AnyTir),
21 Reduced(AnyTir),
22 InputsResolved(AnyTir),
23 FinalReduced(AnyTir),
24 Compiled(CompiledTx),
25 Converged,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ResolveLogEntry {
30 pub round: usize,
31 pub event: ResolveLog,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ResolveJob {
36 pub original_tir: AnyTir,
38 pub args: ArgMap,
39 pub compiler: Value,
40
41 pub round: usize,
43 pub last_eval: Option<CompiledTx>,
44 pub converged: bool,
45
46 pub log: Vec<ResolveLogEntry>,
48
49 pub input_queries: Vec<QueryResolution>,
51 pub input_pool: Option<HashMap<UtxoRef, Utxo>>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct QueryResolution {
58 pub name: String,
59 pub query: CanonicalQuery,
60 #[serde(skip)]
62 pub candidates: Vec<Utxo>,
63 pub selection: Option<UtxoSet>,
65}
66
67impl QueryResolution {
68 pub fn ensure_resolved(&self, pool_refs: &[UtxoRef]) -> Result<(), Error> {
69 match &self.selection {
70 Some(sel) if !sel.is_empty() => Ok(()),
71 _ => Err(Error::InputNotResolved(
72 self.name.clone(),
73 self.query.clone(),
74 pool_refs.to_vec(),
75 )),
76 }
77 }
78}
79
80impl ResolveJob {
81 pub fn new(tx: AnyTir, args: ArgMap) -> Self {
82 Self {
83 original_tir: tx,
84 args,
85 compiler: Value::Null,
86 round: 0,
87 last_eval: None,
88 converged: false,
89 log: Vec::new(),
90 input_queries: Vec::new(),
91 input_pool: None,
92 }
93 }
94
95 pub fn record(&mut self, event: ResolveLog) {
96 self.log.push(ResolveLogEntry {
97 round: self.round,
98 event,
99 });
100 }
101
102 pub fn resolved_tir(&self) -> &AnyTir {
103 self.log
104 .iter()
105 .rev()
106 .find_map(|entry| match &entry.event {
107 ResolveLog::ArgsApplied(tir) => Some(tir),
108 _ => None,
109 })
110 .expect("apply_args must be called before eval_pass")
111 }
112
113 pub fn set_input_queries(&mut self, queries: Vec<(String, CanonicalQuery)>) {
114 self.input_queries = queries
115 .into_iter()
116 .map(|(name, query)| QueryResolution {
117 name,
118 query,
119 candidates: Vec::new(),
120 selection: None,
121 })
122 .collect();
123 }
124
125 pub fn pool_refs(&self) -> Vec<UtxoRef> {
126 self.input_pool
127 .as_ref()
128 .map(|p| p.keys().cloned().collect())
129 .unwrap_or_default()
130 }
131
132 pub fn to_input_map(&self) -> BTreeMap<String, UtxoSet> {
133 self.input_queries
134 .iter()
135 .filter_map(|qr| {
136 qr.selection
137 .as_ref()
138 .map(|sel| (qr.name.clone(), sel.clone()))
139 })
140 .collect()
141 }
142
143 fn apply_args(&mut self) -> Result<(), Error> {
144 let params = tx3_tir::reduce::find_params(&self.original_tir);
145
146 for (key, ty) in params.iter() {
147 if !self.args.contains_key(key) {
148 return Err(Error::MissingTxArg {
149 key: key.to_string(),
150 ty: ty.clone(),
151 });
152 };
153 }
154
155 let tir = tx3_tir::reduce::apply_args(self.original_tir.clone(), &self.args)?;
156 self.record(ResolveLog::ArgsApplied(tir));
157
158 Ok(())
159 }
160
161 async fn eval_pass<C, S>(&mut self, compiler: &mut C, utxos: &S) -> Result<(), Error>
162 where
163 C: Compiler<Expression = tir::Expression, CompilerOp = tir::CompilerOp>,
164 S: UtxoStore,
165 {
166 let base_tir = self.resolved_tir().clone();
167 let fees = self.last_eval.as_ref().map(|e| e.fee).unwrap_or(0);
168
169 let attempt = tx3_tir::reduce::apply_fees(base_tir, fees)?;
170 self.record(ResolveLog::FeesApplied(attempt.clone()));
171
172 let attempt = attempt.apply(compiler)?;
173 self.record(ResolveLog::CompilerApplied(attempt.clone()));
174
175 let attempt = tx3_tir::reduce::reduce(attempt)?;
176 self.record(ResolveLog::Reduced(attempt.clone()));
177
178 let attempt = self.resolve_inputs(attempt, utxos).await?;
179 self.record(ResolveLog::InputsResolved(attempt.clone()));
180
181 let attempt = tx3_tir::reduce::reduce(attempt)?;
182 self.record(ResolveLog::FinalReduced(attempt.clone()));
183
184 if !attempt.is_constant() {
185 return Err(Error::CantCompileNonConstantTir);
186 }
187
188 let eval = compiler.compile(&attempt)?;
189
190 let converged = self.last_eval.as_ref().map_or(false, |prev| eval == *prev);
191
192 self.record(ResolveLog::Compiled(eval.clone()));
193 self.last_eval = Some(eval);
194
195 if converged {
196 self.converged = true;
197 self.record(ResolveLog::Converged);
198 }
199
200 self.round += 1;
201
202 Ok(())
203 }
204
205 pub async fn execute<C, S>(
206 &mut self,
207 compiler: &mut C,
208 utxos: &S,
209 max_optimize_rounds: usize,
210 ) -> Result<CompiledTx, Error>
211 where
212 C: Compiler<Expression = tir::Expression, CompilerOp = tir::CompilerOp>,
213 S: UtxoStore,
214 {
215 let max_optimize_rounds = max_optimize_rounds.max(3);
216
217 self.compiler = match serde_json::to_value(&*compiler) {
218 Ok(value) => value,
219 Err(err) => {
220 tracing::warn!(error = %err, "failed to serialize compiler for diagnostic dump");
221 Value::Null
222 }
223 };
224
225 self.apply_args()?;
226
227 loop {
228 self.eval_pass(compiler, utxos).await?;
229
230 if self.converged || self.round > max_optimize_rounds {
231 break;
232 }
233 }
234
235 Ok(self.last_eval.clone().unwrap())
236 }
237}
238
239pub async fn resolve_tx<C, S>(
240 tx: AnyTir,
241 args: &ArgMap,
242 compiler: &mut C,
243 utxos: &S,
244 max_optimize_rounds: usize,
245) -> Result<CompiledTx, Error>
246where
247 C: Compiler<Expression = tir::Expression, CompilerOp = tir::CompilerOp>,
248 S: UtxoStore,
249{
250 let mut job = ResolveJob::new(tx, args.clone());
251
252 let result = job.execute(compiler, utxos, max_optimize_rounds).await;
253
254 if let Ok(dir) = std::env::var("TX3_DIAGNOSTIC_DUMP") {
255 let _ = crate::dump::dump_to_dir(&job, Path::new(&dir));
256 }
257
258 result
259}