whisky-csl 1.0.13

Wrapper around the cardano-serialization-lib for easier transaction building, heavily inspired by cardano-cli APIs
Documentation
use cardano_serialization_lib::{self as csl};
use pallas_codec::minicbor::data::Tag;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use whisky_common::{
    Budget, DatumSource, InlineDatumSource, InlineScriptSource, InlineSimpleScriptSource,
    LanguageVersion, ProvidedDatumSource, ProvidedScriptSource, ProvidedSimpleScriptSource,
    Redeemer, RefTxIn, UTxO, UtxoInput, WError,
};

use pallas_codec::minicbor::Encoder;

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ScriptWitness {
    pub datums: HashMap<String, DatumSource>,
    pub redeemers: HashMap<RedeemerIndex, Redeemer>,
    pub scripts: HashMap<csl::ScriptHash, Script>,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ParserContext {
    pub resolved_utxos: HashMap<csl::TransactionInput, UTxO>,
    pub script_witness: ScriptWitness,
}

impl ParserContext {
    pub fn new() -> Self {
        Self {
            resolved_utxos: HashMap::new(),
            script_witness: ScriptWitness {
                datums: HashMap::new(),
                redeemers: HashMap::new(),
                scripts: HashMap::new(),
            },
        }
    }

    pub fn fill_resolved_utxos(
        &mut self,
        tx_body: &csl::TransactionBody,
        resolved_utxos: &[UTxO],
    ) -> Result<(), String> {
        let inputs = tx_body.inputs();
        let ref_inputs = tx_body.reference_inputs();
        let collateral_inputs = tx_body.collateral();
        let all_transaction_inputs: Vec<csl::TransactionInput> = inputs
            .into_iter()
            .chain(
                ref_inputs
                    .unwrap_or(csl::TransactionInputs::new())
                    .into_iter(),
            )
            .chain(
                collateral_inputs
                    .unwrap_or(csl::TransactionInputs::new())
                    .into_iter(),
            )
            .map(|input| input.clone())
            .collect();

        let utxo_map = resolved_utxos
            .iter()
            .map(|utxo| (utxo.input.clone(), utxo.clone()))
            .collect::<HashMap<UtxoInput, UTxO>>();

        for input in all_transaction_inputs {
            let tx_hash = input.transaction_id().to_hex();
            let index = input.index();
            let utxo = utxo_map.get(&UtxoInput {
                tx_hash,
                output_index: index,
            });
            if let Some(utxo) = utxo {
                self.resolved_utxos.insert(input, utxo.clone());
            }
        }
        Ok(())
    }

    pub fn collect_script_witnesses_from_tx_witnesses_set(
        &mut self,
        tx_witnesses_set: csl::TransactionWitnessSet,
    ) -> Result<(), String> {
        let mut datums = HashMap::new();
        let mut redeemers = HashMap::new();
        let mut scripts = HashMap::new();

        if let Some(plutus_data_list) = tx_witnesses_set.plutus_data() {
            for i in 0..plutus_data_list.len() {
                let plutus_data = plutus_data_list.get(i);
                let data_hash = csl::hash_plutus_data(&plutus_data).to_hex();
                let datum_source = DatumSource::ProvidedDatumSource(ProvidedDatumSource {
                    data: plutus_data.to_hex(),
                });
                datums.insert(data_hash, datum_source);
            }
        }

        if let Some(redeemer_list) = tx_witnesses_set.redeemers() {
            for i in 0..redeemer_list.len() {
                let redeemer = redeemer_list.get(i);
                let tag = redeemer.tag().kind();
                let index = redeemer
                    .index()
                    .to_string()
                    .parse::<usize>()
                    .map_err(|e| format!("Failed to parse redeemer index: {:?}", e))?;
                let redeemer_index = match tag {
                    csl::RedeemerTagKind::Spend => RedeemerIndex::Spend(index),
                    csl::RedeemerTagKind::Mint => RedeemerIndex::Mint(index),
                    csl::RedeemerTagKind::Cert => RedeemerIndex::Cert(index),
                    csl::RedeemerTagKind::Reward => RedeemerIndex::Reward(index),
                    csl::RedeemerTagKind::Vote => RedeemerIndex::Vote(index),
                    csl::RedeemerTagKind::VotingProposal => RedeemerIndex::VotingProposal(index),
                };
                let whisky_redeemer = csl_redeemer_to_redeemer(redeemer);
                redeemers.insert(redeemer_index, whisky_redeemer);
            }
        }

        if let Some(native_scripts) = tx_witnesses_set.native_scripts() {
            for i in 0..native_scripts.len() {
                let script = native_scripts.get(i);
                let script_hash = script.hash();
                scripts.insert(
                    script_hash,
                    Script::ProvidedNative(csl_native_script_to_native_script(script)),
                );
            }
        }

        if let Some(plutus_scripts) = tx_witnesses_set.plutus_scripts() {
            for i in 0..plutus_scripts.len() {
                let script = plutus_scripts.get(i);
                let script_hash = script.hash();
                scripts.insert(
                    script_hash,
                    Script::ProvidedPlutus(csl_plutus_script_to_script(script)),
                );
            }
        }

        self.script_witness.datums.extend(datums);
        self.script_witness.redeemers.extend(redeemers);
        self.script_witness.scripts.extend(scripts);
        Ok(())
    }

    pub fn collect_script_witnesses_from_tx_body(
        &mut self,
        tx_body: csl::TransactionBody,
    ) -> Result<(), WError> {
        let inputs = tx_body.inputs();
        let ref_inputs = tx_body.reference_inputs();
        let all_transaction_inputs: Vec<csl::TransactionInput> = inputs
            .into_iter()
            .chain(
                ref_inputs
                    .unwrap_or(csl::TransactionInputs::new())
                    .into_iter(),
            )
            .map(|input| input.clone())
            .collect();

        for input in all_transaction_inputs {
            let utxo = self.resolved_utxos.get(&input).unwrap();
            let (simple_script_source, plutus_script_source, datum_source) =
                utxo_to_inline_sources(utxo).map_err(WError::from_err("utxo_to_inline_sources"))?;
            if let Some((datum_source, datum_hash)) = datum_source {
                self.script_witness
                    .datums
                    .insert(datum_hash, DatumSource::InlineDatumSource(datum_source));
            }
            if let Some((script_source, script_hash)) = simple_script_source {
                self.script_witness
                    .scripts
                    .insert(script_hash, Script::ReferencedNative(script_source));
            }
            if let Some((script_source, script_hash)) = plutus_script_source {
                self.script_witness
                    .scripts
                    .insert(script_hash, Script::ReferencedPlutus(script_source));
            }
        }

        Ok(())
    }
}

impl ScriptWitness {
    pub fn new() -> Self {
        Self {
            datums: HashMap::new(),
            redeemers: HashMap::new(),
            scripts: HashMap::new(),
        }
    }

    pub fn extend(&mut self, other: ScriptWitness) {
        self.datums.extend(other.datums);
        self.redeemers.extend(other.redeemers);
        self.scripts.extend(other.scripts);
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RedeemerIndex {
    Spend(usize),
    Mint(usize),
    Cert(usize),
    Reward(usize),
    Vote(usize),
    VotingProposal(usize),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Script {
    ProvidedNative(ProvidedSimpleScriptSource),
    ProvidedPlutus(ProvidedScriptSource),
    ReferencedNative(InlineSimpleScriptSource),
    ReferencedPlutus(InlineScriptSource),
}

fn csl_redeemer_to_redeemer(redeemer: csl::Redeemer) -> Redeemer {
    Redeemer {
        data: redeemer.data().to_hex(),
        ex_units: Budget {
            mem: redeemer.ex_units().mem().to_str().parse::<u64>().unwrap(),
            steps: redeemer.ex_units().steps().to_str().parse::<u64>().unwrap(),
        },
    }
}

fn csl_plutus_script_to_script(script: csl::PlutusScript) -> ProvidedScriptSource {
    ProvidedScriptSource {
        script_cbor: script.to_hex(),
        language_version: csl_language_version_to_language_version(
            script.language_version().kind(),
        ),
    }
}

fn csl_native_script_to_native_script(script: csl::NativeScript) -> ProvidedSimpleScriptSource {
    ProvidedSimpleScriptSource {
        script_cbor: script.to_hex(),
    }
}

fn utxo_to_inline_sources(
    utxo: &UTxO,
) -> Result<
    (
        Option<(InlineSimpleScriptSource, csl::ScriptHash)>,
        Option<(InlineScriptSource, csl::ScriptHash)>,
        Option<(InlineDatumSource, String)>,
    ),
    String,
> {
    let csl_script_ref = if let Some(script_ref) = &utxo.output.script_ref {
        Some(normalize_script_ref(script_ref, &utxo.input)?)
    } else {
        None
    };

    let script_size = utxo
        .output
        .script_ref
        .as_ref()
        .map_or(0, |script_ref| script_ref.len() / 2);

    let ref_tx_in = RefTxIn {
        tx_hash: utxo.input.tx_hash.clone(),
        tx_index: utxo.input.output_index,
        script_size: Some(script_size),
    };

    let simple_script_source = if let Some(ref csl_script_ref) = csl_script_ref {
        if csl_script_ref.is_native_script() {
            let simple_script_hash = get_script_hash_from_script_ref(csl_script_ref);
            Some((
                InlineSimpleScriptSource {
                    ref_tx_in: ref_tx_in.clone(),
                    simple_script_hash: simple_script_hash.to_hex(),
                    script_size,
                },
                simple_script_hash,
            ))
        } else {
            None
        }
    } else {
        None
    };

    let plutus_script_source = if let Some(ref csl_script_ref) = csl_script_ref {
        if csl_script_ref.is_plutus_script() {
            let plutus_script = csl_script_ref.plutus_script().unwrap();

            let script_hash = get_script_hash_from_script_ref(csl_script_ref);
            Some((
                InlineScriptSource {
                    ref_tx_in: ref_tx_in.clone(),
                    script_hash: script_hash.to_hex(),
                    script_size,
                    language_version: csl_language_version_to_language_version(
                        plutus_script.language_version().kind(),
                    ),
                },
                script_hash,
            ))
        } else {
            None
        }
    } else {
        None
    };

    let datum_source = if let Some(datum) = &utxo.output.plutus_data {
        let data_hash = get_datum_hash_from_datum(datum, &utxo.output.data_hash)?;
        Some((
            InlineDatumSource {
                tx_hash: utxo.input.tx_hash.clone(),
                tx_index: utxo.input.output_index,
            },
            data_hash.to_hex(),
        ))
    } else {
        None
    };

    Ok((simple_script_source, plutus_script_source, datum_source))
}

fn get_script_hash_from_script_ref(script_ref: &csl::ScriptRef) -> csl::ScriptHash {
    if let Some(plutus_script) = script_ref.plutus_script() {
        plutus_script.hash()
    } else {
        script_ref.native_script().unwrap().hash()
    }
}

fn get_datum_hash_from_datum(
    datum: &String,
    datum_hash: &Option<String>,
) -> Result<csl::DataHash, String> {
    if let Some(datum_hash) = datum_hash {
        csl::DataHash::from_hex(datum_hash)
            .map_err(|e| format!("Failed to parse datum hash: {:?}", e))
    } else {
        let datum = csl::PlutusData::from_hex(datum)
            .map_err(|e| format!("Failed to parse datum: {:?}", e))?;
        Ok(csl::hash_plutus_data(&datum))
    }
}

fn csl_language_version_to_language_version(
    language_version: csl::LanguageKind,
) -> LanguageVersion {
    match language_version {
        csl::LanguageKind::PlutusV1 => LanguageVersion::V1,
        csl::LanguageKind::PlutusV2 => LanguageVersion::V2,
        csl::LanguageKind::PlutusV3 => LanguageVersion::V3,
    }
}

fn normalize_script_ref(
    script_ref: &String,
    tx_input: &UtxoInput,
) -> Result<csl::ScriptRef, String> {
    if script_ref.starts_with("82") {
        let bytes = hex::decode(script_ref.clone())
            .map_err(|e| format!("Failed to decode script ref hex: {:?}", e))?;
        let mut encoder = Encoder::new(Vec::new());
        encoder
            .tag(Tag::new(24))
            .map_err(|_| "Failed to write tag")?;
        encoder
            .bytes(&bytes)
            .map_err(|e| format!("Failed to encode script ref bytes: {:?}", e))?;
        let write_buffer = encoder.writer().clone();
        csl::ScriptRef::from_bytes(write_buffer)
            .map_err(|e| format!("Failed to decode script ref hex: {:?}", e))
    } else {
        csl::ScriptRef::from_hex(&script_ref).map_err(|e| {
            format!(
                "Failed to parse script ref: {:?} - {}#{} - with ref: {:?}",
                e, tx_input.tx_hash, tx_input.output_index, script_ref
            )
        })
    }
}