#![cfg_attr(not(feature = "std"), no_std)]
pub use pallet::*;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod types;
pub mod weights;
#[frame_support::pallet]
pub mod pallet {
pub use crate::{
types::{DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, ScheduledTask, Task},
weights::WeightInfo,
};
use frame_support::{
dispatch::DispatchResultWithPostInfo,
fail,
pallet_prelude::*,
require_transactional,
storage::bounded_btree_map::BoundedBTreeMap,
traits::{tokens::BalanceStatus, ExistenceRequirement},
};
use frame_system::pallet_prelude::*;
use orml_traits::{MultiCurrency, MultiReservableCurrency};
use sp_runtime::{
traits::{CheckedAdd, Saturating},
Percent,
};
use sp_std::vec::Vec;
pub type BalanceOf<T> = <<T as Config>::Asset as MultiCurrency<<T as frame_system::Config>::AccountId>>::Balance;
pub type AssetIdOf<T> = <<T as Config>::Asset as MultiCurrency<<T as frame_system::Config>::AccountId>>::CurrencyId;
pub type BoundedDataOf<T> = BoundedVec<u8, <T as Config>::MaxRemarkLength>;
pub type ScheduledTaskOf<T> = ScheduledTask<BlockNumberFor<T>>;
pub type ScheduledTaskList<T> = BoundedBTreeMap<
(
<T as frame_system::Config>::AccountId,
<T as frame_system::Config>::AccountId,
),
ScheduledTaskOf<T>,
<T as Config>::MaxRemarkLength,
>;
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Asset: MultiReservableCurrency<Self::AccountId>;
type DisputeResolver: DisputeResolver<Self::AccountId>;
type FeeHandler: FeeHandler<Self>;
#[pallet::constant]
type IncentivePercentage: Get<Percent>;
#[pallet::constant]
type MaxRemarkLength: Get<u32>;
#[pallet::constant]
type CancelBufferBlockLength: Get<BlockNumberFor<Self>>;
#[pallet::constant]
type MaxScheduledTaskListLength: Get<u32>;
type WeightInfo: WeightInfo;
}
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::storage]
#[pallet::getter(fn payment)]
pub(super) type Payment<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId, Blake2_128Concat,
T::AccountId, PaymentDetail<T>,
>;
#[pallet::storage]
#[pallet::getter(fn tasks)]
pub(super) type ScheduledTasks<T: Config> = StorageValue<_, ScheduledTaskList<T>, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
PaymentCreated {
from: T::AccountId,
asset: AssetIdOf<T>,
amount: BalanceOf<T>,
remark: Option<BoundedDataOf<T>>,
},
PaymentReleased { from: T::AccountId, to: T::AccountId },
PaymentCancelled { from: T::AccountId, to: T::AccountId },
PaymentResolved {
from: T::AccountId,
to: T::AccountId,
recipient_share: Percent,
},
PaymentCreatorRequestedRefund {
from: T::AccountId,
to: T::AccountId,
expiry: BlockNumberFor<T>,
},
PaymentRefundDisputed { from: T::AccountId, to: T::AccountId },
PaymentRequestCreated { from: T::AccountId, to: T::AccountId },
PaymentRequestCompleted { from: T::AccountId, to: T::AccountId },
}
#[pallet::error]
pub enum Error<T> {
InvalidPayment,
PaymentAlreadyReleased,
PaymentAlreadyInProcess,
InvalidAction,
PaymentNeedsReview,
MathError,
RefundNotRequested,
DisputePeriodNotPassed,
RefundQueueFull,
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_idle(now: BlockNumberFor<T>, remaining_weight: Weight) -> Weight {
const MAX_TASKS_TO_PROCESS: usize = 5;
let mut used_weight = T::WeightInfo::remove_task();
let cancel_weight = T::WeightInfo::cancel();
let possible_task_count: usize = remaining_weight
.saturating_sub(used_weight)
.saturating_div(cancel_weight.ref_time())
.ref_time()
.try_into()
.unwrap_or(MAX_TASKS_TO_PROCESS);
ScheduledTasks::<T>::mutate(|tasks| {
let mut task_list: Vec<_> = tasks
.clone()
.into_iter()
.take(possible_task_count)
.filter(|(_, ScheduledTask { when, task })| when <= &now && matches!(task, Task::Cancel))
.collect();
task_list.sort_by(|(_, t), (_, x)| x.when.cmp(&t.when));
while !task_list.is_empty() && used_weight.all_lte(remaining_weight) {
if let Some((account_pair, _)) = task_list.pop() {
used_weight = used_weight.saturating_add(cancel_weight);
tasks.remove(&account_pair);
if <Self as PaymentHandler<T>>::settle_payment(
&account_pair.0,
&account_pair.1,
Percent::from_percent(0),
)
.is_err()
{
log::warn!(
target: "runtime::payments",
"Warning: Unable to process payment refund!"
);
} else {
Self::deposit_event(Event::PaymentCancelled {
from: account_pair.0,
to: account_pair.1,
});
}
}
}
});
used_weight
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::pay(T::MaxRemarkLength::get()))]
pub fn pay(
origin: OriginFor<T>,
recipient: T::AccountId,
asset: AssetIdOf<T>,
#[pallet::compact] amount: BalanceOf<T>,
remark: Option<BoundedDataOf<T>>,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let payment_detail = <Self as PaymentHandler<T>>::create_payment(
&who,
&recipient,
asset,
amount,
PaymentState::Created,
T::IncentivePercentage::get(),
remark.as_ref().map(|x| x.as_slice()),
)?;
<Self as PaymentHandler<T>>::reserve_payment_amount(&who, &recipient, payment_detail)?;
Self::deposit_event(Event::PaymentCreated {
from: who,
asset,
amount,
remark,
});
Ok(().into())
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::release())]
pub fn release(origin: OriginFor<T>, to: T::AccountId) -> DispatchResultWithPostInfo {
let from = ensure_signed(origin)?;
let payment = Payment::<T>::get(&from, &to).ok_or(Error::<T>::InvalidPayment)?;
ensure!(payment.state == PaymentState::Created, Error::<T>::InvalidAction);
<Self as PaymentHandler<T>>::settle_payment(&from, &to, Percent::from_percent(100))?;
Self::deposit_event(Event::PaymentReleased { from, to });
Ok(().into())
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::cancel())]
pub fn cancel(origin: OriginFor<T>, creator: T::AccountId) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
if let Some(payment) = Payment::<T>::get(&creator, &who) {
match payment.state {
PaymentState::Created => {
<Self as PaymentHandler<T>>::settle_payment(&creator, &who, Percent::from_percent(0))?;
Self::deposit_event(Event::PaymentCancelled { from: creator, to: who });
}
PaymentState::PaymentRequested => Payment::<T>::remove(&creator, &who),
_ => fail!(Error::<T>::InvalidAction),
}
}
Ok(().into())
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::resolve_payment())]
pub fn resolve_payment(
origin: OriginFor<T>,
from: T::AccountId,
recipient: T::AccountId,
recipient_share: Percent,
) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let account_pair = (from, recipient);
if let Some(payment) = Payment::<T>::get(&account_pair.0, &account_pair.1) {
ensure!(who == payment.resolver_account, Error::<T>::InvalidAction);
ensure!(
payment.state != PaymentState::PaymentRequested,
Error::<T>::InvalidAction
);
if matches!(payment.state, PaymentState::RefundRequested { .. }) {
ScheduledTasks::<T>::mutate(|tasks| {
tasks.remove(&account_pair);
})
}
}
<Self as PaymentHandler<T>>::settle_payment(&account_pair.0, &account_pair.1, recipient_share)?;
Self::deposit_event(Event::PaymentResolved {
from: account_pair.0,
to: account_pair.1,
recipient_share,
});
Ok(().into())
}
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::request_refund())]
pub fn request_refund(origin: OriginFor<T>, recipient: T::AccountId) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
Payment::<T>::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult {
let payment = maybe_payment.as_mut().ok_or(Error::<T>::InvalidPayment)?;
ensure!(payment.state == PaymentState::Created, Error::<T>::InvalidAction);
let current_block = frame_system::Pallet::<T>::block_number();
let cancel_block = current_block
.checked_add(&T::CancelBufferBlockLength::get())
.ok_or(Error::<T>::MathError)?;
ScheduledTasks::<T>::try_mutate(|task_list| -> DispatchResult {
task_list
.try_insert(
(who.clone(), recipient.clone()),
ScheduledTask {
task: Task::Cancel,
when: cancel_block,
},
)
.map_err(|_| Error::<T>::RefundQueueFull)?;
Ok(())
})?;
payment.state = PaymentState::RefundRequested { cancel_block };
Self::deposit_event(Event::PaymentCreatorRequestedRefund {
from: who,
to: recipient,
expiry: cancel_block,
});
Ok(())
})?;
Ok(().into())
}
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::dispute_refund())]
pub fn dispute_refund(origin: OriginFor<T>, creator: T::AccountId) -> DispatchResultWithPostInfo {
use PaymentState::*;
let who = ensure_signed(origin)?;
Payment::<T>::try_mutate(
creator.clone(),
who.clone(), |maybe_payment| -> DispatchResult {
let payment = maybe_payment.as_mut().ok_or(Error::<T>::InvalidPayment)?;
match payment.state {
RefundRequested { cancel_block } => {
ensure!(
cancel_block > frame_system::Pallet::<T>::block_number(),
Error::<T>::InvalidAction
);
payment.state = PaymentState::NeedsReview;
ScheduledTasks::<T>::try_mutate(|task_list| -> DispatchResult {
task_list
.remove(&(creator.clone(), who.clone()))
.ok_or(Error::<T>::InvalidAction)?;
Ok(())
})?;
Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who });
}
_ => fail!(Error::<T>::InvalidAction),
}
Ok(())
},
)?;
Ok(().into())
}
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::request_payment())]
pub fn request_payment(
origin: OriginFor<T>,
from: T::AccountId,
asset: AssetIdOf<T>,
#[pallet::compact] amount: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let to = ensure_signed(origin)?;
<Self as PaymentHandler<T>>::create_payment(
&from,
&to,
asset,
amount,
PaymentState::PaymentRequested,
Percent::from_percent(0),
None,
)?;
Self::deposit_event(Event::PaymentRequestCreated { from, to });
Ok(().into())
}
#[pallet::call_index(7)]
#[pallet::weight(T::WeightInfo::accept_and_pay())]
pub fn accept_and_pay(origin: OriginFor<T>, to: T::AccountId) -> DispatchResultWithPostInfo {
let from = ensure_signed(origin)?;
let payment = Payment::<T>::get(&from, &to).ok_or(Error::<T>::InvalidPayment)?;
ensure!(
payment.state == PaymentState::PaymentRequested,
Error::<T>::InvalidAction
);
<Self as PaymentHandler<T>>::reserve_payment_amount(&from, &to, payment)?;
<Self as PaymentHandler<T>>::settle_payment(&from, &to, Percent::from_percent(100))?;
Self::deposit_event(Event::PaymentRequestCompleted { from, to });
Ok(().into())
}
}
impl<T: Config> PaymentHandler<T> for Pallet<T> {
#[require_transactional]
fn create_payment(
from: &T::AccountId,
recipient: &T::AccountId,
asset: AssetIdOf<T>,
amount: BalanceOf<T>,
payment_state: PaymentState<T>,
incentive_percentage: Percent,
remark: Option<&[u8]>,
) -> Result<PaymentDetail<T>, sp_runtime::DispatchError> {
Payment::<T>::try_mutate(
from,
recipient,
|maybe_payment| -> Result<PaymentDetail<T>, sp_runtime::DispatchError> {
if let Some(payment) = maybe_payment {
ensure!(
payment.state == PaymentState::PaymentRequested,
Error::<T>::PaymentAlreadyInProcess
);
}
let incentive_amount = incentive_percentage.mul_floor(amount);
let mut new_payment = PaymentDetail {
asset,
amount,
incentive_amount,
state: payment_state,
resolver_account: T::DisputeResolver::get_resolver_account(),
fee_detail: None,
};
let (fee_recipient, fee_percent) = T::FeeHandler::apply_fees(from, recipient, &new_payment, remark);
let fee_amount = fee_percent.mul_floor(amount);
new_payment.fee_detail = Some((fee_recipient, fee_amount));
*maybe_payment = Some(new_payment.clone());
Ok(new_payment)
},
)
}
#[require_transactional]
fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail<T>) -> DispatchResult {
let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or_else(|| 0u32.into());
let total_fee_amount = payment.incentive_amount.saturating_add(fee_amount);
let total_amount = total_fee_amount.saturating_add(payment.amount);
T::Asset::reserve(payment.asset, from, total_amount)?;
T::Asset::repatriate_reserved(payment.asset, from, to, payment.amount, BalanceStatus::Reserved)?;
Ok(())
}
fn settle_payment(from: &T::AccountId, to: &T::AccountId, recipient_share: Percent) -> DispatchResult {
Payment::<T>::try_mutate(from, to, |maybe_payment| -> DispatchResult {
let payment = maybe_payment.take().ok_or(Error::<T>::InvalidPayment)?;
match payment.fee_detail {
Some((fee_recipient, fee_amount)) => {
T::Asset::unreserve(payment.asset, from, payment.incentive_amount.saturating_add(fee_amount));
if recipient_share != Percent::zero() {
T::Asset::transfer(
payment.asset,
from, &fee_recipient, fee_amount, ExistenceRequirement::AllowDeath,
)?;
}
}
None => {
T::Asset::unreserve(payment.asset, from, payment.incentive_amount);
}
};
T::Asset::unreserve(payment.asset, to, payment.amount);
let amount_to_recipient = recipient_share.mul_floor(payment.amount);
let amount_to_sender = payment.amount.saturating_sub(amount_to_recipient);
T::Asset::transfer(
payment.asset,
to,
from,
amount_to_sender,
ExistenceRequirement::AllowDeath,
)?;
Ok(())
})?;
Ok(())
}
fn get_payment_details(from: &T::AccountId, to: &T::AccountId) -> Option<PaymentDetail<T>> {
Payment::<T>::get(from, to)
}
}
}