whisky_csl/utils/
evaluator.rs

1use super::phase_two::{eval_phase_two, PhaseTwoEvalResult};
2use crate::*;
3use cardano_serialization_lib::{self as csl};
4use pallas_codec::utils::NonEmptyKeyValuePairs;
5use pallas_codec::utils::{Bytes, CborWrap, PositiveCoin};
6use pallas_primitives::conway::{Redeemer, RedeemerTag as PRedeemerTag};
7use pallas_primitives::{
8    conway::{
9        AssetName, Coin, CostModels, DatumOption, PlutusData, PolicyId,
10        PostAlonzoTransactionOutput, ScriptRef, TransactionOutput, Value,
11    },
12    Fragment,
13};
14use pallas_traverse::{Era, MultiEraTx};
15use std::collections::HashMap;
16use uplc::tx::SlotConfig;
17use uplc::{tx::error::Error as UplcError, tx::ResolvedInput, Hash, TransactionInput};
18use whisky_common::*;
19
20#[derive(serde::Deserialize, serde::Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct JsonSlotConfig {
23    pub slot_length: u32,
24    pub zero_slot: u64,
25    pub zero_time: u64,
26}
27
28pub fn evaluate_tx_scripts(
29    tx_hex: &str,
30    inputs: &[UTxO],
31    additional_txs: &[String],
32    network: &Network,
33    slot_config: &SlotConfig,
34) -> Result<Vec<EvalResult>, WError> {
35    let tx_bytes = hex::decode(tx_hex).expect("Invalid tx hex");
36    let mtx = MultiEraTx::decode_for_era(Era::Conway, &tx_bytes);
37    let tx = match mtx {
38        Ok(MultiEraTx::Conway(tx)) => tx.into_owned(),
39        Ok(_) => {
40            return Err(WError::new(
41                "evaluate_tx_scripts - Invalid Tx Era",
42                "Expected Conway era transaction",
43            ))
44        }
45        Err(err) => {
46            return Err(WError::new(
47                "evaluate_tx_scripts - decode_for_era",
48                &format!("{:?}", err),
49            ))
50        }
51    };
52
53    let tx_outs: Vec<UTxO> = additional_txs
54        .iter()
55        .flat_map(|tx| {
56            let parsed_tx = TxParser::new(tx).unwrap();
57            println!(
58                "txout: {:?}",
59                &parsed_tx.get_tx_outs_utxo().unwrap().clone()
60            );
61            println!("txout_cbor: {:?}", &parsed_tx.get_tx_outs_cbor().clone());
62            parsed_tx.get_tx_outs_utxo().unwrap() // TODO: err handling
63        })
64        .collect();
65
66    // combine inputs and tx_outs
67    let all_inputs: Vec<UTxO> = inputs.iter().chain(tx_outs.iter()).cloned().collect();
68
69    eval_phase_two(
70        &tx,
71        &to_pallas_utxos(&all_inputs)?,
72        Some(&get_cost_mdls(network)?),
73        slot_config,
74    )
75    .map_err(|err| {
76        WError::new(
77            "evaluate_tx_scripts",
78            &format!("Error occurred during evaluation: {}", err),
79        )
80    })
81    .map(|reds| reds.into_iter().map(map_eval_result).collect())
82}
83
84pub fn map_eval_result(result: PhaseTwoEvalResult) -> EvalResult {
85    match result {
86        PhaseTwoEvalResult::Success(redeemer) => {
87            EvalResult::Success(map_redeemer_to_action(redeemer))
88        }
89        PhaseTwoEvalResult::Error(redeemer, err) => {
90            EvalResult::Error(map_error_to_eval_error(err, redeemer))
91        }
92    }
93}
94
95pub fn map_error_to_eval_error(err: UplcError, original_redeemer: Redeemer) -> EvalError {
96    match err {
97        UplcError::Machine(err, budget, logs) => EvalError {
98            index: original_redeemer.index,
99            budget: Budget {
100                mem: budget.mem as u64,
101                steps: budget.cpu as u64,
102            },
103            tag: map_redeemer_tag(&original_redeemer.tag),
104            error_message: format!("{}", err),
105            logs,
106        },
107        UplcError::RedeemerError { err, .. } => match *err {
108            UplcError::Machine(err, budget, logs) => EvalError {
109                index: original_redeemer.index,
110                budget: Budget {
111                    mem: budget.mem as u64,
112                    steps: budget.cpu as u64,
113                },
114                tag: map_redeemer_tag(&original_redeemer.tag),
115                error_message: format!("{}", err),
116                logs,
117            },
118            _ => EvalError {
119                index: original_redeemer.index,
120                budget: Budget { mem: 0, steps: 0 },
121                tag: map_redeemer_tag(&original_redeemer.tag),
122                error_message: format!("{}", err),
123                logs: vec![],
124            },
125        },
126        _ => EvalError {
127            index: original_redeemer.index,
128            budget: Budget { mem: 0, steps: 0 },
129            tag: map_redeemer_tag(&original_redeemer.tag),
130            error_message: format!("{}", err),
131            logs: vec![],
132        },
133    }
134}
135
136pub fn map_redeemer_to_action(redeemer: Redeemer) -> Action {
137    Action {
138        index: redeemer.index,
139        budget: Budget {
140            mem: redeemer.ex_units.mem,
141            steps: redeemer.ex_units.steps,
142        },
143        tag: map_redeemer_tag(&redeemer.tag),
144    }
145}
146
147pub fn map_redeemer_tag(tag: &PRedeemerTag) -> RedeemerTag {
148    match tag {
149        PRedeemerTag::Spend => RedeemerTag::Spend,
150        PRedeemerTag::Mint => RedeemerTag::Mint,
151        PRedeemerTag::Cert => RedeemerTag::Cert,
152        PRedeemerTag::Reward => RedeemerTag::Reward,
153        PRedeemerTag::Vote => RedeemerTag::Vote,
154        PRedeemerTag::Propose => RedeemerTag::Propose,
155    }
156}
157
158pub fn get_cost_mdls(network: &Network) -> Result<CostModels, WError> {
159    let cost_model_list = get_cost_models_from_network(network);
160    if cost_model_list.len() < 3 {
161        return Err(WError::new(
162            "get_cost_mdls",
163            "Cost models have to contain at least PlutusV1, PlutusV2, and PlutusV3 costs",
164        ));
165    };
166    Ok(CostModels {
167        plutus_v1: Some(cost_model_list[0].clone()),
168        plutus_v2: Some(cost_model_list[1].clone()),
169        plutus_v3: Some(cost_model_list[2].clone()),
170    })
171}
172
173pub fn to_pallas_utxos(utxos: &Vec<UTxO>) -> Result<Vec<ResolvedInput>, WError> {
174    let mut resolved_inputs = Vec::new();
175    for utxo in utxos {
176        let tx_hash: [u8; 32] = hex::decode(&utxo.input.tx_hash)
177            .map_err(|err| {
178                WError::new(
179                    "to_pallas_utxos",
180                    &format!("Invalid tx hash found: {}", err),
181                )
182            })?
183            .try_into()
184            .map_err(|_e| WError::new("to_pallas_utxos", "Invalid tx hash length found"))?;
185
186        let resolved_input = ResolvedInput {
187            input: TransactionInput {
188                transaction_id: Hash::from(tx_hash),
189                index: utxo.input.output_index.into(),
190            },
191            output: TransactionOutput::PostAlonzo(PostAlonzoTransactionOutput {
192                address: Bytes::from(
193                    csl::Address::from_bech32(&utxo.output.address)
194                        .map_err(|err| {
195                            WError::new(
196                                "to_pallas_utxos",
197                                &format!("Invalid address found: {:?}", err),
198                            )
199                        })?
200                        .to_bytes(),
201                ),
202                value: to_pallas_value(&utxo.output.amount)
203                    .map_err(WError::add_err_trace("to_pallas_utxos"))?,
204                datum_option: to_pallas_datum(&utxo.output)
205                    .map_err(WError::add_err_trace("to_pallas_utxos"))?,
206                script_ref: to_pallas_script_ref(&utxo.output.script_ref)
207                    .map_err(WError::add_err_trace("to_pallas_utxos"))?,
208            }),
209        };
210        resolved_inputs.push(resolved_input);
211    }
212    Ok(resolved_inputs)
213}
214
215pub fn to_pallas_script_ref(
216    script_ref: &Option<String>,
217) -> Result<Option<CborWrap<ScriptRef>>, WError> {
218    if let Some(script_ref) = script_ref {
219        let script_bytes = hex::decode(script_ref).map_err(WError::from_err(
220            "to_pallas_script_ref - Invalid script ref hex",
221        ))?;
222
223        let pallas_script = ScriptRef::decode_fragment(&script_bytes).map_err(WError::from_err(
224            "to_pallas_script_ref - Invalid script ref bytes",
225        ))?;
226
227        Ok(Some(CborWrap(pallas_script)))
228    } else {
229        Ok(None)
230    }
231}
232
233pub fn to_pallas_datum(utxo_output: &UtxoOutput) -> Result<Option<DatumOption>, WError> {
234    if let Some(inline_datum) = &utxo_output.plutus_data {
235        //hex to bytes
236        let plutus_data_bytes = hex::decode(inline_datum).map_err(WError::from_err(
237            "to_pallas_datum - Invalid plutus data hex",
238        ))?;
239        let datum = CborWrap(PlutusData::decode_fragment(&plutus_data_bytes).map_err(
240            WError::from_err("to_pallas_datum - Invalid plutus data bytes"),
241        )?);
242        Ok(Some(DatumOption::Data(datum)))
243    } else if let Some(datum_hash) = &utxo_output.data_hash {
244        let datum_hash_bytes: [u8; 32] = hex::decode(datum_hash)
245            .map_err(WError::from_err("to_pallas_datum - Invalid datum hash hex"))?
246            .try_into()
247            .map_err(|_e| {
248                WError::new("to_pallas_datum", "Invalid byte length of datum hash found")
249            })?;
250        Ok(Some(DatumOption::Hash(Hash::from(datum_hash_bytes))))
251    } else {
252        Ok(None)
253    }
254}
255
256pub fn to_pallas_value(assets: &Vec<Asset>) -> Result<Value, WError> {
257    if assets.len() == 1 {
258        match assets[0].unit().as_str() {
259            "lovelace" => Ok(Value::Coin(assets[0].quantity().parse::<u64>().unwrap())),
260            _ => Err(WError::new("to_pallas_value", "Invalid value")),
261        }
262    } else {
263        to_pallas_multi_asset_value(assets)
264    }
265}
266
267pub fn to_pallas_multi_asset_value(assets: &Vec<Asset>) -> Result<Value, WError> {
268    let mut coins: Coin = 0;
269    let mut asset_mapping: HashMap<String, Vec<(String, String)>> = HashMap::new();
270    for asset in assets {
271        if asset.unit() == "lovelace" || asset.unit().is_empty() {
272            coins = asset.quantity().parse::<u64>().unwrap();
273        } else {
274            let asset_unit = asset.unit();
275            let (policy_id, asset_name) = asset_unit.split_at(56);
276            asset_mapping
277                .entry(policy_id.to_string())
278                .or_default()
279                .push((asset_name.to_string(), asset.quantity().clone()))
280        }
281    }
282
283    let mut multi_asset = Vec::new();
284    for (policy_id, asset_list) in &asset_mapping {
285        let policy_id_bytes: [u8; 28] = hex::decode(policy_id)
286            .map_err(WError::from_err(
287                "to_pallas_multi_asset_value - Invalid policy id hex",
288            ))?
289            .try_into()
290            .map_err(|_e| {
291                WError::new(
292                    "to_pallas_multi_asset_vale",
293                    "Invalid length policy id found",
294                )
295            })?;
296
297        let policy_id = PolicyId::from(policy_id_bytes);
298        let mut mapped_assets = Vec::new();
299        for asset in asset_list {
300            let (asset_name, asset_quantity) = asset;
301            let asset_name_bytes = AssetName::from(hex::decode(asset_name).map_err(
302                WError::from_err("to_pallas_multi_asset_value - Invalid asset name hex"),
303            )?);
304            mapped_assets.push((
305                asset_name_bytes,
306                PositiveCoin::try_from(asset_quantity.parse::<u64>().unwrap()).unwrap(),
307            ));
308        }
309        multi_asset.push((policy_id, NonEmptyKeyValuePairs::Def(mapped_assets)));
310    }
311    let pallas_multi_asset = pallas_codec::utils::NonEmptyKeyValuePairs::Def(multi_asset);
312    Ok(Value::Multiasset(coins, pallas_multi_asset))
313}