From f3d9719b5dd8a58b2e16cce9d2d41fe2a59165bd Mon Sep 17 00:00:00 2001 From: Harper Date: Sun, 3 Dec 2023 12:51:06 +0000 Subject: [PATCH] feat: introduce transaction builder crate (#338) --- Cargo.toml | 1 + pallas-txbuilder/Cargo.toml | 24 + pallas-txbuilder/src/babbage.rs | 340 +++++++++ pallas-txbuilder/src/lib.rs | 33 + pallas-txbuilder/src/transaction/mod.rs | 63 ++ pallas-txbuilder/src/transaction/model.rs | 712 ++++++++++++++++++ pallas-txbuilder/src/transaction/serialise.rs | 513 +++++++++++++ pallas/Cargo.toml | 1 + pallas/src/lib.rs | 4 + 9 files changed, 1691 insertions(+) create mode 100644 pallas-txbuilder/Cargo.toml create mode 100644 pallas-txbuilder/src/babbage.rs create mode 100644 pallas-txbuilder/src/lib.rs create mode 100644 pallas-txbuilder/src/transaction/mod.rs create mode 100644 pallas-txbuilder/src/transaction/model.rs create mode 100644 pallas-txbuilder/src/transaction/serialise.rs diff --git a/Cargo.toml b/Cargo.toml index c54da963..5cb25c28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "pallas-primitives", "pallas-rolldb", "pallas-traverse", + "pallas-txbuilder", "pallas-utxorpc", "pallas", "examples/block-download", diff --git a/pallas-txbuilder/Cargo.toml b/pallas-txbuilder/Cargo.toml new file mode 100644 index 00000000..5ff9983f --- /dev/null +++ b/pallas-txbuilder/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pallas-txbuilder" +version = "0.20.0" +edition = "2021" +repository = "https://github.com/txpipe/pallas" +homepage = "https://github.com/txpipe/pallas" +documentation = "https://docs.rs/pallas-txbuilder" +license = "Apache-2.0" +readme = "README.md" +authors = [ + "Santiago Carmuega ", + "CainĂ£ Costa ", +] + +[dependencies] +pallas-codec = { path = "../pallas-codec", version = "=0.20.0" } +pallas-crypto = { path = "../pallas-crypto", version = "=0.20.0" } +pallas-primitives = { path = "../pallas-primitives", version = "=0.20.0" } +pallas-traverse = { path = "../pallas-traverse", version = "=0.20.0" } +pallas-addresses = { path = "../pallas-addresses", version = "=0.20.0" } +serde = { version = "1.0.188", features = ["derive"] } +serde_json = "1.0.107" +thiserror = "1.0.44" +hex = "0.4.3" diff --git a/pallas-txbuilder/src/babbage.rs b/pallas-txbuilder/src/babbage.rs new file mode 100644 index 00000000..24ca4464 --- /dev/null +++ b/pallas-txbuilder/src/babbage.rs @@ -0,0 +1,340 @@ +use std::ops::Deref; + +use pallas_codec::utils::{CborWrap, KeyValuePairs}; +use pallas_crypto::hash::Hash; +use pallas_primitives::{ + babbage::{ + DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, PlutusData, PlutusV1Script, + PlutusV2Script, PostAlonzoTransactionOutput, PseudoTransactionOutput, Redeemer, + RedeemerTag, Script as PallasScript, TransactionBody, TransactionInput, Tx as BabbageTx, + Value, WitnessSet, + }, + Fragment, +}; +use pallas_traverse::ComputeHash; + +use crate::{ + transaction::{ + model::{ + BuilderEra, BuiltTransaction, DatumKind, ExUnits, Output, RedeemerPurpose, ScriptKind, + StagingTransaction, + }, + opt_if_empty, Bytes, Bytes32, TransactionStatus, + }, + TxBuilderError, +}; + +pub trait BuildBabbage { + fn build_babbage_raw(self) -> Result; + + // fn build_babbage(staging_tx: StagingTransaction, resolver: (), params: ()) -> Result; +} + +impl BuildBabbage for StagingTransaction { + fn build_babbage_raw(self) -> Result { + let mut inputs = self + .inputs + .unwrap_or_default() + .iter() + .map(|x| TransactionInput { + transaction_id: x.tx_hash.0.into(), + index: x.txo_index, + }) + .collect::>(); + + inputs.sort_unstable_by_key(|x| (x.transaction_id, x.index)); + + let outputs = self + .outputs + .unwrap_or_default() + .iter() + .map(babbage_output) + .collect::, _>>()?; + + let mint: Option, KeyValuePairs<_, _>>> = + if let Some(massets) = self.mint { + Some( + massets + .deref() + .iter() + .map(|(pid, assets)| { + ( + pid.0.into(), + assets + .into_iter() + .map(|(n, x)| (n.clone().into(), *x)) + .collect::>() + .into(), + ) + }) + .collect::>() + .into(), + ) + } else { + None + }; + + let collateral = self + .collateral_inputs + .unwrap_or_default() + .iter() + .map(|x| TransactionInput { + transaction_id: x.tx_hash.0.into(), + index: x.txo_index, + }) + .collect(); + + let required_signers = self + .disclosed_signers + .unwrap_or_default() + .iter() + .map(|x| x.0.into()) + .collect(); + + let network_id = if let Some(nid) = self.network_id { + match nid { + 0 => Some(NetworkId::One), + 1 => Some(NetworkId::Two), + _ => return Err(TxBuilderError::InvalidNetworkId), + } + } else { + None + }; + + let collateral_return = self + .collateral_output + .as_ref() + .map(babbage_output) + .transpose()?; + + let reference_inputs = self + .reference_inputs + .unwrap_or_default() + .iter() + .map(|x| TransactionInput { + transaction_id: x.tx_hash.0.into(), + index: x.txo_index, + }) + .collect(); + + let (mut native_script, mut plutus_v1_script, mut plutus_v2_script) = + (vec![], vec![], vec![]); + + for (_, script) in self.scripts.unwrap_or_default() { + match script.kind { + ScriptKind::Native => { + let script = NativeScript::decode_fragment(&script.bytes.0) + .map_err(|_| TxBuilderError::MalformedScript)?; + + native_script.push(script) + } + ScriptKind::PlutusV1 => { + let script = PlutusV1Script(script.bytes.into()); + + plutus_v1_script.push(script) + } + ScriptKind::PlutusV2 => { + let script = PlutusV2Script(script.bytes.into()); + + plutus_v2_script.push(script) + } + } + } + + let plutus_data = self + .datums + .unwrap_or_default() + .iter() + .map(|x| { + PlutusData::decode_fragment(x.1.as_ref()) + .map_err(|_| TxBuilderError::MalformedDatum) + }) + .collect::, _>>()?; + + let mut mint_policies = mint + .clone() + .unwrap_or(vec![].into()) + .iter() + .map(|(p, _)| **p) + .collect::>(); + mint_policies.sort_unstable_by_key(|x| *x); + + let mut redeemers = vec![]; + + if let Some(rdmrs) = self.redeemers { + for (purpose, (pd, ex_units)) in rdmrs.deref().iter() { + let ex_units = if let Some(ExUnits { mem, steps }) = ex_units { + PallasExUnits { + mem: *mem, + steps: *steps, + } + } else { + todo!("ExUnits budget calculation not yet implement") // TODO + }; + + let data = PlutusData::decode_fragment(pd.as_ref()) + .map_err(|_| TxBuilderError::MalformedDatum)?; + + match purpose { + RedeemerPurpose::Spend(ref txin) => { + let index = inputs + .iter() + .position(|x| { + (*x.transaction_id, x.index) == (txin.tx_hash.0, txin.txo_index) + }) + .ok_or(TxBuilderError::RedeemerTargetMissing)? + as u32; + + redeemers.push(Redeemer { + tag: RedeemerTag::Spend, + index, + data, + ex_units, + }) + } + RedeemerPurpose::Mint(pid) => { + let index = mint_policies + .iter() + .position(|x| *x == pid.0) + .ok_or(TxBuilderError::RedeemerTargetMissing)? + as u32; + + redeemers.push(Redeemer { + tag: RedeemerTag::Mint, + index, + data, + ex_units, + }) + } // todo!("reward and cert redeemers not yet supported"), // TODO + } + } + }; + + let mut pallas_tx = BabbageTx { + transaction_body: TransactionBody { + inputs, + outputs, + ttl: self.invalid_from_slot, + validity_interval_start: self.valid_from_slot, + fee: self.fee.unwrap_or_default(), + certificates: None, // TODO + withdrawals: None, // TODO + update: None, // TODO + auxiliary_data_hash: None, // TODO (accept user input) + mint, + script_data_hash: self.script_data_hash.map(|x| x.0.into()), + collateral: opt_if_empty(collateral), + required_signers: opt_if_empty(required_signers), + network_id, + collateral_return, + total_collateral: None, // TODO + reference_inputs: opt_if_empty(reference_inputs), + }, + transaction_witness_set: WitnessSet { + vkeywitness: None, + native_script: opt_if_empty(native_script), + bootstrap_witness: None, + plutus_v1_script: opt_if_empty(plutus_v1_script), + plutus_v2_script: opt_if_empty(plutus_v2_script), + plutus_data: opt_if_empty(plutus_data), + redeemer: opt_if_empty(redeemers), + }, + success: true, // TODO + auxiliary_data: None.into(), // TODO + }; + + // TODO: pallas auxiliary_data_hash should be Hash<32> not Bytes + pallas_tx.transaction_body.auxiliary_data_hash = pallas_tx + .auxiliary_data + .clone() + .map(|ad| ad.compute_hash().to_vec().into()) + .into(); + + Ok(BuiltTransaction { + version: self.version, + era: BuilderEra::Babbage, + status: TransactionStatus::Built, + tx_hash: Bytes32(*pallas_tx.transaction_body.compute_hash()), + tx_bytes: Bytes(pallas_tx.encode_fragment().unwrap()), + signatures: None, + }) + } + + // fn build_babbage(staging_tx: StagingTransaction) -> Result { + // todo!() + // } +} + +fn babbage_output( + output: &Output, +) -> Result, TxBuilderError> { + let value = if let Some(ref assets) = output.assets { + let txb_assets = assets + .deref() + .iter() + .map(|(pid, assets)| { + ( + pid.0.into(), + assets + .into_iter() + .map(|(n, x)| (n.clone().into(), *x)) + .collect::>() + .into(), + ) + }) + .collect::>() + .into(); + + Value::Multiasset(output.lovelace, txb_assets) + } else { + Value::Coin(output.lovelace) + }; + + let datum_option = if let Some(ref d) = output.datum { + match d.kind { + DatumKind::Hash => { + let dh: [u8; 32] = d + .bytes + .as_ref() + .try_into() + .map_err(|_| TxBuilderError::MalformedDatumHash)?; + Some(DatumOption::Hash(dh.into())) + } + DatumKind::Inline => { + let pd = PlutusData::decode_fragment(d.bytes.as_ref()) + .map_err(|_| TxBuilderError::MalformedDatum)?; + Some(DatumOption::Data(CborWrap(pd))) + } + } + } else { + None + }; + + let script_ref = if let Some(ref s) = output.script { + let script = match s.kind { + ScriptKind::Native => PallasScript::NativeScript( + NativeScript::decode_fragment(s.bytes.as_ref()) + .map_err(|_| TxBuilderError::MalformedScript)?, + ), + ScriptKind::PlutusV1 => { + PallasScript::PlutusV1Script(PlutusV1Script(s.bytes.as_ref().to_vec().into())) + } + ScriptKind::PlutusV2 => { + PallasScript::PlutusV2Script(PlutusV2Script(s.bytes.as_ref().to_vec().into())) + } + }; + + Some(CborWrap(script)) + } else { + None + }; + + Ok(PseudoTransactionOutput::PostAlonzo( + PostAlonzoTransactionOutput { + address: output.address.to_vec().into(), + value, + datum_option, + script_ref, + }, + )) +} diff --git a/pallas-txbuilder/src/lib.rs b/pallas-txbuilder/src/lib.rs new file mode 100644 index 00000000..7ccbf4e5 --- /dev/null +++ b/pallas-txbuilder/src/lib.rs @@ -0,0 +1,33 @@ +mod babbage; +mod transaction; + +pub use babbage::BuildBabbage; +pub use transaction::model::{BuiltTransaction, Input, Output, ScriptKind, StagingTransaction}; + +#[derive(Debug, Clone, PartialEq, thiserror::Error)] +pub enum TxBuilderError { + /// Provided bytes could not be decoded into a script + #[error("Transaction has no inputs")] + MalformedScript, + /// Provided bytes could not be decoded into a datum + #[error("Could not decode datum bytes")] + MalformedDatum, + /// Provided datum hash was not 32 bytes in length + #[error("Invalid bytes length for datum hash")] + MalformedDatumHash, + /// Input, policy, etc pointed to by a redeemer was not found in the transaction + #[error("Input/policy pointed to by redeemer not found in tx")] + RedeemerTargetMissing, + /// Provided network ID is invalid (must be 0 or 1) + #[error("Invalid network ID")] + InvalidNetworkId, + /// Transaction bytes in built transaction object could not be decoded + #[error("Corrupted transaction bytes in built transaction")] + CorruptedTxBytes, + /// Public key generated from private key was of unexpected length + #[error("Public key for private key is malformed")] + MalformedKey, + /// Asset name is too long, it must be 32 bytes or less + #[error("Asset name must be 32 bytes or less")] + AssetNameTooLong, +} diff --git a/pallas-txbuilder/src/transaction/mod.rs b/pallas-txbuilder/src/transaction/mod.rs new file mode 100644 index 00000000..c63d4796 --- /dev/null +++ b/pallas-txbuilder/src/transaction/mod.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; + +pub mod model; +pub mod serialise; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default)] +#[serde(rename_all = "snake_case")] +pub enum TransactionStatus { + #[default] + Staging, + Built, +} + +#[derive(PartialEq, Eq, Hash, Debug)] +pub struct Bytes32(pub [u8; 32]); + +#[derive(Hash, PartialEq, Eq, Debug)] +pub struct Bytes64(pub [u8; 64]); + +type PublicKey = Bytes32; +type Signature = Bytes64; + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct Hash28(pub [u8; 28]); + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct Bytes(pub Vec); + +impl Into for Bytes { + fn into(self) -> pallas_codec::utils::Bytes { + self.0.into() + } +} + +impl From> for Bytes { + fn from(value: Vec) -> Self { + Bytes(value) + } +} + +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +pub type TxHash = Bytes32; +pub type PubKeyHash = Hash28; +pub type ScriptHash = Hash28; +pub type ScriptBytes = Bytes; +pub type PolicyId = ScriptHash; +pub type DatumHash = Bytes32; +pub type DatumBytes = Bytes; +pub type AssetName = Bytes; + +/// If a Vec is empty, returns None, or Some(Vec) if not empty +pub fn opt_if_empty(v: Vec) -> Option> { + if v.is_empty() { + None + } else { + Some(v) + } +} diff --git a/pallas-txbuilder/src/transaction/model.rs b/pallas-txbuilder/src/transaction/model.rs new file mode 100644 index 00000000..2e8e76f6 --- /dev/null +++ b/pallas-txbuilder/src/transaction/model.rs @@ -0,0 +1,712 @@ +use pallas_addresses::Address as PallasAddress; +use pallas_crypto::{ + hash::{Hash, Hasher}, + key::ed25519, +}; +use pallas_primitives::{babbage, Fragment}; + +use std::{collections::HashMap, ops::Deref}; + +use serde::{Deserialize, Serialize}; + +use crate::TxBuilderError; + +use super::{ + AssetName, Bytes, Bytes32, Bytes64, DatumBytes, DatumHash, Hash28, PolicyId, PubKeyHash, + PublicKey, ScriptBytes, ScriptHash, Signature, TransactionStatus, TxHash, +}; + +// TODO: Don't make wrapper types public +#[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug)] +pub struct StagingTransaction { + pub version: String, + pub status: TransactionStatus, + pub inputs: Option>, + pub reference_inputs: Option>, + pub outputs: Option>, + pub fee: Option, + pub mint: Option, + pub valid_from_slot: Option, + pub invalid_from_slot: Option, + pub network_id: Option, + pub collateral_inputs: Option>, + pub collateral_output: Option, + pub disclosed_signers: Option>, + pub scripts: Option>, + pub datums: Option>, + pub redeemers: Option, + pub script_data_hash: Option, + pub signature_amount_override: Option, + pub change_address: Option
, + // pub certificates: TODO + // pub withdrawals: TODO + // pub updates: TODO + // pub auxiliary_data: TODO + // pub phase_2_valid: TODO +} + +impl StagingTransaction { + pub fn new() -> Self { + Self { + version: String::from("v1"), + status: TransactionStatus::Staging, + ..Default::default() + } + } + + pub fn input(mut self, input: Input) -> Self { + let mut txins = self.inputs.unwrap_or_default(); + txins.push(input); + self.inputs = Some(txins); + self + } + + pub fn remove_input(mut self, input: Input) -> Self { + let mut txins = self.inputs.unwrap_or_default(); + txins.retain(|x| *x != input); + self.inputs = Some(txins); + self + } + + pub fn reference_input(mut self, input: Input) -> Self { + let mut ref_txins = self.reference_inputs.unwrap_or_default(); + ref_txins.push(input); + self.reference_inputs = Some(ref_txins); + self + } + + pub fn remove_reference_input(mut self, input: Input) -> Self { + let mut ref_txins = self.reference_inputs.unwrap_or_default(); + ref_txins.retain(|x| *x != input); + self.reference_inputs = Some(ref_txins); + self + } + + pub fn output(mut self, output: Output) -> Self { + let mut txouts = self.outputs.unwrap_or_default(); + txouts.push(output); + self.outputs = Some(txouts); + self + } + + pub fn remove_output(mut self, index: usize) -> Self { + let mut txouts = self.outputs.unwrap_or_default(); + txouts.remove(index); + self.outputs = Some(txouts); + self + } + + pub fn fee(mut self, fee: u64) -> Self { + self.fee = Some(fee); + self + } + + pub fn clear_fee(mut self) -> Self { + self.fee = None; + self + } + + pub fn mint_asset( + mut self, + policy: Hash<28>, + name: Vec, + amount: i64, + ) -> Result { + if name.len() > 32 { + return Err(TxBuilderError::AssetNameTooLong); + } + + let mut mint = self.mint.map(|x| x.0).unwrap_or_default(); + + mint.entry(Hash28(*policy)) + .and_modify(|policy_map| { + policy_map + .entry(name.clone().into()) + .and_modify(|asset_map| { + *asset_map += amount; + }) + .or_insert(amount); + }) + .or_insert_with(|| { + let mut map: HashMap = HashMap::new(); + map.insert(name.clone().into(), amount); + map + }); + + self.mint = Some(MintAssets(mint)); + + Ok(self) + } + + pub fn remove_mint_asset(mut self, policy: Hash<28>, name: Vec) -> Self { + let mut mint = if let Some(mint) = self.mint { + mint.0 + } else { + return self; + }; + + if let Some(assets) = mint.get_mut(&Hash28(*policy)) { + assets.remove(&name.into()); + if assets.is_empty() { + mint.remove(&Hash28(*policy)); + } + } + + self.mint = Some(MintAssets(mint)); + + self + } + + pub fn valid_from_slot(mut self, slot: u64) -> Self { + self.valid_from_slot = Some(slot); + self + } + + pub fn clear_valid_from_slot(mut self) -> Self { + self.valid_from_slot = None; + self + } + + pub fn invalid_from_slot(mut self, slot: u64) -> Self { + self.invalid_from_slot = Some(slot); + self + } + + pub fn clear_invalid_from_slot(mut self) -> Self { + self.invalid_from_slot = None; + self + } + + pub fn network_id(mut self, id: u8) -> Self { + self.network_id = Some(id); + self + } + + pub fn clear_network_id(mut self) -> Self { + self.network_id = None; + self + } + + pub fn collateral_input(mut self, input: Input) -> Self { + let mut coll_ins = self.collateral_inputs.unwrap_or_default(); + coll_ins.push(input); + self.collateral_inputs = Some(coll_ins); + self + } + + pub fn remove_collateral_input(mut self, input: Input) -> Self { + let mut coll_ins = self.collateral_inputs.unwrap_or_default(); + coll_ins.retain(|x| *x != input); + self.collateral_inputs = Some(coll_ins); + self + } + + pub fn collateral_output(mut self, output: Output) -> Self { + self.collateral_output = Some(output); + self + } + + pub fn clear_collateral_output(mut self) -> Self { + self.collateral_output = None; + self + } + + pub fn disclosed_signer(mut self, pub_key_hash: Hash<28>) -> Self { + let mut disclosed_signers = self.disclosed_signers.unwrap_or_default(); + disclosed_signers.push(Hash28(*pub_key_hash)); + self.disclosed_signers = Some(disclosed_signers); + self + } + + pub fn remove_disclosed_signer(mut self, pub_key_hash: Hash<28>) -> Self { + let mut disclosed_signers = self.disclosed_signers.unwrap_or_default(); + disclosed_signers.retain(|x| *x != Hash28(*pub_key_hash)); + self.disclosed_signers = Some(disclosed_signers); + self + } + + pub fn script(mut self, language: ScriptKind, bytes: Vec) -> Self { + let mut scripts = self.scripts.unwrap_or_default(); + + let hash = match language { + ScriptKind::Native => Hasher::<224>::hash_tagged(bytes.as_ref(), 0), + ScriptKind::PlutusV1 => Hasher::<224>::hash_tagged(bytes.as_ref(), 1), + ScriptKind::PlutusV2 => Hasher::<224>::hash_tagged(bytes.as_ref(), 2), + }; + + scripts.insert( + Hash28(*hash), + Script { + kind: language, + bytes: bytes.into(), + }, + ); + + self.scripts = Some(scripts); + self + } + + pub fn remove_script_by_hash(mut self, script_hash: Hash<28>) -> Self { + let mut scripts = self.scripts.unwrap_or_default(); + + scripts.remove(&Hash28(*script_hash)); + + self.scripts = Some(scripts); + self + } + + pub fn datum(mut self, datum: Vec) -> Self { + let mut datums = self.datums.unwrap_or_default(); + + let hash = Hasher::<256>::hash_cbor(&datum); + + datums.insert(Bytes32(*hash), datum.into()); + self.datums = Some(datums); + self + } + + pub fn remove_datum(mut self, datum: Vec) -> Self { + let mut datums = self.datums.unwrap_or_default(); + + let hash = Hasher::<256>::hash_cbor(&datum); + + datums.remove(&Bytes32(*hash)); + self.datums = Some(datums); + self + } + + pub fn remove_datum_by_hash(mut self, datum_hash: Hash<32>) -> Self { + let mut datums = self.datums.unwrap_or_default(); + + datums.remove(&Bytes32(*datum_hash)); + self.datums = Some(datums); + self + } + + pub fn add_spend_redeemer( + mut self, + input: Input, + plutus_data: Vec, + ex_units: Option, + ) -> Self { + let mut rdmrs = self.redeemers.map(|x| x.0).unwrap_or_default(); + + rdmrs.insert( + RedeemerPurpose::Spend(input), + (plutus_data.into(), ex_units), + ); + + self.redeemers = Some(Redeemers(rdmrs)); + + self + } + + pub fn remove_spend_redeemer(mut self, input: Input) -> Self { + let mut rdmrs = self.redeemers.map(|x| x.0).unwrap_or_default(); + + rdmrs.remove(&RedeemerPurpose::Spend(input)); + + self.redeemers = Some(Redeemers(rdmrs)); + + self + } + + pub fn add_mint_redeemer( + mut self, + policy: Hash<28>, + plutus_data: Vec, + ex_units: Option, + ) -> Self { + let mut rdmrs = self.redeemers.map(|x| x.0).unwrap_or_default(); + + rdmrs.insert( + RedeemerPurpose::Mint(Hash28(*policy)), + (plutus_data.into(), ex_units), + ); + + self.redeemers = Some(Redeemers(rdmrs)); + + self + } + + pub fn remove_mint_redeemer(mut self, policy: Hash<28>) -> Self { + let mut rdmrs = self.redeemers.map(|x| x.0).unwrap_or_default(); + + rdmrs.remove(&RedeemerPurpose::Mint(Hash28(*policy))); + + self.redeemers = Some(Redeemers(rdmrs)); + + self + } + + // TODO: script_data_hash computation + pub fn script_data_hash(mut self, hash: Hash<32>) -> Self { + self.script_data_hash = Some(Bytes32(*hash)); + self + } + + pub fn clear_script_data_hash(mut self) -> Self { + self.script_data_hash = None; + self + } + + pub fn signature_amount_override(mut self, amount: u8) -> Self { + self.signature_amount_override = Some(amount); + self + } + + pub fn clear_signature_amount_override(mut self) -> Self { + self.signature_amount_override = None; + self + } + + pub fn change_address(mut self, address: PallasAddress) -> Self { + self.change_address = Some(Address(address)); + self + } + + pub fn clear_change_address(mut self) -> Self { + self.change_address = None; + self + } +} + +// TODO: Don't want our wrapper types in fields public +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Hash)] +pub struct Input { + pub tx_hash: TxHash, + pub txo_index: u64, +} + +impl Input { + pub fn new(tx_hash: Hash<32>, txo_index: u64) -> Self { + Self { + tx_hash: Bytes32(*tx_hash), + txo_index, + } + } +} + +// TODO: Don't want our wrapper types in fields public +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct Output { + pub address: Address, + pub lovelace: u64, + pub assets: Option, + pub datum: Option, + pub script: Option