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