Skip to content

Commit

Permalink
Governance Builders (#333)
Browse files Browse the repository at this point in the history
* Governance Builders

Adds `VoteBuilder` and `ProposalBuilder` to facilitate creation of votes
and voting proposals to transactions.

* Hook Vote/Proposal Builders into TxBuilder/RedeemerBuilder

* Fix deposit/required certs

* Fix proposal deposits
  • Loading branch information
rooooooooob authored Jul 16, 2024
1 parent 508f31b commit 4cb8432
Show file tree
Hide file tree
Showing 18 changed files with 873 additions and 20 deletions.
11 changes: 8 additions & 3 deletions chain/rust/src/builders/certificate_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,14 @@ pub fn add_cert_vkeys(
vkeys.insert(*hash);
}
},
Certificate::RegDrepCert(_cert) => {
// does not need a witness
}
Certificate::RegDrepCert(cert) => match &cert.drep_credential {
StakeCredential::Script { hash, .. } => {
return Err(CertBuilderError::ExpectedKeyHash(*hash))
}
StakeCredential::PubKey { hash, .. } => {
vkeys.insert(*hash);
}
},
Certificate::UnregDrepCert(cert) => match &cert.drep_credential {
StakeCredential::Script { hash, .. } => {
return Err(CertBuilderError::ExpectedKeyHash(*hash))
Expand Down
7 changes: 2 additions & 5 deletions chain/rust/src/builders/input_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::builders::witness_builder::{InputAggregateWitnessData, PartialPlutusW

use super::{
tx_builder::TransactionUnspentOutput,
utils::required_wits_from_required_signers,
witness_builder::{NativeScriptWitnessInfo, RequiredWitnessSet},
};

Expand Down Expand Up @@ -138,11 +139,7 @@ impl SingleInputBuilder {
required_signers: RequiredSigners,
datum: Option<PlutusData>,
) -> Result<InputBuilderResult, InputBuilderError> {
let mut required_wits = RequiredWitnessSet::default();
required_signers
.as_ref()
.iter()
.for_each(|required_signer| required_wits.add_vkey_key_hash(*required_signer));
let mut required_wits = required_wits_from_required_signers(&required_signers);
input_required_wits(&self.utxo_info, &mut required_wits);
let mut required_wits_left = required_wits.clone();

Expand Down
3 changes: 3 additions & 0 deletions chain/rust/src/builders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ pub mod certificate_builder;
pub mod input_builder;
pub mod mint_builder;
pub mod output_builder;
pub mod proposal_builder;
pub mod redeemer_builder;
pub mod tx_builder;
mod utils;
pub mod vote_builder;
pub mod withdrawal_builder;
pub mod witness_builder;
165 changes: 165 additions & 0 deletions chain/rust/src/builders/proposal_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use crate::{
crypto::hash::hash_plutus_data, governance::ProposalProcedure, plutus::PlutusData,
transaction::NativeScript, RequiredSigners,
};

use super::{
utils::required_wits_from_required_signers,
witness_builder::{
InputAggregateWitnessData, NativeScriptWitnessInfo, PartialPlutusWitness,
RequiredWitnessSet,
},
};

#[derive(Debug, thiserror::Error)]
pub enum ProposalBuilderError {
#[error("Proposal uses script. Call with_plutus_proposal() instead.")]
ProposalIsScript,
#[error("Proposal uses key hash. Call with_proposal() instead.")]
ProposalIsKeyHash,
#[error("Missing the following witnesses for the input: {0:?}")]
MissingWitnesses(Box<RequiredWitnessSet>),
}

#[derive(Clone, Debug, Default)]
pub struct ProposalBuilderResult {
pub proposals: Vec<ProposalProcedure>,
pub required_wits: RequiredWitnessSet,
pub aggregate_witnesses: Vec<InputAggregateWitnessData>,
}

#[derive(Clone, Debug)]
pub struct ProposalBuilder {
result: ProposalBuilderResult,
}

impl Default for ProposalBuilder {
fn default() -> Self {
Self::new()
}
}

impl ProposalBuilder {
pub fn new() -> Self {
Self {
result: ProposalBuilderResult::default(),
}
}

pub fn with_proposal(
mut self,
proposal: ProposalProcedure,
) -> Result<Self, ProposalBuilderError> {
if proposal.gov_action.script_hash().is_some() {
return Err(ProposalBuilderError::ProposalIsScript);
}

self.result.proposals.push(proposal.clone());

Ok(self)
}

pub fn with_native_script_proposal(
mut self,
proposal: ProposalProcedure,
native_script: NativeScript,
witness_info: NativeScriptWitnessInfo,
) -> Result<Self, ProposalBuilderError> {
if let Some(script_hash) = proposal.gov_action.script_hash() {
if *script_hash != native_script.hash() {
let mut err_req_wits = RequiredWitnessSet::new();
err_req_wits.add_script_hash(*script_hash);
return Err(ProposalBuilderError::MissingWitnesses(Box::new(
err_req_wits,
)));
}
self.result.required_wits.add_script_hash(*script_hash);
} else {
return Err(ProposalBuilderError::ProposalIsKeyHash);
}

self.result.proposals.push(proposal);

self.result
.aggregate_witnesses
.push(InputAggregateWitnessData::NativeScript(
native_script,
witness_info,
));

Ok(self)
}

pub fn with_plutus_proposal(
self,
proposal: ProposalProcedure,
partial_witness: PartialPlutusWitness,
required_signers: RequiredSigners,
datum: PlutusData,
) -> Result<Self, ProposalBuilderError> {
self.with_plutus_proposal_impl(proposal, partial_witness, required_signers, Some(datum))
}

pub fn with_plutus_proposal_inline_datum(
self,
proposal: ProposalProcedure,
partial_witness: PartialPlutusWitness,
required_signers: RequiredSigners,
) -> Result<Self, ProposalBuilderError> {
self.with_plutus_proposal_impl(proposal, partial_witness, required_signers, None)
}

fn with_plutus_proposal_impl(
mut self,
proposal: ProposalProcedure,
partial_witness: PartialPlutusWitness,
required_signers: RequiredSigners,
datum: Option<PlutusData>,
) -> Result<Self, ProposalBuilderError> {
let mut required_wits = required_wits_from_required_signers(&required_signers);
if let Some(script_hash) = proposal.gov_action.script_hash() {
required_wits.add_script_hash(*script_hash);
} else {
return Err(ProposalBuilderError::ProposalIsKeyHash);
}

let mut required_wits_left = required_wits.clone();

// no way to know these at this time
required_wits_left.vkeys.clear();

let script_hash = partial_witness.script.hash();

// check the user provided all the required witnesses
required_wits_left.scripts.remove(&script_hash);
if let Some(datum) = &datum {
required_wits_left
.plutus_data
.remove(&hash_plutus_data(datum));
}

if required_wits_left.len() > 0 {
return Err(ProposalBuilderError::MissingWitnesses(Box::new(
required_wits_left,
)));
}

self.result.proposals.push(proposal);

self.result.required_wits.add_all(required_wits);

self.result
.aggregate_witnesses
.push(InputAggregateWitnessData::PlutusScript(
partial_witness,
required_signers,
datum,
));

Ok(self)
}

pub fn build(self) -> ProposalBuilderResult {
self.result
}
}
53 changes: 50 additions & 3 deletions chain/rust/src/builders/redeemer_builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::{
certificate_builder::CertificateBuilderResult, input_builder::InputBuilderResult,
mint_builder::MintBuilderResult, withdrawal_builder::WithdrawalBuilderResult,
mint_builder::MintBuilderResult, proposal_builder::ProposalBuilderResult,
vote_builder::VoteBuilderResult, withdrawal_builder::WithdrawalBuilderResult,
};
use crate::{
address::RewardAddress,
Expand Down Expand Up @@ -97,6 +98,10 @@ pub struct RedeemerSetBuilder {
// certificates in the DCert list are indexed in the order in which they arranged in the (full, unfiltered)
// list of certificates inside the transaction
cert: Vec<Option<UntaggedRedeemerPlaceholder>>,

proposals: Vec<Option<UntaggedRedeemerPlaceholder>>,

votes: Vec<Option<UntaggedRedeemerPlaceholder>>,
}

impl RedeemerSetBuilder {
Expand Down Expand Up @@ -143,10 +148,18 @@ impl RedeemerSetBuilder {
)));
}
RedeemerTag::Proposing => {
todo!("https://github.com/dcSpark/cardano-multiplatform-lib/issues/323")
let entry = self.proposals.get_mut(key.index as usize).unwrap();
*entry = Some(UntaggedRedeemerPlaceholder::Full(UntaggedRedeemer::new(
entry.as_ref().unwrap().data().clone(),
ex_units,
)));
}
RedeemerTag::Voting => {
todo!("https://github.com/dcSpark/cardano-multiplatform-lib/issues/323")
let entry = self.votes.get_mut(key.index as usize).unwrap();
*entry = Some(UntaggedRedeemerPlaceholder::Full(UntaggedRedeemer::new(
entry.as_ref().unwrap().data().clone(),
ex_units,
)));
}
}
}
Expand Down Expand Up @@ -217,6 +230,28 @@ impl RedeemerSetBuilder {
}
}

pub fn add_proposal(&mut self, result: &ProposalBuilderResult) {
for aggregate_witness in &result.aggregate_witnesses {
if let Some(data) = aggregate_witness.redeemer_plutus_data() {
self.proposals
.push(Some(UntaggedRedeemerPlaceholder::JustData(data.clone())));
} else {
self.proposals.push(None);
}
}
}

pub fn add_vote(&mut self, result: &VoteBuilderResult) {
for aggregate_witness in &result.aggregate_witnesses {
if let Some(data) = aggregate_witness.redeemer_plutus_data() {
self.votes
.push(Some(UntaggedRedeemerPlaceholder::JustData(data.clone())));
} else {
self.votes.push(None);
}
}
}

pub fn build(&self, default_to_dummy_exunits: bool) -> Result<Redeemers, RedeemerBuilderError> {
let mut redeemers = Vec::new();
// Calling iter on a BTreeMap returns a list of sorted keys
Expand Down Expand Up @@ -244,6 +279,18 @@ impl RedeemerSetBuilder {
&mut self.cert.iter().map(|entry| (&(), entry)),
default_to_dummy_exunits,
)?;
self.remove_placeholders_and_tag(
&mut redeemers,
RedeemerTag::Proposing,
&mut self.proposals.iter().map(|entry| (&(), entry)),
default_to_dummy_exunits,
)?;
self.remove_placeholders_and_tag(
&mut redeemers,
RedeemerTag::Voting,
&mut self.votes.iter().map(|entry| (&(), entry)),
default_to_dummy_exunits,
)?;

Ok(Redeemers::new_arr_legacy_redeemer(redeemers))
}
Expand Down
Loading

0 comments on commit 4cb8432

Please sign in to comment.