referrerpolicy=no-referrer-when-downgrade

pallet_collective_content/
lib.rs

1// Copyright (C) 2023 Parity Technologies (UK) Ltd.
2// SPDX-License-Identifier: Apache-2.0
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// 	http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Managed Collective Content Pallet
17//!
18//! The pallet provides the functionality to store different types of content. This would typically
19//! be used by an on-chain collective, such as the Polkadot Alliance or Ambassador Program.
20//!
21//! The pallet stores content as an [OpaqueCid], which should correspond to some off-chain hosting
22//! service, such as IPFS, and contain any type of data. Each type of content has its own origin
23//! from which it can be managed. The origins are configurable in the runtime. Storing content does
24//! not require a deposit, as it is expected to be managed by a trusted collective.
25//!
26//! Content types:
27//!
28//! - Collective [charter](pallet::Charter): A single document (`OpaqueCid`) managed by
29//!   [CharterOrigin](pallet::Config::CharterOrigin).
30//! - Collective [announcements](pallet::Announcements): A list of announcements managed by
31//!   [AnnouncementOrigin](pallet::Config::AnnouncementOrigin).
32
33#![cfg_attr(not(feature = "std"), no_std)]
34
35#[cfg(test)]
36mod mock;
37#[cfg(test)]
38mod tests;
39
40#[cfg(feature = "runtime-benchmarks")]
41mod benchmarking;
42pub mod weights;
43
44pub use pallet::*;
45pub use weights::WeightInfo;
46
47use frame_support::{traits::schedule::DispatchTime, BoundedVec};
48use sp_core::ConstU32;
49
50/// IPFS compatible CID.
51// Worst case 2 bytes base and codec, 2 bytes hash type and size, 64 bytes hash digest.
52pub type OpaqueCid = BoundedVec<u8, ConstU32<68>>;
53
54#[frame_support::pallet]
55pub mod pallet {
56	use super::*;
57	use frame_support::{ensure, pallet_prelude::*};
58	use frame_system::pallet_prelude::*;
59	use sp_runtime::{traits::BadOrigin, Saturating};
60
61	/// The in-code storage version.
62	const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
63
64	#[pallet::pallet]
65	#[pallet::storage_version(STORAGE_VERSION)]
66	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
67
68	/// The module configuration trait.
69	#[pallet::config]
70	pub trait Config<I: 'static = ()>: frame_system::Config {
71		/// The overarching event type.
72		#[allow(deprecated)]
73		type RuntimeEvent: From<Event<Self, I>>
74			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
75
76		/// Default lifetime for an announcement before it expires.
77		type AnnouncementLifetime: Get<BlockNumberFor<Self>>;
78
79		/// The origin to control the collective announcements.
80		type AnnouncementOrigin: EnsureOrigin<Self::RuntimeOrigin>;
81
82		/// Maximum number of announcements in the storage.
83		#[pallet::constant]
84		type MaxAnnouncements: Get<u32>;
85
86		/// The origin to control the collective charter.
87		type CharterOrigin: EnsureOrigin<Self::RuntimeOrigin>;
88
89		/// Weight information needed for the pallet.
90		type WeightInfo: WeightInfo;
91	}
92
93	#[pallet::error]
94	pub enum Error<T, I = ()> {
95		/// The announcement is not found.
96		MissingAnnouncement,
97		/// Number of announcements exceeds `MaxAnnouncementsCount`.
98		TooManyAnnouncements,
99		/// Cannot expire in the past.
100		InvalidExpiration,
101	}
102
103	#[pallet::event]
104	#[pallet::generate_deposit(pub(super) fn deposit_event)]
105	pub enum Event<T: Config<I>, I: 'static = ()> {
106		/// A new charter has been set.
107		NewCharterSet { cid: OpaqueCid },
108		/// A new announcement has been made.
109		AnnouncementAnnounced { cid: OpaqueCid, expire_at: BlockNumberFor<T> },
110		/// An on-chain announcement has been removed.
111		AnnouncementRemoved { cid: OpaqueCid },
112	}
113
114	/// The collective charter.
115	#[pallet::storage]
116	pub type Charter<T: Config<I>, I: 'static = ()> = StorageValue<_, OpaqueCid, OptionQuery>;
117
118	/// The collective announcements.
119	#[pallet::storage]
120	pub type Announcements<T: Config<I>, I: 'static = ()> =
121		CountedStorageMap<_, Blake2_128Concat, OpaqueCid, BlockNumberFor<T>, OptionQuery>;
122
123	#[pallet::call]
124	impl<T: Config<I>, I: 'static> Pallet<T, I> {
125		/// Set the collective charter.
126		///
127		/// Parameters:
128		/// - `origin`: Must be the [Config::CharterOrigin].
129		/// - `cid`: [CID](super::OpaqueCid) of the IPFS document of the collective charter.
130		#[pallet::call_index(0)]
131		#[pallet::weight(T::WeightInfo::set_charter())]
132		pub fn set_charter(origin: OriginFor<T>, cid: OpaqueCid) -> DispatchResult {
133			T::CharterOrigin::ensure_origin(origin)?;
134
135			Charter::<T, I>::put(&cid);
136
137			Self::deposit_event(Event::<T, I>::NewCharterSet { cid });
138			Ok(())
139		}
140
141		/// Publish an announcement.
142		///
143		/// Parameters:
144		/// - `origin`: Must be the [Config::AnnouncementOrigin].
145		/// - `cid`: [CID](super::OpaqueCid) of the IPFS document to announce.
146		/// - `maybe_expire`: Expiration block of the announcement. If `None`
147		///   [`Config::AnnouncementLifetime`]
148		/// used as a default.
149		#[pallet::call_index(1)]
150		#[pallet::weight(T::WeightInfo::announce())]
151		pub fn announce(
152			origin: OriginFor<T>,
153			cid: OpaqueCid,
154			maybe_expire: Option<DispatchTime<BlockNumberFor<T>>>,
155		) -> DispatchResult {
156			T::AnnouncementOrigin::ensure_origin(origin)?;
157
158			let now = frame_system::Pallet::<T>::block_number();
159			let expire_at = maybe_expire
160				.map_or(now.saturating_add(T::AnnouncementLifetime::get()), |e| e.evaluate(now));
161			ensure!(expire_at > now, Error::<T, I>::InvalidExpiration);
162			ensure!(
163				T::MaxAnnouncements::get() > <Announcements<T, I>>::count(),
164				Error::<T, I>::TooManyAnnouncements
165			);
166
167			<Announcements<T, I>>::insert(cid.clone(), expire_at);
168
169			Self::deposit_event(Event::<T, I>::AnnouncementAnnounced { cid, expire_at });
170			Ok(())
171		}
172
173		/// Remove an announcement.
174		///
175		/// Transaction fee refunded for expired announcements.
176		///
177		/// Parameters:
178		/// - `origin`: Must be the [Config::AnnouncementOrigin] or signed for expired
179		///   announcements.
180		/// - `cid`: [CID](super::OpaqueCid) of the IPFS document to remove.
181		#[pallet::call_index(2)]
182		#[pallet::weight(T::WeightInfo::remove_announcement())]
183		pub fn remove_announcement(
184			origin: OriginFor<T>,
185			cid: OpaqueCid,
186		) -> DispatchResultWithPostInfo {
187			let maybe_who = match T::AnnouncementOrigin::try_origin(origin) {
188				Ok(_) => None,
189				Err(origin) => Some(ensure_signed(origin)?),
190			};
191			let expire_at = <Announcements<T, I>>::get(cid.clone())
192				.ok_or(Error::<T, I>::MissingAnnouncement)?;
193			let now = frame_system::Pallet::<T>::block_number();
194			ensure!(maybe_who.is_none() || now >= expire_at, BadOrigin);
195
196			<Announcements<T, I>>::remove(cid.clone());
197
198			Self::deposit_event(Event::<T, I>::AnnouncementRemoved { cid });
199
200			if now >= expire_at {
201				return Ok(Pays::No.into())
202			}
203			Ok(Pays::Yes.into())
204		}
205	}
206}