diff --git a/pallas-txbuilder/src/conway.rs b/pallas-txbuilder/src/conway.rs index a958078..6bbef86 100644 --- a/pallas-txbuilder/src/conway.rs +++ b/pallas-txbuilder/src/conway.rs @@ -4,8 +4,8 @@ use pallas_codec::utils::CborWrap; use pallas_crypto::hash::Hash; use pallas_primitives::{ conway::{ - DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, PlutusData, - PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript, + AuxiliaryData, DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, + PlutusData, PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript, PseudoTransactionOutput, Redeemer, RedeemerTag, TransactionBody, TransactionInput, Tx, Value, WitnessSet, }, @@ -270,8 +270,14 @@ impl BuildConway for StagingTransaction { Some(witness_set_redeemers) }, }, - success: true, // TODO - auxiliary_data: None.into(), // TODO + success: true, // TODO + auxiliary_data: self + .auxiliary_data + .as_deref() + .map(AuxiliaryData::decode_fragment) + .transpose() + .map_err(|_| TxBuilderError::CorruptedTxBytes)? + .into(), }; // TODO: pallas auxiliary_data_hash should be Hash<32> not Bytes @@ -375,3 +381,74 @@ impl Output { )) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::transaction::model::StagingTransaction; + use pallas_codec::minicbor; + use pallas_primitives::alonzo::{AuxiliaryData, Metadatum, PostAlonzoAuxiliaryData}; + use pallas_traverse::ComputeHash; + + /// Encode a CIP-25-shaped auxiliary data, attach it to a tx, and + /// confirm the built tx body carries the matching auxiliary_data_hash. + /// Round-trip protection for the fork's metadata plumbing. + #[test] + fn auxiliary_data_round_trips_through_build() { + // Minimal CIP-25-style metadata: label 721 → text "test". + let metadata: pallas_primitives::alonzo::Metadata = vec![ + (721u64, Metadatum::Text("test".into())), + ] + .into(); + let aux = AuxiliaryData::PostAlonzo(PostAlonzoAuxiliaryData { + metadata: Some(metadata), + native_scripts: None, + plutus_scripts: None, + }); + let aux_bytes = minicbor::to_vec(&aux).expect("encode aux"); + let expected_hash = aux.compute_hash(); + + let tx = StagingTransaction::new() + .auxiliary_data(aux_bytes) + .network_id(0) + .fee(180_000) + .build_conway_raw() + .expect("build_conway_raw"); + + // The body's auxiliary_data_hash should match the aux's + // compute_hash output. + let body = + pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref()) + .expect("decode tx_bytes"); + let body_aux_hash_bytes: Vec = body + .transaction_body + .auxiliary_data_hash + .clone() + .expect("auxiliary_data_hash should be set when aux is attached") + .to_vec(); + assert_eq!(body_aux_hash_bytes, expected_hash.to_vec()); + + // And the auxiliary_data should round-trip back. + let pallas_aux = match body.auxiliary_data { + pallas_codec::utils::Nullable::Some(a) => a, + other => panic!("expected Some(aux), got {other:?}"), + }; + // Re-encode the aux that came back and compare. + let round_tripped_bytes = minicbor::to_vec(&pallas_aux).expect("re-encode aux"); + let original_again = minicbor::to_vec(&aux).expect("re-encode original"); + assert_eq!(round_tripped_bytes, original_again); + } + + #[test] + fn no_auxiliary_data_means_no_hash() { + let tx = StagingTransaction::new() + .network_id(0) + .fee(180_000) + .build_conway_raw() + .expect("build_conway_raw"); + let body = + pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref()) + .expect("decode tx_bytes"); + assert!(body.transaction_body.auxiliary_data_hash.is_none()); + } +} diff --git a/pallas-txbuilder/src/transaction/model.rs b/pallas-txbuilder/src/transaction/model.rs index 5e1227e..b56e947 100644 --- a/pallas-txbuilder/src/transaction/model.rs +++ b/pallas-txbuilder/src/transaction/model.rs @@ -40,10 +40,14 @@ pub struct StagingTransaction { pub signature_amount_override: Option, pub change_address: Option
, pub language_view: Option, + /// CBOR-encoded `pallas_primitives::alonzo::AuxiliaryData`. Stored + /// as bytes rather than the typed enum because `AuxiliaryData` + /// doesn't implement `Eq`, which the rest of this struct requires. + /// `conway::build_conway_raw` decodes on the way out. + pub auxiliary_data: Option>, // pub certificates: TODO // pub withdrawals: TODO // pub updates: TODO - // pub auxiliary_data: TODO // pub phase_2_valid: TODO } @@ -372,6 +376,26 @@ impl StagingTransaction { self.change_address = None; self } + + /// Attach pre-encoded auxiliary data (CBOR bytes of an + /// `alonzo::AuxiliaryData`). The conway builder will decode on + /// build and compute `auxiliary_data_hash` automatically. + /// + /// Most callers use this for transaction metadata (CIP-25 / CIP-20 + /// / CIP-68) and/or to ship native scripts as auxiliary data. + /// Construct an `AuxiliaryData::PostAlonzo(...)` with the + /// metadata + scripts you want, encode with `Fragment::encode_fragment` + /// or `minicbor::to_vec`, and pass the bytes here. + pub fn auxiliary_data(mut self, cbor_bytes: Vec) -> Self { + self.auxiliary_data = Some(cbor_bytes); + self + } + + /// Drop any auxiliary data previously attached. + pub fn clear_auxiliary_data(mut self) -> Self { + self.auxiliary_data = None; + self + } } // TODO: Don't want our wrapper types in fields public diff --git a/pallas-txbuilder/src/transaction/serialise.rs b/pallas-txbuilder/src/transaction/serialise.rs index e60008f..28e0c08 100644 --- a/pallas-txbuilder/src/transaction/serialise.rs +++ b/pallas-txbuilder/src/transaction/serialise.rs @@ -480,6 +480,7 @@ mod tests { change_address: Some(Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap())), script_data_hash: Some(Bytes32([0; 32])), language_view: Some(crate::scriptdata::LanguageView(1, vec![1, 2, 3])), + auxiliary_data: None, }; let serialised_tx = serde_json::to_string(&tx).unwrap();