Skip to content

Commit

Permalink
feat: introduce transaction builder crate (#338)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmhrpr authored Dec 3, 2023
1 parent 6459894 commit f3d9719
Show file tree
Hide file tree
Showing 9 changed files with 1,691 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"pallas-primitives",
"pallas-rolldb",
"pallas-traverse",
"pallas-txbuilder",
"pallas-utxorpc",
"pallas",
"examples/block-download",
Expand Down
24 changes: 24 additions & 0 deletions pallas-txbuilder/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
"Cainã Costa <[email protected]>",
]

[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"
340 changes: 340 additions & 0 deletions pallas-txbuilder/src/babbage.rs
Original file line number Diff line number Diff line change
@@ -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<BuiltTransaction, TxBuilderError>;

// fn build_babbage(staging_tx: StagingTransaction, resolver: (), params: ()) -> Result<BuiltTransaction, TxBuilderError>;
}

impl BuildBabbage for StagingTransaction {
fn build_babbage_raw(self) -> Result<BuiltTransaction, TxBuilderError> {
let mut inputs = self
.inputs
.unwrap_or_default()
.iter()
.map(|x| TransactionInput {
transaction_id: x.tx_hash.0.into(),
index: x.txo_index,
})
.collect::<Vec<_>>();

inputs.sort_unstable_by_key(|x| (x.transaction_id, x.index));

let outputs = self
.outputs
.unwrap_or_default()
.iter()
.map(babbage_output)
.collect::<Result<Vec<_>, _>>()?;

let mint: Option<KeyValuePairs<Hash<28>, 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::<Vec<_>>()
.into(),
)
})
.collect::<Vec<_>>()
.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::<Result<Vec<_>, _>>()?;

let mut mint_policies = mint
.clone()
.unwrap_or(vec![].into())
.iter()
.map(|(p, _)| **p)
.collect::<Vec<_>>();
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<BuiltTransaction, TxBuilderError> {
// todo!()
// }
}

fn babbage_output(
output: &Output,
) -> Result<PseudoTransactionOutput<PostAlonzoTransactionOutput>, 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::<Vec<_>>()
.into(),
)
})
.collect::<Vec<_>>()
.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,
},
))
}
Loading

0 comments on commit f3d9719

Please sign in to comment.