polkadot_runtime_common/purchase/
mod.rs1use alloc::vec::Vec;
20use codec::{Decode, Encode};
21use frame_support::{
22 pallet_prelude::*,
23 traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, VestingSchedule},
24};
25use frame_system::pallet_prelude::*;
26pub use pallet::*;
27use scale_info::TypeInfo;
28use sp_core::sr25519;
29use sp_runtime::{
30 traits::{CheckedAdd, Saturating, Verify, Zero},
31 AnySignature, Debug, DispatchError, DispatchResult, Permill,
32};
33
34type BalanceOf<T> =
35 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
36
37#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Copy, Eq, PartialEq, Debug, TypeInfo)]
39pub enum AccountValidity {
40 Invalid,
42 Initiated,
44 Pending,
46 ValidLow,
48 ValidHigh,
50 Completed,
52}
53
54impl Default for AccountValidity {
55 fn default() -> Self {
56 AccountValidity::Invalid
57 }
58}
59
60impl AccountValidity {
61 fn is_valid(&self) -> bool {
62 match self {
63 Self::Invalid => false,
64 Self::Initiated => false,
65 Self::Pending => false,
66 Self::ValidLow => true,
67 Self::ValidHigh => true,
68 Self::Completed => false,
69 }
70 }
71}
72
73#[derive(Encode, Decode, Default, Clone, Eq, PartialEq, Debug, TypeInfo)]
75pub struct AccountStatus<Balance> {
76 validity: AccountValidity,
79 free_balance: Balance,
81 locked_balance: Balance,
83 signature: Vec<u8>,
85 vat: Permill,
88}
89
90#[frame_support::pallet]
91pub mod pallet {
92 use super::*;
93
94 #[pallet::pallet]
95 #[pallet::without_storage_info]
96 pub struct Pallet<T>(_);
97
98 #[pallet::config]
99 pub trait Config: frame_system::Config {
100 #[allow(deprecated)]
102 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
103
104 type Currency: Currency<Self::AccountId>;
106
107 type VestingSchedule: VestingSchedule<
109 Self::AccountId,
110 Moment = BlockNumberFor<Self>,
111 Currency = Self::Currency,
112 >;
113
114 type ValidityOrigin: EnsureOrigin<Self::RuntimeOrigin>;
116
117 type ConfigurationOrigin: EnsureOrigin<Self::RuntimeOrigin>;
119
120 #[pallet::constant]
122 type MaxStatementLength: Get<u32>;
123
124 #[pallet::constant]
126 type UnlockedProportion: Get<Permill>;
127
128 #[pallet::constant]
130 type MaxUnlocked: Get<BalanceOf<Self>>;
131 }
132
133 #[pallet::event]
134 #[pallet::generate_deposit(pub(super) fn deposit_event)]
135 pub enum Event<T: Config> {
136 AccountCreated { who: T::AccountId },
138 ValidityUpdated { who: T::AccountId, validity: AccountValidity },
140 BalanceUpdated { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
142 PaymentComplete { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
144 PaymentAccountSet { who: T::AccountId },
146 StatementUpdated,
148 UnlockBlockUpdated { block_number: BlockNumberFor<T> },
150 }
151
152 #[pallet::error]
153 pub enum Error<T> {
154 InvalidAccount,
156 ExistingAccount,
158 InvalidSignature,
160 AlreadyCompleted,
162 Overflow,
164 InvalidStatement,
166 InvalidUnlockBlock,
168 VestingScheduleExists,
170 }
171
172 #[pallet::storage]
174 pub(super) type Accounts<T: Config> =
175 StorageMap<_, Blake2_128Concat, T::AccountId, AccountStatus<BalanceOf<T>>, ValueQuery>;
176
177 #[pallet::storage]
179 pub(super) type PaymentAccount<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
180
181 #[pallet::storage]
183 pub(super) type Statement<T> = StorageValue<_, Vec<u8>, ValueQuery>;
184
185 #[pallet::storage]
187 pub(super) type UnlockBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
188
189 #[pallet::hooks]
190 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
191
192 #[pallet::call]
193 impl<T: Config> Pallet<T> {
194 #[pallet::call_index(0)]
200 #[pallet::weight(Weight::from_parts(200_000_000, 0) + T::DbWeight::get().reads_writes(4, 1))]
201 pub fn create_account(
202 origin: OriginFor<T>,
203 who: T::AccountId,
204 signature: Vec<u8>,
205 ) -> DispatchResult {
206 T::ValidityOrigin::ensure_origin(origin)?;
207 ensure!(!Accounts::<T>::contains_key(&who), Error::<T>::ExistingAccount);
209 ensure!(
211 T::VestingSchedule::vesting_balance(&who).is_none(),
212 Error::<T>::VestingScheduleExists
213 );
214
215 Self::verify_signature(&who, &signature)?;
217
218 let status = AccountStatus {
220 validity: AccountValidity::Initiated,
221 signature,
222 free_balance: Zero::zero(),
223 locked_balance: Zero::zero(),
224 vat: Permill::zero(),
225 };
226 Accounts::<T>::insert(&who, status);
227 Self::deposit_event(Event::<T>::AccountCreated { who });
228 Ok(())
229 }
230
231 #[pallet::call_index(1)]
238 #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
239 pub fn update_validity_status(
240 origin: OriginFor<T>,
241 who: T::AccountId,
242 validity: AccountValidity,
243 ) -> DispatchResult {
244 T::ValidityOrigin::ensure_origin(origin)?;
245 ensure!(Accounts::<T>::contains_key(&who), Error::<T>::InvalidAccount);
246 Accounts::<T>::try_mutate(
247 &who,
248 |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
249 ensure!(
250 status.validity != AccountValidity::Completed,
251 Error::<T>::AlreadyCompleted
252 );
253 status.validity = validity;
254 Ok(())
255 },
256 )?;
257 Self::deposit_event(Event::<T>::ValidityUpdated { who, validity });
258 Ok(())
259 }
260
261 #[pallet::call_index(2)]
267 #[pallet::weight(T::DbWeight::get().reads_writes(2, 1))]
268 pub fn update_balance(
269 origin: OriginFor<T>,
270 who: T::AccountId,
271 free_balance: BalanceOf<T>,
272 locked_balance: BalanceOf<T>,
273 vat: Permill,
274 ) -> DispatchResult {
275 T::ValidityOrigin::ensure_origin(origin)?;
276
277 Accounts::<T>::try_mutate(
278 &who,
279 |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
280 ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
282
283 free_balance.checked_add(&locked_balance).ok_or(Error::<T>::Overflow)?;
284 status.free_balance = free_balance;
285 status.locked_balance = locked_balance;
286 status.vat = vat;
287 Ok(())
288 },
289 )?;
290 Self::deposit_event(Event::<T>::BalanceUpdated {
291 who,
292 free: free_balance,
293 locked: locked_balance,
294 });
295 Ok(())
296 }
297
298 #[pallet::call_index(3)]
305 #[pallet::weight(T::DbWeight::get().reads_writes(4, 2))]
306 pub fn payout(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
307 let payment_account = ensure_signed(origin)?;
309 let test_against = PaymentAccount::<T>::get().ok_or(DispatchError::BadOrigin)?;
310 ensure!(payment_account == test_against, DispatchError::BadOrigin);
311
312 ensure!(
314 T::VestingSchedule::vesting_balance(&who).is_none(),
315 Error::<T>::VestingScheduleExists
316 );
317
318 Accounts::<T>::try_mutate(
319 &who,
320 |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
321 ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
323
324 let total_balance = status
326 .free_balance
327 .checked_add(&status.locked_balance)
328 .ok_or(Error::<T>::Overflow)?;
329 T::Currency::transfer(
330 &payment_account,
331 &who,
332 total_balance,
333 ExistenceRequirement::AllowDeath,
334 )?;
335
336 if !status.locked_balance.is_zero() {
337 let unlock_block = UnlockBlock::<T>::get();
338 let unlocked = (T::UnlockedProportion::get() * status.locked_balance)
341 .min(T::MaxUnlocked::get());
342 let locked = status.locked_balance.saturating_sub(unlocked);
343 let _ = T::VestingSchedule::add_vesting_schedule(
347 &who,
349 locked,
351 locked,
353 unlock_block,
355 );
356 }
357
358 status.validity = AccountValidity::Completed;
361 Self::deposit_event(Event::<T>::PaymentComplete {
362 who: who.clone(),
363 free: status.free_balance,
364 locked: status.locked_balance,
365 });
366 Ok(())
367 },
368 )?;
369 Ok(())
370 }
371
372 #[pallet::call_index(4)]
378 #[pallet::weight(T::DbWeight::get().writes(1))]
379 pub fn set_payment_account(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
380 T::ConfigurationOrigin::ensure_origin(origin)?;
381 PaymentAccount::<T>::put(who.clone());
383 Self::deposit_event(Event::<T>::PaymentAccountSet { who });
384 Ok(())
385 }
386
387 #[pallet::call_index(5)]
391 #[pallet::weight(T::DbWeight::get().writes(1))]
392 pub fn set_statement(origin: OriginFor<T>, statement: Vec<u8>) -> DispatchResult {
393 T::ConfigurationOrigin::ensure_origin(origin)?;
394 ensure!(
395 (statement.len() as u32) < T::MaxStatementLength::get(),
396 Error::<T>::InvalidStatement
397 );
398 Statement::<T>::set(statement);
400 Self::deposit_event(Event::<T>::StatementUpdated);
401 Ok(())
402 }
403
404 #[pallet::call_index(6)]
408 #[pallet::weight(T::DbWeight::get().writes(1))]
409 pub fn set_unlock_block(
410 origin: OriginFor<T>,
411 unlock_block: BlockNumberFor<T>,
412 ) -> DispatchResult {
413 T::ConfigurationOrigin::ensure_origin(origin)?;
414 ensure!(
415 unlock_block > frame_system::Pallet::<T>::block_number(),
416 Error::<T>::InvalidUnlockBlock
417 );
418 UnlockBlock::<T>::set(unlock_block);
420 Self::deposit_event(Event::<T>::UnlockBlockUpdated { block_number: unlock_block });
421 Ok(())
422 }
423 }
424}
425
426impl<T: Config> Pallet<T> {
427 fn verify_signature(who: &T::AccountId, signature: &[u8]) -> Result<(), DispatchError> {
428 let signature: AnySignature = sr25519::Signature::try_from(signature)
430 .map_err(|_| Error::<T>::InvalidSignature)?
431 .into();
432
433 let account_bytes: [u8; 32] = account_to_bytes(who)?;
435 let public_key = sr25519::Public::from_raw(account_bytes);
436
437 let message = Statement::<T>::get();
438
439 match signature.verify(message.as_slice(), &public_key) {
441 true => Ok(()),
442 false => Err(Error::<T>::InvalidSignature)?,
443 }
444 }
445}
446
447fn account_to_bytes<AccountId>(account: &AccountId) -> Result<[u8; 32], DispatchError>
449where
450 AccountId: Encode,
451{
452 let account_vec = account.encode();
453 ensure!(account_vec.len() == 32, "AccountId must be 32 bytes.");
454 let mut bytes = [0u8; 32];
455 bytes.copy_from_slice(&account_vec);
456 Ok(bytes)
457}
458
459pub fn remove_pallet<T>() -> frame_support::weights::Weight
462where
463 T: frame_system::Config,
464{
465 #[allow(deprecated)]
466 use frame_support::migration::remove_storage_prefix;
467 #[allow(deprecated)]
468 remove_storage_prefix(b"Purchase", b"Accounts", b"");
469 #[allow(deprecated)]
470 remove_storage_prefix(b"Purchase", b"PaymentAccount", b"");
471 #[allow(deprecated)]
472 remove_storage_prefix(b"Purchase", b"Statement", b"");
473 #[allow(deprecated)]
474 remove_storage_prefix(b"Purchase", b"UnlockBlock", b"");
475
476 <T as frame_system::Config>::BlockWeights::get().max_block
477}
478
479#[cfg(test)]
480mod mock;
481
482#[cfg(test)]
483mod tests;