feat: introduce transaction builder crate (#338)
This commit is contained in:
parent
645989465d
commit
f3d9719b5d
9 changed files with 1691 additions and 0 deletions
|
|
@ -10,6 +10,7 @@ members = [
|
|||
"pallas-primitives",
|
||||
"pallas-rolldb",
|
||||
"pallas-traverse",
|
||||
"pallas-txbuilder",
|
||||
"pallas-utxorpc",
|
||||
"pallas",
|
||||
"examples/block-download",
|
||||
|
|
|
|||
24
pallas-txbuilder/Cargo.toml
Normal file
24
pallas-txbuilder/Cargo.toml
Normal 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"
|
||||
340
pallas-txbuilder/src/babbage.rs
Normal file
340
pallas-txbuilder/src/babbage.rs
Normal 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,
|
||||
},
|
||||
))
|
||||
}
|
||||
33
pallas-txbuilder/src/lib.rs
Normal file
33
pallas-txbuilder/src/lib.rs
Normal 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,
|
||||
}
|
||||
63
pallas-txbuilder/src/transaction/mod.rs
Normal file
63
pallas-txbuilder/src/transaction/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
712
pallas-txbuilder/src/transaction/model.rs
Normal file
712
pallas-txbuilder/src/transaction/model.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
513
pallas-txbuilder/src/transaction/serialise.rs
Normal file
513
pallas-txbuilder/src/transaction/serialise.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue