feat: introduce transaction builder crate (#338)

This commit is contained in:
Harper 2023-12-03 12:51:06 +00:00 committed by GitHub
parent 645989465d
commit f3d9719b5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1691 additions and 0 deletions

View file

@ -10,6 +10,7 @@ members = [
"pallas-primitives",
"pallas-rolldb",
"pallas-traverse",
"pallas-txbuilder",
"pallas-utxorpc",
"pallas",
"examples/block-download",

View file

@ -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 <santiago@carmuega.me>",
"Cainã Costa <me@cfcosta.com>",
]
[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"

View file

@ -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,
},
))
}

View file

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

View file

@ -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<u8>);
impl Into<pallas_codec::utils::Bytes> for Bytes {
fn into(self) -> pallas_codec::utils::Bytes {
self.0.into()
}
}
impl From<Vec<u8>> for Bytes {
fn from(value: Vec<u8>) -> 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<T>(v: Vec<T>) -> Option<Vec<T>> {
if v.is_empty() {
None
} else {
Some(v)
}
}

View file

@ -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<Vec<Input>>,
pub reference_inputs: Option<Vec<Input>>,
pub outputs: Option<Vec<Output>>,
pub fee: Option<u64>,
pub mint: Option<MintAssets>,
pub valid_from_slot: Option<u64>,
pub invalid_from_slot: Option<u64>,
pub network_id: Option<u8>,
pub collateral_inputs: Option<Vec<Input>>,
pub collateral_output: Option<Output>,
pub disclosed_signers: Option<Vec<PubKeyHash>>,
pub scripts: Option<HashMap<ScriptHash, Script>>,
pub datums: Option<HashMap<DatumHash, DatumBytes>>,
pub redeemers: Option<Redeemers>,
pub script_data_hash: Option<Bytes32>,
pub signature_amount_override: Option<u8>,
pub change_address: Option<Address>,
// 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<u8>,
amount: i64,
) -> Result<Self, TxBuilderError> {
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<Bytes, i64> = 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<u8>) -> 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<u8>) -> 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<u8>) -> 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<u8>) -> 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<u8>,
ex_units: Option<ExUnits>,
) -> 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<u8>,
ex_units: Option<ExUnits>,
) -> 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<OutputAssets>,
pub datum: Option<Datum>,
pub script: Option<Script>,
}
impl Output {
pub fn new(address: PallasAddress, lovelace: u64) -> Self {
Self {
address: Address(address),
lovelace,
assets: None,
datum: None,
script: None,
}
}
pub fn add_asset(
mut self,
policy: Hash<28>,
name: Vec<u8>,
amount: u64,
) -> Result<Self, TxBuilderError> {
if name.len() > 32 {
return Err(TxBuilderError::AssetNameTooLong);
}
let mut assets = self.assets.map(|x| x.0).unwrap_or_default();
assets
.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<Bytes, u64> = HashMap::new();
map.insert(name.clone().into(), amount);
map
});
self.assets = Some(OutputAssets(assets));
Ok(self)
}
pub fn set_inline_datum(mut self, plutus_data: Vec<u8>) -> Self {
self.datum = Some(Datum {
kind: DatumKind::Inline,
bytes: plutus_data.into(),
});
self
}
pub fn set_datum_hash(mut self, datum_hash: Hash<32>) -> Self {
self.datum = Some(Datum {
kind: DatumKind::Inline,
bytes: datum_hash.to_vec().into(),
});
self
}
pub fn set_inline_script(mut self, language: ScriptKind, bytes: Vec<u8>) -> Self {
self.script = Some(Script {
kind: language,
bytes: bytes.into(),
});
self
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct OutputAssets(HashMap<PolicyId, HashMap<AssetName, u64>>);
impl Deref for OutputAssets {
type Target = HashMap<PolicyId, HashMap<Bytes, u64>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl OutputAssets {
pub fn new() -> Self {
Self(HashMap::new())
}
pub fn from_map(map: HashMap<PolicyId, HashMap<Bytes, u64>>) -> Self {
Self(map)
}
}
#[derive(PartialEq, Eq, Debug, Clone, Default)]
pub struct MintAssets(HashMap<PolicyId, HashMap<AssetName, i64>>);
impl Deref for MintAssets {
type Target = HashMap<PolicyId, HashMap<Bytes, i64>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl MintAssets {
pub fn new() -> Self {
MintAssets(HashMap::new())
}
pub fn from_map(map: HashMap<PolicyId, HashMap<Bytes, i64>>) -> Self {
Self(map)
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum ScriptKind {
Native,
PlutusV1,
PlutusV2,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct Script {
pub kind: ScriptKind,
pub bytes: ScriptBytes,
}
impl Script {
pub fn new(kind: ScriptKind, bytes: Vec<u8>) -> Self {
Self {
kind,
bytes: bytes.into(),
}
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum DatumKind {
Hash,
Inline,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct Datum {
pub kind: DatumKind,
pub bytes: DatumBytes,
}
#[derive(PartialEq, Eq, Hash, Debug)]
pub enum RedeemerPurpose {
Spend(Input),
Mint(PolicyId),
// Reward TODO
// Cert TODO
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct ExUnits {
pub mem: u32,
pub steps: u64,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct Redeemers(HashMap<RedeemerPurpose, (Bytes, Option<ExUnits>)>);
impl Deref for Redeemers {
type Target = HashMap<RedeemerPurpose, (Bytes, Option<ExUnits>)>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Redeemers {
pub fn new() -> Self {
Redeemers(HashMap::new())
}
pub fn from_map(map: HashMap<RedeemerPurpose, (Bytes, Option<ExUnits>)>) -> Self {
Self(map)
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Address(pub PallasAddress);
impl Deref for Address {
type Target = PallasAddress;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<PallasAddress> for Address {
fn from(value: PallasAddress) -> Self {
Self(value)
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
#[serde(rename_all = "snake_case")]
pub enum BuilderEra {
Babbage,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
pub struct BuiltTransaction {
pub version: String,
pub era: BuilderEra,
pub status: TransactionStatus,
pub tx_hash: TxHash,
pub tx_bytes: Bytes,
pub signatures: Option<HashMap<PublicKey, Signature>>,
}
impl BuiltTransaction {
pub fn sign(mut self, secret_key: ed25519::SecretKey) -> Result<Self, TxBuilderError> {
let pubkey: [u8; 32] = secret_key
.public_key()
.as_ref()
.try_into()
.map_err(|_| TxBuilderError::MalformedKey)?;
let signature: [u8; 64] = secret_key.sign(self.tx_hash.0).as_ref().try_into().unwrap();
match self.era {
BuilderEra::Babbage => {
let mut new_sigs = self.signatures.unwrap_or_default();
new_sigs.insert(Bytes32(pubkey), Bytes64(signature));
self.signatures = Some(new_sigs);
// TODO: chance for serialisation round trip issues?
let mut tx = babbage::Tx::decode_fragment(&self.tx_hash.0)
.map_err(|_| TxBuilderError::CorruptedTxBytes)?;
let mut vkey_witnesses = tx.transaction_witness_set.vkeywitness.unwrap_or_default();
vkey_witnesses.push(babbage::VKeyWitness {
vkey: Vec::from(pubkey.as_ref()).into(),
signature: Vec::from(signature.as_ref()).into(),
});
tx.transaction_witness_set.vkeywitness = Some(vkey_witnesses);
self.tx_bytes = tx.encode_fragment().unwrap().into();
}
}
Ok(self)
}
pub fn add_signature(mut self, pub_key: ed25519::PublicKey, signature: [u8; 64]) -> Result<Self, TxBuilderError> {
match self.era {
BuilderEra::Babbage => {
let mut new_sigs = self.signatures.unwrap_or_default();
new_sigs.insert(Bytes32(pub_key.as_ref().try_into().map_err(|_| TxBuilderError::MalformedKey)?), Bytes64(signature));
self.signatures = Some(new_sigs);
// TODO: chance for serialisation round trip issues?
let mut tx = babbage::Tx::decode_fragment(&self.tx_hash.0)
.map_err(|_| TxBuilderError::CorruptedTxBytes)?;
let mut vkey_witnesses = tx.transaction_witness_set.vkeywitness.unwrap_or_default();
vkey_witnesses.push(babbage::VKeyWitness {
vkey: Vec::from(pub_key.as_ref()).into(),
signature: Vec::from(signature.as_ref()).into(),
});
tx.transaction_witness_set.vkeywitness = Some(vkey_witnesses);
self.tx_bytes = tx.encode_fragment().unwrap().into();
}
}
Ok(self)
}
pub fn remove_signature(mut self, pub_key: ed25519::PublicKey) -> Result<Self, TxBuilderError> {
match self.era {
BuilderEra::Babbage => {
let mut new_sigs = self.signatures.unwrap_or_default();
let pk = Bytes32(pub_key.as_ref().try_into().map_err(|_| TxBuilderError::MalformedKey)?);
new_sigs.remove(&pk);
self.signatures = Some(new_sigs);
// TODO: chance for serialisation round trip issues?
let mut tx = babbage::Tx::decode_fragment(&self.tx_hash.0)
.map_err(|_| TxBuilderError::CorruptedTxBytes)?;
let mut vkey_witnesses = tx.transaction_witness_set.vkeywitness.unwrap_or_default();
vkey_witnesses.retain(|x| *x.vkey != pk.0.to_vec());
tx.transaction_witness_set.vkeywitness = Some(vkey_witnesses);
self.tx_bytes = tx.encode_fragment().unwrap().into();
}
}
Ok(self)
}
}

View file

@ -0,0 +1,513 @@
use core::fmt;
use std::{collections::HashMap, ops::Deref, str::FromStr};
use pallas_addresses::Address as PallasAddress;
use serde::{
de::{self, Visitor},
ser::SerializeMap,
Deserialize, Deserializer, Serialize, Serializer,
};
use super::{
model::{Address, Input, MintAssets, OutputAssets, RedeemerPurpose},
Bytes, Bytes32, Bytes64, Hash28,
};
impl Serialize for Bytes32 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex::encode(&self.0))
}
}
impl<'de> Deserialize<'de> for Bytes32 {
fn deserialize<D>(deserializer: D) -> Result<Bytes32, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(Bytes32Visitor)
}
}
struct Bytes32Visitor;
impl<'de> Visitor<'de> for Bytes32Visitor {
type Value = Bytes32;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("32 bytes hex encoded")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Bytes32(
hex::decode(v)
.map_err(|_| E::custom("invalid hex"))?
.try_into()
.map_err(|_| E::custom("invalid length"))?,
))
}
}
impl Serialize for Hash28 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex::encode(&self.0))
}
}
impl<'de> Deserialize<'de> for Hash28 {
fn deserialize<D>(deserializer: D) -> Result<Hash28, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(Hash28Visitor)
}
}
struct Hash28Visitor;
impl<'de> Visitor<'de> for Hash28Visitor {
type Value = Hash28;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("28 bytes hex encoded")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Hash28(
hex::decode(v)
.map_err(|_| E::custom("invalid hex"))?
.try_into()
.map_err(|_| E::custom("invalid length"))?,
))
}
}
impl Serialize for Bytes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex::encode(&self.0))
}
}
impl<'de> Deserialize<'de> for Bytes {
fn deserialize<D>(deserializer: D) -> Result<Bytes, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(BytesVisitor)
}
}
struct BytesVisitor;
impl<'de> Visitor<'de> for BytesVisitor {
type Value = Bytes;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("bytes hex encoded")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Bytes(hex::decode(v).map_err(|_| E::custom("invalid hex"))?))
}
}
impl Serialize for OutputAssets {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.deref().len()))?;
for (policy, assets) in self.deref().iter() {
let mut assets_map: HashMap<String, u64> = HashMap::new();
for (asset, amount) in assets {
assets_map.insert(hex::encode(&asset.0), *amount);
}
map.serialize_entry(policy, &assets_map)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for OutputAssets {
fn deserialize<D>(deserializer: D) -> Result<OutputAssets, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(OutputAssetsVisitor)
}
}
struct OutputAssetsVisitor;
impl<'de> Visitor<'de> for OutputAssetsVisitor {
type Value = OutputAssets;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
"map of hex encoded policy ids to map of hex encoded asset names to u64 amounts",
)
}
fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut out_map = HashMap::new();
while let Some((key, value)) = access.next_entry()? {
out_map.insert(key, value);
}
Ok(OutputAssets::from_map(out_map))
}
}
impl Serialize for MintAssets {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(self.deref().len()))?;
for (policy, assets) in self.deref().iter() {
let mut assets_map: HashMap<String, i64> = HashMap::new();
for (asset, amount) in assets {
assets_map.insert(hex::encode(&asset.0), *amount);
}
map.serialize_entry(policy, &assets_map)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for MintAssets {
fn deserialize<D>(deserializer: D) -> Result<MintAssets, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_map(MintAssetsVisitor)
}
}
struct MintAssetsVisitor;
impl<'de> Visitor<'de> for MintAssetsVisitor {
type Value = MintAssets;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
"map of hex encoded policy ids to map of hex encoded asset names to u64 amounts",
)
}
fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
where
A: de::MapAccess<'de>,
{
let mut out_map = HashMap::new();
while let Some((key, value)) = access.next_entry()? {
out_map.insert(key, value);
}
Ok(MintAssets::from_map(out_map))
}
}
impl Serialize for RedeemerPurpose {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let str = match self {
RedeemerPurpose::Spend(Input { tx_hash, txo_index }) => {
format!("spend:{}#{}", hex::encode(&tx_hash.0), txo_index)
}
RedeemerPurpose::Mint(hash) => format!("mint:{}", hex::encode(&hash.0)),
};
serializer.serialize_str(&str)
}
}
impl<'de> Deserialize<'de> for RedeemerPurpose {
fn deserialize<D>(deserializer: D) -> Result<RedeemerPurpose, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(RedeemerPurposeVisitor)
}
}
struct RedeemerPurposeVisitor;
impl<'de> Visitor<'de> for RedeemerPurposeVisitor {
type Value = RedeemerPurpose;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("'spend:{hex_txid}#{index}' or 'mint:{hex_policyid}'")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let (tag, item) = v
.split_once(":")
.ok_or(E::custom("invalid redeemer purpose"))?;
match tag {
"spend" => {
let (hash, index) = item
.split_once("#")
.ok_or(E::custom("invalid spend redeemer item"))?;
let tx_hash = Bytes32(
hex::decode(hash)
.map_err(|_| E::custom("invalid spend redeemer item txid hex"))?
.try_into()
.map_err(|_| E::custom("invalid spend redeemer txid len"))?,
);
let txo_index = index
.parse()
.map_err(|_| E::custom("invalid spend redeemer item index"))?;
Ok(RedeemerPurpose::Spend(Input { tx_hash, txo_index }))
}
"mint" => {
let hash = Hash28(
hex::decode(item)
.map_err(|_| E::custom("invalid mint redeemer item policy hex"))?
.try_into()
.map_err(|_| E::custom("invalid mint redeemer policy len"))?,
);
Ok(RedeemerPurpose::Mint(hash))
}
_ => Err(E::custom("invalid redeemer tag")),
}
}
}
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0.to_string())
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Address, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(AddressVisitor)
}
}
struct AddressVisitor;
impl<'de> Visitor<'de> for AddressVisitor {
type Value = Address;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("bech32 shelley address or base58 byron address")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Address(
PallasAddress::from_str(v).map_err(|_| E::custom("invalid address"))?,
))
}
}
impl Serialize for Bytes64 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex::encode(&self.0))
}
}
impl<'de> Deserialize<'de> for Bytes64 {
fn deserialize<D>(deserializer: D) -> Result<Bytes64, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(Bytes64Visitor)
}
}
struct Bytes64Visitor;
impl<'de> Visitor<'de> for Bytes64Visitor {
type Value = Bytes64;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("64 bytes hex encoded")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(Bytes64(
hex::decode(v)
.map_err(|_| E::custom("invalid hex"))?
.try_into()
.map_err(|_| E::custom("invalid length"))?,
))
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use pallas_addresses::Address as PallasAddress;
use pallas_primitives::{babbage::PlutusData, Fragment};
use crate::transaction::{model::*, Bytes64, DatumBytes, DatumHash, Hash28, TransactionStatus};
use super::*;
#[test]
fn staging_json_roundtrip() {
let mut datums: HashMap<DatumHash, DatumBytes> = HashMap::new();
datums.insert(Bytes32([0; 32]), Bytes([0; 100].to_vec()));
let tx = StagingTransaction {
version: String::from("v1"),
status: TransactionStatus::Staging,
inputs: Some(
vec![
Input {
tx_hash: Bytes32([0; 32]),
txo_index: 1
}
]
) ,
reference_inputs: Some(vec![
Input {
tx_hash: Bytes32([1; 32]),
txo_index: 0
}
]),
outputs: Some(vec![
Output {
address: Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap()),
lovelace: 1337,
assets: Some(
OutputAssets::from_map(
vec![
(
Hash28([0; 28]),
(vec![(Bytes(vec![0]), 1337)]).into_iter().collect::<HashMap<_, _>>()
)
].into_iter().collect::<HashMap<_, _>>()
)
),
datum: Some(Datum { kind: DatumKind::Hash, bytes: Bytes([0; 32].to_vec()) }),
script: Some(Script { kind: ScriptKind::Native, bytes: Bytes([1; 100].to_vec()) }),
}
]),
fee: Some(1337),
mint: Some(
MintAssets::from_map(
vec![
(
Hash28([0; 28]),
(vec![(Bytes(vec![0]), -1337)]).into_iter().collect::<HashMap<_, _>>()
)
].into_iter().collect::<HashMap<_, _>>()
)
),
valid_from_slot: Some(1337),
invalid_from_slot: Some(1337),
network_id: Some(1),
collateral_inputs: Some(vec![
Input {
tx_hash: Bytes32([2; 32]),
txo_index: 0
}
]),
collateral_output: Some(Output { address: Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap()), lovelace: 1337, assets: None, datum: None, script: None }),
disclosed_signers: Some(vec![Hash28([0; 28])]),
scripts: Some(
vec![
(
Hash28([0; 28]),
Script { kind: ScriptKind::PlutusV1, bytes: Bytes([0; 100].to_vec()) }
)
].into_iter().collect::<HashMap<_, _>>()
),
datums: Some(datums),
redeemers: Some(Redeemers::from_map(vec![
(RedeemerPurpose::Spend(Input { tx_hash: Bytes32([4; 32]), txo_index: 1 }), (Bytes(PlutusData::Array(vec![]).encode_fragment().unwrap()), Some(ExUnits { mem: 1337, steps: 7331 }))),
(RedeemerPurpose::Mint(Hash28([5; 28])), (Bytes(PlutusData::Array(vec![]).encode_fragment().unwrap()), None)),
].into_iter().collect::<HashMap<_, _>>())),
signature_amount_override: Some(5),
change_address: Some(Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap())),
script_data_hash: Some(Bytes32([0; 32])),
};
let serialised_tx = serde_json::to_string(&tx).unwrap();
dbg!(&serialised_tx);
let deserialised_tx: StagingTransaction = serde_json::from_str(&serialised_tx).unwrap();
assert_eq!(tx, deserialised_tx)
}
#[test]
fn built_json_roundtrip() {
let tx = BuiltTransaction {
version: "3".into(),
status: TransactionStatus::Built,
era: BuilderEra::Babbage,
tx_hash: Bytes32([0; 32]),
tx_bytes: Bytes([6; 100].to_vec()),
signatures: Some(
vec![(Bytes32([20; 32]), Bytes64([9; 64]))]
.into_iter()
.collect::<HashMap<_, _>>(),
),
};
let serialised_tx = serde_json::to_string(&tx).unwrap();
let deserialised_tx: BuiltTransaction = serde_json::from_str(&serialised_tx).unwrap();
assert_eq!(tx, deserialised_tx)
}
}

View file

@ -21,6 +21,7 @@ pallas-codec = { version = "=0.20.0", path = "../pallas-codec/" }
pallas-utxorpc = { version = "=0.20.0", path = "../pallas-utxorpc/" }
pallas-configs = { version = "=0.20.0", path = "../pallas-configs/" }
pallas-rolldb = { version = "=0.20.0", path = "../pallas-rolldb/", optional = true }
pallas-txbuilder = { version = "=0.20.0", path = "../pallas-txbuilder/" }
[features]
unstable = ["pallas-rolldb"]

View file

@ -52,3 +52,7 @@ pub mod storage {
#[doc(inline)]
#[cfg(feature = "unstable")]
pub use pallas_applying as applying;
#[doc(inline)]
#[cfg(feature = "unstable")]
pub use pallas_txbuilder as txbuilder;