Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Support multiple fee recipients #18

Merged
merged 5 commits into from
May 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions payments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,18 @@ pub struct PaymentDetail<T: pallet::Config> {
/// type of asset used for payment
pub asset: AssetIdOf<T>,
/// amount of asset used for payment
#[codec(compact)]
pub amount: BalanceOf<T>,
/// incentive amount that is credited to creator for resolving
#[codec(compact)]
pub incentive_amount: BalanceOf<T>,
/// enum to track payment lifecycle [Created, NeedsReview]
pub state: PaymentState<T::BlockNumber>,
/// enum to track payment lifecycle [Created, NeedsReview, RefundRequested,
/// Requested]
pub state: PaymentState<T>,
/// account that can settle any disputes created in the payment
pub resolver_account: T::AccountId,
/// fee charged and recipient account details
pub fee_detail: Option<(T::AccountId, BalanceOf<T>)>,
/// remarks to give context to payment
pub remark: Option<BoundedDataOf<T>>,
pub fee_detail: Option<FeeRecipientList<T>>,
}
```

Expand All @@ -78,6 +79,37 @@ pub enum PaymentState<BlockNumber> {
}
```

The `FeeHandler` trait lets the implementation specify how much fees to charge for a payment and to which account it should be credited.
The below example charges a 10% fee for every payment and distributes it evenly to two accounts.

```rust
pub struct MockFeeHandler;
impl orml_payment::types::FeeHandler<T> for MockFeeHandler {
fn apply_fees(
_from: &AccountId,
_to: &AccountId,
_detail: &PaymentDetail<Test>,
_remark: Option<&[u8]>,
) -> FeeRecipientShareList<Test> {
pub const MARKETPLACE_FEE_PERCENTAGE: u8 = 10;
let mut fee_recipient_share_list: FeeRecipientShareList<Test> = Default::default();
fee_recipient_share_list
.try_push(FeeRecipientShare {
account_id: FEE_RECIPIENT_ACCOUNT,
percent_of_fees: Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE / 2),
}).unwrap();

fee_recipient_share_list
.try_push(FeeRecipientShare {
account_id: SECOND_FEE_RECIPIENT_ACCOUNT,
percent_of_fees: Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE / 2),
}).unwrap();

fee_recipient_share_list
}
}
```

## GenesisConfig

The rates_provider pallet does not depend on the `GenesisConfig`
Expand Down
60 changes: 46 additions & 14 deletions payments/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,18 @@ pub mod weights;
#[frame_support::pallet]
pub mod pallet {
pub use crate::{
types::{DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, ScheduledTask, Task},
types::{
calculate_fee_amount, DisputeResolver, FeeHandler, FeeRecipient, FeeRecipientShare, 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, transactional,
};
use frame_system::pallet_prelude::*;
use orml_traits::{LockIdentifier, MultiCurrency, NamedMultiReservableCurrency};
use orml_traits::{arithmetic::Zero, LockIdentifier, MultiCurrency, NamedMultiReservableCurrency};
use sp_runtime::{
traits::{CheckedAdd, Saturating},
Percent,
Expand All @@ -99,6 +102,12 @@ pub mod pallet {
ScheduledTaskOf<T>,
<T as Config>::MaxRemarkLength,
>;
pub type FeeRecipientList<T> = BoundedVec<
FeeRecipient<<T as frame_system::Config>::AccountId, BalanceOf<T>>,
<T as Config>::FeeRecipientLimit,
>;
pub type FeeRecipientShareList<T> =
BoundedVec<FeeRecipientShare<<T as frame_system::Config>::AccountId>, <T as Config>::FeeRecipientLimit>;

#[pallet::config]
pub trait Config: frame_system::Config {
Expand All @@ -125,6 +134,9 @@ pub mod pallet {
/// canceled payment
#[pallet::constant]
type MaxScheduledTaskListLength: Get<u32>;
/// The maximum fee recipient accounts that be specified
#[pallet::constant]
type FeeRecipientLimit: Get<u32>;
//// Type representing the weight of this pallet
type WeightInfo: WeightInfo;
}
Expand Down Expand Up @@ -583,10 +595,20 @@ pub mod pallet {

// Calculate fee amount - this will be implemented based on the custom
// implementation of the fee provider
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));

let recipients = T::FeeHandler::apply_fees(from, recipient, &new_payment, remark);

let fee_recipient_list: FeeRecipientList<T> = recipients
.iter()
.map(|fee_recipient| FeeRecipient {
account_id: fee_recipient.account_id.clone(),
fee_amount: fee_recipient.percent_of_fees.mul_floor(amount),
})
.collect::<Vec<_>>()
.try_into()
// this should not happen since max length of both vecs are FeeRecipientLimit
.expect("recipients should not exceed limit; qed");

new_payment.fee_detail = Some(fee_recipient_list);
*maybe_payment = Some(new_payment.clone());

// increment provider to prevent sender data from getting reaped
Expand All @@ -602,7 +624,11 @@ pub mod pallet {
/// the recipient but will stay in Reserve state.
#[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());
// calculate total fee amount
let fee_amount = match payment.fee_detail {
Some(recipient_list) => calculate_fee_amount::<T>(&recipient_list),
None => Zero::zero(),
};

let total_fee_amount = payment.incentive_amount.saturating_add(fee_amount);
let total_amount = total_fee_amount.saturating_add(payment.amount);
Expand Down Expand Up @@ -634,21 +660,27 @@ pub mod pallet {

// unreserve the incentive amount and fees from the owner account
match payment.fee_detail {
Some((fee_recipient, fee_amount)) => {
Some(recipient_list) => {
// calculate total fee amount
let fee_amount = calculate_fee_amount::<T>(&recipient_list);

T::Asset::unreserve_named(
&PALLET_RESERVE_ID,
payment.asset,
from,
payment.incentive_amount + fee_amount,
);

// transfer fee to marketplace if operation is not cancel
if recipient_share != Percent::zero() {
T::Asset::transfer(
payment.asset,
from, // fee is paid by payment creator
&fee_recipient, // account of fee recipient
fee_amount, // amount of fee
)?;
for fee_recipient in recipient_list {
T::Asset::transfer(
payment.asset,
from, // fee is paid by payment creator
&fee_recipient.account_id, // account of fee recipient
fee_recipient.fee_amount, // amount of fee
)?;
}
}
}
None => {
Expand Down
44 changes: 39 additions & 5 deletions payments/src/mock.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate as payment;
use crate::PaymentDetail;
use crate::*;
use frame_support::{
parameter_types,
traits::{ConstU32, Contains, Everything, GenesisBuild, Hooks, OnFinalize},
Expand Down Expand Up @@ -27,6 +27,8 @@ pub const CURRENCY_ID: u32 = 1;
pub const RESOLVER_ACCOUNT: AccountId = 12;
pub const FEE_RECIPIENT_ACCOUNT: AccountId = 20;
pub const PAYMENT_RECIPENT_FEE_CHARGED: AccountId = 21;
pub const PAYMENT_RECIPENT_MULTIPLE_FEE_CHARGED: AccountId = 22;
pub const SECOND_FEE_RECIPIENT_ACCOUNT: AccountId = 23;
pub const INCENTIVE_PERCENTAGE: u8 = 10;
pub const MARKETPLACE_FEE_PERCENTAGE: u8 = 10;
pub const CANCEL_BLOCK_BUFFER: u64 = 600;
Expand Down Expand Up @@ -120,11 +122,41 @@ impl crate::types::FeeHandler<Test> for MockFeeHandler {
to: &AccountId,
_detail: &PaymentDetail<Test>,
_remark: Option<&[u8]>,
) -> (AccountId, Percent) {
) -> FeeRecipientShareList<Test> {
let mut fee_recipient_share_list: FeeRecipientShareList<Test> = Default::default();
match to {
&PAYMENT_RECIPENT_FEE_CHARGED => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE)),
_ => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(0)),
}
// to test a single fee recipient
&PAYMENT_RECIPENT_FEE_CHARGED => fee_recipient_share_list
.try_push(FeeRecipientShare {
account_id: FEE_RECIPIENT_ACCOUNT,
percent_of_fees: Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE),
})
.unwrap(),
// to test multiple fee recipients, both recipient receive 50% of fee
&PAYMENT_RECIPENT_MULTIPLE_FEE_CHARGED => {
fee_recipient_share_list
.try_push(FeeRecipientShare {
account_id: FEE_RECIPIENT_ACCOUNT,
percent_of_fees: Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE / 2),
})
.unwrap();

fee_recipient_share_list
.try_push(FeeRecipientShare {
account_id: SECOND_FEE_RECIPIENT_ACCOUNT,
percent_of_fees: Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE / 2),
})
.unwrap();
}
// to test no fee charged
_ => fee_recipient_share_list
.try_push(FeeRecipientShare {
account_id: FEE_RECIPIENT_ACCOUNT,
percent_of_fees: Percent::from_percent(0),
})
.unwrap(),
};
fee_recipient_share_list
}
}

Expand All @@ -133,6 +165,7 @@ parameter_types! {
pub const MaxRemarkLength: u32 = 50;
pub const CancelBufferBlockLength: u64 = CANCEL_BLOCK_BUFFER;
pub const MaxScheduledTaskListLength : u32 = 5;
pub const FeeRecipientLimit : u32 = 3;
}

impl payment::Config for Test {
Expand All @@ -144,6 +177,7 @@ impl payment::Config for Test {
type MaxRemarkLength = MaxRemarkLength;
type CancelBufferBlockLength = CancelBufferBlockLength;
type MaxScheduledTaskListLength = MaxScheduledTaskListLength;
type FeeRecipientLimit = FeeRecipientLimit;
type WeightInfo = ();
}

Expand Down
Loading