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 if Prime::<T, I>::get() == Some(remove) {
317 Prime::<T, I>::put(&new);
318 T::MembershipChanged::set_prime(Some(new));
319 }
320
321 Self::deposit_event(Event::KeyChanged);
322 Ok(Some(T::WeightInfo::change_key(members_length)).into())
323 }
324
325 #[pallet::call_index(5)]
329 #[pallet::weight(T::WeightInfo::set_prime(T::MaxMembers::get()))]
330 pub fn set_prime(
331 origin: OriginFor<T>,
332 who: AccountIdLookupOf<T>,
333 ) -> DispatchResultWithPostInfo {
334 T::PrimeOrigin::ensure_origin(origin)?;
335 let who = T::Lookup::lookup(who)?;
336 let members = Members::<T, I>::get();
337 members.binary_search(&who).ok().ok_or(Error::<T, I>::NotMember)?;
338 Prime::<T, I>::put(&who);
339 T::MembershipChanged::set_prime(Some(who));
340 Ok(Some(T::WeightInfo::set_prime(members.len() as u32)).into())
341 }
342
343 #[pallet::call_index(6)]
347 #[pallet::weight(T::WeightInfo::clear_prime())]
348 pub fn clear_prime(origin: OriginFor<T>) -> DispatchResult {
349 T::PrimeOrigin::ensure_origin(origin)?;
350 Prime::<T, I>::kill();
351 T::MembershipChanged::set_prime(None);
352 Ok(())
353 }
354 }
355}
356
357impl<T: Config<I>, I: 'static> Pallet<T, I> {
358 pub fn members() -> BoundedVec<T::AccountId, T::MaxMembers> {
360 Members::<T, I>::get()
361 }
362
363 pub fn prime() -> Option<T::AccountId> {
365 Prime::<T, I>::get()
366 }
367
368 fn rejig_prime(members: &[T::AccountId]) {
369 if let Some(prime) = Prime::<T, I>::get() {
370 match members.binary_search(&prime) {
371 Ok(_) => T::MembershipChanged::set_prime(Some(prime)),
372 Err(_) => Prime::<T, I>::kill(),
373 }
374 }
375 }
376}
377
378impl<T: Config<I>, I: 'static> Contains<T::AccountId> for Pallet<T, I> {
379 fn contains(t: &T::AccountId) -> bool {
380 Members::<T, I>::get().binary_search(t).is_ok()
381 }
382}
383
384impl<T: Config> ContainsLengthBound for Pallet<T> {
385 fn min_len() -> usize {
386 0
387 }
388
389 fn max_len() -> usize {
391 T::MaxMembers::get() as usize
392 }
393}
394
395impl<T: Config<I>, I: 'static> SortedMembers<T::AccountId> for Pallet<T, I> {
396 fn sorted_members() -> Vec<T::AccountId> {
397 Members::<T, I>::get().to_vec()
398 }
399
400 fn count() -> usize {
401 Members::<T, I>::decode_len().unwrap_or(0)
402 }
403
404 #[cfg(feature = "runtime-benchmarks")]
405 fn add(new_member: &T::AccountId) {
406 use frame_support::{assert_ok, traits::EnsureOrigin};
407 let new_member_lookup = T::Lookup::unlookup(new_member.clone());
408
409 if let Ok(origin) = T::AddOrigin::try_successful_origin() {
410 assert_ok!(Pallet::<T, I>::add_member(origin, new_member_lookup,));
411 } else {
412 log::error!(target: LOG_TARGET, "Failed to add `{new_member:?}` in `SortedMembers::add`.")
413 }
414 }
415}