whisky_pallas/utils/
evaluator.rs

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