whisky_csl/tx_parser/
context.rs

1use cardano_serialization_lib::{self as csl};
2use pallas_codec::minicbor::data::Tag;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use whisky_common::{
6    Budget, DatumSource, InlineDatumSource, InlineScriptSource, InlineSimpleScriptSource,
7    LanguageVersion, ProvidedDatumSource, ProvidedScriptSource, ProvidedSimpleScriptSource,
8    Redeemer, RefTxIn, UTxO, UtxoInput, WError,
9};
10
11use pallas_codec::minicbor::Encoder;
12
13#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
14pub struct ScriptWitness {
15    pub datums: HashMap<String, DatumSource>,
16    pub redeemers: HashMap<RedeemerIndex, Redeemer>,
17    pub scripts: HashMap<csl::ScriptHash, Script>,
18}
19
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21pub struct ParserContext {
22    pub resolved_utxos: HashMap<csl::TransactionInput, UTxO>,
23    pub script_witness: ScriptWitness,
24}
25
26impl ParserContext {
27    pub fn new() -> Self {
28        Self {
29            resolved_utxos: HashMap::new(),
30            script_witness: ScriptWitness {
31                datums: HashMap::new(),
32                redeemers: HashMap::new(),
33                scripts: HashMap::new(),
34            },
35        }
36    }
37
38    pub fn fill_resolved_utxos(
39        &mut self,
40        tx_body: &csl::TransactionBody,
41        resolved_utxos: &[UTxO],
42    ) -> Result<(), String> {
43        let inputs = tx_body.inputs();
44        let ref_inputs = tx_body.reference_inputs();
45        let collateral_inputs = tx_body.collateral();
46        let all_transaction_inputs: Vec<csl::TransactionInput> = inputs
47            .into_iter()
48            .chain(
49                ref_inputs
50                    .unwrap_or(csl::TransactionInputs::new())
51                    .into_iter(),
52            )
53            .chain(
54                collateral_inputs
55                    .unwrap_or(csl::TransactionInputs::new())
56                    .into_iter(),
57            )
58            .map(|input| input.clone())
59            .collect();
60
61        let utxo_map = resolved_utxos
62            .iter()
63            .map(|utxo| (utxo.input.clone(), utxo.clone()))
64            .collect::<HashMap<UtxoInput, UTxO>>();
65
66        for input in all_transaction_inputs {
67            let tx_hash = input.transaction_id().to_hex();
68            let index = input.index();
69            let utxo = utxo_map.get(&UtxoInput {
70                tx_hash,
71                output_index: index,
72            });
73            if let Some(utxo) = utxo {
74                self.resolved_utxos.insert(input, utxo.clone());
75            }
76        }
77        Ok(())
78    }
79
80    pub fn collect_script_witnesses_from_tx_witnesses_set(
81        &mut self,
82        tx_witnesses_set: csl::TransactionWitnessSet,
83    ) -> Result<(), String> {
84        let mut datums = HashMap::new();
85        let mut redeemers = HashMap::new();
86        let mut scripts = HashMap::new();
87
88        if let Some(plutus_data_list) = tx_witnesses_set.plutus_data() {
89            for i in 0..plutus_data_list.len() {
90                let plutus_data = plutus_data_list.get(i);
91                let data_hash = csl::hash_plutus_data(&plutus_data).to_hex();
92                let datum_source = DatumSource::ProvidedDatumSource(ProvidedDatumSource {
93                    data: plutus_data.to_hex(),
94                });
95                datums.insert(data_hash, datum_source);
96            }
97        }
98
99        if let Some(redeemer_list) = tx_witnesses_set.redeemers() {
100            for i in 0..redeemer_list.len() {
101                let redeemer = redeemer_list.get(i);
102                let tag = redeemer.tag().kind();
103                let index = redeemer
104                    .index()
105                    .to_string()
106                    .parse::<usize>()
107                    .map_err(|e| format!("Failed to parse redeemer index: {:?}", e))?;
108                let redeemer_index = match tag {
109                    csl::RedeemerTagKind::Spend => RedeemerIndex::Spend(index),
110                    csl::RedeemerTagKind::Mint => RedeemerIndex::Mint(index),
111                    csl::RedeemerTagKind::Cert => RedeemerIndex::Cert(index),
112                    csl::RedeemerTagKind::Reward => RedeemerIndex::Reward(index),
113                    csl::RedeemerTagKind::Vote => RedeemerIndex::Vote(index),
114                    csl::RedeemerTagKind::VotingProposal => RedeemerIndex::VotingProposal(index),
115                };
116                let whisky_redeemer = csl_redeemer_to_redeemer(redeemer);
117                redeemers.insert(redeemer_index, whisky_redeemer);
118            }
119        }
120
121        if let Some(native_scripts) = tx_witnesses_set.native_scripts() {
122            for i in 0..native_scripts.len() {
123                let script = native_scripts.get(i);
124                let script_hash = script.hash();
125                scripts.insert(
126                    script_hash,
127                    Script::ProvidedNative(csl_native_script_to_native_script(script)),
128                );
129            }
130        }
131
132        if let Some(plutus_scripts) = tx_witnesses_set.plutus_scripts() {
133            for i in 0..plutus_scripts.len() {
134                let script = plutus_scripts.get(i);
135                let script_hash = script.hash();
136                scripts.insert(
137                    script_hash,
138                    Script::ProvidedPlutus(csl_plutus_script_to_script(script)),
139                );
140            }
141        }
142
143        self.script_witness.datums.extend(datums);
144        self.script_witness.redeemers.extend(redeemers);
145        self.script_witness.scripts.extend(scripts);
146        Ok(())
147    }
148
149    pub fn collect_script_witnesses_from_tx_body(
150        &mut self,
151        tx_body: csl::TransactionBody,
152    ) -> Result<(), WError> {
153        let inputs = tx_body.inputs();
154        let ref_inputs = tx_body.reference_inputs();
155        let all_transaction_inputs: Vec<csl::TransactionInput> = inputs
156            .into_iter()
157            .chain(
158                ref_inputs
159                    .unwrap_or(csl::TransactionInputs::new())
160                    .into_iter(),
161            )
162            .map(|input| input.clone())
163            .collect();
164
165        for input in all_transaction_inputs {
166            let utxo = self.resolved_utxos.get(&input).unwrap();
167            let (simple_script_source, plutus_script_source, datum_source) =
168                utxo_to_inline_sources(utxo).map_err(WError::from_err("utxo_to_inline_sources"))?;
169            if let Some((datum_source, datum_hash)) = datum_source {
170                self.script_witness
171                    .datums
172                    .insert(datum_hash, DatumSource::InlineDatumSource(datum_source));
173            }
174            if let Some((script_source, script_hash)) = simple_script_source {
175                self.script_witness
176                    .scripts
177                    .insert(script_hash, Script::ReferencedNative(script_source));
178            }
179            if let Some((script_source, script_hash)) = plutus_script_source {
180                self.script_witness
181                    .scripts
182                    .insert(script_hash, Script::ReferencedPlutus(script_source));
183            }
184        }
185
186        Ok(())
187    }
188}
189
190impl ScriptWitness {
191    pub fn new() -> Self {
192        Self {
193            datums: HashMap::new(),
194            redeemers: HashMap::new(),
195            scripts: HashMap::new(),
196        }
197    }
198
199    pub fn extend(&mut self, other: ScriptWitness) {
200        self.datums.extend(other.datums);
201        self.redeemers.extend(other.redeemers);
202        self.scripts.extend(other.scripts);
203    }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
207pub enum RedeemerIndex {
208    Spend(usize),
209    Mint(usize),
210    Cert(usize),
211    Reward(usize),
212    Vote(usize),
213    VotingProposal(usize),
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub enum Script {
218    ProvidedNative(ProvidedSimpleScriptSource),
219    ProvidedPlutus(ProvidedScriptSource),
220    ReferencedNative(InlineSimpleScriptSource),
221    ReferencedPlutus(InlineScriptSource),
222}
223
224fn csl_redeemer_to_redeemer(redeemer: csl::Redeemer) -> Redeemer {
225    Redeemer {
226        data: redeemer.data().to_hex(),
227        ex_units: Budget {
228            mem: redeemer.ex_units().mem().to_str().parse::<u64>().unwrap(),
229            steps: redeemer.ex_units().steps().to_str().parse::<u64>().unwrap(),
230        },
231    }
232}
233
234fn csl_plutus_script_to_script(script: csl::PlutusScript) -> ProvidedScriptSource {
235    ProvidedScriptSource {
236        script_cbor: script.to_hex(),
237        language_version: csl_language_version_to_language_version(
238            script.language_version().kind(),
239        ),
240    }
241}
242
243fn csl_native_script_to_native_script(script: csl::NativeScript) -> ProvidedSimpleScriptSource {
244    ProvidedSimpleScriptSource {
245        script_cbor: script.to_hex(),
246    }
247}
248
249fn utxo_to_inline_sources(
250    utxo: &UTxO,
251) -> Result<
252    (
253        Option<(InlineSimpleScriptSource, csl::ScriptHash)>,
254        Option<(InlineScriptSource, csl::ScriptHash)>,
255        Option<(InlineDatumSource, String)>,
256    ),
257    String,
258> {
259    let csl_script_ref = if let Some(script_ref) = &utxo.output.script_ref {
260        Some(normalize_script_ref(script_ref, &utxo.input)?)
261    } else {
262        None
263    };
264
265    let script_size = utxo
266        .output
267        .script_ref
268        .as_ref()
269        .map_or(0, |script_ref| script_ref.len() / 2);
270
271    let ref_tx_in = RefTxIn {
272        tx_hash: utxo.input.tx_hash.clone(),
273        tx_index: utxo.input.output_index,
274        script_size: Some(script_size),
275    };
276
277    let simple_script_source = if let Some(ref csl_script_ref) = csl_script_ref {
278        if csl_script_ref.is_native_script() {
279            let simple_script_hash = get_script_hash_from_script_ref(csl_script_ref);
280            Some((
281                InlineSimpleScriptSource {
282                    ref_tx_in: ref_tx_in.clone(),
283                    simple_script_hash: simple_script_hash.to_hex(),
284                    script_size,
285                },
286                simple_script_hash,
287            ))
288        } else {
289            None
290        }
291    } else {
292        None
293    };
294
295    let plutus_script_source = if let Some(ref csl_script_ref) = csl_script_ref {
296        if csl_script_ref.is_plutus_script() {
297            let plutus_script = csl_script_ref.plutus_script().unwrap();
298
299            let script_hash = get_script_hash_from_script_ref(csl_script_ref);
300            Some((
301                InlineScriptSource {
302                    ref_tx_in: ref_tx_in.clone(),
303                    script_hash: script_hash.to_hex(),
304                    script_size,
305                    language_version: csl_language_version_to_language_version(
306                        plutus_script.language_version().kind(),
307                    ),
308                },
309                script_hash,
310            ))
311        } else {
312            None
313        }
314    } else {
315        None
316    };
317
318    let datum_source = if let Some(datum) = &utxo.output.plutus_data {
319        let data_hash = get_datum_hash_from_datum(datum, &utxo.output.data_hash)?;
320        Some((
321            InlineDatumSource {
322                tx_hash: utxo.input.tx_hash.clone(),
323                tx_index: utxo.input.output_index,
324            },
325            data_hash.to_hex(),
326        ))
327    } else {
328        None
329    };
330
331    Ok((simple_script_source, plutus_script_source, datum_source))
332}
333
334fn get_script_hash_from_script_ref(script_ref: &csl::ScriptRef) -> csl::ScriptHash {
335    if let Some(plutus_script) = script_ref.plutus_script() {
336        plutus_script.hash()
337    } else {
338        script_ref.native_script().unwrap().hash()
339    }
340}
341
342fn get_datum_hash_from_datum(
343    datum: &String,
344    datum_hash: &Option<String>,
345) -> Result<csl::DataHash, String> {
346    if let Some(datum_hash) = datum_hash {
347        csl::DataHash::from_hex(datum_hash)
348            .map_err(|e| format!("Failed to parse datum hash: {:?}", e))
349    } else {
350        let datum = csl::PlutusData::from_hex(datum)
351            .map_err(|e| format!("Failed to parse datum: {:?}", e))?;
352        Ok(csl::hash_plutus_data(&datum))
353    }
354}
355
356fn csl_language_version_to_language_version(
357    language_version: csl::LanguageKind,
358) -> LanguageVersion {
359    match language_version {
360        csl::LanguageKind::PlutusV1 => LanguageVersion::V1,
361        csl::LanguageKind::PlutusV2 => LanguageVersion::V2,
362        csl::LanguageKind::PlutusV3 => LanguageVersion::V3,
363    }
364}
365
366fn normalize_script_ref(
367    script_ref: &String,
368    tx_input: &UtxoInput,
369) -> Result<csl::ScriptRef, String> {
370    if script_ref.starts_with("82") {
371        let bytes = hex::decode(script_ref.clone())
372            .map_err(|e| format!("Failed to decode script ref hex: {:?}", e))?;
373        let mut encoder = Encoder::new(Vec::new());
374        encoder
375            .tag(Tag::new(24))
376            .map_err(|_| "Failed to write tag")?;
377        encoder
378            .bytes(&bytes)
379            .map_err(|e| format!("Failed to encode script ref bytes: {:?}", e))?;
380        let write_buffer = encoder.writer().clone();
381        csl::ScriptRef::from_bytes(write_buffer)
382            .map_err(|e| format!("Failed to decode script ref hex: {:?}", e))
383    } else {
384        csl::ScriptRef::from_hex(&script_ref).map_err(|e| {
385            format!(
386                "Failed to parse script ref: {:?} - {}#{} - with ref: {:?}",
387                e, tx_input.tx_hash, tx_input.output_index, script_ref
388            )
389        })
390    }
391}