use std::collections::{HashMap, HashSet};
use serde_json::Value;
use tx3_cardano::Compiler;
use tx3_resolver::interop::TirEnvelope;
use tx3_resolver::{resolve_tx, Error, UtxoPattern, UtxoRef, UtxoSet, UtxoStore};
use tx3_tir::encoding::AnyTir;
use tx3_tir::model::assets::AssetClass;
use tx3_tir::model::core::Utxo;
use tx3_tir::reduce::ArgMap;
fn compiler_from_json(compiler: &Value) -> Compiler {
serde_json::from_value(compiler.clone()).expect("compiler should deserialize from dump")
}
struct ResolveFixture {
tir: AnyTir,
args: ArgMap,
input_pool: HashMap<UtxoRef, Utxo>,
compiler: Value,
expected_hash: String,
expected_fee: u64,
}
fn load_fixture(name: &str) -> ResolveFixture {
let path = format!("{}/tests/golden/{name}.json", env!("CARGO_MANIFEST_DIR"));
let contents =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("failed to read {path}: {e}"));
let value: Value =
serde_json::from_str(&contents).unwrap_or_else(|e| panic!("failed to parse {path}: {e}"));
let tir_envelope: TirEnvelope = serde_json::from_value(value["original_tir"].clone())
.expect("original_tir should be a valid TirEnvelope");
let tir = AnyTir::try_from(tir_envelope).expect("tir should decode from TirEnvelope");
let params = tx3_tir::reduce::find_params(&tir);
let mut args = ArgMap::new();
if let Some(args_obj) = value["args"].as_object() {
for (key, val) in args_obj {
if let Some(ty) = params.get(key) {
let arg = tx3_resolver::interop::from_json(val.clone(), ty)
.unwrap_or_else(|e| panic!("failed to parse arg '{key}': {e}"));
args.insert(key.clone(), arg);
}
}
}
let mut input_pool = HashMap::new();
if let Some(pool_obj) = value["input_pool"].as_object() {
for (_, utxo_val) in pool_obj {
let utxo = tx3_resolver::interop::utxo_from_json(utxo_val)
.unwrap_or_else(|e| panic!("failed to parse utxo: {e}"));
input_pool.insert(utxo.r#ref.clone(), utxo);
}
}
let compiler = value["compiler"].clone();
assert!(
!compiler.is_null(),
"golden fixture '{name}' is missing compiler — regenerate the dump"
);
let expected_hash = value["last_eval"]["hash"]
.as_str()
.expect("last_eval.hash should be a hex string")
.to_string();
let expected_fee = value["last_eval"]["fee"]
.as_u64()
.expect("last_eval.fee should be a number");
ResolveFixture {
tir,
args,
input_pool,
compiler,
expected_hash,
expected_fee,
}
}
fn golden_fixtures() -> Vec<String> {
let dir = format!("{}/tests/golden", env!("CARGO_MANIFEST_DIR"));
let mut names = Vec::new();
for entry in std::fs::read_dir(&dir).expect("golden directory should exist") {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("json") {
let stem = path.file_stem().unwrap().to_str().unwrap().to_string();
names.push(stem);
}
}
names.sort();
names
}
struct DumpStore {
pool: HashMap<UtxoRef, Utxo>,
}
impl From<HashMap<UtxoRef, Utxo>> for DumpStore {
fn from(pool: HashMap<UtxoRef, Utxo>) -> Self {
Self { pool }
}
}
impl DumpStore {
fn matches_pattern(utxo: &Utxo, pattern: &UtxoPattern<'_>) -> bool {
match pattern {
UtxoPattern::ByAddress(addr) => utxo.address == *addr,
UtxoPattern::ByAssetPolicy(policy) => utxo
.assets
.iter()
.any(|(class, _)| matches!(class, AssetClass::Defined(p, _) if p == policy)),
UtxoPattern::ByAsset(policy, name) => utxo.assets.asset_amount2(policy, name).is_some(),
}
}
}
impl UtxoStore for DumpStore {
async fn narrow_refs(&self, pattern: UtxoPattern<'_>) -> Result<HashSet<UtxoRef>, Error> {
let refs = self
.pool
.iter()
.filter(|(_, utxo)| Self::matches_pattern(utxo, &pattern))
.map(|(r, _)| r.clone())
.collect();
Ok(refs)
}
async fn fetch_utxos(&self, refs: HashSet<UtxoRef>) -> Result<UtxoSet, Error> {
Ok(refs
.iter()
.filter_map(|r| self.pool.get(r).cloned())
.collect())
}
}
#[pollster::test]
async fn golden_resolve_jobs() {
let fixtures = golden_fixtures();
if fixtures.is_empty() {
eprintln!("no golden dump files found in tests/golden/, skipping");
return;
}
for name in &fixtures {
let fixture = load_fixture(name);
let store = DumpStore::from(fixture.input_pool);
let mut compiler = compiler_from_json(&fixture.compiler);
let compiled = resolve_tx(fixture.tir, &fixture.args, &mut compiler, &store, 10)
.await
.unwrap_or_else(|e| panic!("resolve_tx failed for '{name}': {e}"));
dbg!(hex::encode(compiled.payload));
assert_eq!(
hex::encode(&compiled.hash),
fixture.expected_hash,
"hash mismatch for '{name}'"
);
assert_eq!(
compiled.fee, fixture.expected_fee,
"fee mismatch for '{name}'"
);
}
}