Skip to main content

tx3_resolver/
dump.rs

1use std::path::Path;
2
3use serde_json::{json, Map, Value};
4
5use tx3_tir::compile::CompiledTx;
6use tx3_tir::encoding::AnyTir;
7use tx3_tir::model::assets::CanonicalAssets;
8use tx3_tir::model::core::{Utxo, UtxoRef};
9use tx3_tir::reduce::ArgValue;
10
11use crate::inputs::CanonicalQuery;
12use crate::interop::{self, TirEnvelope};
13use crate::job::{QueryResolution, ResolveJob, ResolveLog, ResolveLogEntry};
14
15pub trait DiagnosticDump {
16    fn to_dump(&self) -> Value;
17}
18
19// ---------------------------------------------------------------------------
20// Leaf types
21// ---------------------------------------------------------------------------
22
23impl DiagnosticDump for UtxoRef {
24    fn to_dump(&self) -> Value {
25        interop::utxo_ref_to_json(self)
26    }
27}
28
29impl DiagnosticDump for AnyTir {
30    fn to_dump(&self) -> Value {
31        let envelope = TirEnvelope::from(self.clone());
32        serde_json::to_value(envelope).expect("TirEnvelope should serialize")
33    }
34}
35
36impl DiagnosticDump for CompiledTx {
37    fn to_dump(&self) -> Value {
38        json!({
39            "payload": hex::encode(&self.payload),
40            "hash": hex::encode(&self.hash),
41            "fee": self.fee,
42            "ex_units": self.ex_units,
43        })
44    }
45}
46
47impl DiagnosticDump for ArgValue {
48    fn to_dump(&self) -> Value {
49        interop::arg_to_json(self)
50    }
51}
52
53impl DiagnosticDump for CanonicalAssets {
54    fn to_dump(&self) -> Value {
55        let map: Map<String, Value> = self
56            .iter()
57            .map(|(class, amount)| (class.to_string(), json!(amount)))
58            .collect();
59        Value::Object(map)
60    }
61}
62
63impl DiagnosticDump for Utxo {
64    fn to_dump(&self) -> Value {
65        interop::utxo_to_json(self)
66    }
67}
68
69// ---------------------------------------------------------------------------
70// Canonical query
71// ---------------------------------------------------------------------------
72
73impl DiagnosticDump for CanonicalQuery {
74    fn to_dump(&self) -> Value {
75        json!({
76            "address": self.address.as_ref().map(|a| hex::encode(a)),
77            "min_amount": self.min_amount.as_ref().map(|a| a.to_dump()),
78            "refs": self.refs.iter().map(|r| r.to_dump()).collect::<Vec<_>>(),
79            "support_many": self.support_many,
80            "collateral": self.collateral,
81        })
82    }
83}
84
85// ---------------------------------------------------------------------------
86// Resolve pipeline types
87// ---------------------------------------------------------------------------
88
89impl DiagnosticDump for QueryResolution {
90    fn to_dump(&self) -> Value {
91        json!({
92            "name": self.name,
93            "query": self.query.to_dump(),
94            "selection": self.selection.as_ref().map(|sel| {
95                sel.iter().map(|u| u.to_dump()).collect::<Vec<_>>()
96            }),
97        })
98    }
99}
100
101impl DiagnosticDump for ResolveLog {
102    fn to_dump(&self) -> Value {
103        match self {
104            ResolveLog::ArgsApplied(tir) => json!({"args_applied": tir.to_dump()}),
105            ResolveLog::FeesApplied(tir) => json!({"fees_applied": tir.to_dump()}),
106            ResolveLog::CompilerApplied(tir) => json!({"compiler_applied": tir.to_dump()}),
107            ResolveLog::Reduced(tir) => json!({"reduced": tir.to_dump()}),
108            ResolveLog::InputsResolved(tir) => json!({"inputs_resolved": tir.to_dump()}),
109            ResolveLog::FinalReduced(tir) => json!({"final_reduced": tir.to_dump()}),
110            ResolveLog::Compiled(tx) => json!({"compiled": tx.to_dump()}),
111            ResolveLog::Converged => json!("converged"),
112        }
113    }
114}
115
116impl DiagnosticDump for ResolveLogEntry {
117    fn to_dump(&self) -> Value {
118        json!({
119            "round": self.round,
120            "event": self.event.to_dump(),
121        })
122    }
123}
124
125// ---------------------------------------------------------------------------
126// ResolveJob — top-level
127// ---------------------------------------------------------------------------
128
129impl DiagnosticDump for ResolveJob {
130    fn to_dump(&self) -> Value {
131        let args: Map<String, Value> = self
132            .args
133            .iter()
134            .map(|(k, v)| (k.clone(), interop::arg_to_json(v)))
135            .collect();
136
137        let input_pool = self.input_pool.as_ref().map(|pool| {
138            let map: Map<String, Value> = pool
139                .iter()
140                .map(|(r, u)| {
141                    let key = format!("{}#{}", hex::encode(&r.txid), r.index);
142                    (key, interop::utxo_to_json(u))
143                })
144                .collect();
145            Value::Object(map)
146        });
147
148        json!({
149            "original_tir": self.original_tir.to_dump(),
150            "args": args,
151            "compiler": &self.compiler,
152            "round": self.round,
153            "last_eval": self.last_eval.as_ref().map(|e| e.to_dump()),
154            "converged": self.converged,
155            "log": self.log.iter().map(|e| e.to_dump()).collect::<Vec<_>>(),
156            "input_queries": self.input_queries.iter().map(|q| q.to_dump()).collect::<Vec<_>>(),
157            "input_pool": input_pool,
158        })
159    }
160}
161
162pub fn dump_to_dir(job: &ResolveJob, dir: &Path) -> Result<String, std::io::Error> {
163    std::fs::create_dir_all(dir)?;
164    let dump_id = uuid::Uuid::new_v4().to_string();
165    let path = dir.join(format!("resolve-job-{dump_id}.json"));
166    let file = std::fs::File::create(&path)?;
167    serde_json::to_writer_pretty(file, &job.to_dump())
168        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
169    tracing::debug!(dump_id = %dump_id, path = %path.display(), "diagnostic dump written");
170    Ok(dump_id)
171}
172
173#[cfg(test)]
174mod tests {
175    use tx3_tir::encoding::AnyTir;
176    use tx3_tir::model::v1beta0 as tir;
177    use tx3_tir::reduce::ArgMap;
178
179    use crate::{
180        dump::dump_to_dir,
181        job::{ResolveJob, ResolveLog},
182    };
183
184    fn dummy_tir() -> AnyTir {
185        AnyTir::V1Beta0(tir::Tx {
186            fees: tir::Expression::None,
187            references: vec![],
188            inputs: vec![],
189            outputs: vec![],
190            validity: None,
191            mints: vec![],
192            burns: vec![],
193            adhoc: vec![],
194            collateral: vec![],
195            signers: None,
196            metadata: vec![],
197        })
198    }
199
200    #[test]
201    fn dump_creates_valid_json_file() {
202        let mut job = ResolveJob::new(dummy_tir(), ArgMap::new());
203        job.record(ResolveLog::ArgsApplied(dummy_tir()));
204        job.record(ResolveLog::Converged);
205
206        let dir = std::env::temp_dir().join("tx3-test-dump");
207        let dump_id = dump_to_dir(&job, &dir).expect("dump should succeed");
208
209        let path = dir.join(format!("resolve-job-{dump_id}.json"));
210        assert!(path.exists());
211
212        let contents = std::fs::read_to_string(&path).unwrap();
213        let value: serde_json::Value = serde_json::from_str(&contents).unwrap();
214        assert!(value.get("compiler").is_some());
215
216        let _ = std::fs::remove_file(&path);
217    }
218}