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