feat(governance): v0.0.30 — submit_proposal + cast_vote + finalize_proposal impl complete + mock runtime integration tests E2E (10 scenarios)
Phase 5.1 GOVERNANCE pallet REAL IMPL canonical. Full dispatchables canonical 7+6+5 step CHN-4 flow + storage assertions + tally + voting period enforcement + mock runtime + 10 integration tests E2E pattern v0.0.26 mirror. ## Real dispatchables canonical - submit_proposal (7 steps): ensure_signed + stake>=MIN + derive_id BLAKE2-256 + collision + build + insert + emit - cast_vote (6 steps): ensure_signed + get_proposal + status_voting + block<=end + !already_voted + update_tally + emit - finalize_proposal (5 steps): ensure_signed + get_proposal + status_voting + block>end + tally + status + emit ## Mock runtime + 10 integration tests E2E canonical - scenario_1: Submit happy path → stored + event - scenario_2: Stake insufficient → StakeInsufficient (CHN-4 5.3) - scenario_3: Vote Aye tally → yes_weight += 500 - scenario_4: Vote Nay tally → nay_weight += 750 - scenario_5: Double vote → AlreadyVoted - scenario_6: Finalize voting open → VotingPeriodOpen - scenario_7: Finalize Approved yes majority → status=Approved + event - scenario_8: Finalize Rejected nay majority → status=Rejected - scenario_9: Vote closed period → VotingPeriodClosed - scenario_10: Full lifecycle (submit → 3 votes → finalize Approved) ## Files - EDIT: crates/origin-chain-governance/src/lib.rs (real impl + MIN_PROPOSAL_STAKE + account_bytes helper) - NEW: crates/origin-chain-governance/src/mock.rs (85 LOC mock runtime) - NEW: crates/origin-chain-governance/src/tests_integration.rs (290 LOC, 10 tests) - EDIT: CHANGELOG.md (v0.0.30 entry) ## Verification - cargo test -p origin-chain-governance — 17 tests verde, 0 failures (10 integration + 5 standalone + 2 auto-generated) 🛠️ Phase 5.1 real impl canonical. Runtime API v0.0.31 next → Phase 5 FOUNDATION COMPLETE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2a700ee6ca
commit
7c1425ff66
4 files changed
+559
-19
No files matched your search
@@ -6,7 +6,7 @@
|
||||
//! canonical, non-negotiable. Runtime programmatically rejects vote extrinsics
|
||||
//! from `governance_advisor` accounts.
|
||||
//!
|
||||
//! ## Status canonical: v0.0.28 — Scaffold trait-first foundation
|
||||
//! ## Status canonical: v0.0.30 — Real impl + mock runtime integration tests E2E
|
||||
//!
|
||||
//! Este crate provee canonical:
|
||||
//! - `Proposal` struct + `ProposalStatus` enum canonical ✅
|
||||
@@ -103,9 +103,10 @@ sp_api::decl_runtime_apis! {
|
||||
pub mod pallet {
|
||||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
use origin_chain_types::{ProposalId, VoteStance};
|
||||
use origin_chain_types::{AccountId, ProposalId, VoteStance};
|
||||
use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_runtime::traits::SaturatedConversion;
|
||||
|
||||
/// ProposalStatus canonical — proposal lifecycle stages canonical.
|
||||
#[derive(
|
||||
@@ -242,51 +243,175 @@ pub mod pallet {
|
||||
ProposalIdCollision,
|
||||
}
|
||||
|
||||
/// Dispatchables canonical (scaffold v0.0.28 — real impl v0.0.30).
|
||||
/// Minimum stake canonical proposal submission per CHN-4 5.3.
|
||||
/// 1,000 OGC = 1_000 × 10^18 (18 decimals OGC canonical).
|
||||
pub const MIN_PROPOSAL_STAKE: u128 = 1_000 * 10u128.pow(18);
|
||||
|
||||
/// Dispatchables canonical (real impl v0.0.30).
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// CHN-4 5.3 canonical: `submit_proposal` scaffold dispatchable.
|
||||
/// CHN-4 5.3 canonical: `submit_proposal` real dispatchable canonical.
|
||||
///
|
||||
/// 7-step canonical flow: ensure_signed + validate_stake + derive_id +
|
||||
/// collision_check + build + insert + emit.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(Weight::from_parts(10_000, 0))]
|
||||
pub fn submit_proposal(
|
||||
origin: OriginFor<T>,
|
||||
_title_hash: [u8; 32],
|
||||
_body_hash: [u8; 32],
|
||||
_voting_period_blocks: u64,
|
||||
_stake: u128,
|
||||
title_hash: [u8; 32],
|
||||
body_hash: [u8; 32],
|
||||
voting_period_blocks: u64,
|
||||
stake: u128,
|
||||
) -> DispatchResult {
|
||||
let _who = ensure_signed(origin)?;
|
||||
// SCAFFOLD v0.0.28: real impl v0.0.30
|
||||
let who = ensure_signed(origin)?;
|
||||
ensure!(stake >= MIN_PROPOSAL_STAKE, Error::<T>::StakeInsufficient);
|
||||
|
||||
let proposer_acct = AccountId::new(account_bytes::<T>(&who));
|
||||
let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash);
|
||||
ensure!(!Proposals::<T>::contains_key(id), Error::<T>::ProposalIdCollision);
|
||||
|
||||
let start_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||||
.saturated_into::<u64>();
|
||||
let end_block = start_block.saturating_add(voting_period_blocks);
|
||||
|
||||
let proposal = Proposal {
|
||||
id,
|
||||
proposer: proposer_acct,
|
||||
title_hash,
|
||||
body_hash,
|
||||
stake,
|
||||
start_block,
|
||||
end_block,
|
||||
status: ProposalStatus::Voting,
|
||||
yes_weight: 0,
|
||||
nay_weight: 0,
|
||||
abstain_weight: 0,
|
||||
};
|
||||
|
||||
Proposals::<T>::insert(id, proposal);
|
||||
|
||||
Self::deposit_event(Event::ProposalSubmitted {
|
||||
id,
|
||||
proposer: who,
|
||||
voting_period: voting_period_blocks,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// CHN-4 5.4 canonical: `cast_vote` scaffold dispatchable.
|
||||
/// CHN-4 5.4 canonical: `cast_vote` real dispatchable canonical.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(Weight::from_parts(10_000, 0))]
|
||||
pub fn cast_vote(
|
||||
origin: OriginFor<T>,
|
||||
_proposal_id: ProposalId,
|
||||
_stance: VoteStance,
|
||||
_stake_weight: u128,
|
||||
proposal_id: ProposalId,
|
||||
stance: VoteStance,
|
||||
stake_weight: u128,
|
||||
) -> DispatchResult {
|
||||
let _who = ensure_signed(origin)?;
|
||||
// SCAFFOLD v0.0.28: real impl v0.0.30
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
let mut proposal = Proposals::<T>::get(proposal_id)
|
||||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||||
|
||||
ensure!(
|
||||
matches!(proposal.status, ProposalStatus::Voting),
|
||||
Error::<T>::ProposalAlreadyFinalized
|
||||
);
|
||||
|
||||
let current_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||||
.saturated_into::<u64>();
|
||||
ensure!(
|
||||
current_block <= proposal.end_block,
|
||||
Error::<T>::VotingPeriodClosed
|
||||
);
|
||||
|
||||
ensure!(
|
||||
!Votes::<T>::contains_key(proposal_id, &who),
|
||||
Error::<T>::AlreadyVoted
|
||||
);
|
||||
|
||||
match stance {
|
||||
VoteStance::Aye => {
|
||||
proposal.yes_weight = proposal.yes_weight.saturating_add(stake_weight)
|
||||
}
|
||||
VoteStance::Nay => {
|
||||
proposal.nay_weight = proposal.nay_weight.saturating_add(stake_weight)
|
||||
}
|
||||
VoteStance::Abstain => {
|
||||
proposal.abstain_weight =
|
||||
proposal.abstain_weight.saturating_add(stake_weight)
|
||||
}
|
||||
}
|
||||
|
||||
Proposals::<T>::insert(proposal_id, proposal);
|
||||
Votes::<T>::insert(proposal_id, &who, stance);
|
||||
|
||||
Self::deposit_event(Event::VoteCast {
|
||||
id: proposal_id,
|
||||
voter: who,
|
||||
stance,
|
||||
weight: stake_weight,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// CHN-4 canonical: `finalize_proposal` scaffold dispatchable.
|
||||
/// CHN-4 canonical: `finalize_proposal` real dispatchable canonical.
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(Weight::from_parts(10_000, 0))]
|
||||
pub fn finalize_proposal(
|
||||
origin: OriginFor<T>,
|
||||
_proposal_id: ProposalId,
|
||||
proposal_id: ProposalId,
|
||||
) -> DispatchResult {
|
||||
let _who = ensure_signed(origin)?;
|
||||
// SCAFFOLD v0.0.28: real impl v0.0.30
|
||||
|
||||
let mut proposal = Proposals::<T>::get(proposal_id)
|
||||
.ok_or(Error::<T>::ProposalNotFound)?;
|
||||
|
||||
ensure!(
|
||||
matches!(proposal.status, ProposalStatus::Voting),
|
||||
Error::<T>::ProposalAlreadyFinalized
|
||||
);
|
||||
|
||||
let current_block: u64 = <frame_system::Pallet<T>>::block_number()
|
||||
.saturated_into::<u64>();
|
||||
ensure!(
|
||||
current_block > proposal.end_block,
|
||||
Error::<T>::VotingPeriodOpen
|
||||
);
|
||||
|
||||
let final_status = if proposal.yes_weight > proposal.nay_weight {
|
||||
ProposalStatus::Approved
|
||||
} else {
|
||||
ProposalStatus::Rejected
|
||||
};
|
||||
|
||||
proposal.status = final_status;
|
||||
let yes_weight = proposal.yes_weight;
|
||||
let nay_weight = proposal.nay_weight;
|
||||
|
||||
Proposals::<T>::insert(proposal_id, proposal);
|
||||
|
||||
Self::deposit_event(Event::ProposalFinalized {
|
||||
id: proposal_id,
|
||||
status: final_status,
|
||||
yes_weight,
|
||||
nay_weight,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper canonical para convert T::AccountId → 32-byte canonical.
|
||||
fn account_bytes<T: Config>(account: &T::AccountId) -> [u8; 32] {
|
||||
let encoded = account.encode();
|
||||
let mut bytes = [0u8; 32];
|
||||
let copy_len = encoded.len().min(32);
|
||||
bytes[..copy_len].copy_from_slice(&encoded[..copy_len]);
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Pure helpers canonical para Runtime API v0.0.31 consumers + tests.
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Query proposal canonical sin storage mutation (read-only canonical).
|
||||
@@ -301,6 +426,14 @@ pub mod pallet {
|
||||
}
|
||||
}
|
||||
|
||||
// v0.0.30 — Mock runtime canonical + integration tests E2E CHN-4 flow.
|
||||
// Pattern v0.0.18 + v0.0.22 + v0.0.26 mirror.
|
||||
#[cfg(test)]
|
||||
pub mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_integration;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user