diff --git a/BRANCH-NOTES.md b/BRANCH-NOTES.md index 7458e8b..d638d19 100644 --- a/BRANCH-NOTES.md +++ b/BRANCH-NOTES.md @@ -57,7 +57,32 @@ Tests added: asserts the cert round-trips byte-for-byte. - `no_certificates_means_none` — negative path. -### 3. `pallas-addresses` — `pub fn new` on `StakeAddress` +### 3. `pallas-txbuilder` — `voting_procedures` field on `StagingTransaction` + +Upstream had `voting_procedures: None, // TODO` in the conway builder. +Without this, **DRep / SPO / committee voting on Conway governance +actions can't ride a tx built with pallas-txbuilder.** + +Added 2026-05-06: +- `pub voting_procedures: Option>` field (opaque CBOR for the + same `Eq`-derive reason as `auxiliary_data`). +- Builder methods `.voting_procedures(cbor_bytes)` and + `.clear_voting_procedures()`. +- `conway::build_conway_raw` decodes via `VotingProcedures::decode_fragment` + and assigns to `TransactionBody.voting_procedures`. + +The `VotingProcedures` type is itself a single map +(`NonEmptyKeyValuePairs>`), so callers pre-assemble the full map and encode +once. No staged accumulator needed. + +Tests added: +- `voting_procedures_plumb_through_to_tx_body` — encodes a + single DRepKey-vote-Yes on a synthetic GovActionId, builds, decodes, + asserts the vote round-trips byte-for-byte. +- `no_voting_procedures_means_none` — negative path. + +### 4. `pallas-addresses` — `pub fn new` on `StakeAddress` Upstream defined `pub struct StakeAddress(Network, StakePayload)` with **unexported tuple fields** and no `new()` constructor — so external @@ -103,6 +128,7 @@ per change) for upstream review ergonomics: - `pallas-txbuilder: thread auxiliary_data through StagingTransaction → Conway build` - `pallas-txbuilder: thread certificates through StagingTransaction → Conway build` +- `pallas-txbuilder: thread voting_procedures through StagingTransaction → Conway build` - `pallas-addresses: pub fn new on StakeAddress` ## Change discipline going forward diff --git a/pallas-txbuilder/src/conway.rs b/pallas-txbuilder/src/conway.rs index b63a27e..9d7aab2 100644 --- a/pallas-txbuilder/src/conway.rs +++ b/pallas-txbuilder/src/conway.rs @@ -7,7 +7,7 @@ use pallas_primitives::{ AuxiliaryData, Certificate, DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, PlutusData, PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript, PseudoTransactionOutput, Redeemer, RedeemerTag, - TransactionBody, TransactionInput, Tx, Value, WitnessSet, + TransactionBody, TransactionInput, Tx, Value, VotingProcedures, WitnessSet, }, Fragment, NonEmptyKeyValuePairs, NonEmptySet, PositiveCoin, }; @@ -261,7 +261,14 @@ impl BuildConway for StagingTransaction { collateral_return, reference_inputs, total_collateral: None, // TODO - voting_procedures: None, // TODO + voting_procedures: self + .voting_procedures + .as_deref() + .map(|bytes| { + VotingProcedures::decode_fragment(bytes) + .map_err(|_| TxBuilderError::CorruptedTxBytes) + }) + .transpose()?, proposal_procedures: None, // TODO treasury_value: None, // TODO donation: None, // TODO @@ -494,6 +501,67 @@ mod tests { assert!(body.transaction_body.certificates.is_none()); } + #[test] + fn voting_procedures_plumb_through_to_tx_body() { + use pallas_primitives::conway::{ + GovActionId, Vote, Voter, VotingProcedure, VotingProcedures, + }; + use pallas_codec::utils::Nullable; + use pallas_crypto::hash::Hash; + + // Construct a single-voter, single-action vote. + let voter_hash = Hash::<28>::from([7u8; 28]); + let voter = Voter::DRepKey(voter_hash); + let action = GovActionId { + transaction_id: Hash::<32>::from([9u8; 32]), + action_index: 0, + }; + let procedure = VotingProcedure { + vote: Vote::Yes, + anchor: Nullable::Null, + }; + + let inner = NonEmptyKeyValuePairs::Def(vec![(action, procedure)]); + let outer: VotingProcedures = NonEmptyKeyValuePairs::Def(vec![(voter, inner)]); + let vp_bytes = minicbor::to_vec(&outer).expect("encode voting procedures"); + + let tx = StagingTransaction::new() + .voting_procedures(vp_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 vp = body + .transaction_body + .voting_procedures + .expect("voting_procedures field populated"); + let outer_vec = vp.to_vec(); + assert_eq!(outer_vec.len(), 1); + let (got_voter, inner_kv) = &outer_vec[0]; + assert!(matches!(got_voter, Voter::DRepKey(h) if h.as_ref() == &[7u8; 28])); + let inner_vec = inner_kv.to_vec(); + assert_eq!(inner_vec.len(), 1); + let (got_action, got_proc) = &inner_vec[0]; + assert_eq!(got_action.transaction_id.as_ref(), &[9u8; 32]); + assert_eq!(got_action.action_index, 0); + assert!(matches!(got_proc.vote, Vote::Yes)); + } + + #[test] + fn no_voting_procedures_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.voting_procedures.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 35264a7..bc4e4c4 100644 --- a/pallas-txbuilder/src/transaction/model.rs +++ b/pallas-txbuilder/src/transaction/model.rs @@ -50,6 +50,12 @@ pub struct StagingTransaction { /// plumbs them into `TransactionBody.certificates`. Used for /// stake registration/delegation, pool registration, DRep ops, etc. pub certificates: Option>>, + /// CBOR-encoded `pallas_primitives::conway::VotingProcedures`. A single + /// blob (the type is `NonEmptyKeyValuePairs>`, so callers + /// pre-assemble the full map and encode it once). `conway::build_conway_raw` + /// decodes on the way out into `TransactionBody.voting_procedures`. + pub voting_procedures: Option>, // pub withdrawals: TODO // pub updates: TODO // pub phase_2_valid: TODO @@ -420,6 +426,23 @@ impl StagingTransaction { self.certificates = None; self } + + /// Attach pre-encoded `voting_procedures` (CBOR bytes of a + /// `conway::VotingProcedures`). Callers assemble the full + /// `NonEmptyKeyValuePairs>` map and pass the encoded bytes here. + /// + /// Used for DRep / SPO / committee voting on Conway governance actions. + pub fn voting_procedures(mut self, cbor_bytes: Vec) -> Self { + self.voting_procedures = Some(cbor_bytes); + self + } + + /// Drop any voting procedures previously attached. + pub fn clear_voting_procedures(mut self) -> Self { + self.voting_procedures = None; + self + } } // TODO: Don't want our wrapper types in fields public