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 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}