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
19impl 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
69impl 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
85impl 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
125impl 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}