diff --git a/pallas-txbuilder/src/conway.rs b/pallas-txbuilder/src/conway.rs index 6bbef86..b63a27e 100644 --- a/pallas-txbuilder/src/conway.rs +++ b/pallas-txbuilder/src/conway.rs @@ -4,10 +4,10 @@ use pallas_codec::utils::CborWrap; use pallas_crypto::hash::Hash; use pallas_primitives::{ conway::{ - AuxiliaryData, DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, - PlutusData, PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript, - PseudoTransactionOutput, Redeemer, RedeemerTag, TransactionBody, TransactionInput, Tx, - Value, WitnessSet, + AuxiliaryData, Certificate, DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, + NonZeroInt, PlutusData, PlutusScript, PostAlonzoTransactionOutput, + PseudoScript as PallasScript, PseudoTransactionOutput, Redeemer, RedeemerTag, + TransactionBody, TransactionInput, Tx, Value, WitnessSet, }, Fragment, NonEmptyKeyValuePairs, NonEmptySet, PositiveCoin, }; @@ -240,8 +240,18 @@ impl BuildConway for StagingTransaction { ttl: self.invalid_from_slot, validity_interval_start: self.valid_from_slot, fee: self.fee.unwrap_or_default(), - certificates: None, // TODO - withdrawals: None, // TODO + certificates: NonEmptySet::from_vec( + self.certificates + .as_deref() + .unwrap_or(&[]) + .iter() + .map(|bytes| { + Certificate::decode_fragment(bytes) + .map_err(|_| TxBuilderError::CorruptedTxBytes) + }) + .collect::, _>>()?, + ), + withdrawals: None, // TODO auxiliary_data_hash: None, // TODO (accept user input) mint, script_data_hash, @@ -439,6 +449,51 @@ mod tests { assert_eq!(round_tripped_bytes, original_again); } + #[test] + fn certificates_plumb_through_to_tx_body() { + use pallas_primitives::conway::{Certificate, StakeCredential}; + use pallas_crypto::hash::Hash; + + // A trivial stake-registration cert. + let stake_pkh = Hash::<28>::from([7u8; 28]); + let cert = Certificate::StakeRegistration(StakeCredential::AddrKeyhash(stake_pkh)); + let cert_bytes = minicbor::to_vec(&cert).expect("encode cert"); + + let tx = StagingTransaction::new() + .add_certificate(cert_bytes) + .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"); + let certs = body + .transaction_body + .certificates + .expect("certificates field populated"); + let certs_vec: Vec<_> = certs.to_vec(); + assert_eq!(certs_vec.len(), 1); + match &certs_vec[0] { + Certificate::StakeRegistration(StakeCredential::AddrKeyhash(h)) => { + assert_eq!(h.as_ref(), &[7u8; 28]); + } + other => panic!("expected StakeRegistration, got {other:?}"), + } + } + + #[test] + fn no_certificates_means_none() { + 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.certificates.is_none()); + } + #[test] fn no_auxiliary_data_means_no_hash() { let tx = StagingTransaction::new() diff --git a/pallas-txbuilder/src/transaction/model.rs b/pallas-txbuilder/src/transaction/model.rs index b56e947..35264a7 100644 --- a/pallas-txbuilder/src/transaction/model.rs +++ b/pallas-txbuilder/src/transaction/model.rs @@ -45,7 +45,11 @@ pub struct StagingTransaction { /// 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 + /// CBOR-encoded `pallas_primitives::conway::Certificate` entries, + /// in tx order. `conway::build_conway_raw` decodes each one and + /// plumbs them into `TransactionBody.certificates`. Used for + /// stake registration/delegation, pool registration, DRep ops, etc. + pub certificates: Option>>, // pub withdrawals: TODO // pub updates: TODO // pub phase_2_valid: TODO @@ -396,6 +400,26 @@ impl StagingTransaction { self.auxiliary_data = None; self } + + /// Append a CBOR-encoded `conway::Certificate` to the tx body. + /// Common cases: stake registration, stake delegation, DRep + /// registration, pool retirement. + /// + /// Encode the typed `Certificate` via + /// `pallas_codec::minicbor::to_vec(&cert)` or + /// `Fragment::encode_fragment` and pass the bytes here. + pub fn add_certificate(mut self, cbor_bytes: Vec) -> Self { + let mut certs = self.certificates.unwrap_or_default(); + certs.push(cbor_bytes); + self.certificates = Some(certs); + self + } + + /// Drop any certificates previously attached. + pub fn clear_certificates(mut self) -> Self { + self.certificates = 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 28e0c08..f1b8bd5 100644 --- a/pallas-txbuilder/src/transaction/serialise.rs +++ b/pallas-txbuilder/src/transaction/serialise.rs @@ -481,6 +481,7 @@ mod tests { script_data_hash: Some(Bytes32([0; 32])), language_view: Some(crate::scriptdata::LanguageView(1, vec![1, 2, 3])), auxiliary_data: None, + certificates: None, }; let serialised_tx = serde_json::to_string(&tx).unwrap();