1#![cfg_attr(not(feature = "std"), no_std)]
85
86#[cfg(test)]
87mod mock;
88#[cfg(test)]
89mod tests;
90
91#[cfg(feature = "runtime-benchmarks")]
92mod benchmarking;
93pub mod migration;
94mod types;
95pub mod weights;
96
97extern crate alloc;
98
99use alloc::{boxed::Box, vec, vec::Vec};
100use codec::{Decode, Encode, MaxEncodedLen};
101use pezframe_support::pezpallet_prelude::*;
102use pezframe_system::pezpallet_prelude::*;
103use pezsp_runtime::{
104 traits::{Dispatchable, Saturating, StaticLookup, Zero},
105 DispatchError, RuntimeDebug,
106};
107
108use pezframe_support::{
109 dispatch::{DispatchResult, DispatchResultWithPostInfo, GetDispatchInfo, PostDispatchInfo},
110 ensure,
111 traits::{
112 ChangeMembers, Currency, Get, InitializeMembers, IsSubType, OnUnbalanced,
113 ReservableCurrency,
114 },
115 weights::Weight,
116};
117use scale_info::TypeInfo;
118
119pub use pezpallet::*;
120pub use types::*;
121pub use weights::*;
122
123pub const LOG_TARGET: &str = "runtime::alliance";
125
126pub type ProposalIndex = u32;
128
129type UrlOf<T, I> = BoundedVec<u8, <T as pezpallet::Config<I>>::MaxWebsiteUrlLength>;
130
131type BalanceOf<T, I> =
132 <<T as Config<I>>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
133type NegativeImbalanceOf<T, I> = <<T as Config<I>>::Currency as Currency<
134 <T as pezframe_system::Config>::AccountId,
135>>::NegativeImbalance;
136
137pub trait IdentityVerifier<AccountId> {
139 fn has_required_identities(who: &AccountId) -> bool;
142
143 fn has_good_judgement(who: &AccountId) -> bool;
145
146 fn super_account_id(who: &AccountId) -> Option<AccountId>;
149}
150
151impl<AccountId> IdentityVerifier<AccountId> for () {
153 fn has_required_identities(_who: &AccountId) -> bool {
154 true
155 }
156
157 fn has_good_judgement(_who: &AccountId) -> bool {
158 true
159 }
160
161 fn super_account_id(_who: &AccountId) -> Option<AccountId> {
162 None
163 }
164}
165
166pub trait ProposalProvider<AccountId, Hash, Proposal> {
169 fn propose_proposal(
172 who: AccountId,
173 threshold: u32,
174 proposal: Box<Proposal>,
175 length_bound: u32,
176 ) -> Result<(u32, u32), DispatchError>;
177
178 fn vote_proposal(
181 who: AccountId,
182 proposal: Hash,
183 index: ProposalIndex,
184 approve: bool,
185 ) -> Result<bool, DispatchError>;
186
187 fn close_proposal(
189 proposal_hash: Hash,
190 index: ProposalIndex,
191 proposal_weight_bound: Weight,
192 length_bound: u32,
193 ) -> DispatchResultWithPostInfo;
194
195 fn proposal_of(proposal_hash: Hash) -> Option<Proposal>;
197}
198
199#[derive(Copy, Clone, PartialEq, Eq, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)]
201pub enum MemberRole {
202 Fellow,
203 Ally,
204 Retiring,
205}
206
207#[derive(
209 Clone,
210 PartialEq,
211 Eq,
212 RuntimeDebug,
213 Encode,
214 Decode,
215 DecodeWithMemTracking,
216 TypeInfo,
217 MaxEncodedLen,
218)]
219pub enum UnscrupulousItem<AccountId, Url> {
220 AccountId(AccountId),
221 Website(Url),
222}
223
224type UnscrupulousItemOf<T, I> =
225 UnscrupulousItem<<T as pezframe_system::Config>::AccountId, UrlOf<T, I>>;
226
227type AccountIdLookupOf<T> = <<T as pezframe_system::Config>::Lookup as StaticLookup>::Source;
228
229#[pezframe_support::pezpallet]
230pub mod pezpallet {
231 use super::*;
232
233 #[pezpallet::pezpallet]
234 #[pezpallet::storage_version(migration::STORAGE_VERSION)]
235 pub struct Pezpallet<T, I = ()>(PhantomData<(T, I)>);
236
237 #[pezpallet::config]
238 pub trait Config<I: 'static = ()>: pezframe_system::Config {
239 #[allow(deprecated)]
241 type RuntimeEvent: From<Event<Self, I>>
242 + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
243
244 type Proposal: Parameter
246 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
247 + From<pezframe_system::Call<Self>>
248 + From<Call<Self, I>>
249 + GetDispatchInfo
250 + IsSubType<Call<Self, I>>
251 + IsType<<Self as pezframe_system::Config>::RuntimeCall>;
252
253 type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
255
256 type MembershipManager: EnsureOrigin<Self::RuntimeOrigin>;
258
259 type AnnouncementOrigin: EnsureOrigin<Self::RuntimeOrigin>;
261
262 type Currency: ReservableCurrency<Self::AccountId>;
264
265 type Slashed: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
267
268 type InitializeMembers: InitializeMembers<Self::AccountId>;
270
271 type MembershipChanged: ChangeMembers<Self::AccountId>;
273
274 type IdentityVerifier: IdentityVerifier<Self::AccountId>;
276
277 type ProposalProvider: ProposalProvider<Self::AccountId, Self::Hash, Self::Proposal>;
279
280 type MaxProposals: Get<ProposalIndex>;
282
283 type MaxFellows: Get<u32>;
289
290 type MaxAllies: Get<u32>;
296
297 #[pezpallet::constant]
299 type MaxUnscrupulousItems: Get<u32>;
300
301 #[pezpallet::constant]
303 type MaxWebsiteUrlLength: Get<u32>;
304
305 #[pezpallet::constant]
307 type AllyDeposit: Get<BalanceOf<Self, I>>;
308
309 #[pezpallet::constant]
311 type MaxAnnouncementsCount: Get<u32>;
312
313 #[pezpallet::constant]
315 type MaxMembersCount: Get<u32>;
316
317 type WeightInfo: WeightInfo;
319
320 type RetirementPeriod: Get<BlockNumberFor<Self>>;
323 }
324
325 #[pezpallet::error]
326 pub enum Error<T, I = ()> {
327 AllianceNotYetInitialized,
329 AllianceAlreadyInitialized,
331 AlreadyMember,
333 NotMember,
335 NotAlly,
337 NoVotingRights,
339 AlreadyElevated,
341 AlreadyUnscrupulous,
343 AccountNonGrata,
346 NotListedAsUnscrupulous,
348 TooManyUnscrupulousItems,
350 TooLongWebsiteUrl,
352 InsufficientFunds,
354 WithoutRequiredIdentityFields,
356 WithoutGoodIdentityJudgement,
358 MissingProposalHash,
360 MissingAnnouncement,
362 TooManyMembers,
364 TooManyAnnouncements,
366 BadWitness,
368 AlreadyRetiring,
370 RetirementNoticeNotGiven,
372 RetirementPeriodNotPassed,
374 FellowsMissing,
376 }
377
378 #[pezpallet::event]
379 #[pezpallet::generate_deposit(pub(super) fn deposit_event)]
380 pub enum Event<T: Config<I>, I: 'static = ()> {
381 NewRuleSet { rule: Cid },
383 Announced { announcement: Cid },
385 AnnouncementRemoved { announcement: Cid },
387 MembersInitialized { fellows: Vec<T::AccountId>, allies: Vec<T::AccountId> },
389 NewAllyJoined {
391 ally: T::AccountId,
392 nominator: Option<T::AccountId>,
393 reserved: Option<BalanceOf<T, I>>,
394 },
395 AllyElevated { ally: T::AccountId },
397 MemberRetirementPeriodStarted { member: T::AccountId },
399 MemberRetired { member: T::AccountId, unreserved: Option<BalanceOf<T, I>> },
401 MemberKicked { member: T::AccountId, slashed: Option<BalanceOf<T, I>> },
403 UnscrupulousItemAdded { items: Vec<UnscrupulousItemOf<T, I>> },
405 UnscrupulousItemRemoved { items: Vec<UnscrupulousItemOf<T, I>> },
407 AllianceDisbanded { fellow_members: u32, ally_members: u32, unreserved: u32 },
409 FellowAbdicated { fellow: T::AccountId },
411 }
412
413 #[pezpallet::genesis_config]
414 #[derive(pezframe_support::DefaultNoBound)]
415 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
416 pub fellows: Vec<T::AccountId>,
417 pub allies: Vec<T::AccountId>,
418 #[serde(skip)]
419 pub phantom: PhantomData<(T, I)>,
420 }
421
422 #[pezpallet::hooks]
423 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pezpallet<T, I> {
424 #[cfg(feature = "try-runtime")]
425 fn try_state(_n: BlockNumberFor<T>) -> Result<(), pezsp_runtime::TryRuntimeError> {
426 Self::do_try_state()
427 }
428 }
429
430 #[pezpallet::genesis_build]
431 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
432 fn build(&self) {
433 for m in self.fellows.iter().chain(self.allies.iter()) {
434 assert!(
435 Pezpallet::<T, I>::has_identity(m).is_ok(),
436 "Member does not set identity!"
437 );
438 }
439
440 if !self.fellows.is_empty() {
441 assert!(
442 !Pezpallet::<T, I>::has_member(MemberRole::Fellow),
443 "Fellows are already initialized!"
444 );
445 let members: BoundedVec<T::AccountId, T::MaxMembersCount> =
446 self.fellows.clone().try_into().expect("Too many genesis fellows");
447 Members::<T, I>::insert(MemberRole::Fellow, members);
448 }
449 if !self.allies.is_empty() {
450 assert!(
451 !Pezpallet::<T, I>::has_member(MemberRole::Ally),
452 "Allies are already initialized!"
453 );
454 assert!(
455 !self.fellows.is_empty(),
456 "Fellows must be provided to initialize the Alliance"
457 );
458 let members: BoundedVec<T::AccountId, T::MaxMembersCount> =
459 self.allies.clone().try_into().expect("Too many genesis allies");
460 Members::<T, I>::insert(MemberRole::Ally, members);
461 }
462
463 T::InitializeMembers::initialize_members(self.fellows.as_slice())
464 }
465 }
466
467 #[pezpallet::storage]
470 pub type Rule<T: Config<I>, I: 'static = ()> = StorageValue<_, Cid, OptionQuery>;
471
472 #[pezpallet::storage]
474 pub type Announcements<T: Config<I>, I: 'static = ()> =
475 StorageValue<_, BoundedVec<Cid, T::MaxAnnouncementsCount>, ValueQuery>;
476
477 #[pezpallet::storage]
479 pub type DepositOf<T: Config<I>, I: 'static = ()> =
480 StorageMap<_, Blake2_128Concat, T::AccountId, BalanceOf<T, I>, OptionQuery>;
481
482 #[pezpallet::storage]
484 pub type Members<T: Config<I>, I: 'static = ()> = StorageMap<
485 _,
486 Twox64Concat,
487 MemberRole,
488 BoundedVec<T::AccountId, T::MaxMembersCount>,
489 ValueQuery,
490 >;
491
492 #[pezpallet::storage]
495 pub type RetiringMembers<T: Config<I>, I: 'static = ()> =
496 StorageMap<_, Blake2_128Concat, T::AccountId, BlockNumberFor<T>, OptionQuery>;
497
498 #[pezpallet::storage]
501 pub type UnscrupulousAccounts<T: Config<I>, I: 'static = ()> =
502 StorageValue<_, BoundedVec<T::AccountId, T::MaxUnscrupulousItems>, ValueQuery>;
503
504 #[pezpallet::storage]
506 pub type UnscrupulousWebsites<T: Config<I>, I: 'static = ()> =
507 StorageValue<_, BoundedVec<UrlOf<T, I>, T::MaxUnscrupulousItems>, ValueQuery>;
508
509 #[pezpallet::call(weight(<T as Config<I>>::WeightInfo))]
510 impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
511 #[pezpallet::call_index(0)]
515 #[pezpallet::weight(T::WeightInfo::propose_proposed(
516 *length_bound, T::MaxFellows::get(), T::MaxProposals::get(), ))]
520 pub fn propose(
521 origin: OriginFor<T>,
522 #[pezpallet::compact] threshold: u32,
523 proposal: Box<<T as Config<I>>::Proposal>,
524 #[pezpallet::compact] length_bound: u32,
525 ) -> DispatchResult {
526 let proposer = ensure_signed(origin)?;
527 ensure!(Self::has_voting_rights(&proposer), Error::<T, I>::NoVotingRights);
528
529 T::ProposalProvider::propose_proposal(proposer, threshold, proposal, length_bound)?;
530 Ok(())
531 }
532
533 #[pezpallet::call_index(1)]
537 #[pezpallet::weight(T::WeightInfo::vote(T::MaxFellows::get()))]
538 pub fn vote(
539 origin: OriginFor<T>,
540 proposal: T::Hash,
541 #[pezpallet::compact] index: ProposalIndex,
542 approve: bool,
543 ) -> DispatchResult {
544 let who = ensure_signed(origin)?;
545 ensure!(Self::has_voting_rights(&who), Error::<T, I>::NoVotingRights);
546
547 T::ProposalProvider::vote_proposal(who, proposal, index, approve)?;
548 Ok(())
549 }
550
551 #[pezpallet::call_index(3)]
559 #[pezpallet::weight(T::WeightInfo::init_members(
560 fellows.len() as u32,
561 allies.len() as u32,
562 ))]
563 pub fn init_members(
564 origin: OriginFor<T>,
565 fellows: Vec<T::AccountId>,
566 allies: Vec<T::AccountId>,
567 ) -> DispatchResult {
568 ensure_root(origin)?;
569
570 ensure!(!fellows.is_empty(), Error::<T, I>::FellowsMissing);
571 ensure!(!Self::is_initialized(), Error::<T, I>::AllianceAlreadyInitialized);
572
573 let mut fellows: BoundedVec<T::AccountId, T::MaxMembersCount> =
574 fellows.try_into().map_err(|_| Error::<T, I>::TooManyMembers)?;
575 let mut allies: BoundedVec<T::AccountId, T::MaxMembersCount> =
576 allies.try_into().map_err(|_| Error::<T, I>::TooManyMembers)?;
577
578 for member in fellows.iter().chain(allies.iter()) {
579 Self::has_identity(member)?;
580 }
581
582 fellows.sort();
583 Members::<T, I>::insert(&MemberRole::Fellow, fellows.clone());
584 allies.sort();
585 Members::<T, I>::insert(&MemberRole::Ally, allies.clone());
586
587 let mut voteable_members = fellows.clone();
588 voteable_members.sort();
589
590 T::InitializeMembers::initialize_members(&voteable_members);
591
592 log::debug!(
593 target: LOG_TARGET,
594 "Initialize alliance fellows: {:?}, allies: {:?}",
595 fellows,
596 allies
597 );
598
599 Self::deposit_event(Event::MembersInitialized {
600 fellows: fellows.into(),
601 allies: allies.into(),
602 });
603 Ok(())
604 }
605
606 #[pezpallet::call_index(4)]
610 #[pezpallet::weight(T::WeightInfo::disband(
611 witness.fellow_members,
612 witness.ally_members,
613 witness.fellow_members.saturating_add(witness.ally_members),
614 ))]
615 pub fn disband(
616 origin: OriginFor<T>,
617 witness: DisbandWitness,
618 ) -> DispatchResultWithPostInfo {
619 ensure_root(origin)?;
620
621 ensure!(!witness.is_zero(), Error::<T, I>::BadWitness);
622 ensure!(
623 Self::voting_members_count() <= witness.fellow_members,
624 Error::<T, I>::BadWitness
625 );
626 ensure!(Self::ally_members_count() <= witness.ally_members, Error::<T, I>::BadWitness);
627 ensure!(Self::is_initialized(), Error::<T, I>::AllianceNotYetInitialized);
628
629 let voting_members = Self::voting_members();
630 T::MembershipChanged::change_members_sorted(&[], &voting_members, &[]);
631
632 let ally_members = Self::members_of(MemberRole::Ally);
633 let mut unreserve_count: u32 = 0;
634 for member in voting_members.iter().chain(ally_members.iter()) {
635 if let Some(deposit) = DepositOf::<T, I>::take(&member) {
636 let err_amount = T::Currency::unreserve(&member, deposit);
637 debug_assert!(err_amount.is_zero());
638 unreserve_count += 1;
639 }
640 }
641
642 Members::<T, I>::remove(&MemberRole::Fellow);
643 Members::<T, I>::remove(&MemberRole::Ally);
644
645 Self::deposit_event(Event::AllianceDisbanded {
646 fellow_members: voting_members.len() as u32,
647 ally_members: ally_members.len() as u32,
648 unreserved: unreserve_count,
649 });
650
651 Ok(Some(T::WeightInfo::disband(
652 voting_members.len() as u32,
653 ally_members.len() as u32,
654 unreserve_count,
655 ))
656 .into())
657 }
658
659 #[pezpallet::call_index(5)]
661 pub fn set_rule(origin: OriginFor<T>, rule: Cid) -> DispatchResult {
662 T::AdminOrigin::ensure_origin(origin)?;
663
664 Rule::<T, I>::put(&rule);
665
666 Self::deposit_event(Event::NewRuleSet { rule });
667 Ok(())
668 }
669
670 #[pezpallet::call_index(6)]
672 pub fn announce(origin: OriginFor<T>, announcement: Cid) -> DispatchResult {
673 T::AnnouncementOrigin::ensure_origin(origin)?;
674
675 let mut announcements = <Announcements<T, I>>::get();
676 announcements
677 .try_push(announcement.clone())
678 .map_err(|_| Error::<T, I>::TooManyAnnouncements)?;
679 <Announcements<T, I>>::put(announcements);
680
681 Self::deposit_event(Event::Announced { announcement });
682 Ok(())
683 }
684
685 #[pezpallet::call_index(7)]
687 pub fn remove_announcement(origin: OriginFor<T>, announcement: Cid) -> DispatchResult {
688 T::AnnouncementOrigin::ensure_origin(origin)?;
689
690 let mut announcements = <Announcements<T, I>>::get();
691 let pos = announcements
692 .binary_search(&announcement)
693 .ok()
694 .ok_or(Error::<T, I>::MissingAnnouncement)?;
695 announcements.remove(pos);
696 <Announcements<T, I>>::put(announcements);
697
698 Self::deposit_event(Event::AnnouncementRemoved { announcement });
699 Ok(())
700 }
701
702 #[pezpallet::call_index(8)]
704 pub fn join_alliance(origin: OriginFor<T>) -> DispatchResult {
705 let who = ensure_signed(origin)?;
706
707 ensure!(Self::is_initialized(), Error::<T, I>::AllianceNotYetInitialized);
715
716 ensure!(!Self::is_unscrupulous_account(&who), Error::<T, I>::AccountNonGrata);
718 ensure!(!Self::is_member(&who), Error::<T, I>::AlreadyMember);
719 Self::has_identity(&who)?;
722
723 let deposit = T::AllyDeposit::get();
724 T::Currency::reserve(&who, deposit).map_err(|_| Error::<T, I>::InsufficientFunds)?;
725 <DepositOf<T, I>>::insert(&who, deposit);
726
727 Self::add_member(&who, MemberRole::Ally)?;
728
729 Self::deposit_event(Event::NewAllyJoined {
730 ally: who,
731 nominator: None,
732 reserved: Some(deposit),
733 });
734 Ok(())
735 }
736
737 #[pezpallet::call_index(9)]
740 pub fn nominate_ally(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
741 let nominator = ensure_signed(origin)?;
742 ensure!(Self::has_voting_rights(&nominator), Error::<T, I>::NoVotingRights);
743 let who = T::Lookup::lookup(who)?;
744
745 ensure!(!Self::is_unscrupulous_account(&who), Error::<T, I>::AccountNonGrata);
747 ensure!(!Self::is_member(&who), Error::<T, I>::AlreadyMember);
748 Self::has_identity(&who)?;
751
752 Self::add_member(&who, MemberRole::Ally)?;
753
754 Self::deposit_event(Event::NewAllyJoined {
755 ally: who,
756 nominator: Some(nominator),
757 reserved: None,
758 });
759 Ok(())
760 }
761
762 #[pezpallet::call_index(10)]
764 pub fn elevate_ally(origin: OriginFor<T>, ally: AccountIdLookupOf<T>) -> DispatchResult {
765 T::MembershipManager::ensure_origin(origin)?;
766 let ally = T::Lookup::lookup(ally)?;
767 ensure!(Self::is_ally(&ally), Error::<T, I>::NotAlly);
768 ensure!(!Self::has_voting_rights(&ally), Error::<T, I>::AlreadyElevated);
769
770 Self::remove_member(&ally, MemberRole::Ally)?;
771 Self::add_member(&ally, MemberRole::Fellow)?;
772
773 Self::deposit_event(Event::AllyElevated { ally });
774 Ok(())
775 }
776
777 #[pezpallet::call_index(11)]
780 pub fn give_retirement_notice(origin: OriginFor<T>) -> DispatchResult {
781 let who = ensure_signed(origin)?;
782 let role = Self::member_role_of(&who).ok_or(Error::<T, I>::NotMember)?;
783 ensure!(role.ne(&MemberRole::Retiring), Error::<T, I>::AlreadyRetiring);
784
785 Self::remove_member(&who, role)?;
786 Self::add_member(&who, MemberRole::Retiring)?;
787 <RetiringMembers<T, I>>::insert(
788 &who,
789 pezframe_system::Pezpallet::<T>::block_number()
790 .saturating_add(T::RetirementPeriod::get()),
791 );
792
793 Self::deposit_event(Event::MemberRetirementPeriodStarted { member: who });
794 Ok(())
795 }
796
797 #[pezpallet::call_index(12)]
802 pub fn retire(origin: OriginFor<T>) -> DispatchResult {
803 let who = ensure_signed(origin)?;
804 let retirement_period_end = RetiringMembers::<T, I>::get(&who)
805 .ok_or(Error::<T, I>::RetirementNoticeNotGiven)?;
806 ensure!(
807 pezframe_system::Pezpallet::<T>::block_number() >= retirement_period_end,
808 Error::<T, I>::RetirementPeriodNotPassed
809 );
810
811 Self::remove_member(&who, MemberRole::Retiring)?;
812 <RetiringMembers<T, I>>::remove(&who);
813 let deposit = DepositOf::<T, I>::take(&who);
814 if let Some(deposit) = deposit {
815 let err_amount = T::Currency::unreserve(&who, deposit);
816 debug_assert!(err_amount.is_zero());
817 }
818 Self::deposit_event(Event::MemberRetired { member: who, unreserved: deposit });
819 Ok(())
820 }
821
822 #[pezpallet::call_index(13)]
824 pub fn kick_member(origin: OriginFor<T>, who: AccountIdLookupOf<T>) -> DispatchResult {
825 T::MembershipManager::ensure_origin(origin)?;
826 let member = T::Lookup::lookup(who)?;
827
828 let role = Self::member_role_of(&member).ok_or(Error::<T, I>::NotMember)?;
829 Self::remove_member(&member, role)?;
830 let deposit = DepositOf::<T, I>::take(member.clone());
831 if let Some(deposit) = deposit {
832 T::Slashed::on_unbalanced(T::Currency::slash_reserved(&member, deposit).0);
833 }
834
835 Self::deposit_event(Event::MemberKicked { member, slashed: deposit });
836 Ok(())
837 }
838
839 #[pezpallet::call_index(14)]
841 #[pezpallet::weight(T::WeightInfo::add_unscrupulous_items(items.len() as u32, T::MaxWebsiteUrlLength::get()))]
842 pub fn add_unscrupulous_items(
843 origin: OriginFor<T>,
844 items: Vec<UnscrupulousItemOf<T, I>>,
845 ) -> DispatchResult {
846 T::AnnouncementOrigin::ensure_origin(origin)?;
847
848 let mut accounts = vec![];
849 let mut webs = vec![];
850 for info in items.iter() {
851 ensure!(!Self::is_unscrupulous(info), Error::<T, I>::AlreadyUnscrupulous);
852 match info {
853 UnscrupulousItem::AccountId(who) => accounts.push(who.clone()),
854 UnscrupulousItem::Website(url) => {
855 ensure!(
856 url.len() as u32 <= T::MaxWebsiteUrlLength::get(),
857 Error::<T, I>::TooLongWebsiteUrl
858 );
859 webs.push(url.clone());
860 },
861 }
862 }
863
864 Self::do_add_unscrupulous_items(&mut accounts, &mut webs)?;
865 Self::deposit_event(Event::UnscrupulousItemAdded { items });
866 Ok(())
867 }
868
869 #[pezpallet::call_index(15)]
871 #[pezpallet::weight(<T as Config<I>>::WeightInfo::remove_unscrupulous_items(
872 items.len() as u32, T::MaxWebsiteUrlLength::get()
873 ))]
874 pub fn remove_unscrupulous_items(
875 origin: OriginFor<T>,
876 items: Vec<UnscrupulousItemOf<T, I>>,
877 ) -> DispatchResult {
878 T::AnnouncementOrigin::ensure_origin(origin)?;
879 let mut accounts = vec![];
880 let mut webs = vec![];
881 for info in items.iter() {
882 ensure!(Self::is_unscrupulous(info), Error::<T, I>::NotListedAsUnscrupulous);
883 match info {
884 UnscrupulousItem::AccountId(who) => accounts.push(who.clone()),
885 UnscrupulousItem::Website(url) => webs.push(url.clone()),
886 }
887 }
888 Self::do_remove_unscrupulous_items(&mut accounts, &mut webs)?;
889 Self::deposit_event(Event::UnscrupulousItemRemoved { items });
890 Ok(())
891 }
892
893 #[pezpallet::call_index(16)]
897 #[pezpallet::weight({
898 let b = *length_bound;
899 let m = T::MaxFellows::get();
900 let p1 = *proposal_weight_bound;
901 let p2 = T::MaxProposals::get();
902 T::WeightInfo::close_early_approved(b, m, p2)
903 .max(T::WeightInfo::close_early_disapproved(m, p2))
904 .max(T::WeightInfo::close_approved(b, m, p2))
905 .max(T::WeightInfo::close_disapproved(m, p2))
906 .saturating_add(p1)
907 })]
908 pub fn close(
909 origin: OriginFor<T>,
910 proposal_hash: T::Hash,
911 #[pezpallet::compact] index: ProposalIndex,
912 proposal_weight_bound: Weight,
913 #[pezpallet::compact] length_bound: u32,
914 ) -> DispatchResultWithPostInfo {
915 let who = ensure_signed(origin)?;
916 ensure!(Self::has_voting_rights(&who), Error::<T, I>::NoVotingRights);
917
918 Self::do_close(proposal_hash, index, proposal_weight_bound, length_bound)
919 }
920
921 #[pezpallet::call_index(17)]
925 pub fn abdicate_fellow_status(origin: OriginFor<T>) -> DispatchResult {
926 let who = ensure_signed(origin)?;
927 let role = Self::member_role_of(&who).ok_or(Error::<T, I>::NotMember)?;
928 ensure!(Self::has_voting_rights(&who), Error::<T, I>::NoVotingRights);
930
931 Self::remove_member(&who, role)?;
932 Self::add_member(&who, MemberRole::Ally)?;
933
934 Self::deposit_event(Event::FellowAbdicated { fellow: who });
935 Ok(())
936 }
937 }
938}
939
940impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
941 fn is_initialized() -> bool {
943 Self::has_member(MemberRole::Fellow) || Self::has_member(MemberRole::Ally)
944 }
945
946 fn has_member(role: MemberRole) -> bool {
948 Members::<T, I>::decode_len(role).unwrap_or_default() > 0
949 }
950
951 fn member_role_of(who: &T::AccountId) -> Option<MemberRole> {
953 Members::<T, I>::iter()
954 .find_map(|(r, members)| if members.contains(who) { Some(r) } else { None })
955 }
956
957 pub fn is_member(who: &T::AccountId) -> bool {
959 Self::member_role_of(who).is_some()
960 }
961
962 pub fn is_member_of(who: &T::AccountId, role: MemberRole) -> bool {
964 Members::<T, I>::get(role).contains(&who)
965 }
966
967 fn is_ally(who: &T::AccountId) -> bool {
969 Self::is_member_of(who, MemberRole::Ally)
970 }
971
972 fn has_voting_rights(who: &T::AccountId) -> bool {
974 Self::is_member_of(who, MemberRole::Fellow)
975 }
976
977 fn ally_members_count() -> u32 {
979 Members::<T, I>::decode_len(MemberRole::Ally).unwrap_or(0) as u32
980 }
981
982 fn voting_members_count() -> u32 {
984 Members::<T, I>::decode_len(MemberRole::Fellow).unwrap_or(0) as u32
985 }
986
987 fn members_of(role: MemberRole) -> Vec<T::AccountId> {
989 Members::<T, I>::get(role).into_inner()
990 }
991
992 fn voting_members() -> Vec<T::AccountId> {
994 Self::members_of(MemberRole::Fellow)
995 }
996
997 fn add_member(who: &T::AccountId, role: MemberRole) -> DispatchResult {
999 <Members<T, I>>::try_mutate(role, |members| -> DispatchResult {
1000 let pos = members.binary_search(who).err().ok_or(Error::<T, I>::AlreadyMember)?;
1001 members
1002 .try_insert(pos, who.clone())
1003 .map_err(|_| Error::<T, I>::TooManyMembers)?;
1004 Ok(())
1005 })?;
1006
1007 if role == MemberRole::Fellow {
1008 let members = Self::voting_members();
1009 T::MembershipChanged::change_members_sorted(&[who.clone()], &[], &members[..]);
1010 }
1011 Ok(())
1012 }
1013
1014 fn remove_member(who: &T::AccountId, role: MemberRole) -> DispatchResult {
1016 <Members<T, I>>::try_mutate(role, |members| -> DispatchResult {
1017 let pos = members.binary_search(who).ok().ok_or(Error::<T, I>::NotMember)?;
1018 members.remove(pos);
1019 Ok(())
1020 })?;
1021
1022 if role == MemberRole::Fellow {
1023 let members = Self::voting_members();
1024 T::MembershipChanged::change_members_sorted(&[], &[who.clone()], &members[..]);
1025 }
1026 Ok(())
1027 }
1028
1029 fn is_unscrupulous(info: &UnscrupulousItemOf<T, I>) -> bool {
1031 match info {
1032 UnscrupulousItem::Website(url) => <UnscrupulousWebsites<T, I>>::get().contains(url),
1033 UnscrupulousItem::AccountId(who) => <UnscrupulousAccounts<T, I>>::get().contains(who),
1034 }
1035 }
1036
1037 fn is_unscrupulous_account(who: &T::AccountId) -> bool {
1039 <UnscrupulousAccounts<T, I>>::get().contains(who)
1040 }
1041
1042 fn do_add_unscrupulous_items(
1044 new_accounts: &mut Vec<T::AccountId>,
1045 new_webs: &mut Vec<UrlOf<T, I>>,
1046 ) -> DispatchResult {
1047 if !new_accounts.is_empty() {
1048 <UnscrupulousAccounts<T, I>>::try_mutate(|accounts| -> DispatchResult {
1049 accounts
1050 .try_append(new_accounts)
1051 .map_err(|_| Error::<T, I>::TooManyUnscrupulousItems)?;
1052 accounts.sort();
1053
1054 Ok(())
1055 })?;
1056 }
1057 if !new_webs.is_empty() {
1058 <UnscrupulousWebsites<T, I>>::try_mutate(|webs| -> DispatchResult {
1059 webs.try_append(new_webs).map_err(|_| Error::<T, I>::TooManyUnscrupulousItems)?;
1060 webs.sort();
1061
1062 Ok(())
1063 })?;
1064 }
1065
1066 Ok(())
1067 }
1068
1069 fn do_remove_unscrupulous_items(
1071 out_accounts: &mut Vec<T::AccountId>,
1072 out_webs: &mut Vec<UrlOf<T, I>>,
1073 ) -> DispatchResult {
1074 if !out_accounts.is_empty() {
1075 <UnscrupulousAccounts<T, I>>::try_mutate(|accounts| -> DispatchResult {
1076 for who in out_accounts.iter() {
1077 let pos = accounts
1078 .binary_search(who)
1079 .ok()
1080 .ok_or(Error::<T, I>::NotListedAsUnscrupulous)?;
1081 accounts.remove(pos);
1082 }
1083 Ok(())
1084 })?;
1085 }
1086 if !out_webs.is_empty() {
1087 <UnscrupulousWebsites<T, I>>::try_mutate(|webs| -> DispatchResult {
1088 for web in out_webs.iter() {
1089 let pos = webs
1090 .binary_search(web)
1091 .ok()
1092 .ok_or(Error::<T, I>::NotListedAsUnscrupulous)?;
1093 webs.remove(pos);
1094 }
1095 Ok(())
1096 })?;
1097 }
1098 Ok(())
1099 }
1100
1101 fn has_identity(who: &T::AccountId) -> DispatchResult {
1102 let judgement = |who: &T::AccountId| -> DispatchResult {
1103 ensure!(
1104 T::IdentityVerifier::has_required_identities(who),
1105 Error::<T, I>::WithoutRequiredIdentityFields
1106 );
1107 ensure!(
1108 T::IdentityVerifier::has_good_judgement(who),
1109 Error::<T, I>::WithoutGoodIdentityJudgement
1110 );
1111 Ok(())
1112 };
1113
1114 let res = judgement(who);
1115 if res.is_err() {
1116 if let Some(parent) = T::IdentityVerifier::super_account_id(who) {
1117 return judgement(&parent);
1118 }
1119 }
1120 res
1121 }
1122
1123 fn do_close(
1124 proposal_hash: T::Hash,
1125 index: ProposalIndex,
1126 proposal_weight_bound: Weight,
1127 length_bound: u32,
1128 ) -> DispatchResultWithPostInfo {
1129 let info = T::ProposalProvider::close_proposal(
1130 proposal_hash,
1131 index,
1132 proposal_weight_bound,
1133 length_bound,
1134 )?;
1135 Ok(info.into())
1136 }
1137}
1138
1139#[cfg(any(feature = "try-runtime", test))]
1140impl<T: Config<I>, I: 'static> Pezpallet<T, I> {
1141 pub fn do_try_state() -> Result<(), pezsp_runtime::TryRuntimeError> {
1145 Self::try_state_members_are_disjoint()?;
1146 Self::try_state_members_are_sorted()?;
1147 Self::try_state_retiring_members_are_consistent()?;
1148 Self::try_state_deposit_of_is_consistent()?;
1149 Self::try_state_unscrupulous_items_are_sorted()?;
1150 Self::try_state_announcements_are_sorted()?;
1151 Ok(())
1152 }
1153
1154 fn try_state_members_are_disjoint() -> Result<(), pezsp_runtime::TryRuntimeError> {
1159 let fellows = Members::<T, I>::get(MemberRole::Fellow);
1160 let allies = Members::<T, I>::get(MemberRole::Ally);
1161
1162 for fellow in fellows.iter() {
1163 ensure!(allies.binary_search(fellow).is_err(), "Member is both Fellow and Ally");
1164 }
1165
1166 Ok(())
1167 }
1168
1169 fn try_state_members_are_sorted() -> Result<(), pezsp_runtime::TryRuntimeError> {
1174 let roles = [MemberRole::Fellow, MemberRole::Ally, MemberRole::Retiring];
1175 for role in roles.iter() {
1176 let members = Members::<T, I>::get(role);
1177 let mut sorted_members = members.clone();
1178 sorted_members.sort();
1179 ensure!(members == sorted_members, "Members of a role are not sorted");
1180 }
1181 Ok(())
1182 }
1183
1184 fn try_state_retiring_members_are_consistent() -> Result<(), pezsp_runtime::TryRuntimeError> {
1189 let retiring_in_members = Members::<T, I>::get(MemberRole::Retiring);
1190 let retiring_keys_count = RetiringMembers::<T, I>::iter_keys().count();
1191
1192 ensure!(
1193 retiring_in_members.len() == retiring_keys_count,
1194 "Count mismatch between Members<Retiring> and RetiringMembers map"
1195 );
1196
1197 for member in retiring_in_members.iter() {
1198 ensure!(
1199 RetiringMembers::<T, I>::contains_key(member),
1200 "Retiring member not found in RetiringMembers map"
1201 );
1202 }
1203
1204 Ok(())
1205 }
1206
1207 fn try_state_deposit_of_is_consistent() -> Result<(), pezsp_runtime::TryRuntimeError> {
1212 for (who, _) in DepositOf::<T, I>::iter() {
1213 ensure!(Self::is_member(&who), "Account with deposit is not an alliance member");
1214 }
1215 Ok(())
1216 }
1217
1218 fn try_state_unscrupulous_items_are_sorted() -> Result<(), pezsp_runtime::TryRuntimeError> {
1223 let accounts = UnscrupulousAccounts::<T, I>::get();
1224 let mut sorted_accounts = accounts.clone();
1225 sorted_accounts.sort();
1226 ensure!(accounts == sorted_accounts, "UnscrupulousAccounts is not sorted");
1227
1228 let websites = UnscrupulousWebsites::<T, I>::get();
1229 let mut sorted_websites = websites.clone();
1230 sorted_websites.sort();
1231 ensure!(websites == sorted_websites, "UnscrupulousWebsites is not sorted");
1232
1233 Ok(())
1234 }
1235
1236 fn try_state_announcements_are_sorted() -> Result<(), pezsp_runtime::TryRuntimeError> {
1241 let announcements = Announcements::<T, I>::get();
1242 let mut sorted_announcements = announcements.clone();
1243 sorted_announcements.sort();
1244 ensure!(announcements == sorted_announcements, "Announcements is not sorted");
1245 Ok(())
1246 }
1247}