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-primitives",
|
||||||
"pallas-rolldb",
|
"pallas-rolldb",
|
||||||
"pallas-traverse",
|
"pallas-traverse",
|
||||||
|
"pallas-txbuilder",
|
||||||
"pallas-utxorpc",
|
"pallas-utxorpc",
|
||||||
"pallas",
|
"pallas",
|
||||||
"examples/block-download",
|
"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-utxorpc = { version = "=0.20.0", path = "../pallas-utxorpc/" }
|
||||||
pallas-configs = { version = "=0.20.0", path = "../pallas-configs/" }
|
pallas-configs = { version = "=0.20.0", path = "../pallas-configs/" }
|
||||||
pallas-rolldb = { version = "=0.20.0", path = "../pallas-rolldb/", optional = true }
|
pallas-rolldb = { version = "=0.20.0", path = "../pallas-rolldb/", optional = true }
|
||||||
|
pallas-txbuilder = { version = "=0.20.0", path = "../pallas-txbuilder/" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
unstable = ["pallas-rolldb"]
|
unstable = ["pallas-rolldb"]
|
||||||
|
|
|
||||||
|
|
@ -52,3 +52,7 @@ pub mod storage {
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
#[cfg(feature = "unstable")]
|
#[cfg(feature = "unstable")]
|
||||||
pub use pallas_applying as applying;
|
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