1#![cfg_attr(not(feature = "std"), no_std)]
25
26extern crate alloc;
27
28use alloc::vec::Vec;
29use frame_support::{
30 traits::{ChangeMembers, Contains, ContainsLengthBound, Get, InitializeMembers, SortedMembers},
31 BoundedVec,
32};
33use sp_runtime::traits::{StaticLookup, UniqueSaturatedInto};
34
35pub mod migrations;
36pub mod weights;
37
38#[cfg(test)]
39mod mock;
40
41#[cfg(feature = "runtime-benchmarks")]
42pub mod benchmarking;
43
44#[cfg(test)]
45mod tests;
46
47pub use pallet::*;
48pub use weights::WeightInfo;
49
50const LOG_TARGET: &str = "runtime::membership";
51
52type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
53
54#[frame_support::pallet]
55pub mod pallet {
56 use super::*;
57 use frame_support::pallet_prelude::*;
58 use frame_system::pallet_prelude::*;
59
60 const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
62
63 #[pallet::pallet]
64 #[pallet::storage_version(STORAGE_VERSION)]
65 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
66
67 #[pallet::config]
68 pub trait Config<I: 'static = ()>: frame_system::Config {
69 #[allow(deprecated)]
71 type RuntimeEvent: From<Event<Self, I>>
72 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
73
74 type AddOrigin: EnsureOrigin<Self::RuntimeOrigin>;
76
77 type RemoveOrigin: EnsureOrigin<Self::RuntimeOrigin>;
79
80 type SwapOrigin: EnsureOrigin<Self::RuntimeOrigin>;
82
83 type ResetOrigin: EnsureOrigin<Self::RuntimeOrigin>;
85
86 type PrimeOrigin: EnsureOrigin<Self::RuntimeOrigin>;
88
89 type MembershipInitialized: InitializeMembers<Self::AccountId>;
93
94 type MembershipChanged: ChangeMembers<Self::AccountId>;
96
97 type MaxMembers: Get<u32>;
103
104 type WeightInfo: WeightInfo;
106 }
107
108 #[pallet::storage]
110 pub type Members<T: Config<I>, I: 'static = ()> =
111 StorageValue<_, BoundedVec<T::AccountId, T::MaxMembers>, ValueQuery>;
112
113 #[pallet::storage]
115 pub type Prime<T: Config<I>, I: 'static = ()> = StorageValue<_, T::AccountId, OptionQuery>;
116
117 #[pallet::genesis_config]
118 #[derive(frame_support::DefaultNoBound)]
119 pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
120 pub members: BoundedVec<T::AccountId, T::MaxMembers>,
121 #[serde(skip)]
122 pub phantom: PhantomData<I>,
123 }
124
125 #[pallet::genesis_build]
126 impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I> {
127 fn build(&self) {
128 use alloc::collections::btree_set::BTreeSet;
129 let members_set: BTreeSet<_> = self.members.iter().collect();
130 assert_eq!(
131 members_set.len(),
132 self.members.len(),
133 "Members cannot contain duplicate accounts."
134 );
135
136 let mut members = self.members.clone();
137 members.sort();
138 T::MembershipInitialized::initialize_members(&members);
139 Members::<T, I>::put(members);
140 }
141 }
142
143 #[pallet::event]
144 #[pallet::generate_deposit(pub(super) fn deposit_event)]
145 pub enum Event<T: Config<I>, I: 'static = ()> {
146 MemberAdded,
148 MemberRemoved,
150 MembersSwapped,
152 MembersReset,
154 KeyChanged,
156 Dummy { _phantom_data: PhantomData<(T::AccountId, <T as Config<I>>::RuntimeEvent)> },
158 }
159
160 #[pallet::error]
161 pub enum Error<T, I = ()> {
162 AlreadyMember,
164 NotMember,
166 TooManyMembers,
168 }
169
170 #[pallet::call]
171 impl<T: Config<I>, I: 'static> Pallet<T, I> {
172 #[pallet::call_index(0)]
176 #[pallet::weight(T::WeightInfo::add_member(T::MaxMembers::get()))]
177 pub fn add_member(
178 origin: OriginFor<T>,
179 who: AccountIdLookupOf<T>,
180 ) -> DispatchResultWithPostInfo {
181 T::AddOrigin::ensure_origin(origin)?;
182 let who = T::Lookup::lookup(who)?;
183
184 let mut members = Members::<T, I>::get();
185 let init_length = members.len();
186 let location = members.binary_search(&who).err().ok_or(Error::<T, I>::AlreadyMember)?;
187 members
188 .try_insert(location, who.clone())
189 .map_err(|_| Error::<T, I>::TooManyMembers)?;
190
191 Members::<T, I>::put(&members);
192
193 T::MembershipChanged::change_members_sorted(&[who], &[], &members[..]);
194
195 Self::deposit_event(Event::MemberAdded);
196
197 Ok(Some(T::WeightInfo::add_member(init_length as u32)).into())
198 }
199
200 #[pallet::call_index(1)]
204 #[pallet::weight(T::WeightInfo::remove_member(T::MaxMembers::get()))]
205 pub fn remove_member(
206 origin: OriginFor<T>,
207 who: AccountIdLookupOf<T>,
208 ) -> DispatchResultWithPostInfo {
209 T::RemoveOrigin::ensure_origin(origin)?;
210 let who = T::Lookup::lookup(who)?;
211
212 let mut members = Members::<T, I>::get();
213 let init_length = members.len();
214 let location = members.binary_search(&who).ok().ok_or(Error::<T, I>::NotMember)?;
215 members.remove(location);
216
217 Members::<T, I>::put(&members);
218
219 T::MembershipChanged::change_members_sorted(&[], &[who], &members[..]);
220 Self::rejig_prime(&members);
221
222 Self::deposit_event(Event::MemberRemoved);
223 Ok(Some(T::WeightInfo::remove_member(init_length as u32)).into())
224 }
225
226 #[pallet::call_index(2)]
232 #[pallet::weight(T::WeightInfo::swap_member(T::MaxMembers::get()))]
233 pub fn swap_member(
234 origin: OriginFor<T>,
235 remove: AccountIdLookupOf<T>,
236 add: AccountIdLookupOf<T>,
237 ) -> DispatchResultWithPostInfo {
238 T::SwapOrigin::ensure_origin(origin)?;
239 let remove = T::Lookup::lookup(remove)?;
240 let add = T::Lookup::lookup(add)?;
241
242 if remove == add {
243 return Ok(().into());
244 }
245
246 let mut members = Members::<T, I>::get();
247 let location = members.binary_search(&remove).ok().ok_or(Error::<T, I>::NotMember)?;
248 members.binary_search(&add).err().ok_or(Error::<T, I>::AlreadyMember)?;
249 members[location] = add.clone();
250 members.sort();
251
252 Members::<T, I>::put(&members);
253
254 T::MembershipChanged::change_members_sorted(&[add], &[remove], &members[..]);
255 Self::rejig_prime(&members);
256
257 Self::deposit_event(Event::MembersSwapped);
258 Ok(Some(T::WeightInfo::swap_member(members.len() as u32)).into())
259 }
260
261 #[pallet::call_index(3)]
266 #[pallet::weight(T::WeightInfo::reset_members(members.len().unique_saturated_into()))]
267 pub fn reset_members(origin: OriginFor<T>, members: Vec<T::AccountId>) -> DispatchResult {
268 T::ResetOrigin::ensure_origin(origin)?;
269
270 let mut members: BoundedVec<T::AccountId, T::MaxMembers> =
271 BoundedVec::try_from(members).map_err(|_| Error::<T, I>::TooManyMembers)?;
272 members.sort();
273 Members::<T, I>::mutate(|m| {
274 T::MembershipChanged::set_members_sorted(&members[..], m);
275 Self::rejig_prime(&members);
276 *m = members;
277 });
278
279 Self::deposit_event(Event::MembersReset);
280 Ok(())
281 }
282
283 #[pallet::call_index(4)]
289 #[pallet::weight(T::WeightInfo::change_key(T::MaxMembers::get()))]
290 pub fn change_key(
291 origin: OriginFor<T>,
292 new: AccountIdLookupOf<T>,
293 ) -> DispatchResultWithPostInfo {
294 let remove = ensure_signed(origin)?;
295 let new = T::Lookup::lookup(new)?;
296
297 if remove == new {
298 return Ok(().into());
299 }
300
301 let mut members = Members::<T, I>::get();
302 let members_length = members.len() as u32;
303 let location = members.binary_search(&remove).ok().ok_or(Error::<T, I>::NotMember)?;
304 members.binary_search(&new).err().ok_or(Error::<T, I>::AlreadyMember)?;
305 members[location] = new.clone();
306 members.sort();
307
308 Members::<T, I>::put(&members);
309
310 T::MembershipChanged::change_members_sorted(
311 &[new.clone()],
312 &[remove.clone()],
313 &members[..],
314 );
315
316 let mut prime = Prime::<T, I>::get();
317 if prime == Some(remove) {
318 Prime::<T, I>::put(&new);
319 prime = Some(new);
320 }
321
322 T::MembershipChanged::set_prime(prime);
323
324 Self::deposit_event(Event::KeyChanged);
325 Ok(Some(T::WeightInfo::change_key(members_length)).into())
326 }
327
328 #[pallet::call_index(5)]
332 #[pallet::weight(T::WeightInfo::set_prime(T::MaxMembers::get()))]
333 pub fn set_prime(
334 origin: OriginFor<T>,
335 who: AccountIdLookupOf<T>,
336 ) -> DispatchResultWithPostInfo {
337 T::PrimeOrigin::ensure_origin(origin)?;
338 let who = T::Lookup::lookup(who)?;
339 let members = Members::<T, I>::get();
340 members.binary_search(&who).ok().ok_or(Error::<T, I>::NotMember)?;
341 Prime::<T, I>::put(&who);
342 T::MembershipChanged::set_prime(Some(who));
343 Ok(Some(T::WeightInfo::set_prime(members.len() as u32)).into())
344 }
345
346 #[pallet::call_index(6)]
350 #[pallet::weight(T::WeightInfo::clear_prime())]
351 pub fn clear_prime(origin: OriginFor<T>) -> DispatchResult {
352 T::PrimeOrigin::ensure_origin(origin)?;
353 Prime::<T, I>::kill();
354 T::MembershipChanged::set_prime(None);
355 Ok(())
356 }
357 }
358
359 #[pallet::hooks]
360 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
361 #[cfg(feature = "try-runtime")]
362 fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
363 Self::do_try_state()
364 }
365 }
366}
367
368#[cfg(any(feature = "try-runtime", test))]
369impl<T: Config<I>, I: 'static> Pallet<T, I> {
370 pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
374 Self::try_state_membership()?;
375
376 Ok(())
377 }
378
379 fn try_state_membership() -> Result<(), sp_runtime::TryRuntimeError> {
384 use frame_support::ensure;
385 let members = Members::<T, I>::get();
386 if let Some(prime) = Prime::<T, I>::get() {
387 ensure!(members.contains(&prime), "`Prime` must be a member of `Members` in storage");
388 }
389
390 ensure!(
391 Members::<T, I>::decode_len().unwrap_or(0) <= T::MaxMembers::get() as usize,
392 "`Member` cannot be greater than `MaxMember`"
393 );
394 Ok(())
395 }
396}
397
398impl<T: Config<I>, I: 'static> Pallet<T, I> {
399 pub fn members() -> BoundedVec<T::AccountId, T::MaxMembers> {
401 Members::<T, I>::get()
402 }
403
404 pub fn prime() -> Option<T::AccountId> {
406 Prime::<T, I>::get()
407 }
408
409 fn rejig_prime(members: &[T::AccountId]) {
410 if let Some(prime) = Prime::<T, I>::get() {
411 match members.binary_search(&prime) {
412 Ok(_) => T::MembershipChanged::set_prime(Some(prime)),
413 Err(_) => Prime::<T, I>::kill(),
414 }
415 }
416 }
417}
418
419impl<T: Config<I>, I: 'static> Contains<T::AccountId> for Pallet<T, I> {
420 fn contains(t: &T::AccountId) -> bool {
421 Members::<T, I>::get().binary_search(t).is_ok()
422 }
423}
424
425impl<T: Config> ContainsLengthBound for Pallet<T> {
426 fn min_len() -> usize {
427 0
428 }
429
430 fn max_len() -> usize {
432 T::MaxMembers::get() as usize
433 }
434}
435
436impl<T: Config<I>, I: 'static> SortedMembers<T::AccountId> for Pallet<T, I> {
437 fn sorted_members() -> Vec<T::AccountId> {
438 Members::<T, I>::get().to_vec()
439 }
440
441 fn count() -> usize {
442 Members::<T, I>::decode_len().unwrap_or(0)
443 }
444
445 #[cfg(feature = "runtime-benchmarks")]
446 fn add(new_member: &T::AccountId) {
447 use frame_support::{assert_ok, traits::EnsureOrigin};
448 let new_member_lookup = T::Lookup::unlookup(new_member.clone());
449
450 if let Ok(origin) = T::AddOrigin::try_successful_origin() {
451 assert_ok!(Pallet::<T, I>::add_member(origin, new_member_lookup,));
452 } else {
453 log::error!(target: LOG_TARGET, "Failed to add `{new_member:?}` in `SortedMembers::add`.")
454 }
455 }
456}