From 03e5db1fed6693c1695bb7aa910d84d8e302dc02 Mon Sep 17 00:00:00 2001
From: Ludovic_Domingues <ludovic.domingues96@gmail.com>
Date: Thu, 19 Dec 2024 02:13:55 +0100
Subject: [PATCH] Improve pallet purchase file structure (#6780)

Linked to issue #590
I moved the mod, tests, mock and benchmarking to their own seperate file
to reduce the bloat inside purchase.rs

---------

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com>
---
 polkadot/runtime/common/src/purchase.rs       | 1178 -----------------
 polkadot/runtime/common/src/purchase/mock.rs  |  181 +++
 polkadot/runtime/common/src/purchase/mod.rs   |  482 +++++++
 polkadot/runtime/common/src/purchase/tests.rs |  547 ++++++++
 4 files changed, 1210 insertions(+), 1178 deletions(-)
 delete mode 100644 polkadot/runtime/common/src/purchase.rs
 create mode 100644 polkadot/runtime/common/src/purchase/mock.rs
 create mode 100644 polkadot/runtime/common/src/purchase/mod.rs
 create mode 100644 polkadot/runtime/common/src/purchase/tests.rs

diff --git a/polkadot/runtime/common/src/purchase.rs b/polkadot/runtime/common/src/purchase.rs
deleted file mode 100644
index cec92540654..00000000000
--- a/polkadot/runtime/common/src/purchase.rs
+++ /dev/null
@@ -1,1178 +0,0 @@
-// Copyright (C) Parity Technologies (UK) Ltd.
-// This file is part of Polkadot.
-
-// Polkadot is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// Polkadot is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
-
-//! Pallet to process purchase of DOTs.
-
-use alloc::vec::Vec;
-use codec::{Decode, Encode};
-use frame_support::{
-	pallet_prelude::*,
-	traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, VestingSchedule},
-};
-use frame_system::pallet_prelude::*;
-pub use pallet::*;
-use scale_info::TypeInfo;
-use sp_core::sr25519;
-use sp_runtime::{
-	traits::{CheckedAdd, Saturating, Verify, Zero},
-	AnySignature, DispatchError, DispatchResult, Permill, RuntimeDebug,
-};
-
-type BalanceOf<T> =
-	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
-
-/// The kind of statement an account needs to make for a claim to be valid.
-#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug, TypeInfo)]
-pub enum AccountValidity {
-	/// Account is not valid.
-	Invalid,
-	/// Account has initiated the account creation process.
-	Initiated,
-	/// Account is pending validation.
-	Pending,
-	/// Account is valid with a low contribution amount.
-	ValidLow,
-	/// Account is valid with a high contribution amount.
-	ValidHigh,
-	/// Account has completed the purchase process.
-	Completed,
-}
-
-impl Default for AccountValidity {
-	fn default() -> Self {
-		AccountValidity::Invalid
-	}
-}
-
-impl AccountValidity {
-	fn is_valid(&self) -> bool {
-		match self {
-			Self::Invalid => false,
-			Self::Initiated => false,
-			Self::Pending => false,
-			Self::ValidLow => true,
-			Self::ValidHigh => true,
-			Self::Completed => false,
-		}
-	}
-}
-
-/// All information about an account regarding the purchase of DOTs.
-#[derive(Encode, Decode, Default, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)]
-pub struct AccountStatus<Balance> {
-	/// The current validity status of the user. Will denote if the user has passed KYC,
-	/// how much they are able to purchase, and when their purchase process has completed.
-	validity: AccountValidity,
-	/// The amount of free DOTs they have purchased.
-	free_balance: Balance,
-	/// The amount of locked DOTs they have purchased.
-	locked_balance: Balance,
-	/// Their sr25519/ed25519 signature verifying they have signed our required statement.
-	signature: Vec<u8>,
-	/// The percentage of VAT the purchaser is responsible for. This is already factored into
-	/// account balance.
-	vat: Permill,
-}
-
-#[frame_support::pallet]
-pub mod pallet {
-	use super::*;
-
-	#[pallet::pallet]
-	#[pallet::without_storage_info]
-	pub struct Pallet<T>(_);
-
-	#[pallet::config]
-	pub trait Config: frame_system::Config {
-		/// The overarching event type.
-		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
-
-		/// Balances Pallet
-		type Currency: Currency<Self::AccountId>;
-
-		/// Vesting Pallet
-		type VestingSchedule: VestingSchedule<
-			Self::AccountId,
-			Moment = BlockNumberFor<Self>,
-			Currency = Self::Currency,
-		>;
-
-		/// The origin allowed to set account status.
-		type ValidityOrigin: EnsureOrigin<Self::RuntimeOrigin>;
-
-		/// The origin allowed to make configurations to the pallet.
-		type ConfigurationOrigin: EnsureOrigin<Self::RuntimeOrigin>;
-
-		/// The maximum statement length for the statement users to sign when creating an account.
-		#[pallet::constant]
-		type MaxStatementLength: Get<u32>;
-
-		/// The amount of purchased locked DOTs that we will unlock for basic actions on the chain.
-		#[pallet::constant]
-		type UnlockedProportion: Get<Permill>;
-
-		/// The maximum amount of locked DOTs that we will unlock.
-		#[pallet::constant]
-		type MaxUnlocked: Get<BalanceOf<Self>>;
-	}
-
-	#[pallet::event]
-	#[pallet::generate_deposit(pub(super) fn deposit_event)]
-	pub enum Event<T: Config> {
-		/// A new account was created.
-		AccountCreated { who: T::AccountId },
-		/// Someone's account validity was updated.
-		ValidityUpdated { who: T::AccountId, validity: AccountValidity },
-		/// Someone's purchase balance was updated.
-		BalanceUpdated { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
-		/// A payout was made to a purchaser.
-		PaymentComplete { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
-		/// A new payment account was set.
-		PaymentAccountSet { who: T::AccountId },
-		/// A new statement was set.
-		StatementUpdated,
-		/// A new statement was set. `[block_number]`
-		UnlockBlockUpdated { block_number: BlockNumberFor<T> },
-	}
-
-	#[pallet::error]
-	pub enum Error<T> {
-		/// Account is not currently valid to use.
-		InvalidAccount,
-		/// Account used in the purchase already exists.
-		ExistingAccount,
-		/// Provided signature is invalid
-		InvalidSignature,
-		/// Account has already completed the purchase process.
-		AlreadyCompleted,
-		/// An overflow occurred when doing calculations.
-		Overflow,
-		/// The statement is too long to be stored on chain.
-		InvalidStatement,
-		/// The unlock block is in the past!
-		InvalidUnlockBlock,
-		/// Vesting schedule already exists for this account.
-		VestingScheduleExists,
-	}
-
-	// A map of all participants in the DOT purchase process.
-	#[pallet::storage]
-	pub(super) type Accounts<T: Config> =
-		StorageMap<_, Blake2_128Concat, T::AccountId, AccountStatus<BalanceOf<T>>, ValueQuery>;
-
-	// The account that will be used to payout participants of the DOT purchase process.
-	#[pallet::storage]
-	pub(super) type PaymentAccount<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
-
-	// The statement purchasers will need to sign to participate.
-	#[pallet::storage]
-	pub(super) type Statement<T> = StorageValue<_, Vec<u8>, ValueQuery>;
-
-	// The block where all locked dots will unlock.
-	#[pallet::storage]
-	pub(super) type UnlockBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
-
-	#[pallet::hooks]
-	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
-
-	#[pallet::call]
-	impl<T: Config> Pallet<T> {
-		/// Create a new account. Proof of existence through a valid signed message.
-		///
-		/// We check that the account does not exist at this stage.
-		///
-		/// Origin must match the `ValidityOrigin`.
-		#[pallet::call_index(0)]
-		#[pallet::weight(Weight::from_parts(200_000_000, 0) + T::DbWeight::get().reads_writes(4, 1))]
-		pub fn create_account(
-			origin: OriginFor<T>,
-			who: T::AccountId,
-			signature: Vec<u8>,
-		) -> DispatchResult {
-			T::ValidityOrigin::ensure_origin(origin)?;
-			// Account is already being tracked by the pallet.
-			ensure!(!Accounts::<T>::contains_key(&who), Error::<T>::ExistingAccount);
-			// Account should not have a vesting schedule.
-			ensure!(
-				T::VestingSchedule::vesting_balance(&who).is_none(),
-				Error::<T>::VestingScheduleExists
-			);
-
-			// Verify the signature provided is valid for the statement.
-			Self::verify_signature(&who, &signature)?;
-
-			// Create a new pending account.
-			let status = AccountStatus {
-				validity: AccountValidity::Initiated,
-				signature,
-				free_balance: Zero::zero(),
-				locked_balance: Zero::zero(),
-				vat: Permill::zero(),
-			};
-			Accounts::<T>::insert(&who, status);
-			Self::deposit_event(Event::<T>::AccountCreated { who });
-			Ok(())
-		}
-
-		/// Update the validity status of an existing account. If set to completed, the account
-		/// will no longer be able to continue through the crowdfund process.
-		///
-		/// We check that the account exists at this stage, but has not completed the process.
-		///
-		/// Origin must match the `ValidityOrigin`.
-		#[pallet::call_index(1)]
-		#[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
-		pub fn update_validity_status(
-			origin: OriginFor<T>,
-			who: T::AccountId,
-			validity: AccountValidity,
-		) -> DispatchResult {
-			T::ValidityOrigin::ensure_origin(origin)?;
-			ensure!(Accounts::<T>::contains_key(&who), Error::<T>::InvalidAccount);
-			Accounts::<T>::try_mutate(
-				&who,
-				|status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
-					ensure!(
-						status.validity != AccountValidity::Completed,
-						Error::<T>::AlreadyCompleted
-					);
-					status.validity = validity;
-					Ok(())
-				},
-			)?;
-			Self::deposit_event(Event::<T>::ValidityUpdated { who, validity });
-			Ok(())
-		}
-
-		/// Update the balance of a valid account.
-		///
-		/// We check that the account is valid for a balance transfer at this point.
-		///
-		/// Origin must match the `ValidityOrigin`.
-		#[pallet::call_index(2)]
-		#[pallet::weight(T::DbWeight::get().reads_writes(2, 1))]
-		pub fn update_balance(
-			origin: OriginFor<T>,
-			who: T::AccountId,
-			free_balance: BalanceOf<T>,
-			locked_balance: BalanceOf<T>,
-			vat: Permill,
-		) -> DispatchResult {
-			T::ValidityOrigin::ensure_origin(origin)?;
-
-			Accounts::<T>::try_mutate(
-				&who,
-				|status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
-					// Account has a valid status (not Invalid, Pending, or Completed)...
-					ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
-
-					free_balance.checked_add(&locked_balance).ok_or(Error::<T>::Overflow)?;
-					status.free_balance = free_balance;
-					status.locked_balance = locked_balance;
-					status.vat = vat;
-					Ok(())
-				},
-			)?;
-			Self::deposit_event(Event::<T>::BalanceUpdated {
-				who,
-				free: free_balance,
-				locked: locked_balance,
-			});
-			Ok(())
-		}
-
-		/// Pay the user and complete the purchase process.
-		///
-		/// We reverify all assumptions about the state of an account, and complete the process.
-		///
-		/// Origin must match the configured `PaymentAccount` (if it is not configured then this
-		/// will always fail with `BadOrigin`).
-		#[pallet::call_index(3)]
-		#[pallet::weight(T::DbWeight::get().reads_writes(4, 2))]
-		pub fn payout(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
-			// Payments must be made directly by the `PaymentAccount`.
-			let payment_account = ensure_signed(origin)?;
-			let test_against = PaymentAccount::<T>::get().ok_or(DispatchError::BadOrigin)?;
-			ensure!(payment_account == test_against, DispatchError::BadOrigin);
-
-			// Account should not have a vesting schedule.
-			ensure!(
-				T::VestingSchedule::vesting_balance(&who).is_none(),
-				Error::<T>::VestingScheduleExists
-			);
-
-			Accounts::<T>::try_mutate(
-				&who,
-				|status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
-					// Account has a valid status (not Invalid, Pending, or Completed)...
-					ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
-
-					// Transfer funds from the payment account into the purchasing user.
-					let total_balance = status
-						.free_balance
-						.checked_add(&status.locked_balance)
-						.ok_or(Error::<T>::Overflow)?;
-					T::Currency::transfer(
-						&payment_account,
-						&who,
-						total_balance,
-						ExistenceRequirement::AllowDeath,
-					)?;
-
-					if !status.locked_balance.is_zero() {
-						let unlock_block = UnlockBlock::<T>::get();
-						// We allow some configurable portion of the purchased locked DOTs to be
-						// unlocked for basic usage.
-						let unlocked = (T::UnlockedProportion::get() * status.locked_balance)
-							.min(T::MaxUnlocked::get());
-						let locked = status.locked_balance.saturating_sub(unlocked);
-						// We checked that this account has no existing vesting schedule. So this
-						// function should never fail, however if it does, not much we can do about
-						// it at this point.
-						let _ = T::VestingSchedule::add_vesting_schedule(
-							// Apply vesting schedule to this user
-							&who,
-							// For this much amount
-							locked,
-							// Unlocking the full amount after one block
-							locked,
-							// When everything unlocks
-							unlock_block,
-						);
-					}
-
-					// Setting the user account to `Completed` ends the purchase process for this
-					// user.
-					status.validity = AccountValidity::Completed;
-					Self::deposit_event(Event::<T>::PaymentComplete {
-						who: who.clone(),
-						free: status.free_balance,
-						locked: status.locked_balance,
-					});
-					Ok(())
-				},
-			)?;
-			Ok(())
-		}
-
-		/* Configuration Operations */
-
-		/// Set the account that will be used to payout users in the DOT purchase process.
-		///
-		/// Origin must match the `ConfigurationOrigin`
-		#[pallet::call_index(4)]
-		#[pallet::weight(T::DbWeight::get().writes(1))]
-		pub fn set_payment_account(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
-			T::ConfigurationOrigin::ensure_origin(origin)?;
-			// Possibly this is worse than having the caller account be the payment account?
-			PaymentAccount::<T>::put(who.clone());
-			Self::deposit_event(Event::<T>::PaymentAccountSet { who });
-			Ok(())
-		}
-
-		/// Set the statement that must be signed for a user to participate on the DOT sale.
-		///
-		/// Origin must match the `ConfigurationOrigin`
-		#[pallet::call_index(5)]
-		#[pallet::weight(T::DbWeight::get().writes(1))]
-		pub fn set_statement(origin: OriginFor<T>, statement: Vec<u8>) -> DispatchResult {
-			T::ConfigurationOrigin::ensure_origin(origin)?;
-			ensure!(
-				(statement.len() as u32) < T::MaxStatementLength::get(),
-				Error::<T>::InvalidStatement
-			);
-			// Possibly this is worse than having the caller account be the payment account?
-			Statement::<T>::set(statement);
-			Self::deposit_event(Event::<T>::StatementUpdated);
-			Ok(())
-		}
-
-		/// Set the block where locked DOTs will become unlocked.
-		///
-		/// Origin must match the `ConfigurationOrigin`
-		#[pallet::call_index(6)]
-		#[pallet::weight(T::DbWeight::get().writes(1))]
-		pub fn set_unlock_block(
-			origin: OriginFor<T>,
-			unlock_block: BlockNumberFor<T>,
-		) -> DispatchResult {
-			T::ConfigurationOrigin::ensure_origin(origin)?;
-			ensure!(
-				unlock_block > frame_system::Pallet::<T>::block_number(),
-				Error::<T>::InvalidUnlockBlock
-			);
-			// Possibly this is worse than having the caller account be the payment account?
-			UnlockBlock::<T>::set(unlock_block);
-			Self::deposit_event(Event::<T>::UnlockBlockUpdated { block_number: unlock_block });
-			Ok(())
-		}
-	}
-}
-
-impl<T: Config> Pallet<T> {
-	fn verify_signature(who: &T::AccountId, signature: &[u8]) -> Result<(), DispatchError> {
-		// sr25519 always expects a 64 byte signature.
-		let signature: AnySignature = sr25519::Signature::try_from(signature)
-			.map_err(|_| Error::<T>::InvalidSignature)?
-			.into();
-
-		// In Polkadot, the AccountId is always the same as the 32 byte public key.
-		let account_bytes: [u8; 32] = account_to_bytes(who)?;
-		let public_key = sr25519::Public::from_raw(account_bytes);
-
-		let message = Statement::<T>::get();
-
-		// Check if everything is good or not.
-		match signature.verify(message.as_slice(), &public_key) {
-			true => Ok(()),
-			false => Err(Error::<T>::InvalidSignature)?,
-		}
-	}
-}
-
-// This function converts a 32 byte AccountId to its byte-array equivalent form.
-fn account_to_bytes<AccountId>(account: &AccountId) -> Result<[u8; 32], DispatchError>
-where
-	AccountId: Encode,
-{
-	let account_vec = account.encode();
-	ensure!(account_vec.len() == 32, "AccountId must be 32 bytes.");
-	let mut bytes = [0u8; 32];
-	bytes.copy_from_slice(&account_vec);
-	Ok(bytes)
-}
-
-/// WARNING: Executing this function will clear all storage used by this pallet.
-/// Be sure this is what you want...
-pub fn remove_pallet<T>() -> frame_support::weights::Weight
-where
-	T: frame_system::Config,
-{
-	#[allow(deprecated)]
-	use frame_support::migration::remove_storage_prefix;
-	#[allow(deprecated)]
-	remove_storage_prefix(b"Purchase", b"Accounts", b"");
-	#[allow(deprecated)]
-	remove_storage_prefix(b"Purchase", b"PaymentAccount", b"");
-	#[allow(deprecated)]
-	remove_storage_prefix(b"Purchase", b"Statement", b"");
-	#[allow(deprecated)]
-	remove_storage_prefix(b"Purchase", b"UnlockBlock", b"");
-
-	<T as frame_system::Config>::BlockWeights::get().max_block
-}
-
-#[cfg(test)]
-mod tests {
-	use super::*;
-
-	use sp_core::{crypto::AccountId32, H256};
-	use sp_keyring::{Ed25519Keyring, Sr25519Keyring};
-	// The testing primitives are very useful for avoiding having to work with signatures
-	// or public keys. `u64` is used as the `AccountId` and no `Signature`s are required.
-	use crate::purchase;
-	use frame_support::{
-		assert_noop, assert_ok, derive_impl, ord_parameter_types, parameter_types,
-		traits::{Currency, WithdrawReasons},
-	};
-	use sp_runtime::{
-		traits::{BlakeTwo256, Dispatchable, Identity, IdentityLookup},
-		ArithmeticError, BuildStorage,
-		DispatchError::BadOrigin,
-	};
-
-	type Block = frame_system::mocking::MockBlock<Test>;
-
-	frame_support::construct_runtime!(
-		pub enum Test
-		{
-			System: frame_system,
-			Balances: pallet_balances,
-			Vesting: pallet_vesting,
-			Purchase: purchase,
-		}
-	);
-
-	type AccountId = AccountId32;
-
-	#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
-	impl frame_system::Config for Test {
-		type BaseCallFilter = frame_support::traits::Everything;
-		type BlockWeights = ();
-		type BlockLength = ();
-		type DbWeight = ();
-		type RuntimeOrigin = RuntimeOrigin;
-		type RuntimeCall = RuntimeCall;
-		type Nonce = u64;
-		type Hash = H256;
-		type Hashing = BlakeTwo256;
-		type AccountId = AccountId;
-		type Lookup = IdentityLookup<AccountId>;
-		type Block = Block;
-		type RuntimeEvent = RuntimeEvent;
-		type Version = ();
-		type PalletInfo = PalletInfo;
-		type AccountData = pallet_balances::AccountData<u64>;
-		type OnNewAccount = ();
-		type OnKilledAccount = ();
-		type SystemWeightInfo = ();
-		type SS58Prefix = ();
-		type OnSetCode = ();
-		type MaxConsumers = frame_support::traits::ConstU32<16>;
-	}
-
-	#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
-	impl pallet_balances::Config for Test {
-		type AccountStore = System;
-	}
-
-	parameter_types! {
-		pub const MinVestedTransfer: u64 = 1;
-		pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons =
-			WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE);
-	}
-
-	impl pallet_vesting::Config for Test {
-		type RuntimeEvent = RuntimeEvent;
-		type Currency = Balances;
-		type BlockNumberToBalance = Identity;
-		type MinVestedTransfer = MinVestedTransfer;
-		type WeightInfo = ();
-		type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons;
-		type BlockNumberProvider = System;
-		const MAX_VESTING_SCHEDULES: u32 = 28;
-	}
-
-	parameter_types! {
-		pub const MaxStatementLength: u32 =  1_000;
-		pub const UnlockedProportion: Permill = Permill::from_percent(10);
-		pub const MaxUnlocked: u64 = 10;
-	}
-
-	ord_parameter_types! {
-		pub const ValidityOrigin: AccountId = AccountId32::from([0u8; 32]);
-		pub const PaymentOrigin: AccountId = AccountId32::from([1u8; 32]);
-		pub const ConfigurationOrigin: AccountId = AccountId32::from([2u8; 32]);
-	}
-
-	impl Config for Test {
-		type RuntimeEvent = RuntimeEvent;
-		type Currency = Balances;
-		type VestingSchedule = Vesting;
-		type ValidityOrigin = frame_system::EnsureSignedBy<ValidityOrigin, AccountId>;
-		type ConfigurationOrigin = frame_system::EnsureSignedBy<ConfigurationOrigin, AccountId>;
-		type MaxStatementLength = MaxStatementLength;
-		type UnlockedProportion = UnlockedProportion;
-		type MaxUnlocked = MaxUnlocked;
-	}
-
-	// This function basically just builds a genesis storage key/value store according to
-	// our desired mockup. It also executes our `setup` function which sets up this pallet for use.
-	pub fn new_test_ext() -> sp_io::TestExternalities {
-		let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
-		let mut ext = sp_io::TestExternalities::new(t);
-		ext.execute_with(|| setup());
-		ext
-	}
-
-	fn setup() {
-		let statement = b"Hello, World".to_vec();
-		let unlock_block = 100;
-		Purchase::set_statement(RuntimeOrigin::signed(configuration_origin()), statement).unwrap();
-		Purchase::set_unlock_block(RuntimeOrigin::signed(configuration_origin()), unlock_block)
-			.unwrap();
-		Purchase::set_payment_account(
-			RuntimeOrigin::signed(configuration_origin()),
-			payment_account(),
-		)
-		.unwrap();
-		Balances::make_free_balance_be(&payment_account(), 100_000);
-	}
-
-	fn alice() -> AccountId {
-		Sr25519Keyring::Alice.to_account_id()
-	}
-
-	fn alice_ed25519() -> AccountId {
-		Ed25519Keyring::Alice.to_account_id()
-	}
-
-	fn bob() -> AccountId {
-		Sr25519Keyring::Bob.to_account_id()
-	}
-
-	fn alice_signature() -> [u8; 64] {
-		// echo -n "Hello, World" | subkey -s sign "bottom drive obey lake curtain smoke basket hold
-		// race lonely fit walk//Alice"
-		hex_literal::hex!("20e0faffdf4dfe939f2faa560f73b1d01cde8472e2b690b7b40606a374244c3a2e9eb9c8107c10b605138374003af8819bd4387d7c24a66ee9253c2e688ab881")
-	}
-
-	fn bob_signature() -> [u8; 64] {
-		// echo -n "Hello, World" | subkey -s sign "bottom drive obey lake curtain smoke basket hold
-		// race lonely fit walk//Bob"
-		hex_literal::hex!("d6d460187ecf530f3ec2d6e3ac91b9d083c8fbd8f1112d92a82e4d84df552d18d338e6da8944eba6e84afaacf8a9850f54e7b53a84530d649be2e0119c7ce889")
-	}
-
-	fn alice_signature_ed25519() -> [u8; 64] {
-		// echo -n "Hello, World" | subkey -e sign "bottom drive obey lake curtain smoke basket hold
-		// race lonely fit walk//Alice"
-		hex_literal::hex!("ee3f5a6cbfc12a8f00c18b811dc921b550ddf272354cda4b9a57b1d06213fcd8509f5af18425d39a279d13622f14806c3e978e2163981f2ec1c06e9628460b0e")
-	}
-
-	fn validity_origin() -> AccountId {
-		ValidityOrigin::get()
-	}
-
-	fn configuration_origin() -> AccountId {
-		ConfigurationOrigin::get()
-	}
-
-	fn payment_account() -> AccountId {
-		[42u8; 32].into()
-	}
-
-	#[test]
-	fn set_statement_works_and_handles_basic_errors() {
-		new_test_ext().execute_with(|| {
-			let statement = b"Test Set Statement".to_vec();
-			// Invalid origin
-			assert_noop!(
-				Purchase::set_statement(RuntimeOrigin::signed(alice()), statement.clone()),
-				BadOrigin,
-			);
-			// Too Long
-			let long_statement = [0u8; 10_000].to_vec();
-			assert_noop!(
-				Purchase::set_statement(
-					RuntimeOrigin::signed(configuration_origin()),
-					long_statement
-				),
-				Error::<Test>::InvalidStatement,
-			);
-			// Just right...
-			assert_ok!(Purchase::set_statement(
-				RuntimeOrigin::signed(configuration_origin()),
-				statement.clone()
-			));
-			assert_eq!(Statement::<Test>::get(), statement);
-		});
-	}
-
-	#[test]
-	fn set_unlock_block_works_and_handles_basic_errors() {
-		new_test_ext().execute_with(|| {
-			let unlock_block = 69;
-			// Invalid origin
-			assert_noop!(
-				Purchase::set_unlock_block(RuntimeOrigin::signed(alice()), unlock_block),
-				BadOrigin,
-			);
-			// Block Number in Past
-			let bad_unlock_block = 50;
-			System::set_block_number(bad_unlock_block);
-			assert_noop!(
-				Purchase::set_unlock_block(
-					RuntimeOrigin::signed(configuration_origin()),
-					bad_unlock_block
-				),
-				Error::<Test>::InvalidUnlockBlock,
-			);
-			// Just right...
-			assert_ok!(Purchase::set_unlock_block(
-				RuntimeOrigin::signed(configuration_origin()),
-				unlock_block
-			));
-			assert_eq!(UnlockBlock::<Test>::get(), unlock_block);
-		});
-	}
-
-	#[test]
-	fn set_payment_account_works_and_handles_basic_errors() {
-		new_test_ext().execute_with(|| {
-			let payment_account: AccountId = [69u8; 32].into();
-			// Invalid Origin
-			assert_noop!(
-				Purchase::set_payment_account(
-					RuntimeOrigin::signed(alice()),
-					payment_account.clone()
-				),
-				BadOrigin,
-			);
-			// Just right...
-			assert_ok!(Purchase::set_payment_account(
-				RuntimeOrigin::signed(configuration_origin()),
-				payment_account.clone()
-			));
-			assert_eq!(PaymentAccount::<Test>::get(), Some(payment_account));
-		});
-	}
-
-	#[test]
-	fn signature_verification_works() {
-		new_test_ext().execute_with(|| {
-			assert_ok!(Purchase::verify_signature(&alice(), &alice_signature()));
-			assert_ok!(Purchase::verify_signature(&alice_ed25519(), &alice_signature_ed25519()));
-			assert_ok!(Purchase::verify_signature(&bob(), &bob_signature()));
-
-			// Mixing and matching fails
-			assert_noop!(
-				Purchase::verify_signature(&alice(), &bob_signature()),
-				Error::<Test>::InvalidSignature
-			);
-			assert_noop!(
-				Purchase::verify_signature(&bob(), &alice_signature()),
-				Error::<Test>::InvalidSignature
-			);
-		});
-	}
-
-	#[test]
-	fn account_creation_works() {
-		new_test_ext().execute_with(|| {
-			assert!(!Accounts::<Test>::contains_key(alice()));
-			assert_ok!(Purchase::create_account(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				alice_signature().to_vec(),
-			));
-			assert_eq!(
-				Accounts::<Test>::get(alice()),
-				AccountStatus {
-					validity: AccountValidity::Initiated,
-					free_balance: Zero::zero(),
-					locked_balance: Zero::zero(),
-					signature: alice_signature().to_vec(),
-					vat: Permill::zero(),
-				}
-			);
-		});
-	}
-
-	#[test]
-	fn account_creation_handles_basic_errors() {
-		new_test_ext().execute_with(|| {
-			// Wrong Origin
-			assert_noop!(
-				Purchase::create_account(
-					RuntimeOrigin::signed(alice()),
-					alice(),
-					alice_signature().to_vec()
-				),
-				BadOrigin,
-			);
-
-			// Wrong Account/Signature
-			assert_noop!(
-				Purchase::create_account(
-					RuntimeOrigin::signed(validity_origin()),
-					alice(),
-					bob_signature().to_vec()
-				),
-				Error::<Test>::InvalidSignature,
-			);
-
-			// Account with vesting
-			Balances::make_free_balance_be(&alice(), 100);
-			assert_ok!(<Test as Config>::VestingSchedule::add_vesting_schedule(
-				&alice(),
-				100,
-				1,
-				50
-			));
-			assert_noop!(
-				Purchase::create_account(
-					RuntimeOrigin::signed(validity_origin()),
-					alice(),
-					alice_signature().to_vec()
-				),
-				Error::<Test>::VestingScheduleExists,
-			);
-
-			// Duplicate Purchasing Account
-			assert_ok!(Purchase::create_account(
-				RuntimeOrigin::signed(validity_origin()),
-				bob(),
-				bob_signature().to_vec()
-			));
-			assert_noop!(
-				Purchase::create_account(
-					RuntimeOrigin::signed(validity_origin()),
-					bob(),
-					bob_signature().to_vec()
-				),
-				Error::<Test>::ExistingAccount,
-			);
-		});
-	}
-
-	#[test]
-	fn update_validity_status_works() {
-		new_test_ext().execute_with(|| {
-			// Alice account is created.
-			assert_ok!(Purchase::create_account(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				alice_signature().to_vec(),
-			));
-			// She submits KYC, and we update the status to `Pending`.
-			assert_ok!(Purchase::update_validity_status(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				AccountValidity::Pending,
-			));
-			// KYC comes back negative, so we mark the account invalid.
-			assert_ok!(Purchase::update_validity_status(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				AccountValidity::Invalid,
-			));
-			assert_eq!(
-				Accounts::<Test>::get(alice()),
-				AccountStatus {
-					validity: AccountValidity::Invalid,
-					free_balance: Zero::zero(),
-					locked_balance: Zero::zero(),
-					signature: alice_signature().to_vec(),
-					vat: Permill::zero(),
-				}
-			);
-			// She fixes it, we mark her account valid.
-			assert_ok!(Purchase::update_validity_status(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				AccountValidity::ValidLow,
-			));
-			assert_eq!(
-				Accounts::<Test>::get(alice()),
-				AccountStatus {
-					validity: AccountValidity::ValidLow,
-					free_balance: Zero::zero(),
-					locked_balance: Zero::zero(),
-					signature: alice_signature().to_vec(),
-					vat: Permill::zero(),
-				}
-			);
-		});
-	}
-
-	#[test]
-	fn update_validity_status_handles_basic_errors() {
-		new_test_ext().execute_with(|| {
-			// Wrong Origin
-			assert_noop!(
-				Purchase::update_validity_status(
-					RuntimeOrigin::signed(alice()),
-					alice(),
-					AccountValidity::Pending,
-				),
-				BadOrigin
-			);
-			// Inactive Account
-			assert_noop!(
-				Purchase::update_validity_status(
-					RuntimeOrigin::signed(validity_origin()),
-					alice(),
-					AccountValidity::Pending,
-				),
-				Error::<Test>::InvalidAccount
-			);
-			// Already Completed
-			assert_ok!(Purchase::create_account(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				alice_signature().to_vec(),
-			));
-			assert_ok!(Purchase::update_validity_status(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				AccountValidity::Completed,
-			));
-			assert_noop!(
-				Purchase::update_validity_status(
-					RuntimeOrigin::signed(validity_origin()),
-					alice(),
-					AccountValidity::Pending,
-				),
-				Error::<Test>::AlreadyCompleted
-			);
-		});
-	}
-
-	#[test]
-	fn update_balance_works() {
-		new_test_ext().execute_with(|| {
-			// Alice account is created
-			assert_ok!(Purchase::create_account(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				alice_signature().to_vec()
-			));
-			// And approved for basic contribution
-			assert_ok!(Purchase::update_validity_status(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				AccountValidity::ValidLow,
-			));
-			// We set a balance on the user based on the payment they made. 50 locked, 50 free.
-			assert_ok!(Purchase::update_balance(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				50,
-				50,
-				Permill::from_rational(77u32, 1000u32),
-			));
-			assert_eq!(
-				Accounts::<Test>::get(alice()),
-				AccountStatus {
-					validity: AccountValidity::ValidLow,
-					free_balance: 50,
-					locked_balance: 50,
-					signature: alice_signature().to_vec(),
-					vat: Permill::from_parts(77000),
-				}
-			);
-			// We can update the balance based on new information.
-			assert_ok!(Purchase::update_balance(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				25,
-				50,
-				Permill::zero(),
-			));
-			assert_eq!(
-				Accounts::<Test>::get(alice()),
-				AccountStatus {
-					validity: AccountValidity::ValidLow,
-					free_balance: 25,
-					locked_balance: 50,
-					signature: alice_signature().to_vec(),
-					vat: Permill::zero(),
-				}
-			);
-		});
-	}
-
-	#[test]
-	fn update_balance_handles_basic_errors() {
-		new_test_ext().execute_with(|| {
-			// Wrong Origin
-			assert_noop!(
-				Purchase::update_balance(
-					RuntimeOrigin::signed(alice()),
-					alice(),
-					50,
-					50,
-					Permill::zero(),
-				),
-				BadOrigin
-			);
-			// Inactive Account
-			assert_noop!(
-				Purchase::update_balance(
-					RuntimeOrigin::signed(validity_origin()),
-					alice(),
-					50,
-					50,
-					Permill::zero(),
-				),
-				Error::<Test>::InvalidAccount
-			);
-			// Overflow
-			assert_noop!(
-				Purchase::update_balance(
-					RuntimeOrigin::signed(validity_origin()),
-					alice(),
-					u64::MAX,
-					u64::MAX,
-					Permill::zero(),
-				),
-				Error::<Test>::InvalidAccount
-			);
-		});
-	}
-
-	#[test]
-	fn payout_works() {
-		new_test_ext().execute_with(|| {
-			// Alice and Bob accounts are created
-			assert_ok!(Purchase::create_account(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				alice_signature().to_vec()
-			));
-			assert_ok!(Purchase::create_account(
-				RuntimeOrigin::signed(validity_origin()),
-				bob(),
-				bob_signature().to_vec()
-			));
-			// Alice is approved for basic contribution
-			assert_ok!(Purchase::update_validity_status(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				AccountValidity::ValidLow,
-			));
-			// Bob is approved for high contribution
-			assert_ok!(Purchase::update_validity_status(
-				RuntimeOrigin::signed(validity_origin()),
-				bob(),
-				AccountValidity::ValidHigh,
-			));
-			// We set a balance on the users based on the payment they made. 50 locked, 50 free.
-			assert_ok!(Purchase::update_balance(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				50,
-				50,
-				Permill::zero(),
-			));
-			assert_ok!(Purchase::update_balance(
-				RuntimeOrigin::signed(validity_origin()),
-				bob(),
-				100,
-				150,
-				Permill::zero(),
-			));
-			// Now we call payout for Alice and Bob.
-			assert_ok!(Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),));
-			assert_ok!(Purchase::payout(RuntimeOrigin::signed(payment_account()), bob(),));
-			// Payment is made.
-			assert_eq!(<Test as Config>::Currency::free_balance(&payment_account()), 99_650);
-			assert_eq!(<Test as Config>::Currency::free_balance(&alice()), 100);
-			// 10% of the 50 units is unlocked automatically for Alice
-			assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), Some(45));
-			assert_eq!(<Test as Config>::Currency::free_balance(&bob()), 250);
-			// A max of 10 units is unlocked automatically for Bob
-			assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), Some(140));
-			// Status is completed.
-			assert_eq!(
-				Accounts::<Test>::get(alice()),
-				AccountStatus {
-					validity: AccountValidity::Completed,
-					free_balance: 50,
-					locked_balance: 50,
-					signature: alice_signature().to_vec(),
-					vat: Permill::zero(),
-				}
-			);
-			assert_eq!(
-				Accounts::<Test>::get(bob()),
-				AccountStatus {
-					validity: AccountValidity::Completed,
-					free_balance: 100,
-					locked_balance: 150,
-					signature: bob_signature().to_vec(),
-					vat: Permill::zero(),
-				}
-			);
-			// Vesting lock is removed in whole on block 101 (100 blocks after block 1)
-			System::set_block_number(100);
-			let vest_call = RuntimeCall::Vesting(pallet_vesting::Call::<Test>::vest {});
-			assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(alice())));
-			assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(bob())));
-			assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), Some(45));
-			assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), Some(140));
-			System::set_block_number(101);
-			assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(alice())));
-			assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(bob())));
-			assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), None);
-			assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), None);
-		});
-	}
-
-	#[test]
-	fn payout_handles_basic_errors() {
-		new_test_ext().execute_with(|| {
-			// Wrong Origin
-			assert_noop!(Purchase::payout(RuntimeOrigin::signed(alice()), alice(),), BadOrigin);
-			// Account with Existing Vesting Schedule
-			Balances::make_free_balance_be(&bob(), 100);
-			assert_ok!(
-				<Test as Config>::VestingSchedule::add_vesting_schedule(&bob(), 100, 1, 50,)
-			);
-			assert_noop!(
-				Purchase::payout(RuntimeOrigin::signed(payment_account()), bob(),),
-				Error::<Test>::VestingScheduleExists
-			);
-			// Invalid Account (never created)
-			assert_noop!(
-				Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),),
-				Error::<Test>::InvalidAccount
-			);
-			// Invalid Account (created, but not valid)
-			assert_ok!(Purchase::create_account(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				alice_signature().to_vec()
-			));
-			assert_noop!(
-				Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),),
-				Error::<Test>::InvalidAccount
-			);
-			// Not enough funds in payment account
-			assert_ok!(Purchase::update_validity_status(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				AccountValidity::ValidHigh,
-			));
-			assert_ok!(Purchase::update_balance(
-				RuntimeOrigin::signed(validity_origin()),
-				alice(),
-				100_000,
-				100_000,
-				Permill::zero(),
-			));
-			assert_noop!(
-				Purchase::payout(RuntimeOrigin::signed(payment_account()), alice()),
-				ArithmeticError::Underflow
-			);
-		});
-	}
-
-	#[test]
-	fn remove_pallet_works() {
-		new_test_ext().execute_with(|| {
-			let account_status = AccountStatus {
-				validity: AccountValidity::Completed,
-				free_balance: 1234,
-				locked_balance: 4321,
-				signature: b"my signature".to_vec(),
-				vat: Permill::from_percent(50),
-			};
-
-			// Add some storage.
-			Accounts::<Test>::insert(alice(), account_status.clone());
-			Accounts::<Test>::insert(bob(), account_status);
-			PaymentAccount::<Test>::put(alice());
-			Statement::<Test>::put(b"hello, world!".to_vec());
-			UnlockBlock::<Test>::put(4);
-
-			// Verify storage exists.
-			assert_eq!(Accounts::<Test>::iter().count(), 2);
-			assert!(PaymentAccount::<Test>::exists());
-			assert!(Statement::<Test>::exists());
-			assert!(UnlockBlock::<Test>::exists());
-
-			// Remove storage.
-			remove_pallet::<Test>();
-
-			// Verify storage is gone.
-			assert_eq!(Accounts::<Test>::iter().count(), 0);
-			assert!(!PaymentAccount::<Test>::exists());
-			assert!(!Statement::<Test>::exists());
-			assert!(!UnlockBlock::<Test>::exists());
-		});
-	}
-}
diff --git a/polkadot/runtime/common/src/purchase/mock.rs b/polkadot/runtime/common/src/purchase/mock.rs
new file mode 100644
index 00000000000..ec8599f3b79
--- /dev/null
+++ b/polkadot/runtime/common/src/purchase/mock.rs
@@ -0,0 +1,181 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Mocking utilities for testing in purchase pallet.
+
+#[cfg(test)]
+use super::*;
+
+use sp_core::{crypto::AccountId32, H256};
+use sp_keyring::{Ed25519Keyring, Sr25519Keyring};
+// The testing primitives are very useful for avoiding having to work with signatures
+// or public keys. `u64` is used as the `AccountId` and no `Signature`s are required.
+use crate::purchase;
+use frame_support::{
+	derive_impl, ord_parameter_types, parameter_types,
+	traits::{Currency, WithdrawReasons},
+};
+use sp_runtime::{
+	traits::{BlakeTwo256, Identity, IdentityLookup},
+	BuildStorage,
+};
+
+type Block = frame_system::mocking::MockBlock<Test>;
+
+frame_support::construct_runtime!(
+	pub enum Test
+	{
+		System: frame_system,
+		Balances: pallet_balances,
+		Vesting: pallet_vesting,
+		Purchase: purchase,
+	}
+);
+
+type AccountId = AccountId32;
+
+#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
+impl frame_system::Config for Test {
+	type BaseCallFilter = frame_support::traits::Everything;
+	type BlockWeights = ();
+	type BlockLength = ();
+	type DbWeight = ();
+	type RuntimeOrigin = RuntimeOrigin;
+	type RuntimeCall = RuntimeCall;
+	type Nonce = u64;
+	type Hash = H256;
+	type Hashing = BlakeTwo256;
+	type AccountId = AccountId;
+	type Lookup = IdentityLookup<AccountId>;
+	type Block = Block;
+	type RuntimeEvent = RuntimeEvent;
+	type Version = ();
+	type PalletInfo = PalletInfo;
+	type AccountData = pallet_balances::AccountData<u64>;
+	type OnNewAccount = ();
+	type OnKilledAccount = ();
+	type SystemWeightInfo = ();
+	type SS58Prefix = ();
+	type OnSetCode = ();
+	type MaxConsumers = frame_support::traits::ConstU32<16>;
+}
+
+#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
+impl pallet_balances::Config for Test {
+	type AccountStore = System;
+}
+
+parameter_types! {
+	pub const MinVestedTransfer: u64 = 1;
+	pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons =
+		WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE);
+}
+
+impl pallet_vesting::Config for Test {
+	type RuntimeEvent = RuntimeEvent;
+	type Currency = Balances;
+	type BlockNumberToBalance = Identity;
+	type MinVestedTransfer = MinVestedTransfer;
+	type WeightInfo = ();
+	type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons;
+	type BlockNumberProvider = System;
+	const MAX_VESTING_SCHEDULES: u32 = 28;
+}
+
+parameter_types! {
+	pub const MaxStatementLength: u32 =  1_000;
+	pub const UnlockedProportion: Permill = Permill::from_percent(10);
+	pub const MaxUnlocked: u64 = 10;
+}
+
+ord_parameter_types! {
+	pub const ValidityOrigin: AccountId = AccountId32::from([0u8; 32]);
+	pub const PaymentOrigin: AccountId = AccountId32::from([1u8; 32]);
+	pub const ConfigurationOrigin: AccountId = AccountId32::from([2u8; 32]);
+}
+
+impl Config for Test {
+	type RuntimeEvent = RuntimeEvent;
+	type Currency = Balances;
+	type VestingSchedule = Vesting;
+	type ValidityOrigin = frame_system::EnsureSignedBy<ValidityOrigin, AccountId>;
+	type ConfigurationOrigin = frame_system::EnsureSignedBy<ConfigurationOrigin, AccountId>;
+	type MaxStatementLength = MaxStatementLength;
+	type UnlockedProportion = UnlockedProportion;
+	type MaxUnlocked = MaxUnlocked;
+}
+
+// This function basically just builds a genesis storage key/value store according to
+// our desired mockup. It also executes our `setup` function which sets up this pallet for use.
+pub fn new_test_ext() -> sp_io::TestExternalities {
+	let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
+	let mut ext = sp_io::TestExternalities::new(t);
+	ext.execute_with(|| setup());
+	ext
+}
+
+pub fn setup() {
+	let statement = b"Hello, World".to_vec();
+	let unlock_block = 100;
+	Purchase::set_statement(RuntimeOrigin::signed(configuration_origin()), statement).unwrap();
+	Purchase::set_unlock_block(RuntimeOrigin::signed(configuration_origin()), unlock_block)
+		.unwrap();
+	Purchase::set_payment_account(RuntimeOrigin::signed(configuration_origin()), payment_account())
+		.unwrap();
+	Balances::make_free_balance_be(&payment_account(), 100_000);
+}
+
+pub fn alice() -> AccountId {
+	Sr25519Keyring::Alice.to_account_id()
+}
+
+pub fn alice_ed25519() -> AccountId {
+	Ed25519Keyring::Alice.to_account_id()
+}
+
+pub fn bob() -> AccountId {
+	Sr25519Keyring::Bob.to_account_id()
+}
+
+pub fn alice_signature() -> [u8; 64] {
+	// echo -n "Hello, World" | subkey -s sign "bottom drive obey lake curtain smoke basket hold
+	// race lonely fit walk//Alice"
+	hex_literal::hex!("20e0faffdf4dfe939f2faa560f73b1d01cde8472e2b690b7b40606a374244c3a2e9eb9c8107c10b605138374003af8819bd4387d7c24a66ee9253c2e688ab881")
+}
+
+pub fn bob_signature() -> [u8; 64] {
+	// echo -n "Hello, World" | subkey -s sign "bottom drive obey lake curtain smoke basket hold
+	// race lonely fit walk//Bob"
+	hex_literal::hex!("d6d460187ecf530f3ec2d6e3ac91b9d083c8fbd8f1112d92a82e4d84df552d18d338e6da8944eba6e84afaacf8a9850f54e7b53a84530d649be2e0119c7ce889")
+}
+
+pub fn alice_signature_ed25519() -> [u8; 64] {
+	// echo -n "Hello, World" | subkey -e sign "bottom drive obey lake curtain smoke basket hold
+	// race lonely fit walk//Alice"
+	hex_literal::hex!("ee3f5a6cbfc12a8f00c18b811dc921b550ddf272354cda4b9a57b1d06213fcd8509f5af18425d39a279d13622f14806c3e978e2163981f2ec1c06e9628460b0e")
+}
+
+pub fn validity_origin() -> AccountId {
+	ValidityOrigin::get()
+}
+
+pub fn configuration_origin() -> AccountId {
+	ConfigurationOrigin::get()
+}
+
+pub fn payment_account() -> AccountId {
+	[42u8; 32].into()
+}
diff --git a/polkadot/runtime/common/src/purchase/mod.rs b/polkadot/runtime/common/src/purchase/mod.rs
new file mode 100644
index 00000000000..71dc5b57967
--- /dev/null
+++ b/polkadot/runtime/common/src/purchase/mod.rs
@@ -0,0 +1,482 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Pallet to process purchase of DOTs.
+
+use alloc::vec::Vec;
+use codec::{Decode, Encode};
+use frame_support::{
+	pallet_prelude::*,
+	traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, VestingSchedule},
+};
+use frame_system::pallet_prelude::*;
+pub use pallet::*;
+use scale_info::TypeInfo;
+use sp_core::sr25519;
+use sp_runtime::{
+	traits::{CheckedAdd, Saturating, Verify, Zero},
+	AnySignature, DispatchError, DispatchResult, Permill, RuntimeDebug,
+};
+
+type BalanceOf<T> =
+	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
+
+/// The kind of statement an account needs to make for a claim to be valid.
+#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug, TypeInfo)]
+pub enum AccountValidity {
+	/// Account is not valid.
+	Invalid,
+	/// Account has initiated the account creation process.
+	Initiated,
+	/// Account is pending validation.
+	Pending,
+	/// Account is valid with a low contribution amount.
+	ValidLow,
+	/// Account is valid with a high contribution amount.
+	ValidHigh,
+	/// Account has completed the purchase process.
+	Completed,
+}
+
+impl Default for AccountValidity {
+	fn default() -> Self {
+		AccountValidity::Invalid
+	}
+}
+
+impl AccountValidity {
+	fn is_valid(&self) -> bool {
+		match self {
+			Self::Invalid => false,
+			Self::Initiated => false,
+			Self::Pending => false,
+			Self::ValidLow => true,
+			Self::ValidHigh => true,
+			Self::Completed => false,
+		}
+	}
+}
+
+/// All information about an account regarding the purchase of DOTs.
+#[derive(Encode, Decode, Default, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)]
+pub struct AccountStatus<Balance> {
+	/// The current validity status of the user. Will denote if the user has passed KYC,
+	/// how much they are able to purchase, and when their purchase process has completed.
+	validity: AccountValidity,
+	/// The amount of free DOTs they have purchased.
+	free_balance: Balance,
+	/// The amount of locked DOTs they have purchased.
+	locked_balance: Balance,
+	/// Their sr25519/ed25519 signature verifying they have signed our required statement.
+	signature: Vec<u8>,
+	/// The percentage of VAT the purchaser is responsible for. This is already factored into
+	/// account balance.
+	vat: Permill,
+}
+
+#[frame_support::pallet]
+pub mod pallet {
+	use super::*;
+
+	#[pallet::pallet]
+	#[pallet::without_storage_info]
+	pub struct Pallet<T>(_);
+
+	#[pallet::config]
+	pub trait Config: frame_system::Config {
+		/// The overarching event type.
+		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
+
+		/// Balances Pallet
+		type Currency: Currency<Self::AccountId>;
+
+		/// Vesting Pallet
+		type VestingSchedule: VestingSchedule<
+			Self::AccountId,
+			Moment = BlockNumberFor<Self>,
+			Currency = Self::Currency,
+		>;
+
+		/// The origin allowed to set account status.
+		type ValidityOrigin: EnsureOrigin<Self::RuntimeOrigin>;
+
+		/// The origin allowed to make configurations to the pallet.
+		type ConfigurationOrigin: EnsureOrigin<Self::RuntimeOrigin>;
+
+		/// The maximum statement length for the statement users to sign when creating an account.
+		#[pallet::constant]
+		type MaxStatementLength: Get<u32>;
+
+		/// The amount of purchased locked DOTs that we will unlock for basic actions on the chain.
+		#[pallet::constant]
+		type UnlockedProportion: Get<Permill>;
+
+		/// The maximum amount of locked DOTs that we will unlock.
+		#[pallet::constant]
+		type MaxUnlocked: Get<BalanceOf<Self>>;
+	}
+
+	#[pallet::event]
+	#[pallet::generate_deposit(pub(super) fn deposit_event)]
+	pub enum Event<T: Config> {
+		/// A new account was created.
+		AccountCreated { who: T::AccountId },
+		/// Someone's account validity was updated.
+		ValidityUpdated { who: T::AccountId, validity: AccountValidity },
+		/// Someone's purchase balance was updated.
+		BalanceUpdated { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
+		/// A payout was made to a purchaser.
+		PaymentComplete { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
+		/// A new payment account was set.
+		PaymentAccountSet { who: T::AccountId },
+		/// A new statement was set.
+		StatementUpdated,
+		/// A new statement was set. `[block_number]`
+		UnlockBlockUpdated { block_number: BlockNumberFor<T> },
+	}
+
+	#[pallet::error]
+	pub enum Error<T> {
+		/// Account is not currently valid to use.
+		InvalidAccount,
+		/// Account used in the purchase already exists.
+		ExistingAccount,
+		/// Provided signature is invalid
+		InvalidSignature,
+		/// Account has already completed the purchase process.
+		AlreadyCompleted,
+		/// An overflow occurred when doing calculations.
+		Overflow,
+		/// The statement is too long to be stored on chain.
+		InvalidStatement,
+		/// The unlock block is in the past!
+		InvalidUnlockBlock,
+		/// Vesting schedule already exists for this account.
+		VestingScheduleExists,
+	}
+
+	// A map of all participants in the DOT purchase process.
+	#[pallet::storage]
+	pub(super) type Accounts<T: Config> =
+		StorageMap<_, Blake2_128Concat, T::AccountId, AccountStatus<BalanceOf<T>>, ValueQuery>;
+
+	// The account that will be used to payout participants of the DOT purchase process.
+	#[pallet::storage]
+	pub(super) type PaymentAccount<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
+
+	// The statement purchasers will need to sign to participate.
+	#[pallet::storage]
+	pub(super) type Statement<T> = StorageValue<_, Vec<u8>, ValueQuery>;
+
+	// The block where all locked dots will unlock.
+	#[pallet::storage]
+	pub(super) type UnlockBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
+
+	#[pallet::hooks]
+	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
+
+	#[pallet::call]
+	impl<T: Config> Pallet<T> {
+		/// Create a new account. Proof of existence through a valid signed message.
+		///
+		/// We check that the account does not exist at this stage.
+		///
+		/// Origin must match the `ValidityOrigin`.
+		#[pallet::call_index(0)]
+		#[pallet::weight(Weight::from_parts(200_000_000, 0) + T::DbWeight::get().reads_writes(4, 1))]
+		pub fn create_account(
+			origin: OriginFor<T>,
+			who: T::AccountId,
+			signature: Vec<u8>,
+		) -> DispatchResult {
+			T::ValidityOrigin::ensure_origin(origin)?;
+			// Account is already being tracked by the pallet.
+			ensure!(!Accounts::<T>::contains_key(&who), Error::<T>::ExistingAccount);
+			// Account should not have a vesting schedule.
+			ensure!(
+				T::VestingSchedule::vesting_balance(&who).is_none(),
+				Error::<T>::VestingScheduleExists
+			);
+
+			// Verify the signature provided is valid for the statement.
+			Self::verify_signature(&who, &signature)?;
+
+			// Create a new pending account.
+			let status = AccountStatus {
+				validity: AccountValidity::Initiated,
+				signature,
+				free_balance: Zero::zero(),
+				locked_balance: Zero::zero(),
+				vat: Permill::zero(),
+			};
+			Accounts::<T>::insert(&who, status);
+			Self::deposit_event(Event::<T>::AccountCreated { who });
+			Ok(())
+		}
+
+		/// Update the validity status of an existing account. If set to completed, the account
+		/// will no longer be able to continue through the crowdfund process.
+		///
+		/// We check that the account exists at this stage, but has not completed the process.
+		///
+		/// Origin must match the `ValidityOrigin`.
+		#[pallet::call_index(1)]
+		#[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
+		pub fn update_validity_status(
+			origin: OriginFor<T>,
+			who: T::AccountId,
+			validity: AccountValidity,
+		) -> DispatchResult {
+			T::ValidityOrigin::ensure_origin(origin)?;
+			ensure!(Accounts::<T>::contains_key(&who), Error::<T>::InvalidAccount);
+			Accounts::<T>::try_mutate(
+				&who,
+				|status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
+					ensure!(
+						status.validity != AccountValidity::Completed,
+						Error::<T>::AlreadyCompleted
+					);
+					status.validity = validity;
+					Ok(())
+				},
+			)?;
+			Self::deposit_event(Event::<T>::ValidityUpdated { who, validity });
+			Ok(())
+		}
+
+		/// Update the balance of a valid account.
+		///
+		/// We check that the account is valid for a balance transfer at this point.
+		///
+		/// Origin must match the `ValidityOrigin`.
+		#[pallet::call_index(2)]
+		#[pallet::weight(T::DbWeight::get().reads_writes(2, 1))]
+		pub fn update_balance(
+			origin: OriginFor<T>,
+			who: T::AccountId,
+			free_balance: BalanceOf<T>,
+			locked_balance: BalanceOf<T>,
+			vat: Permill,
+		) -> DispatchResult {
+			T::ValidityOrigin::ensure_origin(origin)?;
+
+			Accounts::<T>::try_mutate(
+				&who,
+				|status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
+					// Account has a valid status (not Invalid, Pending, or Completed)...
+					ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
+
+					free_balance.checked_add(&locked_balance).ok_or(Error::<T>::Overflow)?;
+					status.free_balance = free_balance;
+					status.locked_balance = locked_balance;
+					status.vat = vat;
+					Ok(())
+				},
+			)?;
+			Self::deposit_event(Event::<T>::BalanceUpdated {
+				who,
+				free: free_balance,
+				locked: locked_balance,
+			});
+			Ok(())
+		}
+
+		/// Pay the user and complete the purchase process.
+		///
+		/// We reverify all assumptions about the state of an account, and complete the process.
+		///
+		/// Origin must match the configured `PaymentAccount` (if it is not configured then this
+		/// will always fail with `BadOrigin`).
+		#[pallet::call_index(3)]
+		#[pallet::weight(T::DbWeight::get().reads_writes(4, 2))]
+		pub fn payout(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
+			// Payments must be made directly by the `PaymentAccount`.
+			let payment_account = ensure_signed(origin)?;
+			let test_against = PaymentAccount::<T>::get().ok_or(DispatchError::BadOrigin)?;
+			ensure!(payment_account == test_against, DispatchError::BadOrigin);
+
+			// Account should not have a vesting schedule.
+			ensure!(
+				T::VestingSchedule::vesting_balance(&who).is_none(),
+				Error::<T>::VestingScheduleExists
+			);
+
+			Accounts::<T>::try_mutate(
+				&who,
+				|status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
+					// Account has a valid status (not Invalid, Pending, or Completed)...
+					ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
+
+					// Transfer funds from the payment account into the purchasing user.
+					let total_balance = status
+						.free_balance
+						.checked_add(&status.locked_balance)
+						.ok_or(Error::<T>::Overflow)?;
+					T::Currency::transfer(
+						&payment_account,
+						&who,
+						total_balance,
+						ExistenceRequirement::AllowDeath,
+					)?;
+
+					if !status.locked_balance.is_zero() {
+						let unlock_block = UnlockBlock::<T>::get();
+						// We allow some configurable portion of the purchased locked DOTs to be
+						// unlocked for basic usage.
+						let unlocked = (T::UnlockedProportion::get() * status.locked_balance)
+							.min(T::MaxUnlocked::get());
+						let locked = status.locked_balance.saturating_sub(unlocked);
+						// We checked that this account has no existing vesting schedule. So this
+						// function should never fail, however if it does, not much we can do about
+						// it at this point.
+						let _ = T::VestingSchedule::add_vesting_schedule(
+							// Apply vesting schedule to this user
+							&who,
+							// For this much amount
+							locked,
+							// Unlocking the full amount after one block
+							locked,
+							// When everything unlocks
+							unlock_block,
+						);
+					}
+
+					// Setting the user account to `Completed` ends the purchase process for this
+					// user.
+					status.validity = AccountValidity::Completed;
+					Self::deposit_event(Event::<T>::PaymentComplete {
+						who: who.clone(),
+						free: status.free_balance,
+						locked: status.locked_balance,
+					});
+					Ok(())
+				},
+			)?;
+			Ok(())
+		}
+
+		/* Configuration Operations */
+
+		/// Set the account that will be used to payout users in the DOT purchase process.
+		///
+		/// Origin must match the `ConfigurationOrigin`
+		#[pallet::call_index(4)]
+		#[pallet::weight(T::DbWeight::get().writes(1))]
+		pub fn set_payment_account(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
+			T::ConfigurationOrigin::ensure_origin(origin)?;
+			// Possibly this is worse than having the caller account be the payment account?
+			PaymentAccount::<T>::put(who.clone());
+			Self::deposit_event(Event::<T>::PaymentAccountSet { who });
+			Ok(())
+		}
+
+		/// Set the statement that must be signed for a user to participate on the DOT sale.
+		///
+		/// Origin must match the `ConfigurationOrigin`
+		#[pallet::call_index(5)]
+		#[pallet::weight(T::DbWeight::get().writes(1))]
+		pub fn set_statement(origin: OriginFor<T>, statement: Vec<u8>) -> DispatchResult {
+			T::ConfigurationOrigin::ensure_origin(origin)?;
+			ensure!(
+				(statement.len() as u32) < T::MaxStatementLength::get(),
+				Error::<T>::InvalidStatement
+			);
+			// Possibly this is worse than having the caller account be the payment account?
+			Statement::<T>::set(statement);
+			Self::deposit_event(Event::<T>::StatementUpdated);
+			Ok(())
+		}
+
+		/// Set the block where locked DOTs will become unlocked.
+		///
+		/// Origin must match the `ConfigurationOrigin`
+		#[pallet::call_index(6)]
+		#[pallet::weight(T::DbWeight::get().writes(1))]
+		pub fn set_unlock_block(
+			origin: OriginFor<T>,
+			unlock_block: BlockNumberFor<T>,
+		) -> DispatchResult {
+			T::ConfigurationOrigin::ensure_origin(origin)?;
+			ensure!(
+				unlock_block > frame_system::Pallet::<T>::block_number(),
+				Error::<T>::InvalidUnlockBlock
+			);
+			// Possibly this is worse than having the caller account be the payment account?
+			UnlockBlock::<T>::set(unlock_block);
+			Self::deposit_event(Event::<T>::UnlockBlockUpdated { block_number: unlock_block });
+			Ok(())
+		}
+	}
+}
+
+impl<T: Config> Pallet<T> {
+	fn verify_signature(who: &T::AccountId, signature: &[u8]) -> Result<(), DispatchError> {
+		// sr25519 always expects a 64 byte signature.
+		let signature: AnySignature = sr25519::Signature::try_from(signature)
+			.map_err(|_| Error::<T>::InvalidSignature)?
+			.into();
+
+		// In Polkadot, the AccountId is always the same as the 32 byte public key.
+		let account_bytes: [u8; 32] = account_to_bytes(who)?;
+		let public_key = sr25519::Public::from_raw(account_bytes);
+
+		let message = Statement::<T>::get();
+
+		// Check if everything is good or not.
+		match signature.verify(message.as_slice(), &public_key) {
+			true => Ok(()),
+			false => Err(Error::<T>::InvalidSignature)?,
+		}
+	}
+}
+
+// This function converts a 32 byte AccountId to its byte-array equivalent form.
+fn account_to_bytes<AccountId>(account: &AccountId) -> Result<[u8; 32], DispatchError>
+where
+	AccountId: Encode,
+{
+	let account_vec = account.encode();
+	ensure!(account_vec.len() == 32, "AccountId must be 32 bytes.");
+	let mut bytes = [0u8; 32];
+	bytes.copy_from_slice(&account_vec);
+	Ok(bytes)
+}
+
+/// WARNING: Executing this function will clear all storage used by this pallet.
+/// Be sure this is what you want...
+pub fn remove_pallet<T>() -> frame_support::weights::Weight
+where
+	T: frame_system::Config,
+{
+	#[allow(deprecated)]
+	use frame_support::migration::remove_storage_prefix;
+	#[allow(deprecated)]
+	remove_storage_prefix(b"Purchase", b"Accounts", b"");
+	#[allow(deprecated)]
+	remove_storage_prefix(b"Purchase", b"PaymentAccount", b"");
+	#[allow(deprecated)]
+	remove_storage_prefix(b"Purchase", b"Statement", b"");
+	#[allow(deprecated)]
+	remove_storage_prefix(b"Purchase", b"UnlockBlock", b"");
+
+	<T as frame_system::Config>::BlockWeights::get().max_block
+}
+
+#[cfg(test)]
+mod mock;
+
+#[cfg(test)]
+mod tests;
diff --git a/polkadot/runtime/common/src/purchase/tests.rs b/polkadot/runtime/common/src/purchase/tests.rs
new file mode 100644
index 00000000000..8cf2a124d24
--- /dev/null
+++ b/polkadot/runtime/common/src/purchase/tests.rs
@@ -0,0 +1,547 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Tests for the purchase pallet.
+
+#[cfg(test)]
+use super::*;
+
+use sp_core::crypto::AccountId32;
+// The testing primitives are very useful for avoiding having to work with signatures
+// or public keys. `u64` is used as the `AccountId` and no `Signature`s are required.
+use frame_support::{assert_noop, assert_ok, traits::Currency};
+use sp_runtime::{traits::Dispatchable, ArithmeticError, DispatchError::BadOrigin};
+
+use crate::purchase::mock::*;
+
+#[test]
+fn set_statement_works_and_handles_basic_errors() {
+	new_test_ext().execute_with(|| {
+		let statement = b"Test Set Statement".to_vec();
+		// Invalid origin
+		assert_noop!(
+			Purchase::set_statement(RuntimeOrigin::signed(alice()), statement.clone()),
+			BadOrigin,
+		);
+		// Too Long
+		let long_statement = [0u8; 10_000].to_vec();
+		assert_noop!(
+			Purchase::set_statement(RuntimeOrigin::signed(configuration_origin()), long_statement),
+			Error::<Test>::InvalidStatement,
+		);
+		// Just right...
+		assert_ok!(Purchase::set_statement(
+			RuntimeOrigin::signed(configuration_origin()),
+			statement.clone()
+		));
+		assert_eq!(Statement::<Test>::get(), statement);
+	});
+}
+
+#[test]
+fn set_unlock_block_works_and_handles_basic_errors() {
+	new_test_ext().execute_with(|| {
+		let unlock_block = 69;
+		// Invalid origin
+		assert_noop!(
+			Purchase::set_unlock_block(RuntimeOrigin::signed(alice()), unlock_block),
+			BadOrigin,
+		);
+		// Block Number in Past
+		let bad_unlock_block = 50;
+		System::set_block_number(bad_unlock_block);
+		assert_noop!(
+			Purchase::set_unlock_block(
+				RuntimeOrigin::signed(configuration_origin()),
+				bad_unlock_block
+			),
+			Error::<Test>::InvalidUnlockBlock,
+		);
+		// Just right...
+		assert_ok!(Purchase::set_unlock_block(
+			RuntimeOrigin::signed(configuration_origin()),
+			unlock_block
+		));
+		assert_eq!(UnlockBlock::<Test>::get(), unlock_block);
+	});
+}
+
+#[test]
+fn set_payment_account_works_and_handles_basic_errors() {
+	new_test_ext().execute_with(|| {
+		let payment_account: AccountId32 = [69u8; 32].into();
+		// Invalid Origin
+		assert_noop!(
+			Purchase::set_payment_account(RuntimeOrigin::signed(alice()), payment_account.clone()),
+			BadOrigin,
+		);
+		// Just right...
+		assert_ok!(Purchase::set_payment_account(
+			RuntimeOrigin::signed(configuration_origin()),
+			payment_account.clone()
+		));
+		assert_eq!(PaymentAccount::<Test>::get(), Some(payment_account));
+	});
+}
+
+#[test]
+fn signature_verification_works() {
+	new_test_ext().execute_with(|| {
+		assert_ok!(Purchase::verify_signature(&alice(), &alice_signature()));
+		assert_ok!(Purchase::verify_signature(&alice_ed25519(), &alice_signature_ed25519()));
+		assert_ok!(Purchase::verify_signature(&bob(), &bob_signature()));
+
+		// Mixing and matching fails
+		assert_noop!(
+			Purchase::verify_signature(&alice(), &bob_signature()),
+			Error::<Test>::InvalidSignature
+		);
+		assert_noop!(
+			Purchase::verify_signature(&bob(), &alice_signature()),
+			Error::<Test>::InvalidSignature
+		);
+	});
+}
+
+#[test]
+fn account_creation_works() {
+	new_test_ext().execute_with(|| {
+		assert!(!Accounts::<Test>::contains_key(alice()));
+		assert_ok!(Purchase::create_account(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			alice_signature().to_vec(),
+		));
+		assert_eq!(
+			Accounts::<Test>::get(alice()),
+			AccountStatus {
+				validity: AccountValidity::Initiated,
+				free_balance: Zero::zero(),
+				locked_balance: Zero::zero(),
+				signature: alice_signature().to_vec(),
+				vat: Permill::zero(),
+			}
+		);
+	});
+}
+
+#[test]
+fn account_creation_handles_basic_errors() {
+	new_test_ext().execute_with(|| {
+		// Wrong Origin
+		assert_noop!(
+			Purchase::create_account(
+				RuntimeOrigin::signed(alice()),
+				alice(),
+				alice_signature().to_vec()
+			),
+			BadOrigin,
+		);
+
+		// Wrong Account/Signature
+		assert_noop!(
+			Purchase::create_account(
+				RuntimeOrigin::signed(validity_origin()),
+				alice(),
+				bob_signature().to_vec()
+			),
+			Error::<Test>::InvalidSignature,
+		);
+
+		// Account with vesting
+		Balances::make_free_balance_be(&alice(), 100);
+		assert_ok!(<Test as Config>::VestingSchedule::add_vesting_schedule(&alice(), 100, 1, 50));
+		assert_noop!(
+			Purchase::create_account(
+				RuntimeOrigin::signed(validity_origin()),
+				alice(),
+				alice_signature().to_vec()
+			),
+			Error::<Test>::VestingScheduleExists,
+		);
+
+		// Duplicate Purchasing Account
+		assert_ok!(Purchase::create_account(
+			RuntimeOrigin::signed(validity_origin()),
+			bob(),
+			bob_signature().to_vec()
+		));
+		assert_noop!(
+			Purchase::create_account(
+				RuntimeOrigin::signed(validity_origin()),
+				bob(),
+				bob_signature().to_vec()
+			),
+			Error::<Test>::ExistingAccount,
+		);
+	});
+}
+
+#[test]
+fn update_validity_status_works() {
+	new_test_ext().execute_with(|| {
+		// Alice account is created.
+		assert_ok!(Purchase::create_account(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			alice_signature().to_vec(),
+		));
+		// She submits KYC, and we update the status to `Pending`.
+		assert_ok!(Purchase::update_validity_status(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			AccountValidity::Pending,
+		));
+		// KYC comes back negative, so we mark the account invalid.
+		assert_ok!(Purchase::update_validity_status(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			AccountValidity::Invalid,
+		));
+		assert_eq!(
+			Accounts::<Test>::get(alice()),
+			AccountStatus {
+				validity: AccountValidity::Invalid,
+				free_balance: Zero::zero(),
+				locked_balance: Zero::zero(),
+				signature: alice_signature().to_vec(),
+				vat: Permill::zero(),
+			}
+		);
+		// She fixes it, we mark her account valid.
+		assert_ok!(Purchase::update_validity_status(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			AccountValidity::ValidLow,
+		));
+		assert_eq!(
+			Accounts::<Test>::get(alice()),
+			AccountStatus {
+				validity: AccountValidity::ValidLow,
+				free_balance: Zero::zero(),
+				locked_balance: Zero::zero(),
+				signature: alice_signature().to_vec(),
+				vat: Permill::zero(),
+			}
+		);
+	});
+}
+
+#[test]
+fn update_validity_status_handles_basic_errors() {
+	new_test_ext().execute_with(|| {
+		// Wrong Origin
+		assert_noop!(
+			Purchase::update_validity_status(
+				RuntimeOrigin::signed(alice()),
+				alice(),
+				AccountValidity::Pending,
+			),
+			BadOrigin
+		);
+		// Inactive Account
+		assert_noop!(
+			Purchase::update_validity_status(
+				RuntimeOrigin::signed(validity_origin()),
+				alice(),
+				AccountValidity::Pending,
+			),
+			Error::<Test>::InvalidAccount
+		);
+		// Already Completed
+		assert_ok!(Purchase::create_account(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			alice_signature().to_vec(),
+		));
+		assert_ok!(Purchase::update_validity_status(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			AccountValidity::Completed,
+		));
+		assert_noop!(
+			Purchase::update_validity_status(
+				RuntimeOrigin::signed(validity_origin()),
+				alice(),
+				AccountValidity::Pending,
+			),
+			Error::<Test>::AlreadyCompleted
+		);
+	});
+}
+
+#[test]
+fn update_balance_works() {
+	new_test_ext().execute_with(|| {
+		// Alice account is created
+		assert_ok!(Purchase::create_account(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			alice_signature().to_vec()
+		));
+		// And approved for basic contribution
+		assert_ok!(Purchase::update_validity_status(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			AccountValidity::ValidLow,
+		));
+		// We set a balance on the user based on the payment they made. 50 locked, 50 free.
+		assert_ok!(Purchase::update_balance(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			50,
+			50,
+			Permill::from_rational(77u32, 1000u32),
+		));
+		assert_eq!(
+			Accounts::<Test>::get(alice()),
+			AccountStatus {
+				validity: AccountValidity::ValidLow,
+				free_balance: 50,
+				locked_balance: 50,
+				signature: alice_signature().to_vec(),
+				vat: Permill::from_parts(77000),
+			}
+		);
+		// We can update the balance based on new information.
+		assert_ok!(Purchase::update_balance(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			25,
+			50,
+			Permill::zero(),
+		));
+		assert_eq!(
+			Accounts::<Test>::get(alice()),
+			AccountStatus {
+				validity: AccountValidity::ValidLow,
+				free_balance: 25,
+				locked_balance: 50,
+				signature: alice_signature().to_vec(),
+				vat: Permill::zero(),
+			}
+		);
+	});
+}
+
+#[test]
+fn update_balance_handles_basic_errors() {
+	new_test_ext().execute_with(|| {
+		// Wrong Origin
+		assert_noop!(
+			Purchase::update_balance(
+				RuntimeOrigin::signed(alice()),
+				alice(),
+				50,
+				50,
+				Permill::zero(),
+			),
+			BadOrigin
+		);
+		// Inactive Account
+		assert_noop!(
+			Purchase::update_balance(
+				RuntimeOrigin::signed(validity_origin()),
+				alice(),
+				50,
+				50,
+				Permill::zero(),
+			),
+			Error::<Test>::InvalidAccount
+		);
+		// Overflow
+		assert_noop!(
+			Purchase::update_balance(
+				RuntimeOrigin::signed(validity_origin()),
+				alice(),
+				u64::MAX,
+				u64::MAX,
+				Permill::zero(),
+			),
+			Error::<Test>::InvalidAccount
+		);
+	});
+}
+
+#[test]
+fn payout_works() {
+	new_test_ext().execute_with(|| {
+		// Alice and Bob accounts are created
+		assert_ok!(Purchase::create_account(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			alice_signature().to_vec()
+		));
+		assert_ok!(Purchase::create_account(
+			RuntimeOrigin::signed(validity_origin()),
+			bob(),
+			bob_signature().to_vec()
+		));
+		// Alice is approved for basic contribution
+		assert_ok!(Purchase::update_validity_status(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			AccountValidity::ValidLow,
+		));
+		// Bob is approved for high contribution
+		assert_ok!(Purchase::update_validity_status(
+			RuntimeOrigin::signed(validity_origin()),
+			bob(),
+			AccountValidity::ValidHigh,
+		));
+		// We set a balance on the users based on the payment they made. 50 locked, 50 free.
+		assert_ok!(Purchase::update_balance(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			50,
+			50,
+			Permill::zero(),
+		));
+		assert_ok!(Purchase::update_balance(
+			RuntimeOrigin::signed(validity_origin()),
+			bob(),
+			100,
+			150,
+			Permill::zero(),
+		));
+		// Now we call payout for Alice and Bob.
+		assert_ok!(Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),));
+		assert_ok!(Purchase::payout(RuntimeOrigin::signed(payment_account()), bob(),));
+		// Payment is made.
+		assert_eq!(<Test as Config>::Currency::free_balance(&payment_account()), 99_650);
+		assert_eq!(<Test as Config>::Currency::free_balance(&alice()), 100);
+		// 10% of the 50 units is unlocked automatically for Alice
+		assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), Some(45));
+		assert_eq!(<Test as Config>::Currency::free_balance(&bob()), 250);
+		// A max of 10 units is unlocked automatically for Bob
+		assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), Some(140));
+		// Status is completed.
+		assert_eq!(
+			Accounts::<Test>::get(alice()),
+			AccountStatus {
+				validity: AccountValidity::Completed,
+				free_balance: 50,
+				locked_balance: 50,
+				signature: alice_signature().to_vec(),
+				vat: Permill::zero(),
+			}
+		);
+		assert_eq!(
+			Accounts::<Test>::get(bob()),
+			AccountStatus {
+				validity: AccountValidity::Completed,
+				free_balance: 100,
+				locked_balance: 150,
+				signature: bob_signature().to_vec(),
+				vat: Permill::zero(),
+			}
+		);
+		// Vesting lock is removed in whole on block 101 (100 blocks after block 1)
+		System::set_block_number(100);
+		let vest_call = RuntimeCall::Vesting(pallet_vesting::Call::<Test>::vest {});
+		assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(alice())));
+		assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(bob())));
+		assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), Some(45));
+		assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), Some(140));
+		System::set_block_number(101);
+		assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(alice())));
+		assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(bob())));
+		assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), None);
+		assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), None);
+	});
+}
+
+#[test]
+fn payout_handles_basic_errors() {
+	new_test_ext().execute_with(|| {
+		// Wrong Origin
+		assert_noop!(Purchase::payout(RuntimeOrigin::signed(alice()), alice(),), BadOrigin);
+		// Account with Existing Vesting Schedule
+		Balances::make_free_balance_be(&bob(), 100);
+		assert_ok!(<Test as Config>::VestingSchedule::add_vesting_schedule(&bob(), 100, 1, 50,));
+		assert_noop!(
+			Purchase::payout(RuntimeOrigin::signed(payment_account()), bob(),),
+			Error::<Test>::VestingScheduleExists
+		);
+		// Invalid Account (never created)
+		assert_noop!(
+			Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),),
+			Error::<Test>::InvalidAccount
+		);
+		// Invalid Account (created, but not valid)
+		assert_ok!(Purchase::create_account(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			alice_signature().to_vec()
+		));
+		assert_noop!(
+			Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),),
+			Error::<Test>::InvalidAccount
+		);
+		// Not enough funds in payment account
+		assert_ok!(Purchase::update_validity_status(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			AccountValidity::ValidHigh,
+		));
+		assert_ok!(Purchase::update_balance(
+			RuntimeOrigin::signed(validity_origin()),
+			alice(),
+			100_000,
+			100_000,
+			Permill::zero(),
+		));
+		assert_noop!(
+			Purchase::payout(RuntimeOrigin::signed(payment_account()), alice()),
+			ArithmeticError::Underflow
+		);
+	});
+}
+
+#[test]
+fn remove_pallet_works() {
+	new_test_ext().execute_with(|| {
+		let account_status = AccountStatus {
+			validity: AccountValidity::Completed,
+			free_balance: 1234,
+			locked_balance: 4321,
+			signature: b"my signature".to_vec(),
+			vat: Permill::from_percent(50),
+		};
+
+		// Add some storage.
+		Accounts::<Test>::insert(alice(), account_status.clone());
+		Accounts::<Test>::insert(bob(), account_status);
+		PaymentAccount::<Test>::put(alice());
+		Statement::<Test>::put(b"hello, world!".to_vec());
+		UnlockBlock::<Test>::put(4);
+
+		// Verify storage exists.
+		assert_eq!(Accounts::<Test>::iter().count(), 2);
+		assert!(PaymentAccount::<Test>::exists());
+		assert!(Statement::<Test>::exists());
+		assert!(UnlockBlock::<Test>::exists());
+
+		// Remove storage.
+		remove_pallet::<Test>();
+
+		// Verify storage is gone.
+		assert_eq!(Accounts::<Test>::iter().count(), 0);
+		assert!(!PaymentAccount::<Test>::exists());
+		assert!(!Statement::<Test>::exists());
+		assert!(!UnlockBlock::<Test>::exists());
+	});
+}
-- 
GitLab