pezkuwi_runtime_common/purchase/
mod.rs1use alloc::vec::Vec;
20use codec::{Decode, Encode};
21use pezframe_support::{
22 pezpallet_prelude::*,
23 traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, VestingSchedule},
24};
25use pezframe_system::pezpallet_prelude::*;
26pub use pezpallet::*;
27use pezsp_core::sr25519;
28use pezsp_runtime::{
29 traits::{CheckedAdd, Saturating, Verify, Zero},
30 AnySignature, DispatchError, DispatchResult, Permill, RuntimeDebug,
31};
32use scale_info::TypeInfo;
33
34type BalanceOf<T> =
35 <<T as Config>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
36
37#[derive(
39 Encode, Decode, DecodeWithMemTracking, Clone, Copy, Eq, PartialEq, RuntimeDebug, TypeInfo,
40)]
41pub enum AccountValidity {
42 Invalid,
44 Initiated,
46 Pending,
48 ValidLow,
50 ValidHigh,
52 Completed,
54}
55
56impl Default for AccountValidity {
57 fn default() -> Self {
58 AccountValidity::Invalid
59 }
60}
61
62impl AccountValidity {
63 fn is_valid(&self) -> bool {
64 match self {
65 Self::Invalid => false,
66 Self::Initiated => false,
67 Self::Pending => false,
68 Self::ValidLow => true,
69 Self::ValidHigh => true,
70 Self::Completed => false,
71 }
72 }
73}
74
75#[derive(Encode, Decode, Default, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)]
77pub struct AccountStatus<Balance> {
78 validity: AccountValidity,
81 free_balance: Balance,
83 locked_balance: Balance,
85 signature: Vec<u8>,
87 vat: Permill,
90}
91
92#[pezframe_support::pezpallet]
93pub mod pezpallet {
94 use super::*;
95
96 #[pezpallet::pezpallet]
97 #[pezpallet::without_storage_info]
98 pub struct Pezpallet<T>(_);
99
100 #[pezpallet::config]
101 pub trait Config: pezframe_system::Config {
102 #[allow(deprecated)]
104 type RuntimeEvent: From<Event<Self>>
105 + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
106
107 type Currency: Currency<Self::AccountId>;
109
110 type VestingSchedule: VestingSchedule<
112 Self::AccountId,
113 Moment = BlockNumberFor<Self>,
114 Currency = Self::Currency,
115 >;
116
117 type ValidityOrigin: EnsureOrigin<Self::RuntimeOrigin>;
119
120 type ConfigurationOrigin: EnsureOrigin<Self::RuntimeOrigin>;
122
123 #[pezpallet::constant]
125 type MaxStatementLength: Get<u32>;
126
127 #[pezpallet::constant]
129 type UnlockedProportion: Get<Permill>;
130
131 #[pezpallet::constant]
133 type MaxUnlocked: Get<BalanceOf<Self>>;
134 }
135
136 #[pezpallet::event]
137 #[pezpallet::generate_deposit(pub(super) fn deposit_event)]
138 pub enum Event<T: Config> {
139 AccountCreated { who: T::AccountId },
141 ValidityUpdated { who: T::AccountId, validity: AccountValidity },
143 BalanceUpdated { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
145 PaymentComplete { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
147 PaymentAccountSet { who: T::AccountId },
149 StatementUpdated,
151 UnlockBlockUpdated { block_number: BlockNumberFor<T> },
153 }
154
155 #[pezpallet::error]
156 pub enum Error<T> {
157 InvalidAccount,
159 ExistingAccount,
161 InvalidSignature,
163 AlreadyCompleted,
165 Overflow,
167 InvalidStatement,
169 InvalidUnlockBlock,
171 VestingScheduleExists,
173 }
174
175 #[pezpallet::storage]
177 pub(super) type Accounts<T: Config> =
178 StorageMap<_, Blake2_128Concat, T::AccountId, AccountStatus<BalanceOf<T>>, ValueQuery>;
179
180 #[pezpallet::storage]
182 pub(super) type PaymentAccount<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
183
184 #[pezpallet::storage]
186 pub(super) type Statement<T> = StorageValue<_, Vec<u8>, ValueQuery>;
187
188 #[pezpallet::storage]
190 pub(super) type UnlockBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
191
192 #[pezpallet::hooks]
193 impl<T: Config> Hooks<BlockNumberFor<T>> for Pezpallet<T> {}
194
195 #[pezpallet::call]
196 impl<T: Config> Pezpallet<T> {
197 #[pezpallet::call_index(0)]
203 #[pezpallet::weight(Weight::from_parts(200_000_000, 0) + T::DbWeight::get().reads_writes(4, 1))]
204 pub fn create_account(
205 origin: OriginFor<T>,
206 who: T::AccountId,
207 signature: Vec<u8>,
208 ) -> DispatchResult {
209 T::ValidityOrigin::ensure_origin(origin)?;
210 ensure!(!Accounts::<T>::contains_key(&who), Error::<T>::ExistingAccount);
212 ensure!(
214 T::VestingSchedule::vesting_balance(&who).is_none(),
215 Error::<T>::VestingScheduleExists
216 );
217
218 Self::verify_signature(&who, &signature)?;
220
221 let status = AccountStatus {
223 validity: AccountValidity::Initiated,
224 signature,
225 free_balance: Zero::zero(),
226 locked_balance: Zero::zero(),
227 vat: Permill::zero(),
228 };
229 Accounts::<T>::insert(&who, status);
230 Self::deposit_event(Event::<T>::AccountCreated { who });
231 Ok(())
232 }
233
234 #[pezpallet::call_index(1)]
241 #[pezpallet::weight(T::DbWeight::get().reads_writes(1, 1))]
242 pub fn update_validity_status(
243 origin: OriginFor<T>,
244 who: T::AccountId,
245 validity: AccountValidity,
246 ) -> DispatchResult {
247 T::ValidityOrigin::ensure_origin(origin)?;
248 ensure!(Accounts::<T>::contains_key(&who), Error::<T>::InvalidAccount);
249 Accounts::<T>::try_mutate(
250 &who,
251 |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
252 ensure!(
253 status.validity != AccountValidity::Completed,
254 Error::<T>::AlreadyCompleted
255 );
256 status.validity = validity;
257 Ok(())
258 },
259 )?;
260 Self::deposit_event(Event::<T>::ValidityUpdated { who, validity });
261 Ok(())
262 }
263
264 #[pezpallet::call_index(2)]
270 #[pezpallet::weight(T::DbWeight::get().reads_writes(2, 1))]
271 pub fn update_balance(
272 origin: OriginFor<T>,
273 who: T::AccountId,
274 free_balance: BalanceOf<T>,
275 locked_balance: BalanceOf<T>,
276 vat: Permill,
277 ) -> DispatchResult {
278 T::ValidityOrigin::ensure_origin(origin)?;
279
280 Accounts::<T>::try_mutate(
281 &who,
282 |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
283 ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
285
286 free_balance.checked_add(&locked_balance).ok_or(Error::<T>::Overflow)?;
287 status.free_balance = free_balance;
288 status.locked_balance = locked_balance;
289 status.vat = vat;
290 Ok(())
291 },
292 )?;
293 Self::deposit_event(Event::<T>::BalanceUpdated {
294 who,
295 free: free_balance,
296 locked: locked_balance,
297 });
298 Ok(())
299 }
300
301 #[pezpallet::call_index(3)]
308 #[pezpallet::weight(T::DbWeight::get().reads_writes(4, 2))]
309 pub fn payout(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
310 let payment_account = ensure_signed(origin)?;
312 let test_against = PaymentAccount::<T>::get().ok_or(DispatchError::BadOrigin)?;
313 ensure!(payment_account == test_against, DispatchError::BadOrigin);
314
315 ensure!(
317 T::VestingSchedule::vesting_balance(&who).is_none(),
318 Error::<T>::VestingScheduleExists
319 );
320
321 Accounts::<T>::try_mutate(
322 &who,
323 |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
324 ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
326
327 let total_balance = status
329 .free_balance
330 .checked_add(&status.locked_balance)
331 .ok_or(Error::<T>::Overflow)?;
332 T::Currency::transfer(
333 &payment_account,
334 &who,
335 total_balance,
336 ExistenceRequirement::AllowDeath,
337 )?;
338
339 if !status.locked_balance.is_zero() {
340 let unlock_block = UnlockBlock::<T>::get();
341 let unlocked = (T::UnlockedProportion::get() * status.locked_balance)
344 .min(T::MaxUnlocked::get());
345 let locked = status.locked_balance.saturating_sub(unlocked);
346 let _ = T::VestingSchedule::add_vesting_schedule(
350 &who,
352 locked,
354 locked,
356 unlock_block,
358 );
359 }
360
361 status.validity = AccountValidity::Completed;
364 Self::deposit_event(Event::<T>::PaymentComplete {
365 who: who.clone(),
366 free: status.free_balance,
367 locked: status.locked_balance,
368 });
369 Ok(())
370 },
371 )?;
372 Ok(())
373 }
374
375 #[pezpallet::call_index(4)]
381 #[pezpallet::weight(T::DbWeight::get().writes(1))]
382 pub fn set_payment_account(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
383 T::ConfigurationOrigin::ensure_origin(origin)?;
384 PaymentAccount::<T>::put(who.clone());
386 Self::deposit_event(Event::<T>::PaymentAccountSet { who });
387 Ok(())
388 }
389
390 #[pezpallet::call_index(5)]
394 #[pezpallet::weight(T::DbWeight::get().writes(1))]
395 pub fn set_statement(origin: OriginFor<T>, statement: Vec<u8>) -> DispatchResult {
396 T::ConfigurationOrigin::ensure_origin(origin)?;
397 ensure!(
398 (statement.len() as u32) < T::MaxStatementLength::get(),
399 Error::<T>::InvalidStatement
400 );
401 Statement::<T>::set(statement);
403 Self::deposit_event(Event::<T>::StatementUpdated);
404 Ok(())
405 }
406
407 #[pezpallet::call_index(6)]
411 #[pezpallet::weight(T::DbWeight::get().writes(1))]
412 pub fn set_unlock_block(
413 origin: OriginFor<T>,
414 unlock_block: BlockNumberFor<T>,
415 ) -> DispatchResult {
416 T::ConfigurationOrigin::ensure_origin(origin)?;
417 ensure!(
418 unlock_block > pezframe_system::Pezpallet::<T>::block_number(),
419 Error::<T>::InvalidUnlockBlock
420 );
421 UnlockBlock::<T>::set(unlock_block);
423 Self::deposit_event(Event::<T>::UnlockBlockUpdated { block_number: unlock_block });
424 Ok(())
425 }
426 }
427}
428
429impl<T: Config> Pezpallet<T> {
430 fn verify_signature(who: &T::AccountId, signature: &[u8]) -> Result<(), DispatchError> {
431 let signature: AnySignature = sr25519::Signature::try_from(signature)
433 .map_err(|_| Error::<T>::InvalidSignature)?
434 .into();
435
436 let account_bytes: [u8; 32] = account_to_bytes(who)?;
438 let public_key = sr25519::Public::from_raw(account_bytes);
439
440 let message = Statement::<T>::get();
441
442 match signature.verify(message.as_slice(), &public_key) {
444 true => Ok(()),
445 false => Err(Error::<T>::InvalidSignature)?,
446 }
447 }
448}
449
450fn account_to_bytes<AccountId>(account: &AccountId) -> Result<[u8; 32], DispatchError>
452where
453 AccountId: Encode,
454{
455 let account_vec = account.encode();
456 ensure!(account_vec.len() == 32, "AccountId must be 32 bytes.");
457 let mut bytes = [0u8; 32];
458 bytes.copy_from_slice(&account_vec);
459 Ok(bytes)
460}
461
462pub fn remove_pallet<T>() -> pezframe_support::weights::Weight
465where
466 T: pezframe_system::Config,
467{
468 #[allow(deprecated)]
469 use pezframe_support::migration::remove_storage_prefix;
470 #[allow(deprecated)]
471 remove_storage_prefix(b"Purchase", b"Accounts", b"");
472 #[allow(deprecated)]
473 remove_storage_prefix(b"Purchase", b"PaymentAccount", b"");
474 #[allow(deprecated)]
475 remove_storage_prefix(b"Purchase", b"Statement", b"");
476 #[allow(deprecated)]
477 remove_storage_prefix(b"Purchase", b"UnlockBlock", b"");
478
479 <T as pezframe_system::Config>::BlockWeights::get().max_block
480}
481
482#[cfg(test)]
483mod mock;
484
485#[cfg(test)]
486mod tests;