From d044689fcd4fe785092323404a7201227a33f584 Mon Sep 17 00:00:00 2001 From: Daniel Olano Date: Tue, 8 Feb 2022 10:23:49 +0100 Subject: [PATCH 01/28] Create payments pallet crate --- payments/Cargo.toml | 8 +++++ payments/README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++ payments/src/lib.rs | 8 +++++ 3 files changed, 98 insertions(+) create mode 100644 payments/Cargo.toml create mode 100644 payments/README.md create mode 100644 payments/src/lib.rs diff --git a/payments/Cargo.toml b/payments/Cargo.toml new file mode 100644 index 000000000..5fe769fa8 --- /dev/null +++ b/payments/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "payments" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/payments/README.md b/payments/README.md new file mode 100644 index 000000000..94fa4ab70 --- /dev/null +++ b/payments/README.md @@ -0,0 +1,82 @@ +# Payments Pallet + +This pallet allows users to create secure reversible payments that keep funds locked in a merchant's account until the off-chain goods are confirmed to be received. Each payment gets assigned its own *judge* that can help resolve any disputes between the two parties. + +## Terminology + +- Created: A payment has been created and the amount arrived to its destination but it's locked. +- NeedsReview: The payment has bee disputed and is awaiting settlement by a judge. +- IncentivePercentage: A small share of the payment amount is held in escrow until a payment is completed/cancelled. The Incentive Percentage represents this value. +- Resolver Account: A resolver account is assigned to every payment created, this account has the privilege to cancel/release a payment that has been disputed. +- Remark: The pallet allows to create payments by optionally providing some extra(limited) amount of bytes, this is reffered to as Remark. This can be used by a marketplace to seperate/tag payments. +- CancelBufferBlockLength: This is the time window where the recipient can dispute a cancellation request from the payment creator. + +## Interface + +#### Events + +- `PaymentCreated { from: T::AccountId, asset: AssetIdOf, amount: BalanceOf },`, +- `PaymentReleased { from: T::AccountId, to: T::AccountId }`, +- `PaymentCancelled { from: T::AccountId, to: T::AccountId }`, +- `PaymentCreatorRequestedRefund { from: T::AccountId, to: T::AccountId, expiry: T::BlockNumber}` +- `PaymentRefundDisputed { from: T::AccountId, to: T::AccountId }` + +#### Extrinsics + +- `pay` - Create an payment for the given currencyid/amount +- `pay_with_remark` - Create a payment with a remark, can be used to tag payments +- `release` - Release the payment amount to recipent +- `cancel` - Allows the recipient to cancel the payment and release the payment amount to creator +- `resolve_release_payment` - Allows assigned judge to release a payment +- `resolve_cancel_payment` - Allows assigned judge to cancel a payment +- `request_refund` - Allows the creator of the payment to trigger cancel with a buffer time. +- `claim_refund` - Allows the creator to claim payment refund after buffer time +- `dispute_refund` - Allows the recipient to dispute the payment request of sender + +## Implementations + +The RatesProvider module provides implementations for the following traits. +- [`PaymentHandler`](./src/types.rs) + +## Types + +The `PaymentDetail` struct stores information about the payment/escrow. A "payment" in virto network is similar to an escrow, it is used to guarantee proof of funds and can be released once an agreed upon condition has reached between the payment creator and recipient. The payment lifecycle is tracked using the state field. + +```rust +pub struct PaymentDetail { + /// type of asset used for payment + pub asset: AssetIdOf, + /// amount of asset used for payment + pub amount: BalanceOf, + /// incentive amount that is credited to creator for resolving + pub incentive_amount: BalanceOf, + /// enum to track payment lifecycle [Created, NeedsReview] + pub state: PaymentState, + /// 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)>, + /// remarks to give context to payment + pub remark: Option>, +} +``` + +The `PaymentState` enum tracks the possible states that a payment can be in. When a payment is 'completed' or 'cancelled' it is removed from storage and hence not tracked by a state. + +```rust +pub enum PaymentState { + /// Amounts have been reserved and waiting for release/cancel + Created, + /// A judge needs to review and release manually + NeedsReview, + /// The user has requested refund and will be processed by `BlockNumber` + RefundRequested(BlockNumber), +} +``` + +## GenesisConfig + +The rates_provider pallet does not depend on the `GenesisConfig` + +License: Apache-2.0 + diff --git a/payments/src/lib.rs b/payments/src/lib.rs new file mode 100644 index 000000000..bcab31d68 --- /dev/null +++ b/payments/src/lib.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} From 4f49fa2814d806007e02075d05e068948cf745fb Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sun, 20 Feb 2022 21:07:54 +0400 Subject: [PATCH 02/28] sync current version --- payments/Cargo.toml | 45 +- payments/README.md | 5 +- payments/src/lib.rs | 580 ++++++++++++++++++++- payments/src/mock.rs | 156 ++++++ payments/src/tests.rs | 1059 +++++++++++++++++++++++++++++++++++++++ payments/src/types.rs | 96 ++++ payments/src/weights.rs | 133 +++++ 7 files changed, 2064 insertions(+), 10 deletions(-) create mode 100644 payments/src/mock.rs create mode 100644 payments/src/tests.rs create mode 100644 payments/src/types.rs create mode 100644 payments/src/weights.rs diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 5fe769fa8..89352c823 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -1,8 +1,45 @@ [package] -name = "payments" -version = "0.1.0" -edition = "2021" +authors = ["Virto Network "] +edition = '2021' +name = "orml-payments" +version = "0.4.1-dev" +license = "Apache-2.0" +homepage = "https://github.com/virto-network/virto-node" +repository = "https://github.com/virto-network/virto-node" +description = "Allows users to post payment on-chain" +readme = "README.md" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] [dependencies] +parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false, optional = true } +orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false } +scale-info = { version = "1.0.0", default-features = false, features = ["derive"] } + +[dev-dependencies] +serde = { version = "1.0.101" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library" } + +[features] +default = ['std'] +std = [ + 'parity-scale-codec/std', + 'frame-support/std', + 'frame-system/std', + 'sp-runtime/std', + 'sp-std/std', + 'scale-info/std', + 'orml-traits/std', + 'frame-benchmarking/std', +] +runtime-benchmarks = [ + "frame-benchmarking", +] diff --git a/payments/README.md b/payments/README.md index 94fa4ab70..e28fbf26c 100644 --- a/payments/README.md +++ b/payments/README.md @@ -20,6 +20,8 @@ This pallet allows users to create secure reversible payments that keep funds lo - `PaymentCancelled { from: T::AccountId, to: T::AccountId }`, - `PaymentCreatorRequestedRefund { from: T::AccountId, to: T::AccountId, expiry: T::BlockNumber}` - `PaymentRefundDisputed { from: T::AccountId, to: T::AccountId }` +- `PaymentRequestCreated { from: T::AccountId, to: T::AccountId }` +- `PaymentRequestCompleted { from: T::AccountId, to: T::AccountId }` #### Extrinsics @@ -32,6 +34,8 @@ This pallet allows users to create secure reversible payments that keep funds lo - `request_refund` - Allows the creator of the payment to trigger cancel with a buffer time. - `claim_refund` - Allows the creator to claim payment refund after buffer time - `dispute_refund` - Allows the recipient to dispute the payment request of sender +- `request_payment` - Create a payment that can be completed by the sender using the `accept_and_pay` extrinsic. +- `accept_and_pay` - Allows the sender to fulfill a payment request created by a recipient ## Implementations @@ -79,4 +83,3 @@ pub enum PaymentState { The rates_provider pallet does not depend on the `GenesisConfig` License: Apache-2.0 - diff --git a/payments/src/lib.rs b/payments/src/lib.rs index bcab31d68..0d0210e67 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -1,8 +1,578 @@ +#![allow(clippy::unused_unit, unused_qualifications, missing_debug_implementations)] +#![cfg_attr(not(feature = "std"), no_std)] +pub use pallet::*; + #[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); +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}, + weights::WeightInfo, + }; + use frame_support::{ + dispatch::DispatchResultWithPostInfo, fail, pallet_prelude::*, require_transactional, + traits::tokens::BalanceStatus, transactional, + }; + use frame_system::pallet_prelude::*; + use orml_traits::{MultiCurrency, MultiReservableCurrency}; + use sp_runtime::{ + traits::{CheckedAdd, Saturating}, + Percent, + }; + + pub type BalanceOf = <::Asset as MultiCurrency<::AccountId>>::Balance; + pub type AssetIdOf = <::Asset as MultiCurrency<::AccountId>>::CurrencyId; + pub type BoundedDataOf = BoundedVec::MaxRemarkLength>; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Because this pallet emits events, it depends on the runtime's + /// definition of an event. + type Event: From> + IsType<::Event>; + /// the type of assets this pallet can hold in payment + type Asset: MultiReservableCurrency; + /// Dispute resolution account + type DisputeResolver: DisputeResolver; + /// Fee handler trait + type FeeHandler: FeeHandler; + /// Incentive percentage - amount witheld from sender + #[pallet::constant] + type IncentivePercentage: Get; + /// Maximum permitted size of `Remark` + #[pallet::constant] + type MaxRemarkLength: Get; + /// Buffer period - number of blocks to wait before user can claim + /// canceled payment + #[pallet::constant] + type CancelBufferBlockLength: Get; + //// Type representing the weight of this pallet + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + #[pallet::getter(fn rates)] + /// Payments created by a user, this method of storageDoubleMap is chosen + /// since there is no usecase for listing payments by provider/currency. The + /// payment will only be referenced by the creator in any transaction of + /// interest. The storage map keys are the creator and the recipient, this + /// also ensures that for any (sender,recipient) combo, only a single + /// payment is active. The history of payment is not stored. + pub(super) type Payment = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // payment creator + Blake2_128Concat, + T::AccountId, // payment recipient + PaymentDetail, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new payment has been created + PaymentCreated { + from: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + }, + /// Payment amount released to the recipient + PaymentReleased { from: T::AccountId, to: T::AccountId }, + /// Payment has been cancelled by the creator + PaymentCancelled { from: T::AccountId, to: T::AccountId }, + /// the payment creator has created a refund request + PaymentCreatorRequestedRefund { + from: T::AccountId, + to: T::AccountId, + expiry: T::BlockNumber, + }, + /// the refund request from creator was disputed by recipient + PaymentRefundDisputed { from: T::AccountId, to: T::AccountId }, + /// Payment request was created by recipient + PaymentRequestCreated { from: T::AccountId, to: T::AccountId }, + /// Payment request was completed by sender + PaymentRequestCompleted { from: T::AccountId, to: T::AccountId }, + } + + #[pallet::error] + pub enum Error { + /// The selected payment does not exist + InvalidPayment, + /// The selected payment cannot be released + PaymentAlreadyReleased, + /// The selected payment already exists and is in process + PaymentAlreadyInProcess, + /// Action permitted only for whitelisted users + InvalidAction, + /// Payment is in review state and cannot be modified + PaymentNeedsReview, + /// Unexpeted math error + MathError, + /// Payment request has not been created + RefundNotRequested, + /// Dispute period has not passed + DisputePeriodNotPassed, + } + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::call] + impl Pallet { + /// This allows any user to create a new payment, that releases only to + /// specified recipient The only action is to store the details of this + /// payment in storage and reserve the specified amount. + #[transactional] + #[pallet::weight(T::WeightInfo::pay())] + pub fn pay( + origin: OriginFor, + recipient: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + // create PaymentDetail and add to storage + let payment_detail = >::create_payment( + who.clone(), + recipient.clone(), + asset, + amount, + PaymentState::Created, + T::IncentivePercentage::get(), + None, + )?; + // reserve funds for payment + >::reserve_payment_amount(&who, &recipient, payment_detail)?; + // emit paymentcreated event + Self::deposit_event(Event::PaymentCreated { + from: who, + asset, + amount, + }); + Ok(().into()) + } + + /// This allows any user to create a new payment with the option to add + /// a remark, this remark can then be used to run custom logic and + /// trigger alternate payment flows. the specified amount. + #[transactional] + #[pallet::weight(T::WeightInfo::pay_with_remark(remark.len().try_into().unwrap_or(T::MaxRemarkLength::get())))] + pub fn pay_with_remark( + origin: OriginFor, + recipient: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + remark: BoundedDataOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + // create PaymentDetail and add to storage + let payment_detail = >::create_payment( + who.clone(), + recipient.clone(), + asset, + amount, + PaymentState::Created, + T::IncentivePercentage::get(), + Some(remark), + )?; + // reserve funds for payment + >::reserve_payment_amount(&who, &recipient, payment_detail)?; + // emit paymentcreated event + Self::deposit_event(Event::PaymentCreated { + from: who, + asset, + amount, + }); + Ok(().into()) + } + + /// Release any created payment, this will transfer the reserved amount + /// from the creator of the payment to the assigned recipient + #[transactional] + #[pallet::weight(T::WeightInfo::release())] + pub fn release(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { + let from = ensure_signed(origin)?; + + // ensure the payment is in Created state + if let Some(payment) = Payment::::get(from.clone(), to.clone()) { + ensure!(payment.state == PaymentState::Created, Error::::InvalidAction) + } + + // release is a settle_payment with 100% recipient_share + >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + + Self::deposit_event(Event::PaymentReleased { from, to }); + Ok(().into()) + } + + /// Cancel a payment in created state, this will release the reserved + /// back to creator of the payment. This extrinsic can only be called by + /// the recipient of the payment + #[transactional] + #[pallet::weight(T::WeightInfo::cancel())] + pub fn cancel(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + if let Some(payment) = Payment::::get(creator.clone(), who.clone()) { + match payment.state { + // call settle payment with recipient_share=0, this refunds the sender + PaymentState::Created => { + >::settle_payment( + creator.clone(), + who.clone(), + Percent::from_percent(0), + )?; + Self::deposit_event(Event::PaymentCancelled { from: creator, to: who }); + } + // if the payment is in state PaymentRequested, remove from storage + PaymentState::PaymentRequested => Payment::::remove(creator.clone(), who.clone()), + _ => fail!(Error::::InvalidAction), + } + } else { + fail!(Error::::InvalidPayment); + } + Ok(().into()) + } + + /// Allow judge to set state of a payment + /// This extrinsic is used to resolve disputes between the creator and + /// recipient of the payment. This extrinsic allows the assigned judge + /// to cancel the payment + #[transactional] + #[pallet::weight(T::WeightInfo::resolve_cancel_payment())] + pub fn resolve_cancel_payment( + origin: OriginFor, + from: T::AccountId, + recipient: T::AccountId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + // ensure the caller is the assigned resolver + if let Some(payment) = Payment::::get(from.clone(), recipient.clone()) { + ensure!(who == payment.resolver_account, Error::::InvalidAction) + } + // try to update the payment to new state + >::settle_payment(from.clone(), recipient.clone(), Percent::from_percent(0))?; + Self::deposit_event(Event::PaymentCancelled { from, to: recipient }); + Ok(().into()) + } + + /// Allow judge to set state of a payment + /// This extrinsic is used to resolve disputes between the creator and + /// recipient of the payment. This extrinsic allows the assigned judge + /// to send the payment to recipient + #[transactional] + #[pallet::weight(T::WeightInfo::resolve_release_payment())] + pub fn resolve_release_payment( + origin: OriginFor, + from: T::AccountId, + recipient: T::AccountId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + // ensure the caller is the assigned resolver + if let Some(payment) = Payment::::get(from.clone(), recipient.clone()) { + ensure!(who == payment.resolver_account, Error::::InvalidAction) + } + // try to update the payment to new state + >::settle_payment(from.clone(), recipient.clone(), Percent::from_percent(100))?; + Self::deposit_event(Event::PaymentReleased { from, to: recipient }); + Ok(().into()) + } + + /// Allow payment creator to set payment to NeedsReview + /// This extrinsic is used to mark the payment as disputed so the + /// assigned judge can tigger a resolution and that the funds are no + /// longer locked. + #[transactional] + #[pallet::weight(T::WeightInfo::request_refund())] + pub fn request_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + Payment::::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // ensure the payment is not in needsreview state + ensure!( + payment.state != PaymentState::NeedsReview, + Error::::PaymentNeedsReview + ); + + // set the payment to requested refund + let current_block = frame_system::Pallet::::block_number(); + let can_cancel_block = current_block + .checked_add(&T::CancelBufferBlockLength::get()) + .ok_or(Error::::MathError)?; + payment.state = PaymentState::RefundRequested(can_cancel_block); + + Self::deposit_event(Event::PaymentCreatorRequestedRefund { + from: who, + to: recipient, + expiry: can_cancel_block, + }); + + Ok(()) + })?; + + Ok(().into()) + } + + /// Allow payment creator to claim the refund if the payment recipent + /// has not disputed After the payment creator has `request_refund` can + /// then call this extrinsic to cancel the payment and receive the + /// reserved amount to the account if the dispute period has passed. + #[transactional] + #[pallet::weight(T::WeightInfo::claim_refund())] + pub fn claim_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { + use PaymentState::*; + let who = ensure_signed(origin)?; + + if let Some(payment) = Payment::::get(who.clone(), recipient.clone()) { + match payment.state { + NeedsReview => fail!(Error::::PaymentNeedsReview), + Created | PaymentRequested => fail!(Error::::RefundNotRequested), + RefundRequested(cancel_block) => { + let current_block = frame_system::Pallet::::block_number(); + // ensure the dispute period has passed + ensure!(current_block > cancel_block, Error::::DisputePeriodNotPassed); + // cancel the payment and refund the creator + >::settle_payment( + who.clone(), + recipient.clone(), + Percent::from_percent(0), + )?; + Self::deposit_event(Event::PaymentCancelled { + from: who, + to: recipient, + }); + } + } + } else { + fail!(Error::::InvalidPayment); + } + + Ok(().into()) + } + + /// Allow payment recipient to dispute the refund request from the + /// payment creator This does not cancel the request, instead sends the + /// payment to a NeedsReview state The assigned resolver account can + /// then change the state of the payment after review. + #[transactional] + #[pallet::weight(T::WeightInfo::dispute_refund())] + pub fn dispute_refund(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { + use PaymentState::*; + let who = ensure_signed(origin)?; + + Payment::::try_mutate( + creator.clone(), + who.clone(), // should be called by the payment recipient + |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // ensure the payment is in Requested Refund state + match payment.state { + RefundRequested(_) => { + payment.state = PaymentState::NeedsReview; + + Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who }); + } + _ => fail!(Error::::InvalidAction), + } + + Ok(()) + }, + )?; + + Ok(().into()) + } + + // Creates a new payment with the given details. This can be called by the + // recipient of the payment to create a payment and then completed by the sender + // using the `accept_and_pay` extrinsic. The payment will be in PaymentRequested + // State and can only be modified by the `accept_and_pay` extrinsic. + #[transactional] + #[pallet::weight(T::WeightInfo::request_payment())] + pub fn request_payment( + origin: OriginFor, + from: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let to = ensure_signed(origin)?; + + // create PaymentDetail and add to storage + >::create_payment( + from.clone(), + to.clone(), + asset, + amount, + PaymentState::PaymentRequested, + Percent::from_percent(0), + None, + )?; + + Self::deposit_event(Event::PaymentRequestCreated { from, to }); + + Ok(().into()) + } + + // This extrinsic allows the sender to fulfill a payment request created by a + // recipient. The amount will be transferred to the recipient and payment + // removed from storage + #[transactional] + #[pallet::weight(T::WeightInfo::accept_and_pay())] + pub fn accept_and_pay(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { + let from = ensure_signed(origin)?; + + let payment = Payment::::get(from.clone(), to.clone()).ok_or(Error::::InvalidPayment)?; + + ensure!( + payment.state == PaymentState::PaymentRequested, + Error::::InvalidAction + ); + + // reserve all the fees from the sender + >::reserve_payment_amount(&from, &to, payment)?; + + // release the payment and delete the payment from storage + >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + + Self::deposit_event(Event::PaymentRequestCompleted { from, to }); + + Ok(().into()) + } + } + + impl PaymentHandler for Pallet { + /// The function will create a new payment. The fee and incentive + /// amounts will be calculated and the `PaymentDetail` will be added to + /// storage. + #[require_transactional] + fn create_payment( + from: T::AccountId, + recipient: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + payment_state: PaymentState, + incentive_percentage: Percent, + remark: Option>, + ) -> Result, sp_runtime::DispatchError> { + Payment::::try_mutate( + from.clone(), + recipient.clone(), + |maybe_payment| -> Result, sp_runtime::DispatchError> { + if maybe_payment.is_some() { + // ensure the payment is not in created/needsreview state + let current_state = maybe_payment.clone().unwrap().state; + ensure!( + current_state != PaymentState::Created, + Error::::PaymentAlreadyInProcess + ); + ensure!( + current_state != PaymentState::NeedsReview, + Error::::PaymentNeedsReview + ); + } + // Calculate incentive amount - this is to insentivise the user to release + // the funds once a transaction has been completed + 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_origin(), + fee_detail: None, + remark, + }; + + // 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); + 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) + }, + ) + } + + /// The function will reserve the fees+transfer amount from the `from` + /// account. After reserving the payment.amount will be transferred to + /// the recipient but will stay in Reserve state. + #[require_transactional] + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult { + let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or(0u32.into()); + + let total_fee_amount = payment.incentive_amount.saturating_add(fee_amount); + let total_amount = total_fee_amount.saturating_add(payment.amount); + + // reserve the total amount from payment creator + T::Asset::reserve(payment.asset, from, total_amount)?; + // transfer payment amount to recipient -- keeping reserve status + T::Asset::repatriate_reserved(payment.asset, from, to, payment.amount, BalanceStatus::Reserved)?; + Ok(()) + } + + /// This function allows the caller to settle the payment by specifying + /// a recipient_share this will unreserve the fee+incentive to sender + /// and unreserve transferred amount to recipient if the settlement is a + /// release (ie recipient_share=100), the fee is transferred to + /// fee_recipient For cancelling a payment, recipient_share = 0 + /// For releasing a payment, recipient_share = 100 + /// In other cases, the custom recipient_share can be specified + #[require_transactional] + fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult { + Payment::::try_mutate(from.clone(), to.clone(), |maybe_payment| -> DispatchResult { + let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; + + // unreserve the incentive amount and fees from the owner account + match payment.fee_detail { + Some((fee_recipient, fee_amount)) => { + T::Asset::unreserve(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 + )?; + } + } + None => { + T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + } + }; + + // Unreserve the transfer 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); + // send share to recipient + T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; + + Ok(()) + })?; + Ok(()) + } + + fn get_payment_details(from: T::AccountId, to: T::AccountId) -> Option> { + Payment::::get(from, to) + } } } diff --git a/payments/src/mock.rs b/payments/src/mock.rs new file mode 100644 index 000000000..af55dbb0e --- /dev/null +++ b/payments/src/mock.rs @@ -0,0 +1,156 @@ +use crate as payment; +use crate::PaymentDetail; +use frame_support::{ + parameter_types, + traits::{Contains, Everything, GenesisBuild, OnFinalize, OnInitialize}, +}; +use frame_system as system; +use orml_traits::parameter_type_with_key; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + Percent, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; +pub type Balance = u128; + +pub type AccountId = u8; +pub const PAYMENT_CREATOR: AccountId = 10; +pub const PAYMENT_RECIPENT: AccountId = 11; +pub const CURRENCY_ID: u128 = 1; +pub const RESOLVER_ACCOUNT: AccountId = 12; +pub const FEE_RECIPIENT_ACCOUNT: AccountId = 20; +pub const PAYMENT_RECIPENT_FEE_CHARGED: AccountId = 21; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Tokens: orml_tokens::{Pallet, Call, Config, Storage, Event}, + Payment: payment::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: u128| -> Balance { + 0u128 + }; +} +parameter_types! { + pub const MaxLocks: u32 = 50; +} + +pub struct MockDustRemovalWhitelist; +impl Contains for MockDustRemovalWhitelist { + fn contains(_a: &AccountId) -> bool { + false + } +} + +impl orml_tokens::Config for Test { + type Amount = i64; + type Balance = Balance; + type CurrencyId = u128; + type Event = Event; + type ExistentialDeposits = ExistentialDeposits; + type OnDust = (); + type WeightInfo = (); + type MaxLocks = MaxLocks; + type DustRemovalWhitelist = MockDustRemovalWhitelist; +} + +pub struct MockDisputeResolver; +impl crate::types::DisputeResolver for MockDisputeResolver { + fn get_origin() -> AccountId { + RESOLVER_ACCOUNT + } +} + +pub struct MockFeeHandler; +impl crate::types::FeeHandler for MockFeeHandler { + fn apply_fees(_from: &AccountId, to: &AccountId, _remark: &PaymentDetail) -> (AccountId, Percent) { + match to { + &PAYMENT_RECIPENT_FEE_CHARGED => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(10)), + _ => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(0)), + } + } +} + +parameter_types! { + pub const IncentivePercentage: Percent = Percent::from_percent(10); + pub const MaxRemarkLength: u32 = 50; + pub const CancelBufferBlockLength: u64 = 600; +} + +impl payment::Config for Test { + type Event = Event; + type Asset = Tokens; + type DisputeResolver = MockDisputeResolver; + type IncentivePercentage = IncentivePercentage; + type FeeHandler = MockFeeHandler; + type MaxRemarkLength = MaxRemarkLength; + type CancelBufferBlockLength = CancelBufferBlockLength; + type WeightInfo = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = system::GenesisConfig::default().build_storage::().unwrap(); + + orml_tokens::GenesisConfig:: { + balances: vec![(PAYMENT_CREATOR, CURRENCY_ID, 100)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext: sp_io::TestExternalities = t.into(); + // need to set block number to 1 to test events + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + } +} diff --git a/payments/src/tests.rs b/payments/src/tests.rs new file mode 100644 index 000000000..225b11549 --- /dev/null +++ b/payments/src/tests.rs @@ -0,0 +1,1059 @@ +use crate::{ + mock::*, + types::{PaymentDetail, PaymentState}, + Payment as PaymentStore, PaymentHandler, +}; +use frame_support::{assert_noop, assert_ok, storage::with_transaction}; +use orml_traits::MultiCurrency; +use sp_runtime::{Percent, TransactionOutcome}; + +fn last_event() -> Event { + System::events().pop().expect("Event expected").event +} + +#[test] +fn test_pay_works() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + assert_eq!( + last_event(), + crate::Event::::PaymentCreated { + from: PAYMENT_CREATOR, + asset: CURRENCY_ID, + amount: 20 + } + .into() + ); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + // the payment amount should be reserved correctly + // the amount + incentive should be removed from the sender account + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + // the incentive amount should be reserved in the sender account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + // the transferred amount should be reserved in the recipent account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + + // the payment should not be overwritten + assert_noop!( + Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + }); +} + +#[test] +fn test_cancel_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // cancel should fail when called by user + assert_noop!( + Payment::cancel(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::InvalidPayment + ); + + // cancel should succeed when caller is the recipent + assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + // the payment amount should be released back to creator + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_release_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should succeed for valid payment + assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_eq!( + last_event(), + crate::Event::::PaymentReleased { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + // should be able to create another payment since previous is released + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 16); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + }); +} + +#[test] +fn test_set_state_payment_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + + // should fail for non whitelisted caller + assert_noop!( + Payment::resolve_cancel_payment(Origin::signed(PAYMENT_CREATOR), PAYMENT_CREATOR, PAYMENT_RECIPENT,), + crate::Error::::InvalidAction + ); + + // should be able to release a payment + assert_ok!(Payment::resolve_release_payment( + Origin::signed(RESOLVER_ACCOUNT), + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + )); + assert_eq!( + last_event(), + crate::Event::::PaymentReleased { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be removed from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + + // should be able to cancel a payment + assert_ok!(Payment::resolve_cancel_payment( + Origin::signed(RESOLVER_ACCOUNT), + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + )); + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_charging_fee_payment_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 4)), + remark: None + }) + ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should succeed for valid payment + assert_ok!(Payment::release( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED + )); + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 40); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 4); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + }); +} + +#[test] +fn test_charging_fee_payment_works_when_canceled() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 4)), + remark: None + }) + ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should succeed for valid payment + assert_ok!(Payment::cancel( + Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), + PAYMENT_CREATOR + )); + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 0); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + }); +} + +#[test] +fn test_pay_with_remark_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay_with_remark( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + vec![1u8; 10].try_into().unwrap() + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()) + }) + ); + // the payment amount should be reserved correctly + // the amount + incentive should be removed from the sender account + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + // the incentive amount should be reserved in the sender account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + // the transferred amount should be reserved in the recipent account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + + // the payment should not be overwritten + assert_noop!( + Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentCreated { + from: PAYMENT_CREATOR, + asset: CURRENCY_ID, + amount: 20 + } + .into() + ); + }); +} + +#[test] +fn test_do_not_overwrite_logic_works() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_noop!( + Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + crate::Error::::PaymentAlreadyInProcess + ); + + // set payment state to NeedsReview + PaymentStore::::insert( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::NeedsReview, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None, + }, + ); + + // the payment should not be overwritten + assert_noop!( + Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + crate::Error::::PaymentNeedsReview + ); + }); +} + +#[test] +fn test_request_refund() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::RefundRequested(601u64.into()), + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentCreatorRequestedRefund { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + expiry: 601u64.into() + } + .into() + ); + }); +} + +#[test] +fn test_claim_refund() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + // cannot claim refund unless payment is in requested refund state + assert_noop!( + Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::RefundNotRequested + ); + + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + + // cannot cancel before the dispute period has passed + assert_noop!( + Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::DisputePeriodNotPassed + ); + + run_to_block(700); + assert_ok!(Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + // the payment amount should be released back to creator + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_dispute_refund() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + // cannot dispute if refund is not requested + assert_noop!( + Payment::dispute_refund(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR), + crate::Error::::InvalidAction + ); + // creator requests a refund + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + // recipient disputes the refund request + assert_ok!(Payment::dispute_refund( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR + )); + // payment cannot be claimed after disputed + assert_noop!( + Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::PaymentNeedsReview + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::NeedsReview, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentRefundDisputed { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + } + .into() + ); + }); +} + +#[test] +fn test_request_payment() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 0_u128, + state: PaymentState::PaymentRequested, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentRequestCreated { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + } + .into() + ); + }); +} + +#[test] +fn test_requested_payment_cannot_be_released() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + // requested payment cannot be released + assert_noop!( + Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::InvalidAction + ); + }); +} + +#[test] +fn test_requested_payment_can_be_cancelled_by_requestor() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); + + // the request should be removed from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_accept_and_pay() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 0_u128, + state: PaymentState::PaymentRequested, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + + assert_ok!(Payment::accept_and_pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + )); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + assert_eq!( + last_event(), + crate::Event::::PaymentRequestCompleted { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + } + .into() + ); + }); +} + +#[test] +fn test_accept_and_pay_should_fail_for_non_payment_requested() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_noop!( + Payment::accept_and_pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT,), + crate::Error::::InvalidAction + ); + }); +} + +#[test] +fn test_accept_and_pay_should_charge_fee_correctly() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 0_u128, + state: PaymentState::PaymentRequested, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 2)), + remark: None + }) + ); + + assert_ok!(Payment::accept_and_pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + )); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 20); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 2); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentRequestCompleted { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT_FEE_CHARGED, + } + .into() + ); + }); +} + +#[test] +#[should_panic(expected = "Require transaction not called within with_transaction")] +fn test_create_payment_does_not_work_without_transaction() { + new_test_ext().execute_with(|| { + assert_ok!(>::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(0), + None, + )); + }); +} + +#[test] +fn test_create_payment_works() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(10), + Some(vec![1u8; 10].try_into().unwrap()), + ) + }))); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()), + }) + ); + + // the payment should not be overwritten + assert_noop!( + with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(10), + Some(vec![1u8; 10].try_into().unwrap()), + ) + })), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()), + }) + ); + }); +} + +#[test] +fn test_reserve_payment_amount_works() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(10), + Some(vec![1u8; 10].try_into().unwrap()), + ) + }))); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()), + }) + ); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::reserve_payment_amount( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), + ) + }))); + // the payment amount should be reserved correctly + // the amount + incentive should be removed from the sender account + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + // the incentive amount should be reserved in the sender account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + // the transferred amount should be reserved in the recipent account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + + // the payment should not be overwritten + assert_noop!( + with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(10), + Some(vec![1u8; 10].try_into().unwrap()), + ) + })), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()), + }) + ); + }); +} + +#[test] +fn test_settle_payment_works_for_cancel() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + Percent::from_percent(0), + ) + }))); + + // the payment amount should be released back to creator + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_settle_payment_works_for_release() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + Percent::from_percent(100), + ) + }))); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_settle_payment_works_for_70_30() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 10, + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT_FEE_CHARGED, + Percent::from_percent(70), + ) + }))); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 92); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 7); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 1); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); + }); +} + +#[test] +fn test_settle_payment_works_for_50_50() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 10, + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT_FEE_CHARGED, + Percent::from_percent(50), + ) + }))); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 94); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 5); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 1); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); + }); +} diff --git a/payments/src/types.rs b/payments/src/types.rs new file mode 100644 index 000000000..20b9c094b --- /dev/null +++ b/payments/src/types.rs @@ -0,0 +1,96 @@ +#![allow(unused_qualifications)] +use crate::{pallet, AssetIdOf, BalanceOf, BoundedDataOf}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{DispatchResult, Percent}; + +/// The PaymentDetail struct stores information about the payment/escrow +/// A "payment" in virto network is similar to an escrow, it is used to +/// guarantee proof of funds and can be released once an agreed upon condition +/// has reached between the payment creator and recipient. The payment lifecycle +/// is tracked using the state field. +#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound(T: pallet::Config))] +pub struct PaymentDetail { + /// type of asset used for payment + pub asset: AssetIdOf, + /// amount of asset used for payment + pub amount: BalanceOf, + /// incentive amount that is credited to creator for resolving + pub incentive_amount: BalanceOf, + /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, + /// Requested] + pub state: PaymentState, + /// 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)>, + /// remarks to give context to payment + pub remark: Option>, +} + +/// The `PaymentState` enum tracks the possible states that a payment can be in. +/// When a payment is 'completed' or 'cancelled' it is removed from storage and +/// hence not tracked by a state. +#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum PaymentState { + /// Amounts have been reserved and waiting for release/cancel + Created, + /// A judge needs to review and release manually + NeedsReview, + /// The user has requested refund and will be processed by `BlockNumber` + RefundRequested(BlockNumber), + /// The recipient of this transaction has created a request + PaymentRequested, +} + +/// trait that defines how to create/release payments for users +pub trait PaymentHandler { + /// Create a PaymentDetail from the given payment details + /// Calculate the fee amount and store PaymentDetail in storage + /// Possible reasons for failure include: + /// - Payment already exists and cannot be overwritten + fn create_payment( + from: T::AccountId, + to: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + payment_state: PaymentState, + incentive_percentage: Percent, + remark: Option>, + ) -> Result, sp_runtime::DispatchError>; + + /// Attempt to reserve an amount of the given asset from the caller + /// If not possible then return Error. Possible reasons for failure include: + /// - User does not have enough balance. + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult; + + // Settle a payment of `from` to `to`. To release a payment, the + // recipient_share=100, to cancel a payment recipient_share=0 + // Possible reasonse for failure include + /// - The payment does not exist + /// - The unreserve operation fails + /// - The transfer operation fails + fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult; + + /// Attempt to fetch the details of a payment from the given payment_id + /// Possible reasons for failure include: + /// - The payment does not exist + fn get_payment_details(from: T::AccountId, to: T::AccountId) -> Option>; +} + +/// DisputeResolver trait defines how to create/assing judges for solving +/// payment disputes +pub trait DisputeResolver { + /// Get a DisputeResolver (Judge) account + fn get_origin() -> Account; +} + +/// Fee Handler trait that defines how to handle marketplace fees to every +/// payment/swap +pub trait FeeHandler { + /// Get the distribution of fees to marketplace participants + fn apply_fees(from: &T::AccountId, to: &T::AccountId, detail: &PaymentDetail) -> (T::AccountId, Percent); +} diff --git a/payments/src/weights.rs b/payments/src/weights.rs new file mode 100644 index 000000000..153ed4ae5 --- /dev/null +++ b/payments/src/weights.rs @@ -0,0 +1,133 @@ +//! Autogenerated weights for virto_payment +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-02-18, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/virto-parachain +// benchmark +// --chain +// dev +// --execution=wasm +// --wasm-execution +// compiled +// --extrinsic=* +// --pallet=virto-payment +// --steps=20 +// --repeat=10 +// --raw +// --heap-pages=4096 +// --output +// ./pallets/payment/src/weights.rs +// --template +// ./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for virto_payment. +pub trait WeightInfo { + fn pay() -> Weight; + fn pay_with_remark(x: u32, ) -> Weight; + fn release() -> Weight; + fn cancel() -> Weight; + fn resolve_cancel_payment() -> Weight; + fn resolve_release_payment() -> Weight; + fn request_refund() -> Weight; + fn claim_refund() -> Weight; + fn dispute_refund() -> Weight; + fn request_payment() -> Weight; + fn accept_and_pay() -> Weight; +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn pay() -> Weight { + (54_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn pay_with_remark(_x: u32, ) -> Weight { + (54_397_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn release() -> Weight { + (34_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn cancel() -> Weight { + (46_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn resolve_cancel_payment() -> Weight { + (46_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn resolve_release_payment() -> Weight { + (35_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + fn request_refund() -> Weight { + (17_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn claim_refund() -> Weight { + (47_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + fn dispute_refund() -> Weight { + (16_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + fn request_payment() -> Weight { + (18_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn accept_and_pay() -> Weight { + (58_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } +} \ No newline at end of file From 0f98297afe8546a9eaf9a3d0c12ce88220109197 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sat, 12 Mar 2022 20:29:19 +0400 Subject: [PATCH 03/28] sync latest version --- Cargo.dev.toml | 1 + payments/Cargo.toml | 18 +- payments/src/lib.rs | 502 +++++++++++++------------ payments/src/mock.rs | 60 ++- payments/src/tests.rs | 799 +++++++++++++++++++++++++--------------- payments/src/types.rs | 77 ++-- payments/src/weights.rs | 140 +++++-- 7 files changed, 985 insertions(+), 612 deletions(-) diff --git a/Cargo.dev.toml b/Cargo.dev.toml index 8919af8c0..982e82597 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -22,6 +22,7 @@ members = [ "build-script-utils", "weight-gen", "weight-meter", + "payments" ] resolver = "2" diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 89352c823..de32bbf39 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -14,19 +14,19 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } -frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false, optional = true } -orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false, optional = true } +orml-traits = {path = "../traits", version = "0.4.1-dev", default-features = false } scale-info = { version = "1.0.0", default-features = false, features = ["derive"] } [dev-dependencies] serde = { version = "1.0.101" } -sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +orml-tokens = { path = "../tokens", version = "0.4.1-dev", default-features = false } [features] default = ['std'] diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 0d0210e67..8e7f88bf5 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -8,13 +8,19 @@ mod mock; #[cfg(test)] mod tests; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + pub mod types; pub mod weights; #[frame_support::pallet] pub mod pallet { pub use crate::{ - types::{DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState}, + types::{ + DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, + ScheduledTask, Task, + }, weights::WeightInfo, }; use frame_support::{ @@ -27,15 +33,18 @@ pub mod pallet { traits::{CheckedAdd, Saturating}, Percent, }; + use sp_std::vec::Vec; - pub type BalanceOf = <::Asset as MultiCurrency<::AccountId>>::Balance; - pub type AssetIdOf = <::Asset as MultiCurrency<::AccountId>>::CurrencyId; + pub type BalanceOf = + <::Asset as MultiCurrency<::AccountId>>::Balance; + pub type AssetIdOf = + <::Asset as MultiCurrency<::AccountId>>::CurrencyId; pub type BoundedDataOf = BoundedVec::MaxRemarkLength>; + pub type ScheduledTaskOf = ScheduledTask<::BlockNumber>; #[pallet::config] pub trait Config: frame_system::Config { - /// Because this pallet emits events, it depends on the runtime's - /// definition of an event. + /// Because this pallet emits events, it depends on the runtime's definition of an event. type Event: From> + IsType<::Event>; /// the type of assets this pallet can hold in payment type Asset: MultiReservableCurrency; @@ -49,8 +58,7 @@ pub mod pallet { /// Maximum permitted size of `Remark` #[pallet::constant] type MaxRemarkLength: Get; - /// Buffer period - number of blocks to wait before user can claim - /// canceled payment + /// Buffer period - number of blocks to wait before user can claim canceled payment #[pallet::constant] type CancelBufferBlockLength: Get; //// Type representing the weight of this pallet @@ -62,13 +70,12 @@ pub mod pallet { pub struct Pallet(_); #[pallet::storage] - #[pallet::getter(fn rates)] - /// Payments created by a user, this method of storageDoubleMap is chosen - /// since there is no usecase for listing payments by provider/currency. The - /// payment will only be referenced by the creator in any transaction of - /// interest. The storage map keys are the creator and the recipient, this - /// also ensures that for any (sender,recipient) combo, only a single - /// payment is active. The history of payment is not stored. + #[pallet::getter(fn payment)] + /// Payments created by a user, this method of storageDoubleMap is chosen since there is no usecase for + /// listing payments by provider/currency. The payment will only be referenced by the creator in + /// any transaction of interest. + /// The storage map keys are the creator and the recipient, this also ensures + /// that for any (sender,recipient) combo, only a single payment is active. The history of payment is not stored. pub(super) type Payment = StorageDoubleMap< _, Blake2_128Concat, @@ -78,6 +85,18 @@ pub mod pallet { PaymentDetail, >; + #[pallet::storage] + #[pallet::getter(fn tasks)] + /// Store the list of tasks to be executed in the on_idle function + pub(super) type ScheduledTasks = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // payment creator + Blake2_128Concat, + T::AccountId, // payment recipient + ScheduledTaskOf, + >; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -86,11 +105,14 @@ pub mod pallet { from: T::AccountId, asset: AssetIdOf, amount: BalanceOf, + remark: Option>, }, /// Payment amount released to the recipient PaymentReleased { from: T::AccountId, to: T::AccountId }, /// Payment has been cancelled by the creator PaymentCancelled { from: T::AccountId, to: T::AccountId }, + /// A payment that NeedsReview has been resolved by Judge + PaymentResolved { from: T::AccountId, to: T::AccountId, recipient_share: Percent }, /// the payment creator has created a refund request PaymentCreatorRequestedRefund { from: T::AccountId, @@ -123,57 +145,74 @@ pub mod pallet { RefundNotRequested, /// Dispute period has not passed DisputePeriodNotPassed, + /// The automatic cancelation queue cannot accept + RefundQueueFull, } #[pallet::hooks] - impl Hooks> for Pallet {} + impl Hooks> for Pallet { + /// Hook that execute when there is leftover space in a block + /// This function will look for any pending scheduled tasks that can + /// be executed and will process them. + fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight { + let mut task_list: Vec<(T::AccountId, T::AccountId, ScheduledTaskOf)> = + ScheduledTasks::::iter() + // leave out tasks in the future + .filter(|(_, _, ScheduledTask { when, .. })| when <= &now) + .collect(); + + if task_list.is_empty() { + return remaining_weight + } else { + task_list.sort_by(|(_, _, t), (_, _, x)| x.when.partial_cmp(&t.when).unwrap()); + } + + let cancel_weight = + T::WeightInfo::cancel().saturating_add(T::WeightInfo::remove_task()); + + while remaining_weight >= cancel_weight { + match task_list.pop() { + Some((from, to, ScheduledTask { task: Task::Cancel, .. })) => { + remaining_weight = remaining_weight.saturating_sub(cancel_weight); + + // process the cancel payment + if let Err(_) = >::settle_payment( + from.clone(), + to.clone(), + Percent::from_percent(0), + ) { + // panic!("{:?}", e); + } + ScheduledTasks::::remove(from.clone(), to.clone()); + // emit the cancel event + Self::deposit_event(Event::PaymentCancelled { + from: from.clone(), + to: to.clone(), + }); + }, + _ => return remaining_weight, + } + } + + remaining_weight + } + } #[pallet::call] impl Pallet { - /// This allows any user to create a new payment, that releases only to - /// specified recipient The only action is to store the details of this - /// payment in storage and reserve the specified amount. + /// This allows any user to create a new payment, that releases only to specified recipient + /// The only action is to store the details of this payment in storage and reserve + /// the specified amount. User also has the option to add a remark, this remark + /// can then be used to run custom logic and trigger alternate payment flows. + /// the specified amount. #[transactional] - #[pallet::weight(T::WeightInfo::pay())] + #[pallet::weight(T::WeightInfo::pay(T::MaxRemarkLength::get()))] pub fn pay( origin: OriginFor, recipient: T::AccountId, asset: AssetIdOf, - amount: BalanceOf, - ) -> DispatchResultWithPostInfo { - let who = ensure_signed(origin)?; - // create PaymentDetail and add to storage - let payment_detail = >::create_payment( - who.clone(), - recipient.clone(), - asset, - amount, - PaymentState::Created, - T::IncentivePercentage::get(), - None, - )?; - // reserve funds for payment - >::reserve_payment_amount(&who, &recipient, payment_detail)?; - // emit paymentcreated event - Self::deposit_event(Event::PaymentCreated { - from: who, - asset, - amount, - }); - Ok(().into()) - } - - /// This allows any user to create a new payment with the option to add - /// a remark, this remark can then be used to run custom logic and - /// trigger alternate payment flows. the specified amount. - #[transactional] - #[pallet::weight(T::WeightInfo::pay_with_remark(remark.len().try_into().unwrap_or(T::MaxRemarkLength::get())))] - pub fn pay_with_remark( - origin: OriginFor, - recipient: T::AccountId, - asset: AssetIdOf, - amount: BalanceOf, - remark: BoundedDataOf, + #[pallet::compact] amount: BalanceOf, + remark: Option>, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; @@ -185,46 +224,48 @@ pub mod pallet { amount, PaymentState::Created, T::IncentivePercentage::get(), - Some(remark), + remark.as_ref().map(|x| x.as_slice()), )?; // reserve funds for payment >::reserve_payment_amount(&who, &recipient, payment_detail)?; // emit paymentcreated event - Self::deposit_event(Event::PaymentCreated { - from: who, - asset, - amount, - }); + Self::deposit_event(Event::PaymentCreated { from: who, asset, amount, remark }); Ok(().into()) } - /// Release any created payment, this will transfer the reserved amount - /// from the creator of the payment to the assigned recipient + /// Release any created payment, this will transfer the reserved amount from the + /// creator of the payment to the assigned recipient #[transactional] #[pallet::weight(T::WeightInfo::release())] pub fn release(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { let from = ensure_signed(origin)?; // ensure the payment is in Created state - if let Some(payment) = Payment::::get(from.clone(), to.clone()) { + if let Some(payment) = Payment::::get(&from, &to) { ensure!(payment.state == PaymentState::Created, Error::::InvalidAction) + } else { + fail!(Error::::InvalidPayment); } // release is a settle_payment with 100% recipient_share - >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + >::settle_payment( + from.clone(), + to.clone(), + Percent::from_percent(100), + )?; Self::deposit_event(Event::PaymentReleased { from, to }); Ok(().into()) } - /// Cancel a payment in created state, this will release the reserved - /// back to creator of the payment. This extrinsic can only be called by - /// the recipient of the payment + /// Cancel a payment in created state, this will release the reserved back to + /// creator of the payment. This extrinsic can only be called by the recipient + /// of the payment #[transactional] #[pallet::weight(T::WeightInfo::cancel())] pub fn cancel(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - if let Some(payment) = Payment::::get(creator.clone(), who.clone()) { + if let Some(payment) = Payment::::get(&creator, &who) { match payment.state { // call settle payment with recipient_share=0, this refunds the sender PaymentState::Created => { @@ -234,142 +275,101 @@ pub mod pallet { Percent::from_percent(0), )?; Self::deposit_event(Event::PaymentCancelled { from: creator, to: who }); - } + }, // if the payment is in state PaymentRequested, remove from storage - PaymentState::PaymentRequested => Payment::::remove(creator.clone(), who.clone()), + PaymentState::PaymentRequested => Payment::::remove(&creator, &who), _ => fail!(Error::::InvalidAction), } - } else { - fail!(Error::::InvalidPayment); } Ok(().into()) } /// Allow judge to set state of a payment /// This extrinsic is used to resolve disputes between the creator and - /// recipient of the payment. This extrinsic allows the assigned judge - /// to cancel the payment + /// recipient of the payment. This extrinsic allows the assigned judge to cancel/release/partial_release + /// the payment. #[transactional] - #[pallet::weight(T::WeightInfo::resolve_cancel_payment())] - pub fn resolve_cancel_payment( + #[pallet::weight(T::WeightInfo::resolve_payment())] + pub fn resolve_payment( origin: OriginFor, from: T::AccountId, recipient: T::AccountId, + recipient_share: Percent, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; // ensure the caller is the assigned resolver - if let Some(payment) = Payment::::get(from.clone(), recipient.clone()) { + if let Some(payment) = Payment::::get(&from, &recipient) { ensure!(who == payment.resolver_account, Error::::InvalidAction) } // try to update the payment to new state - >::settle_payment(from.clone(), recipient.clone(), Percent::from_percent(0))?; - Self::deposit_event(Event::PaymentCancelled { from, to: recipient }); + >::settle_payment( + from.clone(), + recipient.clone(), + recipient_share, + )?; + Self::deposit_event(Event::PaymentResolved { from, to: recipient, recipient_share }); Ok(().into()) } - /// Allow judge to set state of a payment - /// This extrinsic is used to resolve disputes between the creator and - /// recipient of the payment. This extrinsic allows the assigned judge - /// to send the payment to recipient + /// Allow payment creator to set payment to NeedsReview + /// This extrinsic is used to mark the payment as disputed so the assigned judge can tigger a resolution + /// and that the funds are no longer locked. #[transactional] - #[pallet::weight(T::WeightInfo::resolve_release_payment())] - pub fn resolve_release_payment( + #[pallet::weight(T::WeightInfo::request_refund())] + pub fn request_refund( origin: OriginFor, - from: T::AccountId, recipient: T::AccountId, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - // ensure the caller is the assigned resolver - if let Some(payment) = Payment::::get(from.clone(), recipient.clone()) { - ensure!(who == payment.resolver_account, Error::::InvalidAction) - } - // try to update the payment to new state - >::settle_payment(from.clone(), recipient.clone(), Percent::from_percent(100))?; - Self::deposit_event(Event::PaymentReleased { from, to: recipient }); - Ok(().into()) - } - - /// Allow payment creator to set payment to NeedsReview - /// This extrinsic is used to mark the payment as disputed so the - /// assigned judge can tigger a resolution and that the funds are no - /// longer locked. - #[transactional] - #[pallet::weight(T::WeightInfo::request_refund())] - pub fn request_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { - let who = ensure_signed(origin)?; - - Payment::::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult { - // ensure the payment exists - let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; - // ensure the payment is not in needsreview state - ensure!( - payment.state != PaymentState::NeedsReview, - Error::::PaymentNeedsReview - ); - - // set the payment to requested refund - let current_block = frame_system::Pallet::::block_number(); - let can_cancel_block = current_block - .checked_add(&T::CancelBufferBlockLength::get()) - .ok_or(Error::::MathError)?; - payment.state = PaymentState::RefundRequested(can_cancel_block); - - Self::deposit_event(Event::PaymentCreatorRequestedRefund { - from: who, - to: recipient, - expiry: can_cancel_block, - }); - - Ok(()) - })?; - Ok(().into()) - } - - /// Allow payment creator to claim the refund if the payment recipent - /// has not disputed After the payment creator has `request_refund` can - /// then call this extrinsic to cancel the payment and receive the - /// reserved amount to the account if the dispute period has passed. - #[transactional] - #[pallet::weight(T::WeightInfo::claim_refund())] - pub fn claim_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { - use PaymentState::*; - let who = ensure_signed(origin)?; + Payment::::try_mutate( + who.clone(), + recipient.clone(), + |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // ensure the payment is not in needsreview state + ensure!( + payment.state != PaymentState::NeedsReview, + Error::::PaymentNeedsReview + ); + + // set the payment to requested refund + let current_block = frame_system::Pallet::::block_number(); + let cancel_block = current_block + .checked_add(&T::CancelBufferBlockLength::get()) + .ok_or(Error::::MathError)?; + + ScheduledTasks::::insert( + who.clone(), + recipient.clone(), + ScheduledTask { task: Task::Cancel, when: cancel_block }, + ); + + payment.state = PaymentState::RefundRequested { cancel_block }; + + Self::deposit_event(Event::PaymentCreatorRequestedRefund { + from: who, + to: recipient, + expiry: cancel_block, + }); - if let Some(payment) = Payment::::get(who.clone(), recipient.clone()) { - match payment.state { - NeedsReview => fail!(Error::::PaymentNeedsReview), - Created | PaymentRequested => fail!(Error::::RefundNotRequested), - RefundRequested(cancel_block) => { - let current_block = frame_system::Pallet::::block_number(); - // ensure the dispute period has passed - ensure!(current_block > cancel_block, Error::::DisputePeriodNotPassed); - // cancel the payment and refund the creator - >::settle_payment( - who.clone(), - recipient.clone(), - Percent::from_percent(0), - )?; - Self::deposit_event(Event::PaymentCancelled { - from: who, - to: recipient, - }); - } - } - } else { - fail!(Error::::InvalidPayment); - } + Ok(()) + }, + )?; Ok(().into()) } - /// Allow payment recipient to dispute the refund request from the - /// payment creator This does not cancel the request, instead sends the - /// payment to a NeedsReview state The assigned resolver account can - /// then change the state of the payment after review. + /// Allow payment recipient to dispute the refund request from the payment creator + /// This does not cancel the request, instead sends the payment to a NeedsReview state + /// The assigned resolver account can then change the state of the payment after review. #[transactional] #[pallet::weight(T::WeightInfo::dispute_refund())] - pub fn dispute_refund(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { + pub fn dispute_refund( + origin: OriginFor, + creator: T::AccountId, + ) -> DispatchResultWithPostInfo { use PaymentState::*; let who = ensure_signed(origin)?; @@ -381,11 +381,22 @@ pub mod pallet { let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; // ensure the payment is in Requested Refund state match payment.state { - RefundRequested(_) => { + RefundRequested { cancel_block } => { + ensure!( + cancel_block > frame_system::Pallet::::block_number(), + Error::::InvalidAction + ); + payment.state = PaymentState::NeedsReview; - Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who }); - } + // remove the payment from scheduled tasks + ScheduledTasks::::remove(creator.clone(), who.clone()); + + Self::deposit_event(Event::PaymentRefundDisputed { + from: creator, + to: who, + }); + }, _ => fail!(Error::::InvalidAction), } @@ -396,17 +407,16 @@ pub mod pallet { Ok(().into()) } - // Creates a new payment with the given details. This can be called by the - // recipient of the payment to create a payment and then completed by the sender - // using the `accept_and_pay` extrinsic. The payment will be in PaymentRequested - // State and can only be modified by the `accept_and_pay` extrinsic. + // Creates a new payment with the given details. This can be called by the recipient of the payment + // to create a payment and then completed by the sender using the `accept_and_pay` extrinsic. + // The payment will be in PaymentRequested State and can only be modified by the `accept_and_pay` extrinsic. #[transactional] #[pallet::weight(T::WeightInfo::request_payment())] pub fn request_payment( origin: OriginFor, from: T::AccountId, asset: AssetIdOf, - amount: BalanceOf, + #[pallet::compact] amount: BalanceOf, ) -> DispatchResultWithPostInfo { let to = ensure_signed(origin)?; @@ -426,26 +436,29 @@ pub mod pallet { Ok(().into()) } - // This extrinsic allows the sender to fulfill a payment request created by a - // recipient. The amount will be transferred to the recipient and payment - // removed from storage + // This extrinsic allows the sender to fulfill a payment request created by a recipient. + // The amount will be transferred to the recipient and payment removed from storage #[transactional] #[pallet::weight(T::WeightInfo::accept_and_pay())] - pub fn accept_and_pay(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { + pub fn accept_and_pay( + origin: OriginFor, + to: T::AccountId, + ) -> DispatchResultWithPostInfo { let from = ensure_signed(origin)?; - let payment = Payment::::get(from.clone(), to.clone()).ok_or(Error::::InvalidPayment)?; + let payment = Payment::::get(&from, &to).ok_or(Error::::InvalidPayment)?; - ensure!( - payment.state == PaymentState::PaymentRequested, - Error::::InvalidAction - ); + ensure!(payment.state == PaymentState::PaymentRequested, Error::::InvalidAction); // reserve all the fees from the sender >::reserve_payment_amount(&from, &to, payment)?; // release the payment and delete the payment from storage - >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + >::settle_payment( + from.clone(), + to.clone(), + Percent::from_percent(100), + )?; Self::deposit_event(Event::PaymentRequestCompleted { from, to }); @@ -454,9 +467,8 @@ pub mod pallet { } impl PaymentHandler for Pallet { - /// The function will create a new payment. The fee and incentive - /// amounts will be calculated and the `PaymentDetail` will be added to - /// storage. + /// The function will create a new payment. The fee and incentive amounts will be calculated and the + /// `PaymentDetail` will be added to storage. #[require_transactional] fn create_payment( from: T::AccountId, @@ -465,7 +477,7 @@ pub mod pallet { amount: BalanceOf, payment_state: PaymentState, incentive_percentage: Percent, - remark: Option>, + remark: Option<&[u8]>, ) -> Result, sp_runtime::DispatchError> { Payment::::try_mutate( from.clone(), @@ -483,6 +495,7 @@ pub mod pallet { Error::::PaymentNeedsReview ); } + // Calculate incentive amount - this is to insentivise the user to release // the funds once a transaction has been completed let incentive_amount = incentive_percentage.mul_floor(amount); @@ -492,14 +505,14 @@ pub mod pallet { amount, incentive_amount, state: payment_state, - resolver_account: T::DisputeResolver::get_origin(), + resolver_account: T::DisputeResolver::get_resolver_account(), fee_detail: None, - remark, }; // 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); + 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)); @@ -510,11 +523,14 @@ pub mod pallet { ) } - /// The function will reserve the fees+transfer amount from the `from` - /// account. After reserving the payment.amount will be transferred to - /// the recipient but will stay in Reserve state. + /// The function will reserve the fees+transfer amount from the `from` account. After reserving + /// the payment.amount will be transferred to the recipient but will stay in Reserve state. #[require_transactional] - fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult { + fn reserve_payment_amount( + from: &T::AccountId, + to: &T::AccountId, + payment: PaymentDetail, + ) -> DispatchResult { let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or(0u32.into()); let total_fee_amount = payment.incentive_amount.saturating_add(fee_amount); @@ -523,55 +539,71 @@ pub mod pallet { // reserve the total amount from payment creator T::Asset::reserve(payment.asset, from, total_amount)?; // transfer payment amount to recipient -- keeping reserve status - T::Asset::repatriate_reserved(payment.asset, from, to, payment.amount, BalanceStatus::Reserved)?; + T::Asset::repatriate_reserved( + payment.asset, + from, + to, + payment.amount, + BalanceStatus::Reserved, + )?; Ok(()) } - /// This function allows the caller to settle the payment by specifying - /// a recipient_share this will unreserve the fee+incentive to sender - /// and unreserve transferred amount to recipient if the settlement is a - /// release (ie recipient_share=100), the fee is transferred to - /// fee_recipient For cancelling a payment, recipient_share = 0 + /// This function allows the caller to settle the payment by specifying a recipient_share + /// this will unreserve the fee+incentive to sender and unreserve transferred amount to recipient + /// if the settlement is a release (ie recipient_share=100), the fee is transferred to fee_recipient + /// For cancelling a payment, recipient_share = 0 /// For releasing a payment, recipient_share = 100 /// In other cases, the custom recipient_share can be specified - #[require_transactional] - fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult { - Payment::::try_mutate(from.clone(), to.clone(), |maybe_payment| -> DispatchResult { - let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; - - // unreserve the incentive amount and fees from the owner account - match payment.fee_detail { - Some((fee_recipient, fee_amount)) => { - T::Asset::unreserve(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( + fn settle_payment( + from: T::AccountId, + to: T::AccountId, + recipient_share: Percent, + ) -> DispatchResult { + Payment::::try_mutate( + from.clone(), + to.clone(), + |maybe_payment| -> DispatchResult { + let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; + + // unreserve the incentive amount and fees from the owner account + match payment.fee_detail { + Some((fee_recipient, fee_amount)) => { + T::Asset::unreserve( payment.asset, - &from, // fee is paid by payment creator - &fee_recipient, // account of fee recipient - fee_amount, // amount of fee - )?; - } - } - None => { - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); - } - }; + &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 + )?; + } + }, + None => { + T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + }, + }; - // Unreserve the transfer amount - T::Asset::unreserve(payment.asset, &to, payment.amount); + // Unreserve the transfer 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); - // send share to recipient - T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; + let amount_to_recipient = recipient_share.mul_floor(payment.amount); + let amount_to_sender = payment.amount.saturating_sub(amount_to_recipient); + // send share to recipient + T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; - Ok(()) - })?; + Ok(()) + }, + )?; Ok(()) } - fn get_payment_details(from: T::AccountId, to: T::AccountId) -> Option> { + fn get_payment_details(from: &T::AccountId, to: &T::AccountId) -> Option> { Payment::::get(from, to) } } diff --git a/payments/src/mock.rs b/payments/src/mock.rs index af55dbb0e..0a5646ab5 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -2,7 +2,8 @@ use crate as payment; use crate::PaymentDetail; use frame_support::{ parameter_types, - traits::{Contains, Everything, GenesisBuild, OnFinalize, OnInitialize}, + traits::{Contains, Everything, GenesisBuild, Hooks, OnFinalize}, + weights::DispatchClass, }; use frame_system as system; use orml_traits::parameter_type_with_key; @@ -12,6 +13,7 @@ use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, Percent, }; +use virto_primitives::{Asset, NetworkAsset}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -20,10 +22,15 @@ pub type Balance = u128; pub type AccountId = u8; pub const PAYMENT_CREATOR: AccountId = 10; pub const PAYMENT_RECIPENT: AccountId = 11; -pub const CURRENCY_ID: u128 = 1; +pub const PAYMENT_CREATOR_TWO: AccountId = 30; +pub const PAYMENT_RECIPENT_TWO: AccountId = 31; +pub const CURRENCY_ID: Asset = Asset::Network(NetworkAsset::KSM); pub const RESOLVER_ACCOUNT: AccountId = 12; pub const FEE_RECIPIENT_ACCOUNT: AccountId = 20; pub const PAYMENT_RECIPENT_FEE_CHARGED: AccountId = 21; +pub const INCENTIVE_PERCENTAGE: u8 = 10; +pub const MARKETPLACE_FEE_PERCENTAGE: u8 = 10; +pub const CANCEL_BLOCK_BUFFER: u64 = 600; frame_support::construct_runtime!( pub enum Test where @@ -70,7 +77,7 @@ impl system::Config for Test { } parameter_type_with_key! { - pub ExistentialDeposits: |_currency_id: u128| -> Balance { + pub ExistentialDeposits: |_currency_id: Asset| -> Balance { 0u128 }; } @@ -88,7 +95,7 @@ impl Contains for MockDustRemovalWhitelist { impl orml_tokens::Config for Test { type Amount = i64; type Balance = Balance; - type CurrencyId = u128; + type CurrencyId = Asset; type Event = Event; type ExistentialDeposits = ExistentialDeposits; type OnDust = (); @@ -99,25 +106,31 @@ impl orml_tokens::Config for Test { pub struct MockDisputeResolver; impl crate::types::DisputeResolver for MockDisputeResolver { - fn get_origin() -> AccountId { + fn get_resolver_account() -> AccountId { RESOLVER_ACCOUNT } } pub struct MockFeeHandler; impl crate::types::FeeHandler for MockFeeHandler { - fn apply_fees(_from: &AccountId, to: &AccountId, _remark: &PaymentDetail) -> (AccountId, Percent) { + fn apply_fees( + _from: &AccountId, + to: &AccountId, + _detail: &PaymentDetail, + _remark: Option<&[u8]>, + ) -> (AccountId, Percent) { match to { - &PAYMENT_RECIPENT_FEE_CHARGED => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(10)), + &PAYMENT_RECIPENT_FEE_CHARGED => + (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE)), _ => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(0)), } } } parameter_types! { - pub const IncentivePercentage: Percent = Percent::from_percent(10); + pub const IncentivePercentage: Percent = Percent::from_percent(INCENTIVE_PERCENTAGE); pub const MaxRemarkLength: u32 = 50; - pub const CancelBufferBlockLength: u64 = 600; + pub const CancelBufferBlockLength: u64 = CANCEL_BLOCK_BUFFER; } impl payment::Config for Test { @@ -136,7 +149,10 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = system::GenesisConfig::default().build_storage::().unwrap(); orml_tokens::GenesisConfig:: { - balances: vec![(PAYMENT_CREATOR, CURRENCY_ID, 100)], + balances: vec![ + (PAYMENT_CREATOR, CURRENCY_ID, 100), + (PAYMENT_CREATOR_TWO, CURRENCY_ID, 100), + ], } .assimilate_storage(&mut t) .unwrap(); @@ -147,10 +163,24 @@ pub fn new_test_ext() -> sp_io::TestExternalities { ext } -pub fn run_to_block(n: u64) { - while System::block_number() < n { - System::on_finalize(System::block_number()); - System::set_block_number(System::block_number() + 1); - System::on_initialize(System::block_number()); +pub fn run_n_blocks(n: u64) -> u64 { + const IDLE_WEIGHT: u64 = 10_000_000_000; + const BUSY_WEIGHT: u64 = IDLE_WEIGHT / 1000; + + let start_block = System::block_number(); + + for block_number in (0..=n).map(|n| n + start_block) { + System::set_block_number(block_number); + + // Odd blocks gets busy + let idle_weight = if block_number % 2 == 0 { IDLE_WEIGHT } else { BUSY_WEIGHT }; + // ensure the on_idle is executed + >::register_extra_weight_unchecked( + Payment::on_idle(block_number, idle_weight), + DispatchClass::Mandatory, + ); + + as OnFinalize>::on_finalize(block_number); } + System::block_number() } diff --git a/payments/src/tests.rs b/payments/src/tests.rs index 225b11549..3a58ceefe 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -1,12 +1,14 @@ use crate::{ mock::*, types::{PaymentDetail, PaymentState}, - Payment as PaymentStore, PaymentHandler, + Payment as PaymentStore, PaymentHandler, ScheduledTask, ScheduledTasks, Task, }; use frame_support::{assert_noop, assert_ok, storage::with_transaction}; use orml_traits::MultiCurrency; use sp_runtime::{Percent, TransactionOutcome}; +type Error = crate::Error; + fn last_event() -> Event { System::events().pop().expect("Event expected").event } @@ -14,8 +16,12 @@ fn last_event() -> Event { #[test] fn test_pay_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should be able to create a payment with available balance @@ -23,41 +29,55 @@ fn test_pay_works() { Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); assert_eq!( last_event(), crate::Event::::PaymentCreated { from: PAYMENT_CREATOR, asset: CURRENCY_ID, - amount: 20 + amount: payment_amount, + remark: None } .into() ); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); // the payment amount should be reserved correctly // the amount + incentive should be removed from the sender account - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); // the incentive amount should be reserved in the sender account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // the transferred amount should be reserved in the recipent account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // the payment should not be overwritten assert_noop!( - Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), crate::Error::::PaymentAlreadyInProcess ); @@ -65,12 +85,11 @@ fn test_pay_works() { PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, + amount: payment_amount, incentive_amount: 2, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); }); @@ -79,49 +98,47 @@ fn test_pay_works() { #[test] fn test_cancel_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 40, - incentive_amount: 4, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - - // cancel should fail when called by user - assert_noop!( - Payment::cancel(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::InvalidPayment + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // cancel should succeed when caller is the recipent assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); assert_eq!( last_event(), - crate::Event::::PaymentCancelled { - from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT - } - .into() + crate::Event::::PaymentCancelled { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } + .into() ); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); // should be released from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -131,43 +148,49 @@ fn test_cancel_works() { #[test] fn test_release_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 40, - incentive_amount: 4, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should succeed for valid payment assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); assert_eq!( last_event(), - crate::Event::::PaymentReleased { - from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT - } - .into() + crate::Event::::PaymentReleased { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } + .into() ); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be deleted from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -177,50 +200,67 @@ fn test_release_works() { Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 16); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - (payment_amount * 2) - expected_incentive_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); }); } #[test] fn test_set_state_payment_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); // should fail for non whitelisted caller assert_noop!( - Payment::resolve_cancel_payment(Origin::signed(PAYMENT_CREATOR), PAYMENT_CREATOR, PAYMENT_RECIPENT,), - crate::Error::::InvalidAction + Payment::resolve_payment( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + Percent::from_percent(100) + ), + Error::InvalidAction ); // should be able to release a payment - assert_ok!(Payment::resolve_release_payment( + assert_ok!(Payment::resolve_payment( Origin::signed(RESOLVER_ACCOUNT), PAYMENT_CREATOR, PAYMENT_RECIPENT, + Percent::from_percent(100) )); assert_eq!( last_event(), - crate::Event::::PaymentReleased { + crate::Event::::PaymentResolved { from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT + to: PAYMENT_RECIPENT, + recipient_share: Percent::from_percent(100) } .into() ); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be removed from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -229,28 +269,33 @@ fn test_set_state_payment_works() { Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); // should be able to cancel a payment - assert_ok!(Payment::resolve_cancel_payment( + assert_ok!(Payment::resolve_payment( Origin::signed(RESOLVER_ACCOUNT), PAYMENT_CREATOR, PAYMENT_RECIPENT, + Percent::from_percent(0) )); assert_eq!( last_event(), - crate::Event::::PaymentCancelled { + crate::Event::::PaymentResolved { from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT + to: PAYMENT_RECIPENT, + recipient_share: Percent::from_percent(0) } .into() ); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be released from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -260,118 +305,154 @@ fn test_set_state_payment_works() { #[test] fn test_charging_fee_payment_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, CURRENCY_ID, - 40, + payment_amount, + None )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 40, - incentive_amount: 4, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 4)), - remark: None + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - + payment_amount - expected_fee_amount - + expected_incentive_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); // should succeed for valid payment - assert_ok!(Payment::release( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT_FEE_CHARGED - )); + assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED)); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 40); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 4); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount + ); + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); }); } #[test] fn test_charging_fee_payment_works_when_canceled() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, CURRENCY_ID, - 40, + payment_amount, + None )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 40, - incentive_amount: 4, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 4)), - remark: None + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - + payment_amount - expected_fee_amount - + expected_incentive_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); // should succeed for valid payment - assert_ok!(Payment::cancel( - Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), - PAYMENT_CREATOR - )); + assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), PAYMENT_CREATOR)); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 0); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); }); } #[test] fn test_pay_with_remark_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + // should be able to create a payment with available balance - assert_ok!(Payment::pay_with_remark( + assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, - vec![1u8; 10].try_into().unwrap() + payment_amount, + Some(vec![1u8; 10].try_into().unwrap()) )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()) }) ); // the payment amount should be reserved correctly // the amount + incentive should be removed from the sender account - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); // the incentive amount should be reserved in the sender account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // the transferred amount should be reserved in the recipent account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // the payment should not be overwritten assert_noop!( - Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), crate::Error::::PaymentAlreadyInProcess ); @@ -380,7 +461,8 @@ fn test_pay_with_remark_works() { crate::Event::::PaymentCreated { from: PAYMENT_CREATOR, asset: CURRENCY_ID, - amount: 20 + amount: payment_amount, + remark: Some(vec![1u8; 10].try_into().unwrap()) } .into() ); @@ -390,15 +472,25 @@ fn test_pay_with_remark_works() { #[test] fn test_do_not_overwrite_logic_works() { new_test_ext().execute_with(|| { + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); assert_noop!( - Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), crate::Error::::PaymentAlreadyInProcess ); @@ -408,18 +500,23 @@ fn test_do_not_overwrite_logic_works() { PAYMENT_RECIPENT, PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::NeedsReview, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None, }, ); // the payment should not be overwritten assert_noop!( - Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), crate::Error::::PaymentNeedsReview ); }); @@ -428,28 +525,29 @@ fn test_do_not_overwrite_logic_works() { #[test] fn test_request_refund() { new_test_ext().execute_with(|| { + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_cancel_block = CANCEL_BLOCK_BUFFER + 1; + assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); - assert_ok!(Payment::request_refund( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT - )); + assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, - state: PaymentState::RefundRequested(601u64.into()), + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::RefundRequested { cancel_block: expected_cancel_block }, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); @@ -458,102 +556,53 @@ fn test_request_refund() { crate::Event::::PaymentCreatorRequestedRefund { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT, - expiry: 601u64.into() - } - .into() - ); - }); -} - -#[test] -fn test_claim_refund() { - new_test_ext().execute_with(|| { - assert_ok!(Payment::pay( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT, - CURRENCY_ID, - 20, - )); - - // cannot claim refund unless payment is in requested refund state - assert_noop!( - Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::RefundNotRequested - ); - - assert_ok!(Payment::request_refund( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT - )); - - // cannot cancel before the dispute period has passed - assert_noop!( - Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::DisputePeriodNotPassed - ); - - run_to_block(700); - assert_ok!(Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); - - assert_eq!( - last_event(), - crate::Event::::PaymentCancelled { - from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT + expiry: expected_cancel_block } .into() ); - // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); - - // should be released from storage - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); }); } #[test] fn test_dispute_refund() { new_test_ext().execute_with(|| { + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_cancel_block = CANCEL_BLOCK_BUFFER + 1; + assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); // cannot dispute if refund is not requested assert_noop!( Payment::dispute_refund(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR), - crate::Error::::InvalidAction + Error::InvalidAction ); // creator requests a refund - assert_ok!(Payment::request_refund( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT - )); - // recipient disputes the refund request - assert_ok!(Payment::dispute_refund( - Origin::signed(PAYMENT_RECIPENT), - PAYMENT_CREATOR - )); - // payment cannot be claimed after disputed - assert_noop!( - Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::PaymentNeedsReview + assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + // ensure the request is added to the refund queue + assert_eq!( + ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), + ScheduledTask { task: Task::Cancel, when: expected_cancel_block } ); + // recipient disputes the refund request + assert_ok!(Payment::dispute_refund(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::NeedsReview, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); @@ -565,29 +614,34 @@ fn test_dispute_refund() { } .into() ); + + // ensure the request is added to the refund queue + assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); }); } #[test] fn test_request_payment() { new_test_ext().execute_with(|| { + let payment_amount = 20; + let expected_incentive_amount = 0; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 0_u128, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::PaymentRequested, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); @@ -605,17 +659,19 @@ fn test_request_payment() { #[test] fn test_requested_payment_cannot_be_released() { new_test_ext().execute_with(|| { + let payment_amount = 20; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); // requested payment cannot be released assert_noop!( Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::InvalidAction + Error::InvalidAction ); }); } @@ -623,11 +679,13 @@ fn test_requested_payment_cannot_be_released() { #[test] fn test_requested_payment_can_be_cancelled_by_requestor() { new_test_ext().execute_with(|| { + let payment_amount = 20; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); @@ -640,35 +698,37 @@ fn test_requested_payment_can_be_cancelled_by_requestor() { #[test] fn test_accept_and_pay() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = 0; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 0_u128, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::PaymentRequested, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); - assert_ok!(Payment::accept_and_pay( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT, - )); + assert_ok!(Payment::accept_and_pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT,)); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be deleted from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -692,11 +752,12 @@ fn test_accept_and_pay_should_fail_for_non_payment_requested() { PAYMENT_RECIPENT, CURRENCY_ID, 20, + None )); assert_noop!( Payment::accept_and_pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT,), - crate::Error::::InvalidAction + Error::InvalidAction ); }); } @@ -704,23 +765,27 @@ fn test_accept_and_pay_should_fail_for_non_payment_requested() { #[test] fn test_accept_and_pay_should_charge_fee_correctly() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = 0; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 0_u128, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::PaymentRequested, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 2)), - remark: None + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); @@ -730,16 +795,18 @@ fn test_accept_and_pay_should_charge_fee_correctly() { )); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 20); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 2); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); - - // should be deleted from storage assert_eq!( - PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), - None + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); assert_eq!( last_event(), @@ -771,21 +838,25 @@ fn test_create_payment_does_not_work_without_transaction() { #[test] fn test_create_payment_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = 0; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( PAYMENT_CREATOR, PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, PaymentState::Created, - Percent::from_percent(10), - Some(vec![1u8; 10].try_into().unwrap()), + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&vec![1u8; 10]), ) }))); @@ -793,12 +864,11 @@ fn test_create_payment_works() { PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()), + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); @@ -809,25 +879,24 @@ fn test_create_payment_works() { PAYMENT_CREATOR, PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, PaymentState::Created, - Percent::from_percent(10), - Some(vec![1u8; 10].try_into().unwrap()), + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&vec![1u8; 10]), ) })), - crate::Error::::PaymentAlreadyInProcess + Error::PaymentAlreadyInProcess ); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()), + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); }); @@ -836,21 +905,25 @@ fn test_create_payment_works() { #[test] fn test_reserve_payment_amount_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = 0; + // the payment amount should not be reserved assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( PAYMENT_CREATOR, PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, PaymentState::Created, - Percent::from_percent(10), - Some(vec![1u8; 10].try_into().unwrap()), + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&vec![1u8; 10]), ) }))); @@ -858,12 +931,11 @@ fn test_reserve_payment_amount_works() { PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()), + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); @@ -876,12 +948,18 @@ fn test_reserve_payment_amount_works() { }))); // the payment amount should be reserved correctly // the amount + incentive should be removed from the sender account - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); // the incentive amount should be reserved in the sender account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // the transferred amount should be reserved in the recipent account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // the payment should not be overwritten assert_noop!( @@ -890,25 +968,24 @@ fn test_reserve_payment_amount_works() { PAYMENT_CREATOR, PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, PaymentState::Created, - Percent::from_percent(10), - Some(vec![1u8; 10].try_into().unwrap()), + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&vec![1u8; 10]), ) })), - crate::Error::::PaymentAlreadyInProcess + Error::PaymentAlreadyInProcess ); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()), + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); }); @@ -917,17 +994,20 @@ fn test_reserve_payment_amount_works() { #[test] fn test_settle_payment_works_for_cancel() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); assert_ok!(with_transaction(|| TransactionOutcome::Commit({ @@ -939,9 +1019,8 @@ fn test_settle_payment_works_for_cancel() { }))); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); // should be released from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -951,17 +1030,20 @@ fn test_settle_payment_works_for_cancel() { #[test] fn test_settle_payment_works_for_release() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); assert_ok!(with_transaction(|| TransactionOutcome::Commit({ @@ -973,9 +1055,11 @@ fn test_settle_payment_works_for_release() { }))); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be deleted from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -985,17 +1069,21 @@ fn test_settle_payment_works_for_release() { #[test] fn test_settle_payment_works_for_70_30() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 10; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, CURRENCY_ID, - 10, + payment_amount, + None )); assert_ok!(with_transaction(|| TransactionOutcome::Commit({ @@ -1006,34 +1094,45 @@ fn test_settle_payment_works_for_70_30() { ) }))); - // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 92); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 7); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 1); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + let expected_amount_for_creator = + creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(30) * payment_amount); + let expected_amount_for_recipient = Percent::from_percent(70) * payment_amount; - // should be deleted from storage + // the payment amount should be transferred assert_eq!( - PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), - None + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + expected_amount_for_creator + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + expected_amount_for_recipient ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); }); } #[test] fn test_settle_payment_works_for_50_50() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 10; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + // the payment amount should not be reserved assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, CURRENCY_ID, - 10, + payment_amount, + None )); assert_ok!(with_transaction(|| TransactionOutcome::Commit({ @@ -1044,16 +1143,142 @@ fn test_settle_payment_works_for_50_50() { ) }))); + let expected_amount_for_creator = + creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(50) * payment_amount); + let expected_amount_for_recipient = Percent::from_percent(50) * payment_amount; + // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 94); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 5); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 1); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + expected_amount_for_creator + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + expected_amount_for_recipient + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); // should be deleted from storage - assert_eq!( - PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); + }); +} + +#[test] +fn test_automatic_refund_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + const CANCEL_PERIOD: u64 = 600; + const CANCEL_BLOCK: u64 = CANCEL_PERIOD + 1; + + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, None + )); + + assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::RefundRequested { cancel_block: CANCEL_BLOCK }, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + + assert_eq!( + ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), + ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } + ); + + // run to one block before cancel and make sure data is same + assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 600); + assert_eq!( + ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), + ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } + ); + + // run to after cancel block but odd blocks are busy + assert_eq!(run_n_blocks(1), 601); + // the payment is still not processed since the block was busy + assert!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).is_some()); + + // next block has spare weight to process the payment + assert_eq!(run_n_blocks(1), 602); + // the payment should be removed from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + // the scheduled storage should be cleared + assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + // test that the refund happened correctly + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } + .into() ); + // the payment amount should be released back to creator + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + }); +} + +#[test] +fn test_automatic_refund_works_for_multiple_payments() { + new_test_ext().execute_with(|| { + const CANCEL_PERIOD: u64 = 600; + + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + None + )); + + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR_TWO), + PAYMENT_RECIPENT_TWO, + CURRENCY_ID, + 20, + None + )); + + assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + run_n_blocks(1); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR_TWO), + PAYMENT_RECIPENT_TWO + )); + + assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 601); + + // Odd block 601 was busy so we still haven't processed the first payment + assert_ok!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).ok_or(())); + + // Even block 602 has enough room to process both pending payments + assert_eq!(run_n_blocks(1), 602); + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), None); + + // the scheduled storage should be cleared + assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), None); + + // test that the refund happened correctly + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR_TWO), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_TWO), 0); }); } diff --git a/payments/src/types.rs b/payments/src/types.rs index 20b9c094b..290c67ce8 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -1,14 +1,13 @@ #![allow(unused_qualifications)] -use crate::{pallet, AssetIdOf, BalanceOf, BoundedDataOf}; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use crate::{pallet, AssetIdOf, BalanceOf}; +use parity_scale_codec::{Decode, Encode, HasCompact, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::{DispatchResult, Percent}; /// The PaymentDetail struct stores information about the payment/escrow -/// A "payment" in virto network is similar to an escrow, it is used to -/// guarantee proof of funds and can be released once an agreed upon condition -/// has reached between the payment creator and recipient. The payment lifecycle -/// is tracked using the state field. +/// A "payment" in virto network is similar to an escrow, it is used to guarantee proof of funds +/// and can be released once an agreed upon condition has reached between the payment creator +/// and recipient. The payment lifecycle is tracked using the state field. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound(T: pallet::Config))] @@ -16,23 +15,21 @@ pub struct PaymentDetail { /// type of asset used for payment pub asset: AssetIdOf, /// amount of asset used for payment + #[codec(compact)] pub amount: BalanceOf, /// incentive amount that is credited to creator for resolving + #[codec(compact)] pub incentive_amount: BalanceOf, - /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, - /// Requested] + /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, Requested] pub state: PaymentState, /// 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)>, - /// remarks to give context to payment - pub remark: Option>, } /// The `PaymentState` enum tracks the possible states that a payment can be in. -/// When a payment is 'completed' or 'cancelled' it is removed from storage and -/// hence not tracked by a state. +/// When a payment is 'completed' or 'cancelled' it is removed from storage and hence not tracked by a state. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum PaymentState { @@ -41,7 +38,7 @@ pub enum PaymentState { /// A judge needs to review and release manually NeedsReview, /// The user has requested refund and will be processed by `BlockNumber` - RefundRequested(BlockNumber), + RefundRequested { cancel_block: BlockNumber }, /// The recipient of this transaction has created a request PaymentRequested, } @@ -59,38 +56,66 @@ pub trait PaymentHandler { amount: BalanceOf, payment_state: PaymentState, incentive_percentage: Percent, - remark: Option>, + remark: Option<&[u8]>, ) -> Result, sp_runtime::DispatchError>; /// Attempt to reserve an amount of the given asset from the caller /// If not possible then return Error. Possible reasons for failure include: /// - User does not have enough balance. - fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult; + fn reserve_payment_amount( + from: &T::AccountId, + to: &T::AccountId, + payment: PaymentDetail, + ) -> DispatchResult; - // Settle a payment of `from` to `to`. To release a payment, the - // recipient_share=100, to cancel a payment recipient_share=0 + // Settle a payment of `from` to `to`. To release a payment, the recipient_share=100, + // to cancel a payment recipient_share=0 // Possible reasonse for failure include /// - The payment does not exist /// - The unreserve operation fails /// - The transfer operation fails - fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult; + fn settle_payment( + from: T::AccountId, + to: T::AccountId, + recipient_share: Percent, + ) -> DispatchResult; /// Attempt to fetch the details of a payment from the given payment_id /// Possible reasons for failure include: /// - The payment does not exist - fn get_payment_details(from: T::AccountId, to: T::AccountId) -> Option>; + fn get_payment_details(from: &T::AccountId, to: &T::AccountId) -> Option>; } -/// DisputeResolver trait defines how to create/assing judges for solving -/// payment disputes +/// DisputeResolver trait defines how to create/assign judges for solving payment disputes pub trait DisputeResolver { - /// Get a DisputeResolver (Judge) account - fn get_origin() -> Account; + /// Returns an `Account` + fn get_resolver_account() -> Account; } -/// Fee Handler trait that defines how to handle marketplace fees to every -/// payment/swap +/// Fee Handler trait that defines how to handle marketplace fees to every payment/swap pub trait FeeHandler { /// Get the distribution of fees to marketplace participants - fn apply_fees(from: &T::AccountId, to: &T::AccountId, detail: &PaymentDetail) -> (T::AccountId, Percent); + fn apply_fees( + from: &T::AccountId, + to: &T::AccountId, + detail: &PaymentDetail, + remark: Option<&[u8]>, + ) -> (T::AccountId, Percent); } + +/// Types of Tasks that can be scheduled in the pallet +#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)] +pub enum Task { + // payment `from` to `to` has to be cancelled + Cancel, +} + +/// The details of a scheduled task +#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)] +pub struct ScheduledTask { + /// the type of scheduled task + pub task: Task, + /// the 'time' at which the task should be executed + #[codec(compact)] + pub when: Time, +} \ No newline at end of file diff --git a/payments/src/weights.rs b/payments/src/weights.rs index 153ed4ae5..df20c5312 100644 --- a/payments/src/weights.rs +++ b/payments/src/weights.rs @@ -1,7 +1,7 @@ //! Autogenerated weights for virto_payment //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-02-18, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-03-11, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: @@ -16,7 +16,6 @@ // --pallet=virto-payment // --steps=20 // --repeat=10 -// --raw // --heap-pages=4096 // --output // ./pallets/payment/src/weights.rs @@ -32,43 +31,108 @@ use sp_std::marker::PhantomData; /// Weight functions needed for virto_payment. pub trait WeightInfo { - fn pay() -> Weight; - fn pay_with_remark(x: u32, ) -> Weight; + fn pay(x: u32, ) -> Weight; fn release() -> Weight; fn cancel() -> Weight; - fn resolve_cancel_payment() -> Weight; - fn resolve_release_payment() -> Weight; + fn resolve_payment() -> Weight; fn request_refund() -> Weight; - fn claim_refund() -> Weight; fn dispute_refund() -> Weight; fn request_payment() -> Weight; fn accept_and_pay() -> Weight; + fn read_task() -> Weight; + fn remove_task() -> Weight; } -// For backwards compatibility and tests -impl WeightInfo for () { +/// Weights for virto_payment using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:1) - fn pay() -> Weight { - (54_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(5 as Weight)) - .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + fn pay(_x: u32, ) -> Weight { + (63_805_000 as Weight) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn release() -> Weight { + (33_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn cancel() -> Weight { + (51_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn resolve_payment() -> Weight { + (39_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:0 w:1) + fn request_refund() -> Weight { + (18_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:0 w:1) + fn dispute_refund() -> Weight { + (19_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) + fn request_payment() -> Weight { + (20_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:1) - fn pay_with_remark(_x: u32, ) -> Weight { - (54_397_000 as Weight) + fn accept_and_pay() -> Weight { + (58_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Payment ScheduledTasks (r:2 w:0) + fn read_task() -> Weight { + (12_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + } + // Storage: Payment ScheduledTasks (r:0 w:1) + fn remove_task() -> Weight { + (2_000_000 as Weight) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn pay(_x: u32, ) -> Weight { + (63_805_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn release() -> Weight { - (34_000_000 as Weight) + (33_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } @@ -76,49 +140,35 @@ impl WeightInfo for () { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:0) fn cancel() -> Weight { - (46_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(3 as Weight)) - } - // Storage: Payment Payment (r:1 w:1) - // Storage: Assets Accounts (r:2 w:2) - // Storage: System Account (r:1 w:0) - fn resolve_cancel_payment() -> Weight { - (46_000_000 as Weight) + (51_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) - fn resolve_release_payment() -> Weight { - (35_000_000 as Weight) + fn resolve_payment() -> Weight { + (39_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:0 w:1) fn request_refund() -> Weight { - (17_000_000 as Weight) + (18_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } - // Storage: Payment Payment (r:1 w:1) - // Storage: Assets Accounts (r:2 w:2) - // Storage: System Account (r:1 w:0) - fn claim_refund() -> Weight { - (47_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:0 w:1) fn dispute_refund() -> Weight { - (16_000_000 as Weight) + (19_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) fn request_payment() -> Weight { - (18_000_000 as Weight) + (20_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } @@ -130,4 +180,14 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } + // Storage: Payment ScheduledTasks (r:2 w:0) + fn read_task() -> Weight { + (12_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + } + // Storage: Payment ScheduledTasks (r:0 w:1) + fn remove_task() -> Weight { + (2_000_000 as Weight) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } } \ No newline at end of file From 0e970e4bd37d973a2f879f19a63d79f1aed83be8 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sat, 12 Mar 2022 20:36:43 +0400 Subject: [PATCH 04/28] remove virto deps --- payments/Cargo.toml | 3 +- payments/src/lib.rs | 330 +++++++++++++++++++----------------------- payments/src/mock.rs | 16 +- payments/src/tests.rs | 210 ++++++++++++++++++++------- payments/src/types.rs | 37 +++-- 5 files changed, 333 insertions(+), 263 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index de32bbf39..0bf88ebe3 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -6,7 +6,7 @@ version = "0.4.1-dev" license = "Apache-2.0" homepage = "https://github.com/virto-network/virto-node" repository = "https://github.com/virto-network/virto-node" -description = "Allows users to post payment on-chain" +description = "Allows users to post escrow payment on-chain" readme = "README.md" [package.metadata.docs.rs] @@ -39,6 +39,7 @@ std = [ 'scale-info/std', 'orml-traits/std', 'frame-benchmarking/std', + 'orml-tokens/std' ] runtime-benchmarks = [ "frame-benchmarking", diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 8e7f88bf5..63dfa7469 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -8,19 +8,13 @@ mod mock; #[cfg(test)] mod tests; -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; - pub mod types; pub mod weights; #[frame_support::pallet] pub mod pallet { pub use crate::{ - types::{ - DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, - ScheduledTask, Task, - }, + types::{DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, ScheduledTask, Task}, weights::WeightInfo, }; use frame_support::{ @@ -35,16 +29,15 @@ pub mod pallet { }; use sp_std::vec::Vec; - pub type BalanceOf = - <::Asset as MultiCurrency<::AccountId>>::Balance; - pub type AssetIdOf = - <::Asset as MultiCurrency<::AccountId>>::CurrencyId; + pub type BalanceOf = <::Asset as MultiCurrency<::AccountId>>::Balance; + pub type AssetIdOf = <::Asset as MultiCurrency<::AccountId>>::CurrencyId; pub type BoundedDataOf = BoundedVec::MaxRemarkLength>; pub type ScheduledTaskOf = ScheduledTask<::BlockNumber>; #[pallet::config] pub trait Config: frame_system::Config { - /// Because this pallet emits events, it depends on the runtime's definition of an event. + /// Because this pallet emits events, it depends on the runtime's + /// definition of an event. type Event: From> + IsType<::Event>; /// the type of assets this pallet can hold in payment type Asset: MultiReservableCurrency; @@ -58,7 +51,8 @@ pub mod pallet { /// Maximum permitted size of `Remark` #[pallet::constant] type MaxRemarkLength: Get; - /// Buffer period - number of blocks to wait before user can claim canceled payment + /// Buffer period - number of blocks to wait before user can claim + /// canceled payment #[pallet::constant] type CancelBufferBlockLength: Get; //// Type representing the weight of this pallet @@ -71,11 +65,12 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn payment)] - /// Payments created by a user, this method of storageDoubleMap is chosen since there is no usecase for - /// listing payments by provider/currency. The payment will only be referenced by the creator in - /// any transaction of interest. - /// The storage map keys are the creator and the recipient, this also ensures - /// that for any (sender,recipient) combo, only a single payment is active. The history of payment is not stored. + /// Payments created by a user, this method of storageDoubleMap is chosen + /// since there is no usecase for listing payments by provider/currency. The + /// payment will only be referenced by the creator in any transaction of + /// interest. The storage map keys are the creator and the recipient, this + /// also ensures that for any (sender,recipient) combo, only a single + /// payment is active. The history of payment is not stored. pub(super) type Payment = StorageDoubleMap< _, Blake2_128Concat, @@ -112,7 +107,11 @@ pub mod pallet { /// Payment has been cancelled by the creator PaymentCancelled { from: T::AccountId, to: T::AccountId }, /// A payment that NeedsReview has been resolved by Judge - PaymentResolved { from: T::AccountId, to: T::AccountId, recipient_share: Percent }, + PaymentResolved { + from: T::AccountId, + to: T::AccountId, + recipient_share: Percent, + }, /// the payment creator has created a refund request PaymentCreatorRequestedRefund { from: T::AccountId, @@ -155,20 +154,18 @@ pub mod pallet { /// This function will look for any pending scheduled tasks that can /// be executed and will process them. fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight { - let mut task_list: Vec<(T::AccountId, T::AccountId, ScheduledTaskOf)> = - ScheduledTasks::::iter() - // leave out tasks in the future - .filter(|(_, _, ScheduledTask { when, .. })| when <= &now) - .collect(); + let mut task_list: Vec<(T::AccountId, T::AccountId, ScheduledTaskOf)> = ScheduledTasks::::iter() + // leave out tasks in the future + .filter(|(_, _, ScheduledTask { when, .. })| when <= &now) + .collect(); if task_list.is_empty() { - return remaining_weight + return remaining_weight; } else { task_list.sort_by(|(_, _, t), (_, _, x)| x.when.partial_cmp(&t.when).unwrap()); } - let cancel_weight = - T::WeightInfo::cancel().saturating_add(T::WeightInfo::remove_task()); + let cancel_weight = T::WeightInfo::cancel().saturating_add(T::WeightInfo::remove_task()); while remaining_weight >= cancel_weight { match task_list.pop() { @@ -189,7 +186,7 @@ pub mod pallet { from: from.clone(), to: to.clone(), }); - }, + } _ => return remaining_weight, } } @@ -200,11 +197,12 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// This allows any user to create a new payment, that releases only to specified recipient - /// The only action is to store the details of this payment in storage and reserve - /// the specified amount. User also has the option to add a remark, this remark - /// can then be used to run custom logic and trigger alternate payment flows. - /// the specified amount. + /// This allows any user to create a new payment, that releases only to + /// specified recipient The only action is to store the details of this + /// payment in storage and reserve the specified amount. User also has + /// the option to add a remark, this remark can then be used to run + /// custom logic and trigger alternate payment flows. the specified + /// amount. #[transactional] #[pallet::weight(T::WeightInfo::pay(T::MaxRemarkLength::get()))] pub fn pay( @@ -229,12 +227,17 @@ pub mod pallet { // reserve funds for payment >::reserve_payment_amount(&who, &recipient, payment_detail)?; // emit paymentcreated event - Self::deposit_event(Event::PaymentCreated { from: who, asset, amount, remark }); + Self::deposit_event(Event::PaymentCreated { + from: who, + asset, + amount, + remark, + }); Ok(().into()) } - /// Release any created payment, this will transfer the reserved amount from the - /// creator of the payment to the assigned recipient + /// Release any created payment, this will transfer the reserved amount + /// from the creator of the payment to the assigned recipient #[transactional] #[pallet::weight(T::WeightInfo::release())] pub fn release(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { @@ -248,19 +251,15 @@ pub mod pallet { } // release is a settle_payment with 100% recipient_share - >::settle_payment( - from.clone(), - to.clone(), - Percent::from_percent(100), - )?; + >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; Self::deposit_event(Event::PaymentReleased { from, to }); Ok(().into()) } - /// Cancel a payment in created state, this will release the reserved back to - /// creator of the payment. This extrinsic can only be called by the recipient - /// of the payment + /// Cancel a payment in created state, this will release the reserved + /// back to creator of the payment. This extrinsic can only be called by + /// the recipient of the payment #[transactional] #[pallet::weight(T::WeightInfo::cancel())] pub fn cancel(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { @@ -275,7 +274,7 @@ pub mod pallet { Percent::from_percent(0), )?; Self::deposit_event(Event::PaymentCancelled { from: creator, to: who }); - }, + } // if the payment is in state PaymentRequested, remove from storage PaymentState::PaymentRequested => Payment::::remove(&creator, &who), _ => fail!(Error::::InvalidAction), @@ -286,8 +285,8 @@ pub mod pallet { /// Allow judge to set state of a payment /// This extrinsic is used to resolve disputes between the creator and - /// recipient of the payment. This extrinsic allows the assigned judge to cancel/release/partial_release - /// the payment. + /// recipient of the payment. This extrinsic allows the assigned judge + /// to cancel/release/partial_release the payment. #[transactional] #[pallet::weight(T::WeightInfo::resolve_payment())] pub fn resolve_payment( @@ -302,74 +301,69 @@ pub mod pallet { ensure!(who == payment.resolver_account, Error::::InvalidAction) } // try to update the payment to new state - >::settle_payment( - from.clone(), - recipient.clone(), + >::settle_payment(from.clone(), recipient.clone(), recipient_share)?; + Self::deposit_event(Event::PaymentResolved { + from, + to: recipient, recipient_share, - )?; - Self::deposit_event(Event::PaymentResolved { from, to: recipient, recipient_share }); + }); Ok(().into()) } /// Allow payment creator to set payment to NeedsReview - /// This extrinsic is used to mark the payment as disputed so the assigned judge can tigger a resolution - /// and that the funds are no longer locked. + /// This extrinsic is used to mark the payment as disputed so the + /// assigned judge can tigger a resolution and that the funds are no + /// longer locked. #[transactional] #[pallet::weight(T::WeightInfo::request_refund())] - pub fn request_refund( - origin: OriginFor, - recipient: T::AccountId, - ) -> DispatchResultWithPostInfo { + pub fn request_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - Payment::::try_mutate( - who.clone(), - recipient.clone(), - |maybe_payment| -> DispatchResult { - // ensure the payment exists - let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; - // ensure the payment is not in needsreview state - ensure!( - payment.state != PaymentState::NeedsReview, - Error::::PaymentNeedsReview - ); - - // set the payment to requested refund - let current_block = frame_system::Pallet::::block_number(); - let cancel_block = current_block - .checked_add(&T::CancelBufferBlockLength::get()) - .ok_or(Error::::MathError)?; - - ScheduledTasks::::insert( - who.clone(), - recipient.clone(), - ScheduledTask { task: Task::Cancel, when: cancel_block }, - ); - - payment.state = PaymentState::RefundRequested { cancel_block }; - - Self::deposit_event(Event::PaymentCreatorRequestedRefund { - from: who, - to: recipient, - expiry: cancel_block, - }); + Payment::::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // ensure the payment is not in needsreview state + ensure!( + payment.state != PaymentState::NeedsReview, + Error::::PaymentNeedsReview + ); + + // set the payment to requested refund + let current_block = frame_system::Pallet::::block_number(); + let cancel_block = current_block + .checked_add(&T::CancelBufferBlockLength::get()) + .ok_or(Error::::MathError)?; + + ScheduledTasks::::insert( + who.clone(), + recipient.clone(), + ScheduledTask { + task: Task::Cancel, + when: cancel_block, + }, + ); - Ok(()) - }, - )?; + payment.state = PaymentState::RefundRequested { cancel_block }; + + Self::deposit_event(Event::PaymentCreatorRequestedRefund { + from: who, + to: recipient, + expiry: cancel_block, + }); + + Ok(()) + })?; Ok(().into()) } - /// Allow payment recipient to dispute the refund request from the payment creator - /// This does not cancel the request, instead sends the payment to a NeedsReview state - /// The assigned resolver account can then change the state of the payment after review. + /// Allow payment recipient to dispute the refund request from the + /// payment creator This does not cancel the request, instead sends the + /// payment to a NeedsReview state The assigned resolver account can + /// then change the state of the payment after review. #[transactional] #[pallet::weight(T::WeightInfo::dispute_refund())] - pub fn dispute_refund( - origin: OriginFor, - creator: T::AccountId, - ) -> DispatchResultWithPostInfo { + pub fn dispute_refund(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { use PaymentState::*; let who = ensure_signed(origin)?; @@ -392,11 +386,8 @@ pub mod pallet { // remove the payment from scheduled tasks ScheduledTasks::::remove(creator.clone(), who.clone()); - Self::deposit_event(Event::PaymentRefundDisputed { - from: creator, - to: who, - }); - }, + Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who }); + } _ => fail!(Error::::InvalidAction), } @@ -407,9 +398,10 @@ pub mod pallet { Ok(().into()) } - // Creates a new payment with the given details. This can be called by the recipient of the payment - // to create a payment and then completed by the sender using the `accept_and_pay` extrinsic. - // The payment will be in PaymentRequested State and can only be modified by the `accept_and_pay` extrinsic. + // Creates a new payment with the given details. This can be called by the + // recipient of the payment to create a payment and then completed by the sender + // using the `accept_and_pay` extrinsic. The payment will be in PaymentRequested + // State and can only be modified by the `accept_and_pay` extrinsic. #[transactional] #[pallet::weight(T::WeightInfo::request_payment())] pub fn request_payment( @@ -436,29 +428,26 @@ pub mod pallet { Ok(().into()) } - // This extrinsic allows the sender to fulfill a payment request created by a recipient. - // The amount will be transferred to the recipient and payment removed from storage + // This extrinsic allows the sender to fulfill a payment request created by a + // recipient. The amount will be transferred to the recipient and payment + // removed from storage #[transactional] #[pallet::weight(T::WeightInfo::accept_and_pay())] - pub fn accept_and_pay( - origin: OriginFor, - to: T::AccountId, - ) -> DispatchResultWithPostInfo { + pub fn accept_and_pay(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { let from = ensure_signed(origin)?; let payment = Payment::::get(&from, &to).ok_or(Error::::InvalidPayment)?; - ensure!(payment.state == PaymentState::PaymentRequested, Error::::InvalidAction); + ensure!( + payment.state == PaymentState::PaymentRequested, + Error::::InvalidAction + ); // reserve all the fees from the sender >::reserve_payment_amount(&from, &to, payment)?; // release the payment and delete the payment from storage - >::settle_payment( - from.clone(), - to.clone(), - Percent::from_percent(100), - )?; + >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; Self::deposit_event(Event::PaymentRequestCompleted { from, to }); @@ -467,8 +456,9 @@ pub mod pallet { } impl PaymentHandler for Pallet { - /// The function will create a new payment. The fee and incentive amounts will be calculated and the - /// `PaymentDetail` will be added to storage. + /// The function will create a new payment. The fee and incentive + /// amounts will be calculated and the `PaymentDetail` will be added to + /// storage. #[require_transactional] fn create_payment( from: T::AccountId, @@ -523,14 +513,11 @@ pub mod pallet { ) } - /// The function will reserve the fees+transfer amount from the `from` account. After reserving - /// the payment.amount will be transferred to the recipient but will stay in Reserve state. + /// The function will reserve the fees+transfer amount from the `from` + /// account. After reserving the payment.amount will be transferred to + /// the recipient but will stay in Reserve state. #[require_transactional] - fn reserve_payment_amount( - from: &T::AccountId, - to: &T::AccountId, - payment: PaymentDetail, - ) -> DispatchResult { + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult { let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or(0u32.into()); let total_fee_amount = payment.incentive_amount.saturating_add(fee_amount); @@ -539,67 +526,50 @@ pub mod pallet { // reserve the total amount from payment creator T::Asset::reserve(payment.asset, from, total_amount)?; // transfer payment amount to recipient -- keeping reserve status - T::Asset::repatriate_reserved( - payment.asset, - from, - to, - payment.amount, - BalanceStatus::Reserved, - )?; + T::Asset::repatriate_reserved(payment.asset, from, to, payment.amount, BalanceStatus::Reserved)?; Ok(()) } - /// This function allows the caller to settle the payment by specifying a recipient_share - /// this will unreserve the fee+incentive to sender and unreserve transferred amount to recipient - /// if the settlement is a release (ie recipient_share=100), the fee is transferred to fee_recipient - /// For cancelling a payment, recipient_share = 0 + /// This function allows the caller to settle the payment by specifying + /// a recipient_share this will unreserve the fee+incentive to sender + /// and unreserve transferred amount to recipient if the settlement is a + /// release (ie recipient_share=100), the fee is transferred to + /// fee_recipient For cancelling a payment, recipient_share = 0 /// For releasing a payment, recipient_share = 100 /// In other cases, the custom recipient_share can be specified - fn settle_payment( - from: T::AccountId, - to: T::AccountId, - recipient_share: Percent, - ) -> DispatchResult { - Payment::::try_mutate( - from.clone(), - to.clone(), - |maybe_payment| -> DispatchResult { - let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; - - // unreserve the incentive amount and fees from the owner account - match payment.fee_detail { - Some((fee_recipient, fee_amount)) => { - T::Asset::unreserve( + fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult { + Payment::::try_mutate(from.clone(), to.clone(), |maybe_payment| -> DispatchResult { + let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; + + // unreserve the incentive amount and fees from the owner account + match payment.fee_detail { + Some((fee_recipient, fee_amount)) => { + T::Asset::unreserve(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, - 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 - )?; - } - }, - None => { - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); - }, - }; + &from, // fee is paid by payment creator + &fee_recipient, // account of fee recipient + fee_amount, // amount of fee + )?; + } + } + None => { + T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + } + }; - // Unreserve the transfer amount - T::Asset::unreserve(payment.asset, &to, payment.amount); + // Unreserve the transfer 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); - // send share to recipient - T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; + let amount_to_recipient = recipient_share.mul_floor(payment.amount); + let amount_to_sender = payment.amount.saturating_sub(amount_to_recipient); + // send share to recipient + T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; - Ok(()) - }, - )?; + Ok(()) + })?; Ok(()) } diff --git a/payments/src/mock.rs b/payments/src/mock.rs index 0a5646ab5..dce2b40f1 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -13,7 +13,6 @@ use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, Percent, }; -use virto_primitives::{Asset, NetworkAsset}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -24,7 +23,7 @@ pub const PAYMENT_CREATOR: AccountId = 10; pub const PAYMENT_RECIPENT: AccountId = 11; pub const PAYMENT_CREATOR_TWO: AccountId = 30; pub const PAYMENT_RECIPENT_TWO: AccountId = 31; -pub const CURRENCY_ID: Asset = Asset::Network(NetworkAsset::KSM); +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; @@ -77,7 +76,7 @@ impl system::Config for Test { } parameter_type_with_key! { - pub ExistentialDeposits: |_currency_id: Asset| -> Balance { + pub ExistentialDeposits: |_currency_id: u32| -> Balance { 0u128 }; } @@ -95,7 +94,7 @@ impl Contains for MockDustRemovalWhitelist { impl orml_tokens::Config for Test { type Amount = i64; type Balance = Balance; - type CurrencyId = Asset; + type CurrencyId = u32; type Event = Event; type ExistentialDeposits = ExistentialDeposits; type OnDust = (); @@ -120,8 +119,7 @@ impl crate::types::FeeHandler for MockFeeHandler { _remark: Option<&[u8]>, ) -> (AccountId, Percent) { match to { - &PAYMENT_RECIPENT_FEE_CHARGED => - (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE)), + &PAYMENT_RECIPENT_FEE_CHARGED => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE)), _ => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(0)), } } @@ -173,7 +171,11 @@ pub fn run_n_blocks(n: u64) -> u64 { System::set_block_number(block_number); // Odd blocks gets busy - let idle_weight = if block_number % 2 == 0 { IDLE_WEIGHT } else { BUSY_WEIGHT }; + let idle_weight = if block_number % 2 == 0 { + IDLE_WEIGHT + } else { + BUSY_WEIGHT + }; // ensure the on_idle is executed >::register_extra_weight_unchecked( Payment::on_idle(block_number, idle_weight), diff --git a/payments/src/tests.rs b/payments/src/tests.rs index 3a58ceefe..666b2fd21 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -21,7 +21,10 @@ fn test_pay_works() { let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should be able to create a payment with available balance @@ -133,11 +136,17 @@ fn test_cancel_works() { assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); assert_eq!( last_event(), - crate::Event::::PaymentCancelled { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } - .into() + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() ); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should be released from storage @@ -182,8 +191,11 @@ fn test_release_works() { assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); assert_eq!( last_event(), - crate::Event::::PaymentReleased { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } - .into() + crate::Event::::PaymentReleased { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() ); // the payment amount should be transferred assert_eq!( @@ -332,14 +344,15 @@ fn test_charging_fee_payment_works() { // the payment amount should be reserved assert_eq!( Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), - creator_initial_balance - - payment_amount - expected_fee_amount - - expected_incentive_amount + creator_initial_balance - payment_amount - expected_fee_amount - expected_incentive_amount ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); // should succeed for valid payment - assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED)); + assert_ok!(Payment::release( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED + )); // the payment amount should be transferred assert_eq!( Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), @@ -353,7 +366,10 @@ fn test_charging_fee_payment_works() { Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), payment_amount ); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); }); } @@ -387,16 +403,20 @@ fn test_charging_fee_payment_works_when_canceled() { // the payment amount should be reserved assert_eq!( Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), - creator_initial_balance - - payment_amount - expected_fee_amount - - expected_incentive_amount + creator_initial_balance - payment_amount - expected_fee_amount - expected_incentive_amount ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); // should succeed for valid payment - assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), PAYMENT_CREATOR)); + assert_ok!(Payment::cancel( + Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), + PAYMENT_CREATOR + )); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 0); @@ -537,7 +557,10 @@ fn test_request_refund() { None )); - assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), @@ -545,7 +568,9 @@ fn test_request_refund() { asset: CURRENCY_ID, amount: payment_amount, incentive_amount: expected_incentive_amount, - state: PaymentState::RefundRequested { cancel_block: expected_cancel_block }, + state: PaymentState::RefundRequested { + cancel_block: expected_cancel_block + }, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), }) @@ -584,15 +609,24 @@ fn test_dispute_refund() { Error::InvalidAction ); // creator requests a refund - assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); // ensure the request is added to the refund queue assert_eq!( ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { task: Task::Cancel, when: expected_cancel_block } + ScheduledTask { + task: Task::Cancel, + when: expected_cancel_block + } ); // recipient disputes the refund request - assert_ok!(Payment::dispute_refund(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); + assert_ok!(Payment::dispute_refund( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR + )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), @@ -721,7 +755,10 @@ fn test_accept_and_pay() { }) ); - assert_ok!(Payment::accept_and_pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT,)); + assert_ok!(Payment::accept_and_pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + )); // the payment amount should be transferred assert_eq!( @@ -803,10 +840,16 @@ fn test_accept_and_pay_should_charge_fee_correctly() { Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), payment_amount ); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); // should be deleted from storage - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); assert_eq!( last_event(), @@ -844,10 +887,14 @@ fn test_create_payment_works() { let expected_fee_amount = 0; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( PAYMENT_CREATOR, @@ -914,7 +961,8 @@ fn test_reserve_payment_amount_works() { assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( PAYMENT_CREATOR, @@ -998,10 +1046,14 @@ fn test_settle_payment_works_for_cancel() { let payment_amount = 20; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, @@ -1019,7 +1071,10 @@ fn test_settle_payment_works_for_cancel() { }))); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should be released from storage @@ -1034,10 +1089,14 @@ fn test_settle_payment_works_for_release() { let payment_amount = 20; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, @@ -1074,10 +1133,14 @@ fn test_settle_payment_works_for_70_30() { let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, @@ -1094,9 +1157,8 @@ fn test_settle_payment_works_for_70_30() { ) }))); - let expected_amount_for_creator = - creator_initial_balance - payment_amount - expected_fee_amount + - (Percent::from_percent(30) * payment_amount); + let expected_amount_for_creator = creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(30) * payment_amount); let expected_amount_for_recipient = Percent::from_percent(70) * payment_amount; // the payment amount should be transferred @@ -1108,10 +1170,16 @@ fn test_settle_payment_works_for_70_30() { Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), expected_amount_for_recipient ); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); // should be deleted from storage - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); }); } @@ -1126,7 +1194,8 @@ fn test_settle_payment_works_for_50_50() { assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, @@ -1143,9 +1212,8 @@ fn test_settle_payment_works_for_50_50() { ) }))); - let expected_amount_for_creator = - creator_initial_balance - payment_amount - expected_fee_amount + - (Percent::from_percent(50) * payment_amount); + let expected_amount_for_creator = creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(50) * payment_amount); let expected_amount_for_recipient = Percent::from_percent(50) * payment_amount; // the payment amount should be transferred @@ -1157,10 +1225,16 @@ fn test_settle_payment_works_for_50_50() { Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), expected_amount_for_recipient ); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); // should be deleted from storage - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); }); } @@ -1181,7 +1255,10 @@ fn test_automatic_refund_works() { None )); - assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), @@ -1189,7 +1266,9 @@ fn test_automatic_refund_works() { asset: CURRENCY_ID, amount: payment_amount, incentive_amount: expected_incentive_amount, - state: PaymentState::RefundRequested { cancel_block: CANCEL_BLOCK }, + state: PaymentState::RefundRequested { + cancel_block: CANCEL_BLOCK + }, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), }) @@ -1197,14 +1276,20 @@ fn test_automatic_refund_works() { assert_eq!( ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } + ScheduledTask { + task: Task::Cancel, + when: CANCEL_BLOCK + } ); // run to one block before cancel and make sure data is same assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 600); assert_eq!( ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } + ScheduledTask { + task: Task::Cancel, + when: CANCEL_BLOCK + } ); // run to after cancel block but odd blocks are busy @@ -1223,11 +1308,17 @@ fn test_automatic_refund_works() { // test that the refund happened correctly assert_eq!( last_event(), - crate::Event::::PaymentCancelled { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } - .into() + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() ); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); }); } @@ -1253,7 +1344,10 @@ fn test_automatic_refund_works_for_multiple_payments() { None )); - assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); run_n_blocks(1); assert_ok!(Payment::request_refund( Origin::signed(PAYMENT_CREATOR_TWO), @@ -1268,11 +1362,17 @@ fn test_automatic_refund_works_for_multiple_payments() { // Even block 602 has enough room to process both pending payments assert_eq!(run_n_blocks(1), 602); assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), None); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), + None + ); // the scheduled storage should be cleared assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); - assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), None); + assert_eq!( + ScheduledTasks::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), + None + ); // test that the refund happened correctly assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); diff --git a/payments/src/types.rs b/payments/src/types.rs index 290c67ce8..60991e607 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -5,9 +5,10 @@ use scale_info::TypeInfo; use sp_runtime::{DispatchResult, Percent}; /// The PaymentDetail struct stores information about the payment/escrow -/// A "payment" in virto network is similar to an escrow, it is used to guarantee proof of funds -/// and can be released once an agreed upon condition has reached between the payment creator -/// and recipient. The payment lifecycle is tracked using the state field. +/// A "payment" in virto network is similar to an escrow, it is used to +/// guarantee proof of funds and can be released once an agreed upon condition +/// has reached between the payment creator and recipient. The payment lifecycle +/// is tracked using the state field. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound(T: pallet::Config))] @@ -20,7 +21,8 @@ pub struct PaymentDetail { /// incentive amount that is credited to creator for resolving #[codec(compact)] pub incentive_amount: BalanceOf, - /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, Requested] + /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, + /// Requested] pub state: PaymentState, /// account that can settle any disputes created in the payment pub resolver_account: T::AccountId, @@ -29,7 +31,8 @@ pub struct PaymentDetail { } /// The `PaymentState` enum tracks the possible states that a payment can be in. -/// When a payment is 'completed' or 'cancelled' it is removed from storage and hence not tracked by a state. +/// When a payment is 'completed' or 'cancelled' it is removed from storage and +/// hence not tracked by a state. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum PaymentState { @@ -62,23 +65,15 @@ pub trait PaymentHandler { /// Attempt to reserve an amount of the given asset from the caller /// If not possible then return Error. Possible reasons for failure include: /// - User does not have enough balance. - fn reserve_payment_amount( - from: &T::AccountId, - to: &T::AccountId, - payment: PaymentDetail, - ) -> DispatchResult; + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult; - // Settle a payment of `from` to `to`. To release a payment, the recipient_share=100, - // to cancel a payment recipient_share=0 + // Settle a payment of `from` to `to`. To release a payment, the + // recipient_share=100, to cancel a payment recipient_share=0 // Possible reasonse for failure include /// - The payment does not exist /// - The unreserve operation fails /// - The transfer operation fails - fn settle_payment( - from: T::AccountId, - to: T::AccountId, - recipient_share: Percent, - ) -> DispatchResult; + fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult; /// Attempt to fetch the details of a payment from the given payment_id /// Possible reasons for failure include: @@ -86,13 +81,15 @@ pub trait PaymentHandler { fn get_payment_details(from: &T::AccountId, to: &T::AccountId) -> Option>; } -/// DisputeResolver trait defines how to create/assign judges for solving payment disputes +/// DisputeResolver trait defines how to create/assign judges for solving +/// payment disputes pub trait DisputeResolver { /// Returns an `Account` fn get_resolver_account() -> Account; } -/// Fee Handler trait that defines how to handle marketplace fees to every payment/swap +/// Fee Handler trait that defines how to handle marketplace fees to every +/// payment/swap pub trait FeeHandler { /// Get the distribution of fees to marketplace participants fn apply_fees( @@ -118,4 +115,4 @@ pub struct ScheduledTask { /// the 'time' at which the task should be executed #[codec(compact)] pub when: Time, -} \ No newline at end of file +} From 36489ac589be8146ff54e808b327b2a0944a489d Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Fri, 18 Mar 2022 10:59:07 +0400 Subject: [PATCH 05/28] trigger ci From c59bad34d8073dad63a76a0c85947f6fd97d45a8 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Fri, 18 Mar 2022 11:15:37 +0400 Subject: [PATCH 06/28] cleanup cargo.toml --- payments/Cargo.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 0bf88ebe3..55dca6654 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -9,9 +9,6 @@ repository = "https://github.com/virto-network/virto-node" description = "Allows users to post escrow payment on-chain" readme = "README.md" -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - [dependencies] parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } @@ -41,6 +38,3 @@ std = [ 'frame-benchmarking/std', 'orml-tokens/std' ] -runtime-benchmarks = [ - "frame-benchmarking", -] From 94756b1dec952a85a6929cf9cd475e0109ee90cb Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sat, 19 Mar 2022 15:19:32 +0400 Subject: [PATCH 07/28] sync latest version --- payments/Cargo.toml | 2 + payments/src/lib.rs | 112 ++++++++++++++++++++++++---------------- payments/src/mock.rs | 2 + payments/src/tests.rs | 36 +++++++------ payments/src/weights.rs | 68 +++++++++++------------- 5 files changed, 123 insertions(+), 97 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 55dca6654..820252d5d 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -11,6 +11,7 @@ readme = "README.md" [dependencies] parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } +log = { version = "0.4.14", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } @@ -31,6 +32,7 @@ std = [ 'parity-scale-codec/std', 'frame-support/std', 'frame-system/std', + 'log/std', 'sp-runtime/std', 'sp-std/std', 'scale-info/std', diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 63dfa7469..7f9e2a55f 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -19,7 +19,7 @@ pub mod pallet { }; use frame_support::{ dispatch::DispatchResultWithPostInfo, fail, pallet_prelude::*, require_transactional, - traits::tokens::BalanceStatus, transactional, + storage::bounded_btree_map::BoundedBTreeMap, traits::tokens::BalanceStatus, transactional, }; use frame_system::pallet_prelude::*; use orml_traits::{MultiCurrency, MultiReservableCurrency}; @@ -32,7 +32,17 @@ pub mod pallet { pub type BalanceOf = <::Asset as MultiCurrency<::AccountId>>::Balance; pub type AssetIdOf = <::Asset as MultiCurrency<::AccountId>>::CurrencyId; pub type BoundedDataOf = BoundedVec::MaxRemarkLength>; + /// type of ScheduledTask used by the pallet pub type ScheduledTaskOf = ScheduledTask<::BlockNumber>; + /// list of ScheduledTasks, stored as a BoundedBTreeMap + pub type ScheduledTaskList = BoundedBTreeMap< + ( + ::AccountId, + ::AccountId, + ), + ScheduledTaskOf, + ::MaxRemarkLength, + >; #[pallet::config] pub trait Config: frame_system::Config { @@ -55,6 +65,10 @@ pub mod pallet { /// canceled payment #[pallet::constant] type CancelBufferBlockLength: Get; + /// Buffer period - number of blocks to wait before user can claim + /// canceled payment + #[pallet::constant] + type MaxScheduledTaskListLength: Get; //// Type representing the weight of this pallet type WeightInfo: WeightInfo; } @@ -83,14 +97,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn tasks)] /// Store the list of tasks to be executed in the on_idle function - pub(super) type ScheduledTasks = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, // payment creator - Blake2_128Concat, - T::AccountId, // payment recipient - ScheduledTaskOf, - >; + pub(super) type ScheduledTasks = StorageValue<_, ScheduledTaskList, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -154,43 +161,51 @@ pub mod pallet { /// This function will look for any pending scheduled tasks that can /// be executed and will process them. fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight { - let mut task_list: Vec<(T::AccountId, T::AccountId, ScheduledTaskOf)> = ScheduledTasks::::iter() - // leave out tasks in the future - .filter(|(_, _, ScheduledTask { when, .. })| when <= &now) - .collect(); + // reduce the weight used to read the task list + remaining_weight = remaining_weight.saturating_sub(T::WeightInfo::remove_task()); - if task_list.is_empty() { - return remaining_weight; - } else { - task_list.sort_by(|(_, _, t), (_, _, x)| x.when.partial_cmp(&t.when).unwrap()); - } + ScheduledTasks::::mutate(|tasks| { + let mut task_list: Vec<_> = tasks + .clone() + .into_iter() + // leave out tasks in the future + .filter(|(_, ScheduledTask { when, .. })| when <= &now) + .collect(); - let cancel_weight = T::WeightInfo::cancel().saturating_add(T::WeightInfo::remove_task()); + // order by oldest task to process + task_list.sort_by(|(_, t), (_, x)| x.when.partial_cmp(&t.when).unwrap()); - while remaining_weight >= cancel_weight { - match task_list.pop() { - Some((from, to, ScheduledTask { task: Task::Cancel, .. })) => { + let cancel_weight = T::WeightInfo::cancel(); + + while !task_list.is_empty() && remaining_weight >= cancel_weight { + if let Some(((from, to), ScheduledTask { task: Task::Cancel, .. })) = task_list.pop() { remaining_weight = remaining_weight.saturating_sub(cancel_weight); + // remove the task form the tasks + tasks.remove(&(from.clone(), to.clone())); // process the cancel payment - if let Err(_) = >::settle_payment( + if >::settle_payment( from.clone(), to.clone(), Percent::from_percent(0), - ) { - // panic!("{:?}", e); + ) + .is_err() + { + // log the payment refund failure + log::warn!( + target: "runtime::payments", + "Warning: Unable to process payment refund!" + ); + } else { + // emit the cancel event if the refund was successful + Self::deposit_event(Event::PaymentCancelled { + from: from.clone(), + to: to.clone(), + }); } - ScheduledTasks::::remove(from.clone(), to.clone()); - // emit the cancel event - Self::deposit_event(Event::PaymentCancelled { - from: from.clone(), - to: to.clone(), - }); } - _ => return remaining_weight, } - } - + }); remaining_weight } } @@ -334,14 +349,18 @@ pub mod pallet { .checked_add(&T::CancelBufferBlockLength::get()) .ok_or(Error::::MathError)?; - ScheduledTasks::::insert( - who.clone(), - recipient.clone(), - ScheduledTask { - task: Task::Cancel, - when: cancel_block, - }, - ); + ScheduledTasks::::try_mutate(|task_list| -> DispatchResult { + task_list + .try_insert( + (who.clone(), recipient.clone()), + ScheduledTask { + task: Task::Cancel, + when: cancel_block, + }, + ) + .map_err(|_| Error::::RefundQueueFull)?; + Ok(()) + })?; payment.state = PaymentState::RefundRequested { cancel_block }; @@ -384,7 +403,12 @@ pub mod pallet { payment.state = PaymentState::NeedsReview; // remove the payment from scheduled tasks - ScheduledTasks::::remove(creator.clone(), who.clone()); + ScheduledTasks::::try_mutate(|task_list| -> DispatchResult { + task_list + .remove(&(creator.clone(), who.clone())) + .ok_or(Error::::InvalidAction)?; + Ok(()) + })?; Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who }); } @@ -518,7 +542,7 @@ 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) -> DispatchResult { - let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or(0u32.into()); + 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); diff --git a/payments/src/mock.rs b/payments/src/mock.rs index dce2b40f1..ca2aaea49 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -129,6 +129,7 @@ parameter_types! { pub const IncentivePercentage: Percent = Percent::from_percent(INCENTIVE_PERCENTAGE); pub const MaxRemarkLength: u32 = 50; pub const CancelBufferBlockLength: u64 = CANCEL_BLOCK_BUFFER; + pub const MaxScheduledTaskListLength : u32 = 5; } impl payment::Config for Test { @@ -139,6 +140,7 @@ impl payment::Config for Test { type FeeHandler = MockFeeHandler; type MaxRemarkLength = MaxRemarkLength; type CancelBufferBlockLength = CancelBufferBlockLength; + type MaxScheduledTaskListLength = MaxScheduledTaskListLength; type WeightInfo = (); } diff --git a/payments/src/tests.rs b/payments/src/tests.rs index 666b2fd21..3f996d4f7 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -614,9 +614,10 @@ fn test_dispute_refund() { PAYMENT_RECIPENT )); // ensure the request is added to the refund queue + let scheduled_tasks_list = ScheduledTasks::::get(); assert_eq!( - ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { task: Task::Cancel, when: expected_cancel_block } @@ -649,8 +650,9 @@ fn test_dispute_refund() { .into() ); - // ensure the request is added to the refund queue - assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + // ensure the request is removed from the refund queue + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)), None); }); } @@ -903,7 +905,7 @@ fn test_create_payment_works() { payment_amount, PaymentState::Created, Percent::from_percent(INCENTIVE_PERCENTAGE), - Some(&vec![1u8; 10]), + Some(&[1u8; 10]), ) }))); @@ -929,7 +931,7 @@ fn test_create_payment_works() { payment_amount, PaymentState::Created, Percent::from_percent(INCENTIVE_PERCENTAGE), - Some(&vec![1u8; 10]), + Some(&[1u8; 10]), ) })), Error::PaymentAlreadyInProcess @@ -971,7 +973,7 @@ fn test_reserve_payment_amount_works() { payment_amount, PaymentState::Created, Percent::from_percent(INCENTIVE_PERCENTAGE), - Some(&vec![1u8; 10]), + Some(&[1u8; 10]), ) }))); @@ -1019,7 +1021,7 @@ fn test_reserve_payment_amount_works() { payment_amount, PaymentState::Created, Percent::from_percent(INCENTIVE_PERCENTAGE), - Some(&vec![1u8; 10]), + Some(&[1u8; 10]), ) })), Error::PaymentAlreadyInProcess @@ -1274,9 +1276,10 @@ fn test_automatic_refund_works() { }) ); + let scheduled_tasks_list = ScheduledTasks::::get(); assert_eq!( - ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } @@ -1284,9 +1287,10 @@ fn test_automatic_refund_works() { // run to one block before cancel and make sure data is same assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 600); + let scheduled_tasks_list = ScheduledTasks::::get(); assert_eq!( - ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } @@ -1303,7 +1307,8 @@ fn test_automatic_refund_works() { assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); // the scheduled storage should be cleared - assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)), None); // test that the refund happened correctly assert_eq!( @@ -1368,9 +1373,10 @@ fn test_automatic_refund_works_for_multiple_payments() { ); // the scheduled storage should be cleared - assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)), None); assert_eq!( - ScheduledTasks::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), + scheduled_tasks_list.get(&(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO)), None ); diff --git a/payments/src/weights.rs b/payments/src/weights.rs index df20c5312..941293c60 100644 --- a/payments/src/weights.rs +++ b/payments/src/weights.rs @@ -1,7 +1,7 @@ //! Autogenerated weights for virto_payment //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-03-11, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-03-19, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: @@ -25,6 +25,7 @@ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] #![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; use sp_std::marker::PhantomData; @@ -39,7 +40,6 @@ pub trait WeightInfo { fn dispute_refund() -> Weight; fn request_payment() -> Weight; fn accept_and_pay() -> Weight; - fn read_task() -> Weight; fn remove_task() -> Weight; } @@ -51,14 +51,14 @@ impl WeightInfo for SubstrateWeight { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:1) fn pay(_x: u32, ) -> Weight { - (63_805_000 as Weight) + (55_900_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn release() -> Weight { - (33_000_000 as Weight) + (36_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } @@ -66,35 +66,35 @@ impl WeightInfo for SubstrateWeight { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:0) fn cancel() -> Weight { - (51_000_000 as Weight) + (48_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn resolve_payment() -> Weight { - (39_000_000 as Weight) + (35_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn request_refund() -> Weight { - (18_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(1 as Weight)) + (20_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn dispute_refund() -> Weight { - (19_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(1 as Weight)) + (21_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) fn request_payment() -> Weight { - (20_000_000 as Weight) + (17_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } @@ -106,14 +106,10 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } - // Storage: Payment ScheduledTasks (r:2 w:0) - fn read_task() -> Weight { - (12_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - } - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn remove_task() -> Weight { - (2_000_000 as Weight) + (4_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } } @@ -125,14 +121,14 @@ impl WeightInfo for () { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:1) fn pay(_x: u32, ) -> Weight { - (63_805_000 as Weight) + (55_900_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn release() -> Weight { - (33_000_000 as Weight) + (36_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } @@ -140,35 +136,35 @@ impl WeightInfo for () { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:0) fn cancel() -> Weight { - (51_000_000 as Weight) + (48_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn resolve_payment() -> Weight { - (39_000_000 as Weight) + (35_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn request_refund() -> Weight { - (18_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + (20_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn dispute_refund() -> Weight { - (19_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + (21_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) fn request_payment() -> Weight { - (20_000_000 as Weight) + (17_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } @@ -180,14 +176,10 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } - // Storage: Payment ScheduledTasks (r:2 w:0) - fn read_task() -> Weight { - (12_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - } - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn remove_task() -> Weight { - (2_000_000 as Weight) + (4_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } } \ No newline at end of file From 86dbb6051315125f5d2f625eef857910d012855b Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sun, 20 Mar 2022 13:00:30 +0400 Subject: [PATCH 08/28] cargo fmt --- payments/src/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/src/types.rs b/payments/src/types.rs index 60991e607..826d1a169 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -70,6 +70,7 @@ pub trait PaymentHandler { // Settle a payment of `from` to `to`. To release a payment, the // recipient_share=100, to cancel a payment recipient_share=0 // Possible reasonse for failure include + /// /// - The payment does not exist /// - The unreserve operation fails /// - The transfer operation fails From 36974ca4517d05a131b12ffcc3cc61e9eaf3f2a5 Mon Sep 17 00:00:00 2001 From: Daniel Olano Date: Mon, 21 Mar 2022 09:37:41 +0100 Subject: [PATCH 09/28] Reduce clones & Handle state change inconsistencies --- payments/src/lib.rs | 115 ++++++++++++++++++++---------------------- payments/src/tests.rs | 57 +++++++++++++-------- payments/src/types.rs | 17 ++++--- 3 files changed, 103 insertions(+), 86 deletions(-) diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 7f9e2a55f..4c02ef76d 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -169,7 +169,7 @@ pub mod pallet { .clone() .into_iter() // leave out tasks in the future - .filter(|(_, ScheduledTask { when, .. })| when <= &now) + .filter(|(_, ScheduledTask { when, task })| when <= &now && matches!(task, Task::Cancel)) .collect(); // order by oldest task to process @@ -178,15 +178,15 @@ pub mod pallet { let cancel_weight = T::WeightInfo::cancel(); while !task_list.is_empty() && remaining_weight >= cancel_weight { - if let Some(((from, to), ScheduledTask { task: Task::Cancel, .. })) = task_list.pop() { + if let Some((account_pair, _)) = task_list.pop() { remaining_weight = remaining_weight.saturating_sub(cancel_weight); // remove the task form the tasks - tasks.remove(&(from.clone(), to.clone())); + tasks.remove(&account_pair); // process the cancel payment if >::settle_payment( - from.clone(), - to.clone(), + &account_pair.0, + &account_pair.1, Percent::from_percent(0), ) .is_err() @@ -199,8 +199,8 @@ pub mod pallet { } else { // emit the cancel event if the refund was successful Self::deposit_event(Event::PaymentCancelled { - from: from.clone(), - to: to.clone(), + from: account_pair.0, + to: account_pair.1, }); } } @@ -231,8 +231,8 @@ pub mod pallet { // create PaymentDetail and add to storage let payment_detail = >::create_payment( - who.clone(), - recipient.clone(), + &who, + &recipient, asset, amount, PaymentState::Created, @@ -266,7 +266,7 @@ pub mod pallet { } // release is a settle_payment with 100% recipient_share - >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + >::settle_payment(&from, &to, Percent::from_percent(100))?; Self::deposit_event(Event::PaymentReleased { from, to }); Ok(().into()) @@ -283,11 +283,7 @@ pub mod pallet { match payment.state { // call settle payment with recipient_share=0, this refunds the sender PaymentState::Created => { - >::settle_payment( - creator.clone(), - who.clone(), - Percent::from_percent(0), - )?; + >::settle_payment(&creator, &who, Percent::from_percent(0))?; Self::deposit_event(Event::PaymentCancelled { from: creator, to: who }); } // if the payment is in state PaymentRequested, remove from storage @@ -298,10 +294,10 @@ pub mod pallet { Ok(().into()) } - /// Allow judge to set state of a payment /// This extrinsic is used to resolve disputes between the creator and - /// recipient of the payment. This extrinsic allows the assigned judge - /// to cancel/release/partial_release the payment. + /// recipient of the payment. + /// This extrinsic allows the assigned judge to + /// cancel/release/partial_release the payment. #[transactional] #[pallet::weight(T::WeightInfo::resolve_payment())] pub fn resolve_payment( @@ -311,24 +307,33 @@ pub mod pallet { recipient_share: Percent, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; + let account_pair = (from, recipient); // ensure the caller is the assigned resolver - if let Some(payment) = Payment::::get(&from, &recipient) { - ensure!(who == payment.resolver_account, Error::::InvalidAction) + if let Some(payment) = Payment::::get(&account_pair.0, &account_pair.1) { + ensure!(who == payment.resolver_account, Error::::InvalidAction); + ensure!( + payment.state != PaymentState::PaymentRequested, + Error::::InvalidAction + ); + if matches!(payment.state, PaymentState::RefundRequested { .. }) { + ScheduledTasks::::mutate(|tasks| { + tasks.remove(&account_pair); + }) + } } // try to update the payment to new state - >::settle_payment(from.clone(), recipient.clone(), recipient_share)?; + >::settle_payment(&account_pair.0, &account_pair.1, recipient_share)?; Self::deposit_event(Event::PaymentResolved { - from, - to: recipient, + from: account_pair.0, + to: account_pair.1, recipient_share, }); Ok(().into()) } - /// Allow payment creator to set payment to NeedsReview - /// This extrinsic is used to mark the payment as disputed so the - /// assigned judge can tigger a resolution and that the funds are no - /// longer locked. + /// Allow the creator of a payment to initiate a refund that will return + /// the funds after a configured amount of time that the reveiver has to + /// react and opose the request #[transactional] #[pallet::weight(T::WeightInfo::request_refund())] pub fn request_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { @@ -337,11 +342,8 @@ pub mod pallet { Payment::::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult { // ensure the payment exists let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; - // ensure the payment is not in needsreview state - ensure!( - payment.state != PaymentState::NeedsReview, - Error::::PaymentNeedsReview - ); + // refunds only possible for payments in created state + ensure!(payment.state == PaymentState::Created, Error::::InvalidAction); // set the payment to requested refund let current_block = frame_system::Pallet::::block_number(); @@ -424,8 +426,9 @@ pub mod pallet { // Creates a new payment with the given details. This can be called by the // recipient of the payment to create a payment and then completed by the sender - // using the `accept_and_pay` extrinsic. The payment will be in PaymentRequested - // State and can only be modified by the `accept_and_pay` extrinsic. + // using the `accept_and_pay` extrinsic. The payment will be in + // PaymentRequested State and can only be modified by the `accept_and_pay` + // extrinsic. #[transactional] #[pallet::weight(T::WeightInfo::request_payment())] pub fn request_payment( @@ -438,8 +441,8 @@ pub mod pallet { // create PaymentDetail and add to storage >::create_payment( - from.clone(), - to.clone(), + &from, + &to, asset, amount, PaymentState::PaymentRequested, @@ -471,7 +474,7 @@ pub mod pallet { >::reserve_payment_amount(&from, &to, payment)?; // release the payment and delete the payment from storage - >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + >::settle_payment(&from, &to, Percent::from_percent(100))?; Self::deposit_event(Event::PaymentRequestCompleted { from, to }); @@ -485,29 +488,24 @@ pub mod pallet { /// storage. #[require_transactional] fn create_payment( - from: T::AccountId, - recipient: T::AccountId, + from: &T::AccountId, + recipient: &T::AccountId, asset: AssetIdOf, amount: BalanceOf, - payment_state: PaymentState, + payment_state: PaymentState, incentive_percentage: Percent, remark: Option<&[u8]>, ) -> Result, sp_runtime::DispatchError> { Payment::::try_mutate( - from.clone(), - recipient.clone(), + from, + recipient, |maybe_payment| -> Result, sp_runtime::DispatchError> { - if maybe_payment.is_some() { - // ensure the payment is not in created/needsreview state - let current_state = maybe_payment.clone().unwrap().state; + // only payment requests can be overwritten + if let Some(payment) = maybe_payment { ensure!( - current_state != PaymentState::Created, + payment.state == PaymentState::PaymentRequested, Error::::PaymentAlreadyInProcess ); - ensure!( - current_state != PaymentState::NeedsReview, - Error::::PaymentNeedsReview - ); } // Calculate incentive amount - this is to insentivise the user to release @@ -525,8 +523,7 @@ 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_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)); @@ -561,36 +558,36 @@ pub mod pallet { /// fee_recipient For cancelling a payment, recipient_share = 0 /// For releasing a payment, recipient_share = 100 /// In other cases, the custom recipient_share can be specified - fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult { - Payment::::try_mutate(from.clone(), to.clone(), |maybe_payment| -> DispatchResult { + fn settle_payment(from: &T::AccountId, to: &T::AccountId, recipient_share: Percent) -> DispatchResult { + Payment::::try_mutate(from, to, |maybe_payment| -> DispatchResult { let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; // unreserve the incentive amount and fees from the owner account match payment.fee_detail { Some((fee_recipient, fee_amount)) => { - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount + fee_amount); + T::Asset::unreserve(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 + from, // fee is paid by payment creator &fee_recipient, // account of fee recipient fee_amount, // amount of fee )?; } } None => { - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + T::Asset::unreserve(payment.asset, from, payment.incentive_amount); } }; // Unreserve the transfer amount - T::Asset::unreserve(payment.asset, &to, payment.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); // send share to recipient - T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; + T::Asset::transfer(payment.asset, to, from, amount_to_sender)?; Ok(()) })?; diff --git a/payments/src/tests.rs b/payments/src/tests.rs index 3f996d4f7..abe3ae93a 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -225,7 +225,7 @@ fn test_release_works() { } #[test] -fn test_set_state_payment_works() { +fn test_resolve_payment_works() { new_test_ext().execute_with(|| { let creator_initial_balance = 100; let payment_amount = 40; @@ -537,7 +537,7 @@ fn test_do_not_overwrite_logic_works() { payment_amount, None ), - crate::Error::::PaymentNeedsReview + crate::Error::::PaymentAlreadyInProcess ); }); } @@ -562,6 +562,18 @@ fn test_request_refund() { PAYMENT_RECIPENT )); + // do not overwrite payment + assert_noop!( + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), + crate::Error::::PaymentAlreadyInProcess + ); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { @@ -669,6 +681,11 @@ fn test_request_payment() { payment_amount, )); + assert_noop!( + Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::InvalidAction + ); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { @@ -869,8 +886,8 @@ fn test_accept_and_pay_should_charge_fee_correctly() { fn test_create_payment_does_not_work_without_transaction() { new_test_ext().execute_with(|| { assert_ok!(>::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, 20, PaymentState::Created, @@ -899,8 +916,8 @@ fn test_create_payment_works() { // transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, payment_amount, PaymentState::Created, @@ -925,8 +942,8 @@ fn test_create_payment_works() { assert_noop!( with_transaction(|| TransactionOutcome::Commit({ >::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, payment_amount, PaymentState::Created, @@ -967,8 +984,8 @@ fn test_reserve_payment_amount_works() { // transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, payment_amount, PaymentState::Created, @@ -1015,8 +1032,8 @@ fn test_reserve_payment_amount_works() { assert_noop!( with_transaction(|| TransactionOutcome::Commit({ >::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, payment_amount, PaymentState::Created, @@ -1066,8 +1083,8 @@ fn test_settle_payment_works_for_cancel() { assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::settle_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, Percent::from_percent(0), ) }))); @@ -1109,8 +1126,8 @@ fn test_settle_payment_works_for_release() { assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::settle_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, Percent::from_percent(100), ) }))); @@ -1153,8 +1170,8 @@ fn test_settle_payment_works_for_70_30() { assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::settle_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT_FEE_CHARGED, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT_FEE_CHARGED, Percent::from_percent(70), ) }))); @@ -1208,8 +1225,8 @@ fn test_settle_payment_works_for_50_50() { assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::settle_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT_FEE_CHARGED, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT_FEE_CHARGED, Percent::from_percent(50), ) }))); diff --git a/payments/src/types.rs b/payments/src/types.rs index 826d1a169..90a4e58c3 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -12,6 +12,7 @@ use sp_runtime::{DispatchResult, Percent}; #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound(T: pallet::Config))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PaymentDetail { /// type of asset used for payment pub asset: AssetIdOf, @@ -23,7 +24,7 @@ pub struct PaymentDetail { pub incentive_amount: BalanceOf, /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, /// Requested] - pub state: PaymentState, + pub state: PaymentState, /// account that can settle any disputes created in the payment pub resolver_account: T::AccountId, /// fee charged and recipient account details @@ -34,14 +35,16 @@ pub struct PaymentDetail { /// When a payment is 'completed' or 'cancelled' it is removed from storage and /// hence not tracked by a state. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound(T: pallet::Config))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum PaymentState { +pub enum PaymentState { /// Amounts have been reserved and waiting for release/cancel Created, /// A judge needs to review and release manually NeedsReview, /// The user has requested refund and will be processed by `BlockNumber` - RefundRequested { cancel_block: BlockNumber }, + RefundRequested { cancel_block: T::BlockNumber }, /// The recipient of this transaction has created a request PaymentRequested, } @@ -53,11 +56,11 @@ pub trait PaymentHandler { /// Possible reasons for failure include: /// - Payment already exists and cannot be overwritten fn create_payment( - from: T::AccountId, - to: T::AccountId, + from: &T::AccountId, + to: &T::AccountId, asset: AssetIdOf, amount: BalanceOf, - payment_state: PaymentState, + payment_state: PaymentState, incentive_percentage: Percent, remark: Option<&[u8]>, ) -> Result, sp_runtime::DispatchError>; @@ -74,7 +77,7 @@ pub trait PaymentHandler { /// - The payment does not exist /// - The unreserve operation fails /// - The transfer operation fails - fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult; + fn settle_payment(from: &T::AccountId, to: &T::AccountId, recipient_share: Percent) -> DispatchResult; /// Attempt to fetch the details of a payment from the given payment_id /// Possible reasons for failure include: From beea72e49000c7247e1c90b068b11c41b49c748c Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Wed, 6 Apr 2022 21:11:56 +0400 Subject: [PATCH 10/28] udpate to 0.9.18 --- payments/Cargo.toml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 820252d5d..9f684d963 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -10,20 +10,19 @@ description = "Allows users to post escrow payment on-chain" readme = "README.md" [dependencies] -parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } +parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } log = { version = "0.4.14", default-features = false } -frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false, optional = true } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } orml-traits = {path = "../traits", version = "0.4.1-dev", default-features = false } -scale-info = { version = "1.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } [dev-dependencies] -serde = { version = "1.0.101" } -sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +serde = { version = "1.0.136" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } orml-tokens = { path = "../tokens", version = "0.4.1-dev", default-features = false } [features] @@ -37,6 +36,5 @@ std = [ 'sp-std/std', 'scale-info/std', 'orml-traits/std', - 'frame-benchmarking/std', 'orml-tokens/std' ] From fb40c4ed205c54978278e4f21433d45c82073965 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Wed, 6 Apr 2022 21:22:53 +0400 Subject: [PATCH 11/28] make orml-tokens dep --- payments/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 9f684d963..76ab1587e 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -17,13 +17,13 @@ frame-system = { git = "https://github.com/paritytech/substrate", branch = "polk sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } orml-traits = {path = "../traits", version = "0.4.1-dev", default-features = false } +orml-tokens = { path = "../tokens", version = "0.4.1-dev", default-features = false } scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } [dev-dependencies] serde = { version = "1.0.136" } sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -orml-tokens = { path = "../tokens", version = "0.4.1-dev", default-features = false } [features] default = ['std'] From 69428a3dc289130cca53ac5a1d5f49a11a1c01b3 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Wed, 6 Apr 2022 23:45:30 +0400 Subject: [PATCH 12/28] remove remove_task benchmark --- payments/src/lib.rs | 2 +- payments/src/weights.rs | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 4c02ef76d..142d29d64 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -162,7 +162,7 @@ pub mod pallet { /// be executed and will process them. fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight { // reduce the weight used to read the task list - remaining_weight = remaining_weight.saturating_sub(T::WeightInfo::remove_task()); + remaining_weight = remaining_weight.saturating_sub(T::DbWeight::get().reads_writes(1, 1)); ScheduledTasks::::mutate(|tasks| { let mut task_list: Vec<_> = tasks diff --git a/payments/src/weights.rs b/payments/src/weights.rs index 941293c60..29d68c043 100644 --- a/payments/src/weights.rs +++ b/payments/src/weights.rs @@ -40,7 +40,6 @@ pub trait WeightInfo { fn dispute_refund() -> Weight; fn request_payment() -> Weight; fn accept_and_pay() -> Weight; - fn remove_task() -> Weight; } /// Weights for virto_payment using the Substrate node and recommended hardware. @@ -106,12 +105,6 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } - // Storage: Payment ScheduledTasks (r:1 w:1) - fn remove_task() -> Weight { - (4_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(1 as Weight)) - .saturating_add(T::DbWeight::get().writes(1 as Weight)) - } } // For backwards compatibility and tests @@ -176,10 +169,4 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } - // Storage: Payment ScheduledTasks (r:1 w:1) - fn remove_task() -> Weight { - (4_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(1 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } } \ No newline at end of file From 0377671b869fca260c25e243b0a32c9b44cde5f1 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Mon, 11 Apr 2022 17:34:52 +0400 Subject: [PATCH 13/28] review fixes - docs and formatting --- payments/src/lib.rs | 56 ++++++++++++++++++++++++++++++++++++++++++- payments/src/types.rs | 1 - 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 4c02ef76d..9bb11dfa0 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -1,4 +1,58 @@ -#![allow(clippy::unused_unit, unused_qualifications, missing_debug_implementations)] +//!This pallet allows users to create secure reversible payments that keep +//! funds locked in a merchant's account until the off-chain goods are confirmed +//! to be received. Each payment gets assigned its own *judge* that can help +//! resolve any disputes between the two parties. + +//! ## Terminology +//! +//! - Created: A payment has been created and the amount arrived to its +//! destination but it's locked. +//! - NeedsReview: The payment has bee disputed and is awaiting settlement by a +//! judge. +//! - IncentivePercentage: A small share of the payment amount is held in escrow +//! until a payment is completed/cancelled. The Incentive Percentage +//! represents this value. +//! - Resolver Account: A resolver account is assigned to every payment created, +//! this account has the privilege to cancel/release a payment that has been +//! disputed. +//! - Remark: The pallet allows to create payments by optionally providing some +//! extra(limited) amount of bytes, this is reffered to as Remark. This can be +//! used by a marketplace to seperate/tag payments. +//! - CancelBufferBlockLength: This is the time window where the recipient can +//! dispute a cancellation request from the payment creator. + +//! Extrinsics +//! +//! - `pay` - Create an payment for the given currencyid/amount +//! - `pay_with_remark` - Create a payment with a remark, can be used to tag +//! payments +//! - `release` - Release the payment amount to recipent +//! - `cancel` - Allows the recipient to cancel the payment and release the +//! payment amount to creator +//! - `resolve_release_payment` - Allows assigned judge to release a payment +//! - `resolve_cancel_payment` - Allows assigned judge to cancel a payment +//! - `request_refund` - Allows the creator of the payment to trigger cancel +//! with a buffer time. +//! - `claim_refund` - Allows the creator to claim payment refund after buffer +//! time +//! - `dispute_refund` - Allows the recipient to dispute the payment request of +//! sender +//! - `request_payment` - Create a payment that can be completed by the sender +//! using the `accept_and_pay` extrinsic. +//! - `accept_and_pay` - Allows the sender to fulfill a payment request created +//! by a recipient + +//! Types +//! +//! The `PaymentDetail` struct stores information about the payment/escrow. A +//! "payment" in virto network is similar to an escrow, it is used to guarantee +//! proof of funds and can be released once an agreed upon condition has reached +//! between the payment creator and recipient. The payment lifecycle is tracked +//! using the state field. + +//! The `PaymentState` enum tracks the possible states that a payment can be in. +//! When a payment is 'completed' or 'cancelled' it is removed from storage and +//! hence not tracked by a state. #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; diff --git a/payments/src/types.rs b/payments/src/types.rs index 90a4e58c3..a4e5058d4 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -117,6 +117,5 @@ pub struct ScheduledTask { /// the type of scheduled task pub task: Task, /// the 'time' at which the task should be executed - #[codec(compact)] pub when: Time, } From de2580f76c91502dc722af1254bd025fb170faf2 Mon Sep 17 00:00:00 2001 From: Daniel Olano Date: Tue, 8 Feb 2022 10:23:49 +0100 Subject: [PATCH 14/28] Create payments pallet crate --- payments/Cargo.toml | 8 +++++ payments/README.md | 82 +++++++++++++++++++++++++++++++++++++++++++++ payments/src/lib.rs | 8 +++++ 3 files changed, 98 insertions(+) create mode 100644 payments/Cargo.toml create mode 100644 payments/README.md create mode 100644 payments/src/lib.rs diff --git a/payments/Cargo.toml b/payments/Cargo.toml new file mode 100644 index 000000000..5fe769fa8 --- /dev/null +++ b/payments/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "payments" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/payments/README.md b/payments/README.md new file mode 100644 index 000000000..94fa4ab70 --- /dev/null +++ b/payments/README.md @@ -0,0 +1,82 @@ +# Payments Pallet + +This pallet allows users to create secure reversible payments that keep funds locked in a merchant's account until the off-chain goods are confirmed to be received. Each payment gets assigned its own *judge* that can help resolve any disputes between the two parties. + +## Terminology + +- Created: A payment has been created and the amount arrived to its destination but it's locked. +- NeedsReview: The payment has bee disputed and is awaiting settlement by a judge. +- IncentivePercentage: A small share of the payment amount is held in escrow until a payment is completed/cancelled. The Incentive Percentage represents this value. +- Resolver Account: A resolver account is assigned to every payment created, this account has the privilege to cancel/release a payment that has been disputed. +- Remark: The pallet allows to create payments by optionally providing some extra(limited) amount of bytes, this is reffered to as Remark. This can be used by a marketplace to seperate/tag payments. +- CancelBufferBlockLength: This is the time window where the recipient can dispute a cancellation request from the payment creator. + +## Interface + +#### Events + +- `PaymentCreated { from: T::AccountId, asset: AssetIdOf, amount: BalanceOf },`, +- `PaymentReleased { from: T::AccountId, to: T::AccountId }`, +- `PaymentCancelled { from: T::AccountId, to: T::AccountId }`, +- `PaymentCreatorRequestedRefund { from: T::AccountId, to: T::AccountId, expiry: T::BlockNumber}` +- `PaymentRefundDisputed { from: T::AccountId, to: T::AccountId }` + +#### Extrinsics + +- `pay` - Create an payment for the given currencyid/amount +- `pay_with_remark` - Create a payment with a remark, can be used to tag payments +- `release` - Release the payment amount to recipent +- `cancel` - Allows the recipient to cancel the payment and release the payment amount to creator +- `resolve_release_payment` - Allows assigned judge to release a payment +- `resolve_cancel_payment` - Allows assigned judge to cancel a payment +- `request_refund` - Allows the creator of the payment to trigger cancel with a buffer time. +- `claim_refund` - Allows the creator to claim payment refund after buffer time +- `dispute_refund` - Allows the recipient to dispute the payment request of sender + +## Implementations + +The RatesProvider module provides implementations for the following traits. +- [`PaymentHandler`](./src/types.rs) + +## Types + +The `PaymentDetail` struct stores information about the payment/escrow. A "payment" in virto network is similar to an escrow, it is used to guarantee proof of funds and can be released once an agreed upon condition has reached between the payment creator and recipient. The payment lifecycle is tracked using the state field. + +```rust +pub struct PaymentDetail { + /// type of asset used for payment + pub asset: AssetIdOf, + /// amount of asset used for payment + pub amount: BalanceOf, + /// incentive amount that is credited to creator for resolving + pub incentive_amount: BalanceOf, + /// enum to track payment lifecycle [Created, NeedsReview] + pub state: PaymentState, + /// 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)>, + /// remarks to give context to payment + pub remark: Option>, +} +``` + +The `PaymentState` enum tracks the possible states that a payment can be in. When a payment is 'completed' or 'cancelled' it is removed from storage and hence not tracked by a state. + +```rust +pub enum PaymentState { + /// Amounts have been reserved and waiting for release/cancel + Created, + /// A judge needs to review and release manually + NeedsReview, + /// The user has requested refund and will be processed by `BlockNumber` + RefundRequested(BlockNumber), +} +``` + +## GenesisConfig + +The rates_provider pallet does not depend on the `GenesisConfig` + +License: Apache-2.0 + diff --git a/payments/src/lib.rs b/payments/src/lib.rs new file mode 100644 index 000000000..bcab31d68 --- /dev/null +++ b/payments/src/lib.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} From fb01b95fc5be2313fd2168ca8722c36c579a68e4 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sun, 20 Feb 2022 21:07:54 +0400 Subject: [PATCH 15/28] sync current version --- payments/Cargo.toml | 45 +- payments/README.md | 5 +- payments/src/lib.rs | 580 ++++++++++++++++++++- payments/src/mock.rs | 156 ++++++ payments/src/tests.rs | 1059 +++++++++++++++++++++++++++++++++++++++ payments/src/types.rs | 96 ++++ payments/src/weights.rs | 133 +++++ 7 files changed, 2064 insertions(+), 10 deletions(-) create mode 100644 payments/src/mock.rs create mode 100644 payments/src/tests.rs create mode 100644 payments/src/types.rs create mode 100644 payments/src/weights.rs diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 5fe769fa8..89352c823 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -1,8 +1,45 @@ [package] -name = "payments" -version = "0.1.0" -edition = "2021" +authors = ["Virto Network "] +edition = '2021' +name = "orml-payments" +version = "0.4.1-dev" +license = "Apache-2.0" +homepage = "https://github.com/virto-network/virto-node" +repository = "https://github.com/virto-network/virto-node" +description = "Allows users to post payment on-chain" +readme = "README.md" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] [dependencies] +parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false, optional = true } +orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false } +scale-info = { version = "1.0.0", default-features = false, features = ["derive"] } + +[dev-dependencies] +serde = { version = "1.0.101" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } +orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library" } + +[features] +default = ['std'] +std = [ + 'parity-scale-codec/std', + 'frame-support/std', + 'frame-system/std', + 'sp-runtime/std', + 'sp-std/std', + 'scale-info/std', + 'orml-traits/std', + 'frame-benchmarking/std', +] +runtime-benchmarks = [ + "frame-benchmarking", +] diff --git a/payments/README.md b/payments/README.md index 94fa4ab70..e28fbf26c 100644 --- a/payments/README.md +++ b/payments/README.md @@ -20,6 +20,8 @@ This pallet allows users to create secure reversible payments that keep funds lo - `PaymentCancelled { from: T::AccountId, to: T::AccountId }`, - `PaymentCreatorRequestedRefund { from: T::AccountId, to: T::AccountId, expiry: T::BlockNumber}` - `PaymentRefundDisputed { from: T::AccountId, to: T::AccountId }` +- `PaymentRequestCreated { from: T::AccountId, to: T::AccountId }` +- `PaymentRequestCompleted { from: T::AccountId, to: T::AccountId }` #### Extrinsics @@ -32,6 +34,8 @@ This pallet allows users to create secure reversible payments that keep funds lo - `request_refund` - Allows the creator of the payment to trigger cancel with a buffer time. - `claim_refund` - Allows the creator to claim payment refund after buffer time - `dispute_refund` - Allows the recipient to dispute the payment request of sender +- `request_payment` - Create a payment that can be completed by the sender using the `accept_and_pay` extrinsic. +- `accept_and_pay` - Allows the sender to fulfill a payment request created by a recipient ## Implementations @@ -79,4 +83,3 @@ pub enum PaymentState { The rates_provider pallet does not depend on the `GenesisConfig` License: Apache-2.0 - diff --git a/payments/src/lib.rs b/payments/src/lib.rs index bcab31d68..0d0210e67 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -1,8 +1,578 @@ +#![allow(clippy::unused_unit, unused_qualifications, missing_debug_implementations)] +#![cfg_attr(not(feature = "std"), no_std)] +pub use pallet::*; + #[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); +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}, + weights::WeightInfo, + }; + use frame_support::{ + dispatch::DispatchResultWithPostInfo, fail, pallet_prelude::*, require_transactional, + traits::tokens::BalanceStatus, transactional, + }; + use frame_system::pallet_prelude::*; + use orml_traits::{MultiCurrency, MultiReservableCurrency}; + use sp_runtime::{ + traits::{CheckedAdd, Saturating}, + Percent, + }; + + pub type BalanceOf = <::Asset as MultiCurrency<::AccountId>>::Balance; + pub type AssetIdOf = <::Asset as MultiCurrency<::AccountId>>::CurrencyId; + pub type BoundedDataOf = BoundedVec::MaxRemarkLength>; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Because this pallet emits events, it depends on the runtime's + /// definition of an event. + type Event: From> + IsType<::Event>; + /// the type of assets this pallet can hold in payment + type Asset: MultiReservableCurrency; + /// Dispute resolution account + type DisputeResolver: DisputeResolver; + /// Fee handler trait + type FeeHandler: FeeHandler; + /// Incentive percentage - amount witheld from sender + #[pallet::constant] + type IncentivePercentage: Get; + /// Maximum permitted size of `Remark` + #[pallet::constant] + type MaxRemarkLength: Get; + /// Buffer period - number of blocks to wait before user can claim + /// canceled payment + #[pallet::constant] + type CancelBufferBlockLength: Get; + //// Type representing the weight of this pallet + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::storage] + #[pallet::getter(fn rates)] + /// Payments created by a user, this method of storageDoubleMap is chosen + /// since there is no usecase for listing payments by provider/currency. The + /// payment will only be referenced by the creator in any transaction of + /// interest. The storage map keys are the creator and the recipient, this + /// also ensures that for any (sender,recipient) combo, only a single + /// payment is active. The history of payment is not stored. + pub(super) type Payment = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // payment creator + Blake2_128Concat, + T::AccountId, // payment recipient + PaymentDetail, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new payment has been created + PaymentCreated { + from: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + }, + /// Payment amount released to the recipient + PaymentReleased { from: T::AccountId, to: T::AccountId }, + /// Payment has been cancelled by the creator + PaymentCancelled { from: T::AccountId, to: T::AccountId }, + /// the payment creator has created a refund request + PaymentCreatorRequestedRefund { + from: T::AccountId, + to: T::AccountId, + expiry: T::BlockNumber, + }, + /// the refund request from creator was disputed by recipient + PaymentRefundDisputed { from: T::AccountId, to: T::AccountId }, + /// Payment request was created by recipient + PaymentRequestCreated { from: T::AccountId, to: T::AccountId }, + /// Payment request was completed by sender + PaymentRequestCompleted { from: T::AccountId, to: T::AccountId }, + } + + #[pallet::error] + pub enum Error { + /// The selected payment does not exist + InvalidPayment, + /// The selected payment cannot be released + PaymentAlreadyReleased, + /// The selected payment already exists and is in process + PaymentAlreadyInProcess, + /// Action permitted only for whitelisted users + InvalidAction, + /// Payment is in review state and cannot be modified + PaymentNeedsReview, + /// Unexpeted math error + MathError, + /// Payment request has not been created + RefundNotRequested, + /// Dispute period has not passed + DisputePeriodNotPassed, + } + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::call] + impl Pallet { + /// This allows any user to create a new payment, that releases only to + /// specified recipient The only action is to store the details of this + /// payment in storage and reserve the specified amount. + #[transactional] + #[pallet::weight(T::WeightInfo::pay())] + pub fn pay( + origin: OriginFor, + recipient: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + // create PaymentDetail and add to storage + let payment_detail = >::create_payment( + who.clone(), + recipient.clone(), + asset, + amount, + PaymentState::Created, + T::IncentivePercentage::get(), + None, + )?; + // reserve funds for payment + >::reserve_payment_amount(&who, &recipient, payment_detail)?; + // emit paymentcreated event + Self::deposit_event(Event::PaymentCreated { + from: who, + asset, + amount, + }); + Ok(().into()) + } + + /// This allows any user to create a new payment with the option to add + /// a remark, this remark can then be used to run custom logic and + /// trigger alternate payment flows. the specified amount. + #[transactional] + #[pallet::weight(T::WeightInfo::pay_with_remark(remark.len().try_into().unwrap_or(T::MaxRemarkLength::get())))] + pub fn pay_with_remark( + origin: OriginFor, + recipient: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + remark: BoundedDataOf, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + // create PaymentDetail and add to storage + let payment_detail = >::create_payment( + who.clone(), + recipient.clone(), + asset, + amount, + PaymentState::Created, + T::IncentivePercentage::get(), + Some(remark), + )?; + // reserve funds for payment + >::reserve_payment_amount(&who, &recipient, payment_detail)?; + // emit paymentcreated event + Self::deposit_event(Event::PaymentCreated { + from: who, + asset, + amount, + }); + Ok(().into()) + } + + /// Release any created payment, this will transfer the reserved amount + /// from the creator of the payment to the assigned recipient + #[transactional] + #[pallet::weight(T::WeightInfo::release())] + pub fn release(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { + let from = ensure_signed(origin)?; + + // ensure the payment is in Created state + if let Some(payment) = Payment::::get(from.clone(), to.clone()) { + ensure!(payment.state == PaymentState::Created, Error::::InvalidAction) + } + + // release is a settle_payment with 100% recipient_share + >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + + Self::deposit_event(Event::PaymentReleased { from, to }); + Ok(().into()) + } + + /// Cancel a payment in created state, this will release the reserved + /// back to creator of the payment. This extrinsic can only be called by + /// the recipient of the payment + #[transactional] + #[pallet::weight(T::WeightInfo::cancel())] + pub fn cancel(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + if let Some(payment) = Payment::::get(creator.clone(), who.clone()) { + match payment.state { + // call settle payment with recipient_share=0, this refunds the sender + PaymentState::Created => { + >::settle_payment( + creator.clone(), + who.clone(), + Percent::from_percent(0), + )?; + Self::deposit_event(Event::PaymentCancelled { from: creator, to: who }); + } + // if the payment is in state PaymentRequested, remove from storage + PaymentState::PaymentRequested => Payment::::remove(creator.clone(), who.clone()), + _ => fail!(Error::::InvalidAction), + } + } else { + fail!(Error::::InvalidPayment); + } + Ok(().into()) + } + + /// Allow judge to set state of a payment + /// This extrinsic is used to resolve disputes between the creator and + /// recipient of the payment. This extrinsic allows the assigned judge + /// to cancel the payment + #[transactional] + #[pallet::weight(T::WeightInfo::resolve_cancel_payment())] + pub fn resolve_cancel_payment( + origin: OriginFor, + from: T::AccountId, + recipient: T::AccountId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + // ensure the caller is the assigned resolver + if let Some(payment) = Payment::::get(from.clone(), recipient.clone()) { + ensure!(who == payment.resolver_account, Error::::InvalidAction) + } + // try to update the payment to new state + >::settle_payment(from.clone(), recipient.clone(), Percent::from_percent(0))?; + Self::deposit_event(Event::PaymentCancelled { from, to: recipient }); + Ok(().into()) + } + + /// Allow judge to set state of a payment + /// This extrinsic is used to resolve disputes between the creator and + /// recipient of the payment. This extrinsic allows the assigned judge + /// to send the payment to recipient + #[transactional] + #[pallet::weight(T::WeightInfo::resolve_release_payment())] + pub fn resolve_release_payment( + origin: OriginFor, + from: T::AccountId, + recipient: T::AccountId, + ) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + // ensure the caller is the assigned resolver + if let Some(payment) = Payment::::get(from.clone(), recipient.clone()) { + ensure!(who == payment.resolver_account, Error::::InvalidAction) + } + // try to update the payment to new state + >::settle_payment(from.clone(), recipient.clone(), Percent::from_percent(100))?; + Self::deposit_event(Event::PaymentReleased { from, to: recipient }); + Ok(().into()) + } + + /// Allow payment creator to set payment to NeedsReview + /// This extrinsic is used to mark the payment as disputed so the + /// assigned judge can tigger a resolution and that the funds are no + /// longer locked. + #[transactional] + #[pallet::weight(T::WeightInfo::request_refund())] + pub fn request_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + + Payment::::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // ensure the payment is not in needsreview state + ensure!( + payment.state != PaymentState::NeedsReview, + Error::::PaymentNeedsReview + ); + + // set the payment to requested refund + let current_block = frame_system::Pallet::::block_number(); + let can_cancel_block = current_block + .checked_add(&T::CancelBufferBlockLength::get()) + .ok_or(Error::::MathError)?; + payment.state = PaymentState::RefundRequested(can_cancel_block); + + Self::deposit_event(Event::PaymentCreatorRequestedRefund { + from: who, + to: recipient, + expiry: can_cancel_block, + }); + + Ok(()) + })?; + + Ok(().into()) + } + + /// Allow payment creator to claim the refund if the payment recipent + /// has not disputed After the payment creator has `request_refund` can + /// then call this extrinsic to cancel the payment and receive the + /// reserved amount to the account if the dispute period has passed. + #[transactional] + #[pallet::weight(T::WeightInfo::claim_refund())] + pub fn claim_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { + use PaymentState::*; + let who = ensure_signed(origin)?; + + if let Some(payment) = Payment::::get(who.clone(), recipient.clone()) { + match payment.state { + NeedsReview => fail!(Error::::PaymentNeedsReview), + Created | PaymentRequested => fail!(Error::::RefundNotRequested), + RefundRequested(cancel_block) => { + let current_block = frame_system::Pallet::::block_number(); + // ensure the dispute period has passed + ensure!(current_block > cancel_block, Error::::DisputePeriodNotPassed); + // cancel the payment and refund the creator + >::settle_payment( + who.clone(), + recipient.clone(), + Percent::from_percent(0), + )?; + Self::deposit_event(Event::PaymentCancelled { + from: who, + to: recipient, + }); + } + } + } else { + fail!(Error::::InvalidPayment); + } + + Ok(().into()) + } + + /// Allow payment recipient to dispute the refund request from the + /// payment creator This does not cancel the request, instead sends the + /// payment to a NeedsReview state The assigned resolver account can + /// then change the state of the payment after review. + #[transactional] + #[pallet::weight(T::WeightInfo::dispute_refund())] + pub fn dispute_refund(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { + use PaymentState::*; + let who = ensure_signed(origin)?; + + Payment::::try_mutate( + creator.clone(), + who.clone(), // should be called by the payment recipient + |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // ensure the payment is in Requested Refund state + match payment.state { + RefundRequested(_) => { + payment.state = PaymentState::NeedsReview; + + Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who }); + } + _ => fail!(Error::::InvalidAction), + } + + Ok(()) + }, + )?; + + Ok(().into()) + } + + // Creates a new payment with the given details. This can be called by the + // recipient of the payment to create a payment and then completed by the sender + // using the `accept_and_pay` extrinsic. The payment will be in PaymentRequested + // State and can only be modified by the `accept_and_pay` extrinsic. + #[transactional] + #[pallet::weight(T::WeightInfo::request_payment())] + pub fn request_payment( + origin: OriginFor, + from: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + ) -> DispatchResultWithPostInfo { + let to = ensure_signed(origin)?; + + // create PaymentDetail and add to storage + >::create_payment( + from.clone(), + to.clone(), + asset, + amount, + PaymentState::PaymentRequested, + Percent::from_percent(0), + None, + )?; + + Self::deposit_event(Event::PaymentRequestCreated { from, to }); + + Ok(().into()) + } + + // This extrinsic allows the sender to fulfill a payment request created by a + // recipient. The amount will be transferred to the recipient and payment + // removed from storage + #[transactional] + #[pallet::weight(T::WeightInfo::accept_and_pay())] + pub fn accept_and_pay(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { + let from = ensure_signed(origin)?; + + let payment = Payment::::get(from.clone(), to.clone()).ok_or(Error::::InvalidPayment)?; + + ensure!( + payment.state == PaymentState::PaymentRequested, + Error::::InvalidAction + ); + + // reserve all the fees from the sender + >::reserve_payment_amount(&from, &to, payment)?; + + // release the payment and delete the payment from storage + >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + + Self::deposit_event(Event::PaymentRequestCompleted { from, to }); + + Ok(().into()) + } + } + + impl PaymentHandler for Pallet { + /// The function will create a new payment. The fee and incentive + /// amounts will be calculated and the `PaymentDetail` will be added to + /// storage. + #[require_transactional] + fn create_payment( + from: T::AccountId, + recipient: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + payment_state: PaymentState, + incentive_percentage: Percent, + remark: Option>, + ) -> Result, sp_runtime::DispatchError> { + Payment::::try_mutate( + from.clone(), + recipient.clone(), + |maybe_payment| -> Result, sp_runtime::DispatchError> { + if maybe_payment.is_some() { + // ensure the payment is not in created/needsreview state + let current_state = maybe_payment.clone().unwrap().state; + ensure!( + current_state != PaymentState::Created, + Error::::PaymentAlreadyInProcess + ); + ensure!( + current_state != PaymentState::NeedsReview, + Error::::PaymentNeedsReview + ); + } + // Calculate incentive amount - this is to insentivise the user to release + // the funds once a transaction has been completed + 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_origin(), + fee_detail: None, + remark, + }; + + // 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); + 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) + }, + ) + } + + /// The function will reserve the fees+transfer amount from the `from` + /// account. After reserving the payment.amount will be transferred to + /// the recipient but will stay in Reserve state. + #[require_transactional] + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult { + let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or(0u32.into()); + + let total_fee_amount = payment.incentive_amount.saturating_add(fee_amount); + let total_amount = total_fee_amount.saturating_add(payment.amount); + + // reserve the total amount from payment creator + T::Asset::reserve(payment.asset, from, total_amount)?; + // transfer payment amount to recipient -- keeping reserve status + T::Asset::repatriate_reserved(payment.asset, from, to, payment.amount, BalanceStatus::Reserved)?; + Ok(()) + } + + /// This function allows the caller to settle the payment by specifying + /// a recipient_share this will unreserve the fee+incentive to sender + /// and unreserve transferred amount to recipient if the settlement is a + /// release (ie recipient_share=100), the fee is transferred to + /// fee_recipient For cancelling a payment, recipient_share = 0 + /// For releasing a payment, recipient_share = 100 + /// In other cases, the custom recipient_share can be specified + #[require_transactional] + fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult { + Payment::::try_mutate(from.clone(), to.clone(), |maybe_payment| -> DispatchResult { + let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; + + // unreserve the incentive amount and fees from the owner account + match payment.fee_detail { + Some((fee_recipient, fee_amount)) => { + T::Asset::unreserve(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 + )?; + } + } + None => { + T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + } + }; + + // Unreserve the transfer 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); + // send share to recipient + T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; + + Ok(()) + })?; + Ok(()) + } + + fn get_payment_details(from: T::AccountId, to: T::AccountId) -> Option> { + Payment::::get(from, to) + } } } diff --git a/payments/src/mock.rs b/payments/src/mock.rs new file mode 100644 index 000000000..af55dbb0e --- /dev/null +++ b/payments/src/mock.rs @@ -0,0 +1,156 @@ +use crate as payment; +use crate::PaymentDetail; +use frame_support::{ + parameter_types, + traits::{Contains, Everything, GenesisBuild, OnFinalize, OnInitialize}, +}; +use frame_system as system; +use orml_traits::parameter_type_with_key; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + Percent, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; +pub type Balance = u128; + +pub type AccountId = u8; +pub const PAYMENT_CREATOR: AccountId = 10; +pub const PAYMENT_RECIPENT: AccountId = 11; +pub const CURRENCY_ID: u128 = 1; +pub const RESOLVER_ACCOUNT: AccountId = 12; +pub const FEE_RECIPIENT_ACCOUNT: AccountId = 20; +pub const PAYMENT_RECIPENT_FEE_CHARGED: AccountId = 21; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Tokens: orml_tokens::{Pallet, Call, Config, Storage, Event}, + Payment: payment::{Pallet, Call, Storage, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: u128| -> Balance { + 0u128 + }; +} +parameter_types! { + pub const MaxLocks: u32 = 50; +} + +pub struct MockDustRemovalWhitelist; +impl Contains for MockDustRemovalWhitelist { + fn contains(_a: &AccountId) -> bool { + false + } +} + +impl orml_tokens::Config for Test { + type Amount = i64; + type Balance = Balance; + type CurrencyId = u128; + type Event = Event; + type ExistentialDeposits = ExistentialDeposits; + type OnDust = (); + type WeightInfo = (); + type MaxLocks = MaxLocks; + type DustRemovalWhitelist = MockDustRemovalWhitelist; +} + +pub struct MockDisputeResolver; +impl crate::types::DisputeResolver for MockDisputeResolver { + fn get_origin() -> AccountId { + RESOLVER_ACCOUNT + } +} + +pub struct MockFeeHandler; +impl crate::types::FeeHandler for MockFeeHandler { + fn apply_fees(_from: &AccountId, to: &AccountId, _remark: &PaymentDetail) -> (AccountId, Percent) { + match to { + &PAYMENT_RECIPENT_FEE_CHARGED => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(10)), + _ => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(0)), + } + } +} + +parameter_types! { + pub const IncentivePercentage: Percent = Percent::from_percent(10); + pub const MaxRemarkLength: u32 = 50; + pub const CancelBufferBlockLength: u64 = 600; +} + +impl payment::Config for Test { + type Event = Event; + type Asset = Tokens; + type DisputeResolver = MockDisputeResolver; + type IncentivePercentage = IncentivePercentage; + type FeeHandler = MockFeeHandler; + type MaxRemarkLength = MaxRemarkLength; + type CancelBufferBlockLength = CancelBufferBlockLength; + type WeightInfo = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = system::GenesisConfig::default().build_storage::().unwrap(); + + orml_tokens::GenesisConfig:: { + balances: vec![(PAYMENT_CREATOR, CURRENCY_ID, 100)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext: sp_io::TestExternalities = t.into(); + // need to set block number to 1 to test events + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + } +} diff --git a/payments/src/tests.rs b/payments/src/tests.rs new file mode 100644 index 000000000..225b11549 --- /dev/null +++ b/payments/src/tests.rs @@ -0,0 +1,1059 @@ +use crate::{ + mock::*, + types::{PaymentDetail, PaymentState}, + Payment as PaymentStore, PaymentHandler, +}; +use frame_support::{assert_noop, assert_ok, storage::with_transaction}; +use orml_traits::MultiCurrency; +use sp_runtime::{Percent, TransactionOutcome}; + +fn last_event() -> Event { + System::events().pop().expect("Event expected").event +} + +#[test] +fn test_pay_works() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + assert_eq!( + last_event(), + crate::Event::::PaymentCreated { + from: PAYMENT_CREATOR, + asset: CURRENCY_ID, + amount: 20 + } + .into() + ); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + // the payment amount should be reserved correctly + // the amount + incentive should be removed from the sender account + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + // the incentive amount should be reserved in the sender account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + // the transferred amount should be reserved in the recipent account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + + // the payment should not be overwritten + assert_noop!( + Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + }); +} + +#[test] +fn test_cancel_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // cancel should fail when called by user + assert_noop!( + Payment::cancel(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::InvalidPayment + ); + + // cancel should succeed when caller is the recipent + assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + // the payment amount should be released back to creator + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_release_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should succeed for valid payment + assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_eq!( + last_event(), + crate::Event::::PaymentReleased { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + // should be able to create another payment since previous is released + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 16); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + }); +} + +#[test] +fn test_set_state_payment_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + + // should fail for non whitelisted caller + assert_noop!( + Payment::resolve_cancel_payment(Origin::signed(PAYMENT_CREATOR), PAYMENT_CREATOR, PAYMENT_RECIPENT,), + crate::Error::::InvalidAction + ); + + // should be able to release a payment + assert_ok!(Payment::resolve_release_payment( + Origin::signed(RESOLVER_ACCOUNT), + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + )); + assert_eq!( + last_event(), + crate::Event::::PaymentReleased { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be removed from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 40, + )); + + // should be able to cancel a payment + assert_ok!(Payment::resolve_cancel_payment( + Origin::signed(RESOLVER_ACCOUNT), + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + )); + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_charging_fee_payment_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 4)), + remark: None + }) + ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should succeed for valid payment + assert_ok!(Payment::release( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED + )); + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 40); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 4); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + }); +} + +#[test] +fn test_charging_fee_payment_works_when_canceled() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 40, + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 40, + incentive_amount: 4, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 4)), + remark: None + }) + ); + // the payment amount should be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should succeed for valid payment + assert_ok!(Payment::cancel( + Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), + PAYMENT_CREATOR + )); + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 0); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + }); +} + +#[test] +fn test_pay_with_remark_works() { + new_test_ext().execute_with(|| { + // should be able to create a payment with available balance + assert_ok!(Payment::pay_with_remark( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + vec![1u8; 10].try_into().unwrap() + )); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()) + }) + ); + // the payment amount should be reserved correctly + // the amount + incentive should be removed from the sender account + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + // the incentive amount should be reserved in the sender account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + // the transferred amount should be reserved in the recipent account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + + // the payment should not be overwritten + assert_noop!( + Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentCreated { + from: PAYMENT_CREATOR, + asset: CURRENCY_ID, + amount: 20 + } + .into() + ); + }); +} + +#[test] +fn test_do_not_overwrite_logic_works() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_noop!( + Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + crate::Error::::PaymentAlreadyInProcess + ); + + // set payment state to NeedsReview + PaymentStore::::insert( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::NeedsReview, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None, + }, + ); + + // the payment should not be overwritten + assert_noop!( + Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + crate::Error::::PaymentNeedsReview + ); + }); +} + +#[test] +fn test_request_refund() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::RefundRequested(601u64.into()), + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentCreatorRequestedRefund { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + expiry: 601u64.into() + } + .into() + ); + }); +} + +#[test] +fn test_claim_refund() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + // cannot claim refund unless payment is in requested refund state + assert_noop!( + Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::RefundNotRequested + ); + + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + + // cannot cancel before the dispute period has passed + assert_noop!( + Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::DisputePeriodNotPassed + ); + + run_to_block(700); + assert_ok!(Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() + ); + // the payment amount should be released back to creator + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_dispute_refund() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + // cannot dispute if refund is not requested + assert_noop!( + Payment::dispute_refund(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR), + crate::Error::::InvalidAction + ); + // creator requests a refund + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); + // recipient disputes the refund request + assert_ok!(Payment::dispute_refund( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR + )); + // payment cannot be claimed after disputed + assert_noop!( + Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::PaymentNeedsReview + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::NeedsReview, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentRefundDisputed { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + } + .into() + ); + }); +} + +#[test] +fn test_request_payment() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 0_u128, + state: PaymentState::PaymentRequested, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentRequestCreated { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + } + .into() + ); + }); +} + +#[test] +fn test_requested_payment_cannot_be_released() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + // requested payment cannot be released + assert_noop!( + Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::InvalidAction + ); + }); +} + +#[test] +fn test_requested_payment_can_be_cancelled_by_requestor() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); + + // the request should be removed from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_accept_and_pay() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 0_u128, + state: PaymentState::PaymentRequested, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: None + }) + ); + + assert_ok!(Payment::accept_and_pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + )); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + assert_eq!( + last_event(), + crate::Event::::PaymentRequestCompleted { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT, + } + .into() + ); + }); +} + +#[test] +fn test_accept_and_pay_should_fail_for_non_payment_requested() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_noop!( + Payment::accept_and_pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT,), + crate::Error::::InvalidAction + ); + }); +} + +#[test] +fn test_accept_and_pay_should_charge_fee_correctly() { + new_test_ext().execute_with(|| { + assert_ok!(Payment::request_payment( + Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), + PAYMENT_CREATOR, + CURRENCY_ID, + 20, + )); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 0_u128, + state: PaymentState::PaymentRequested, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 2)), + remark: None + }) + ); + + assert_ok!(Payment::accept_and_pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + )); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 20); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 2); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); + + assert_eq!( + last_event(), + crate::Event::::PaymentRequestCompleted { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT_FEE_CHARGED, + } + .into() + ); + }); +} + +#[test] +#[should_panic(expected = "Require transaction not called within with_transaction")] +fn test_create_payment_does_not_work_without_transaction() { + new_test_ext().execute_with(|| { + assert_ok!(>::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(0), + None, + )); + }); +} + +#[test] +fn test_create_payment_works() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(10), + Some(vec![1u8; 10].try_into().unwrap()), + ) + }))); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()), + }) + ); + + // the payment should not be overwritten + assert_noop!( + with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(10), + Some(vec![1u8; 10].try_into().unwrap()), + ) + })), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()), + }) + ); + }); +} + +#[test] +fn test_reserve_payment_amount_works() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(10), + Some(vec![1u8; 10].try_into().unwrap()), + ) + }))); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()), + }) + ); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::reserve_payment_amount( + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), + ) + }))); + // the payment amount should be reserved correctly + // the amount + incentive should be removed from the sender account + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + // the incentive amount should be reserved in the sender account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + // the transferred amount should be reserved in the recipent account + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + + // the payment should not be overwritten + assert_noop!( + with_transaction(|| TransactionOutcome::Commit({ + >::create_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + PaymentState::Created, + Percent::from_percent(10), + Some(vec![1u8; 10].try_into().unwrap()), + ) + })), + crate::Error::::PaymentAlreadyInProcess + ); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: 20, + incentive_amount: 2, + state: PaymentState::Created, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + remark: Some(vec![1u8; 10].try_into().unwrap()), + }) + ); + }); +} + +#[test] +fn test_settle_payment_works_for_cancel() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + Percent::from_percent(0), + ) + }))); + + // the payment amount should be released back to creator + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be released from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_settle_payment_works_for_release() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + Percent::from_percent(100), + ) + }))); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + }); +} + +#[test] +fn test_settle_payment_works_for_70_30() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 10, + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT_FEE_CHARGED, + Percent::from_percent(70), + ) + }))); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 92); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 7); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 1); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); + }); +} + +#[test] +fn test_settle_payment_works_for_50_50() { + new_test_ext().execute_with(|| { + // the payment amount should not be reserved + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); + + // should be able to create a payment with available balance within a + // transaction + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED, + CURRENCY_ID, + 10, + )); + + assert_ok!(with_transaction(|| TransactionOutcome::Commit({ + >::settle_payment( + PAYMENT_CREATOR, + PAYMENT_RECIPENT_FEE_CHARGED, + Percent::from_percent(50), + ) + }))); + + // the payment amount should be transferred + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 94); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 5); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 1); + assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + + // should be deleted from storage + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); + }); +} diff --git a/payments/src/types.rs b/payments/src/types.rs new file mode 100644 index 000000000..20b9c094b --- /dev/null +++ b/payments/src/types.rs @@ -0,0 +1,96 @@ +#![allow(unused_qualifications)] +use crate::{pallet, AssetIdOf, BalanceOf, BoundedDataOf}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{DispatchResult, Percent}; + +/// The PaymentDetail struct stores information about the payment/escrow +/// A "payment" in virto network is similar to an escrow, it is used to +/// guarantee proof of funds and can be released once an agreed upon condition +/// has reached between the payment creator and recipient. The payment lifecycle +/// is tracked using the state field. +#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound(T: pallet::Config))] +pub struct PaymentDetail { + /// type of asset used for payment + pub asset: AssetIdOf, + /// amount of asset used for payment + pub amount: BalanceOf, + /// incentive amount that is credited to creator for resolving + pub incentive_amount: BalanceOf, + /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, + /// Requested] + pub state: PaymentState, + /// 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)>, + /// remarks to give context to payment + pub remark: Option>, +} + +/// The `PaymentState` enum tracks the possible states that a payment can be in. +/// When a payment is 'completed' or 'cancelled' it is removed from storage and +/// hence not tracked by a state. +#[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum PaymentState { + /// Amounts have been reserved and waiting for release/cancel + Created, + /// A judge needs to review and release manually + NeedsReview, + /// The user has requested refund and will be processed by `BlockNumber` + RefundRequested(BlockNumber), + /// The recipient of this transaction has created a request + PaymentRequested, +} + +/// trait that defines how to create/release payments for users +pub trait PaymentHandler { + /// Create a PaymentDetail from the given payment details + /// Calculate the fee amount and store PaymentDetail in storage + /// Possible reasons for failure include: + /// - Payment already exists and cannot be overwritten + fn create_payment( + from: T::AccountId, + to: T::AccountId, + asset: AssetIdOf, + amount: BalanceOf, + payment_state: PaymentState, + incentive_percentage: Percent, + remark: Option>, + ) -> Result, sp_runtime::DispatchError>; + + /// Attempt to reserve an amount of the given asset from the caller + /// If not possible then return Error. Possible reasons for failure include: + /// - User does not have enough balance. + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult; + + // Settle a payment of `from` to `to`. To release a payment, the + // recipient_share=100, to cancel a payment recipient_share=0 + // Possible reasonse for failure include + /// - The payment does not exist + /// - The unreserve operation fails + /// - The transfer operation fails + fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult; + + /// Attempt to fetch the details of a payment from the given payment_id + /// Possible reasons for failure include: + /// - The payment does not exist + fn get_payment_details(from: T::AccountId, to: T::AccountId) -> Option>; +} + +/// DisputeResolver trait defines how to create/assing judges for solving +/// payment disputes +pub trait DisputeResolver { + /// Get a DisputeResolver (Judge) account + fn get_origin() -> Account; +} + +/// Fee Handler trait that defines how to handle marketplace fees to every +/// payment/swap +pub trait FeeHandler { + /// Get the distribution of fees to marketplace participants + fn apply_fees(from: &T::AccountId, to: &T::AccountId, detail: &PaymentDetail) -> (T::AccountId, Percent); +} diff --git a/payments/src/weights.rs b/payments/src/weights.rs new file mode 100644 index 000000000..153ed4ae5 --- /dev/null +++ b/payments/src/weights.rs @@ -0,0 +1,133 @@ +//! Autogenerated weights for virto_payment +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2022-02-18, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/virto-parachain +// benchmark +// --chain +// dev +// --execution=wasm +// --wasm-execution +// compiled +// --extrinsic=* +// --pallet=virto-payment +// --steps=20 +// --repeat=10 +// --raw +// --heap-pages=4096 +// --output +// ./pallets/payment/src/weights.rs +// --template +// ./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for virto_payment. +pub trait WeightInfo { + fn pay() -> Weight; + fn pay_with_remark(x: u32, ) -> Weight; + fn release() -> Weight; + fn cancel() -> Weight; + fn resolve_cancel_payment() -> Weight; + fn resolve_release_payment() -> Weight; + fn request_refund() -> Weight; + fn claim_refund() -> Weight; + fn dispute_refund() -> Weight; + fn request_payment() -> Weight; + fn accept_and_pay() -> Weight; +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn pay() -> Weight { + (54_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn pay_with_remark(_x: u32, ) -> Weight { + (54_397_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(5 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn release() -> Weight { + (34_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn cancel() -> Weight { + (46_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn resolve_cancel_payment() -> Weight { + (46_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn resolve_release_payment() -> Weight { + (35_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + fn request_refund() -> Weight { + (17_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn claim_refund() -> Weight { + (47_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + fn dispute_refund() -> Weight { + (16_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + fn request_payment() -> Weight { + (18_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn accept_and_pay() -> Weight { + (58_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(4 as Weight)) + .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + } +} \ No newline at end of file From a5b5dafca83769da967db17dca38abca23ab175e Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sat, 12 Mar 2022 20:29:19 +0400 Subject: [PATCH 16/28] sync latest version --- Cargo.dev.toml | 1 + payments/Cargo.toml | 18 +- payments/src/lib.rs | 502 +++++++++++++------------ payments/src/mock.rs | 60 ++- payments/src/tests.rs | 799 +++++++++++++++++++++++++--------------- payments/src/types.rs | 77 ++-- payments/src/weights.rs | 140 +++++-- 7 files changed, 985 insertions(+), 612 deletions(-) diff --git a/Cargo.dev.toml b/Cargo.dev.toml index 95274286a..b7f228737 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -22,6 +22,7 @@ members = [ "build-script-utils", "weight-gen", "weight-meter", + "payments" ] resolver = "2" diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 89352c823..de32bbf39 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -14,19 +14,19 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } -frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false, optional = true } -orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false, optional = true } +orml-traits = {path = "../traits", version = "0.4.1-dev", default-features = false } scale-info = { version = "1.0.0", default-features = false, features = ["derive"] } [dev-dependencies] serde = { version = "1.0.101" } -sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16", default-features = false } -orml-tokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +orml-tokens = { path = "../tokens", version = "0.4.1-dev", default-features = false } [features] default = ['std'] diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 0d0210e67..8e7f88bf5 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -8,13 +8,19 @@ mod mock; #[cfg(test)] mod tests; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + pub mod types; pub mod weights; #[frame_support::pallet] pub mod pallet { pub use crate::{ - types::{DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState}, + types::{ + DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, + ScheduledTask, Task, + }, weights::WeightInfo, }; use frame_support::{ @@ -27,15 +33,18 @@ pub mod pallet { traits::{CheckedAdd, Saturating}, Percent, }; + use sp_std::vec::Vec; - pub type BalanceOf = <::Asset as MultiCurrency<::AccountId>>::Balance; - pub type AssetIdOf = <::Asset as MultiCurrency<::AccountId>>::CurrencyId; + pub type BalanceOf = + <::Asset as MultiCurrency<::AccountId>>::Balance; + pub type AssetIdOf = + <::Asset as MultiCurrency<::AccountId>>::CurrencyId; pub type BoundedDataOf = BoundedVec::MaxRemarkLength>; + pub type ScheduledTaskOf = ScheduledTask<::BlockNumber>; #[pallet::config] pub trait Config: frame_system::Config { - /// Because this pallet emits events, it depends on the runtime's - /// definition of an event. + /// Because this pallet emits events, it depends on the runtime's definition of an event. type Event: From> + IsType<::Event>; /// the type of assets this pallet can hold in payment type Asset: MultiReservableCurrency; @@ -49,8 +58,7 @@ pub mod pallet { /// Maximum permitted size of `Remark` #[pallet::constant] type MaxRemarkLength: Get; - /// Buffer period - number of blocks to wait before user can claim - /// canceled payment + /// Buffer period - number of blocks to wait before user can claim canceled payment #[pallet::constant] type CancelBufferBlockLength: Get; //// Type representing the weight of this pallet @@ -62,13 +70,12 @@ pub mod pallet { pub struct Pallet(_); #[pallet::storage] - #[pallet::getter(fn rates)] - /// Payments created by a user, this method of storageDoubleMap is chosen - /// since there is no usecase for listing payments by provider/currency. The - /// payment will only be referenced by the creator in any transaction of - /// interest. The storage map keys are the creator and the recipient, this - /// also ensures that for any (sender,recipient) combo, only a single - /// payment is active. The history of payment is not stored. + #[pallet::getter(fn payment)] + /// Payments created by a user, this method of storageDoubleMap is chosen since there is no usecase for + /// listing payments by provider/currency. The payment will only be referenced by the creator in + /// any transaction of interest. + /// The storage map keys are the creator and the recipient, this also ensures + /// that for any (sender,recipient) combo, only a single payment is active. The history of payment is not stored. pub(super) type Payment = StorageDoubleMap< _, Blake2_128Concat, @@ -78,6 +85,18 @@ pub mod pallet { PaymentDetail, >; + #[pallet::storage] + #[pallet::getter(fn tasks)] + /// Store the list of tasks to be executed in the on_idle function + pub(super) type ScheduledTasks = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, // payment creator + Blake2_128Concat, + T::AccountId, // payment recipient + ScheduledTaskOf, + >; + #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { @@ -86,11 +105,14 @@ pub mod pallet { from: T::AccountId, asset: AssetIdOf, amount: BalanceOf, + remark: Option>, }, /// Payment amount released to the recipient PaymentReleased { from: T::AccountId, to: T::AccountId }, /// Payment has been cancelled by the creator PaymentCancelled { from: T::AccountId, to: T::AccountId }, + /// A payment that NeedsReview has been resolved by Judge + PaymentResolved { from: T::AccountId, to: T::AccountId, recipient_share: Percent }, /// the payment creator has created a refund request PaymentCreatorRequestedRefund { from: T::AccountId, @@ -123,57 +145,74 @@ pub mod pallet { RefundNotRequested, /// Dispute period has not passed DisputePeriodNotPassed, + /// The automatic cancelation queue cannot accept + RefundQueueFull, } #[pallet::hooks] - impl Hooks> for Pallet {} + impl Hooks> for Pallet { + /// Hook that execute when there is leftover space in a block + /// This function will look for any pending scheduled tasks that can + /// be executed and will process them. + fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight { + let mut task_list: Vec<(T::AccountId, T::AccountId, ScheduledTaskOf)> = + ScheduledTasks::::iter() + // leave out tasks in the future + .filter(|(_, _, ScheduledTask { when, .. })| when <= &now) + .collect(); + + if task_list.is_empty() { + return remaining_weight + } else { + task_list.sort_by(|(_, _, t), (_, _, x)| x.when.partial_cmp(&t.when).unwrap()); + } + + let cancel_weight = + T::WeightInfo::cancel().saturating_add(T::WeightInfo::remove_task()); + + while remaining_weight >= cancel_weight { + match task_list.pop() { + Some((from, to, ScheduledTask { task: Task::Cancel, .. })) => { + remaining_weight = remaining_weight.saturating_sub(cancel_weight); + + // process the cancel payment + if let Err(_) = >::settle_payment( + from.clone(), + to.clone(), + Percent::from_percent(0), + ) { + // panic!("{:?}", e); + } + ScheduledTasks::::remove(from.clone(), to.clone()); + // emit the cancel event + Self::deposit_event(Event::PaymentCancelled { + from: from.clone(), + to: to.clone(), + }); + }, + _ => return remaining_weight, + } + } + + remaining_weight + } + } #[pallet::call] impl Pallet { - /// This allows any user to create a new payment, that releases only to - /// specified recipient The only action is to store the details of this - /// payment in storage and reserve the specified amount. + /// This allows any user to create a new payment, that releases only to specified recipient + /// The only action is to store the details of this payment in storage and reserve + /// the specified amount. User also has the option to add a remark, this remark + /// can then be used to run custom logic and trigger alternate payment flows. + /// the specified amount. #[transactional] - #[pallet::weight(T::WeightInfo::pay())] + #[pallet::weight(T::WeightInfo::pay(T::MaxRemarkLength::get()))] pub fn pay( origin: OriginFor, recipient: T::AccountId, asset: AssetIdOf, - amount: BalanceOf, - ) -> DispatchResultWithPostInfo { - let who = ensure_signed(origin)?; - // create PaymentDetail and add to storage - let payment_detail = >::create_payment( - who.clone(), - recipient.clone(), - asset, - amount, - PaymentState::Created, - T::IncentivePercentage::get(), - None, - )?; - // reserve funds for payment - >::reserve_payment_amount(&who, &recipient, payment_detail)?; - // emit paymentcreated event - Self::deposit_event(Event::PaymentCreated { - from: who, - asset, - amount, - }); - Ok(().into()) - } - - /// This allows any user to create a new payment with the option to add - /// a remark, this remark can then be used to run custom logic and - /// trigger alternate payment flows. the specified amount. - #[transactional] - #[pallet::weight(T::WeightInfo::pay_with_remark(remark.len().try_into().unwrap_or(T::MaxRemarkLength::get())))] - pub fn pay_with_remark( - origin: OriginFor, - recipient: T::AccountId, - asset: AssetIdOf, - amount: BalanceOf, - remark: BoundedDataOf, + #[pallet::compact] amount: BalanceOf, + remark: Option>, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; @@ -185,46 +224,48 @@ pub mod pallet { amount, PaymentState::Created, T::IncentivePercentage::get(), - Some(remark), + remark.as_ref().map(|x| x.as_slice()), )?; // reserve funds for payment >::reserve_payment_amount(&who, &recipient, payment_detail)?; // emit paymentcreated event - Self::deposit_event(Event::PaymentCreated { - from: who, - asset, - amount, - }); + Self::deposit_event(Event::PaymentCreated { from: who, asset, amount, remark }); Ok(().into()) } - /// Release any created payment, this will transfer the reserved amount - /// from the creator of the payment to the assigned recipient + /// Release any created payment, this will transfer the reserved amount from the + /// creator of the payment to the assigned recipient #[transactional] #[pallet::weight(T::WeightInfo::release())] pub fn release(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { let from = ensure_signed(origin)?; // ensure the payment is in Created state - if let Some(payment) = Payment::::get(from.clone(), to.clone()) { + if let Some(payment) = Payment::::get(&from, &to) { ensure!(payment.state == PaymentState::Created, Error::::InvalidAction) + } else { + fail!(Error::::InvalidPayment); } // release is a settle_payment with 100% recipient_share - >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + >::settle_payment( + from.clone(), + to.clone(), + Percent::from_percent(100), + )?; Self::deposit_event(Event::PaymentReleased { from, to }); Ok(().into()) } - /// Cancel a payment in created state, this will release the reserved - /// back to creator of the payment. This extrinsic can only be called by - /// the recipient of the payment + /// Cancel a payment in created state, this will release the reserved back to + /// creator of the payment. This extrinsic can only be called by the recipient + /// of the payment #[transactional] #[pallet::weight(T::WeightInfo::cancel())] pub fn cancel(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - if let Some(payment) = Payment::::get(creator.clone(), who.clone()) { + if let Some(payment) = Payment::::get(&creator, &who) { match payment.state { // call settle payment with recipient_share=0, this refunds the sender PaymentState::Created => { @@ -234,142 +275,101 @@ pub mod pallet { Percent::from_percent(0), )?; Self::deposit_event(Event::PaymentCancelled { from: creator, to: who }); - } + }, // if the payment is in state PaymentRequested, remove from storage - PaymentState::PaymentRequested => Payment::::remove(creator.clone(), who.clone()), + PaymentState::PaymentRequested => Payment::::remove(&creator, &who), _ => fail!(Error::::InvalidAction), } - } else { - fail!(Error::::InvalidPayment); } Ok(().into()) } /// Allow judge to set state of a payment /// This extrinsic is used to resolve disputes between the creator and - /// recipient of the payment. This extrinsic allows the assigned judge - /// to cancel the payment + /// recipient of the payment. This extrinsic allows the assigned judge to cancel/release/partial_release + /// the payment. #[transactional] - #[pallet::weight(T::WeightInfo::resolve_cancel_payment())] - pub fn resolve_cancel_payment( + #[pallet::weight(T::WeightInfo::resolve_payment())] + pub fn resolve_payment( origin: OriginFor, from: T::AccountId, recipient: T::AccountId, + recipient_share: Percent, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; // ensure the caller is the assigned resolver - if let Some(payment) = Payment::::get(from.clone(), recipient.clone()) { + if let Some(payment) = Payment::::get(&from, &recipient) { ensure!(who == payment.resolver_account, Error::::InvalidAction) } // try to update the payment to new state - >::settle_payment(from.clone(), recipient.clone(), Percent::from_percent(0))?; - Self::deposit_event(Event::PaymentCancelled { from, to: recipient }); + >::settle_payment( + from.clone(), + recipient.clone(), + recipient_share, + )?; + Self::deposit_event(Event::PaymentResolved { from, to: recipient, recipient_share }); Ok(().into()) } - /// Allow judge to set state of a payment - /// This extrinsic is used to resolve disputes between the creator and - /// recipient of the payment. This extrinsic allows the assigned judge - /// to send the payment to recipient + /// Allow payment creator to set payment to NeedsReview + /// This extrinsic is used to mark the payment as disputed so the assigned judge can tigger a resolution + /// and that the funds are no longer locked. #[transactional] - #[pallet::weight(T::WeightInfo::resolve_release_payment())] - pub fn resolve_release_payment( + #[pallet::weight(T::WeightInfo::request_refund())] + pub fn request_refund( origin: OriginFor, - from: T::AccountId, recipient: T::AccountId, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - // ensure the caller is the assigned resolver - if let Some(payment) = Payment::::get(from.clone(), recipient.clone()) { - ensure!(who == payment.resolver_account, Error::::InvalidAction) - } - // try to update the payment to new state - >::settle_payment(from.clone(), recipient.clone(), Percent::from_percent(100))?; - Self::deposit_event(Event::PaymentReleased { from, to: recipient }); - Ok(().into()) - } - - /// Allow payment creator to set payment to NeedsReview - /// This extrinsic is used to mark the payment as disputed so the - /// assigned judge can tigger a resolution and that the funds are no - /// longer locked. - #[transactional] - #[pallet::weight(T::WeightInfo::request_refund())] - pub fn request_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { - let who = ensure_signed(origin)?; - - Payment::::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult { - // ensure the payment exists - let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; - // ensure the payment is not in needsreview state - ensure!( - payment.state != PaymentState::NeedsReview, - Error::::PaymentNeedsReview - ); - - // set the payment to requested refund - let current_block = frame_system::Pallet::::block_number(); - let can_cancel_block = current_block - .checked_add(&T::CancelBufferBlockLength::get()) - .ok_or(Error::::MathError)?; - payment.state = PaymentState::RefundRequested(can_cancel_block); - - Self::deposit_event(Event::PaymentCreatorRequestedRefund { - from: who, - to: recipient, - expiry: can_cancel_block, - }); - - Ok(()) - })?; - Ok(().into()) - } - - /// Allow payment creator to claim the refund if the payment recipent - /// has not disputed After the payment creator has `request_refund` can - /// then call this extrinsic to cancel the payment and receive the - /// reserved amount to the account if the dispute period has passed. - #[transactional] - #[pallet::weight(T::WeightInfo::claim_refund())] - pub fn claim_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { - use PaymentState::*; - let who = ensure_signed(origin)?; + Payment::::try_mutate( + who.clone(), + recipient.clone(), + |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // ensure the payment is not in needsreview state + ensure!( + payment.state != PaymentState::NeedsReview, + Error::::PaymentNeedsReview + ); + + // set the payment to requested refund + let current_block = frame_system::Pallet::::block_number(); + let cancel_block = current_block + .checked_add(&T::CancelBufferBlockLength::get()) + .ok_or(Error::::MathError)?; + + ScheduledTasks::::insert( + who.clone(), + recipient.clone(), + ScheduledTask { task: Task::Cancel, when: cancel_block }, + ); + + payment.state = PaymentState::RefundRequested { cancel_block }; + + Self::deposit_event(Event::PaymentCreatorRequestedRefund { + from: who, + to: recipient, + expiry: cancel_block, + }); - if let Some(payment) = Payment::::get(who.clone(), recipient.clone()) { - match payment.state { - NeedsReview => fail!(Error::::PaymentNeedsReview), - Created | PaymentRequested => fail!(Error::::RefundNotRequested), - RefundRequested(cancel_block) => { - let current_block = frame_system::Pallet::::block_number(); - // ensure the dispute period has passed - ensure!(current_block > cancel_block, Error::::DisputePeriodNotPassed); - // cancel the payment and refund the creator - >::settle_payment( - who.clone(), - recipient.clone(), - Percent::from_percent(0), - )?; - Self::deposit_event(Event::PaymentCancelled { - from: who, - to: recipient, - }); - } - } - } else { - fail!(Error::::InvalidPayment); - } + Ok(()) + }, + )?; Ok(().into()) } - /// Allow payment recipient to dispute the refund request from the - /// payment creator This does not cancel the request, instead sends the - /// payment to a NeedsReview state The assigned resolver account can - /// then change the state of the payment after review. + /// Allow payment recipient to dispute the refund request from the payment creator + /// This does not cancel the request, instead sends the payment to a NeedsReview state + /// The assigned resolver account can then change the state of the payment after review. #[transactional] #[pallet::weight(T::WeightInfo::dispute_refund())] - pub fn dispute_refund(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { + pub fn dispute_refund( + origin: OriginFor, + creator: T::AccountId, + ) -> DispatchResultWithPostInfo { use PaymentState::*; let who = ensure_signed(origin)?; @@ -381,11 +381,22 @@ pub mod pallet { let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; // ensure the payment is in Requested Refund state match payment.state { - RefundRequested(_) => { + RefundRequested { cancel_block } => { + ensure!( + cancel_block > frame_system::Pallet::::block_number(), + Error::::InvalidAction + ); + payment.state = PaymentState::NeedsReview; - Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who }); - } + // remove the payment from scheduled tasks + ScheduledTasks::::remove(creator.clone(), who.clone()); + + Self::deposit_event(Event::PaymentRefundDisputed { + from: creator, + to: who, + }); + }, _ => fail!(Error::::InvalidAction), } @@ -396,17 +407,16 @@ pub mod pallet { Ok(().into()) } - // Creates a new payment with the given details. This can be called by the - // recipient of the payment to create a payment and then completed by the sender - // using the `accept_and_pay` extrinsic. The payment will be in PaymentRequested - // State and can only be modified by the `accept_and_pay` extrinsic. + // Creates a new payment with the given details. This can be called by the recipient of the payment + // to create a payment and then completed by the sender using the `accept_and_pay` extrinsic. + // The payment will be in PaymentRequested State and can only be modified by the `accept_and_pay` extrinsic. #[transactional] #[pallet::weight(T::WeightInfo::request_payment())] pub fn request_payment( origin: OriginFor, from: T::AccountId, asset: AssetIdOf, - amount: BalanceOf, + #[pallet::compact] amount: BalanceOf, ) -> DispatchResultWithPostInfo { let to = ensure_signed(origin)?; @@ -426,26 +436,29 @@ pub mod pallet { Ok(().into()) } - // This extrinsic allows the sender to fulfill a payment request created by a - // recipient. The amount will be transferred to the recipient and payment - // removed from storage + // This extrinsic allows the sender to fulfill a payment request created by a recipient. + // The amount will be transferred to the recipient and payment removed from storage #[transactional] #[pallet::weight(T::WeightInfo::accept_and_pay())] - pub fn accept_and_pay(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { + pub fn accept_and_pay( + origin: OriginFor, + to: T::AccountId, + ) -> DispatchResultWithPostInfo { let from = ensure_signed(origin)?; - let payment = Payment::::get(from.clone(), to.clone()).ok_or(Error::::InvalidPayment)?; + let payment = Payment::::get(&from, &to).ok_or(Error::::InvalidPayment)?; - ensure!( - payment.state == PaymentState::PaymentRequested, - Error::::InvalidAction - ); + ensure!(payment.state == PaymentState::PaymentRequested, Error::::InvalidAction); // reserve all the fees from the sender >::reserve_payment_amount(&from, &to, payment)?; // release the payment and delete the payment from storage - >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + >::settle_payment( + from.clone(), + to.clone(), + Percent::from_percent(100), + )?; Self::deposit_event(Event::PaymentRequestCompleted { from, to }); @@ -454,9 +467,8 @@ pub mod pallet { } impl PaymentHandler for Pallet { - /// The function will create a new payment. The fee and incentive - /// amounts will be calculated and the `PaymentDetail` will be added to - /// storage. + /// The function will create a new payment. The fee and incentive amounts will be calculated and the + /// `PaymentDetail` will be added to storage. #[require_transactional] fn create_payment( from: T::AccountId, @@ -465,7 +477,7 @@ pub mod pallet { amount: BalanceOf, payment_state: PaymentState, incentive_percentage: Percent, - remark: Option>, + remark: Option<&[u8]>, ) -> Result, sp_runtime::DispatchError> { Payment::::try_mutate( from.clone(), @@ -483,6 +495,7 @@ pub mod pallet { Error::::PaymentNeedsReview ); } + // Calculate incentive amount - this is to insentivise the user to release // the funds once a transaction has been completed let incentive_amount = incentive_percentage.mul_floor(amount); @@ -492,14 +505,14 @@ pub mod pallet { amount, incentive_amount, state: payment_state, - resolver_account: T::DisputeResolver::get_origin(), + resolver_account: T::DisputeResolver::get_resolver_account(), fee_detail: None, - remark, }; // 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); + 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)); @@ -510,11 +523,14 @@ pub mod pallet { ) } - /// The function will reserve the fees+transfer amount from the `from` - /// account. After reserving the payment.amount will be transferred to - /// the recipient but will stay in Reserve state. + /// The function will reserve the fees+transfer amount from the `from` account. After reserving + /// the payment.amount will be transferred to the recipient but will stay in Reserve state. #[require_transactional] - fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult { + fn reserve_payment_amount( + from: &T::AccountId, + to: &T::AccountId, + payment: PaymentDetail, + ) -> DispatchResult { let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or(0u32.into()); let total_fee_amount = payment.incentive_amount.saturating_add(fee_amount); @@ -523,55 +539,71 @@ pub mod pallet { // reserve the total amount from payment creator T::Asset::reserve(payment.asset, from, total_amount)?; // transfer payment amount to recipient -- keeping reserve status - T::Asset::repatriate_reserved(payment.asset, from, to, payment.amount, BalanceStatus::Reserved)?; + T::Asset::repatriate_reserved( + payment.asset, + from, + to, + payment.amount, + BalanceStatus::Reserved, + )?; Ok(()) } - /// This function allows the caller to settle the payment by specifying - /// a recipient_share this will unreserve the fee+incentive to sender - /// and unreserve transferred amount to recipient if the settlement is a - /// release (ie recipient_share=100), the fee is transferred to - /// fee_recipient For cancelling a payment, recipient_share = 0 + /// This function allows the caller to settle the payment by specifying a recipient_share + /// this will unreserve the fee+incentive to sender and unreserve transferred amount to recipient + /// if the settlement is a release (ie recipient_share=100), the fee is transferred to fee_recipient + /// For cancelling a payment, recipient_share = 0 /// For releasing a payment, recipient_share = 100 /// In other cases, the custom recipient_share can be specified - #[require_transactional] - fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult { - Payment::::try_mutate(from.clone(), to.clone(), |maybe_payment| -> DispatchResult { - let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; - - // unreserve the incentive amount and fees from the owner account - match payment.fee_detail { - Some((fee_recipient, fee_amount)) => { - T::Asset::unreserve(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( + fn settle_payment( + from: T::AccountId, + to: T::AccountId, + recipient_share: Percent, + ) -> DispatchResult { + Payment::::try_mutate( + from.clone(), + to.clone(), + |maybe_payment| -> DispatchResult { + let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; + + // unreserve the incentive amount and fees from the owner account + match payment.fee_detail { + Some((fee_recipient, fee_amount)) => { + T::Asset::unreserve( payment.asset, - &from, // fee is paid by payment creator - &fee_recipient, // account of fee recipient - fee_amount, // amount of fee - )?; - } - } - None => { - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); - } - }; + &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 + )?; + } + }, + None => { + T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + }, + }; - // Unreserve the transfer amount - T::Asset::unreserve(payment.asset, &to, payment.amount); + // Unreserve the transfer 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); - // send share to recipient - T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; + let amount_to_recipient = recipient_share.mul_floor(payment.amount); + let amount_to_sender = payment.amount.saturating_sub(amount_to_recipient); + // send share to recipient + T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; - Ok(()) - })?; + Ok(()) + }, + )?; Ok(()) } - fn get_payment_details(from: T::AccountId, to: T::AccountId) -> Option> { + fn get_payment_details(from: &T::AccountId, to: &T::AccountId) -> Option> { Payment::::get(from, to) } } diff --git a/payments/src/mock.rs b/payments/src/mock.rs index af55dbb0e..0a5646ab5 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -2,7 +2,8 @@ use crate as payment; use crate::PaymentDetail; use frame_support::{ parameter_types, - traits::{Contains, Everything, GenesisBuild, OnFinalize, OnInitialize}, + traits::{Contains, Everything, GenesisBuild, Hooks, OnFinalize}, + weights::DispatchClass, }; use frame_system as system; use orml_traits::parameter_type_with_key; @@ -12,6 +13,7 @@ use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, Percent, }; +use virto_primitives::{Asset, NetworkAsset}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -20,10 +22,15 @@ pub type Balance = u128; pub type AccountId = u8; pub const PAYMENT_CREATOR: AccountId = 10; pub const PAYMENT_RECIPENT: AccountId = 11; -pub const CURRENCY_ID: u128 = 1; +pub const PAYMENT_CREATOR_TWO: AccountId = 30; +pub const PAYMENT_RECIPENT_TWO: AccountId = 31; +pub const CURRENCY_ID: Asset = Asset::Network(NetworkAsset::KSM); pub const RESOLVER_ACCOUNT: AccountId = 12; pub const FEE_RECIPIENT_ACCOUNT: AccountId = 20; pub const PAYMENT_RECIPENT_FEE_CHARGED: AccountId = 21; +pub const INCENTIVE_PERCENTAGE: u8 = 10; +pub const MARKETPLACE_FEE_PERCENTAGE: u8 = 10; +pub const CANCEL_BLOCK_BUFFER: u64 = 600; frame_support::construct_runtime!( pub enum Test where @@ -70,7 +77,7 @@ impl system::Config for Test { } parameter_type_with_key! { - pub ExistentialDeposits: |_currency_id: u128| -> Balance { + pub ExistentialDeposits: |_currency_id: Asset| -> Balance { 0u128 }; } @@ -88,7 +95,7 @@ impl Contains for MockDustRemovalWhitelist { impl orml_tokens::Config for Test { type Amount = i64; type Balance = Balance; - type CurrencyId = u128; + type CurrencyId = Asset; type Event = Event; type ExistentialDeposits = ExistentialDeposits; type OnDust = (); @@ -99,25 +106,31 @@ impl orml_tokens::Config for Test { pub struct MockDisputeResolver; impl crate::types::DisputeResolver for MockDisputeResolver { - fn get_origin() -> AccountId { + fn get_resolver_account() -> AccountId { RESOLVER_ACCOUNT } } pub struct MockFeeHandler; impl crate::types::FeeHandler for MockFeeHandler { - fn apply_fees(_from: &AccountId, to: &AccountId, _remark: &PaymentDetail) -> (AccountId, Percent) { + fn apply_fees( + _from: &AccountId, + to: &AccountId, + _detail: &PaymentDetail, + _remark: Option<&[u8]>, + ) -> (AccountId, Percent) { match to { - &PAYMENT_RECIPENT_FEE_CHARGED => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(10)), + &PAYMENT_RECIPENT_FEE_CHARGED => + (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE)), _ => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(0)), } } } parameter_types! { - pub const IncentivePercentage: Percent = Percent::from_percent(10); + pub const IncentivePercentage: Percent = Percent::from_percent(INCENTIVE_PERCENTAGE); pub const MaxRemarkLength: u32 = 50; - pub const CancelBufferBlockLength: u64 = 600; + pub const CancelBufferBlockLength: u64 = CANCEL_BLOCK_BUFFER; } impl payment::Config for Test { @@ -136,7 +149,10 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = system::GenesisConfig::default().build_storage::().unwrap(); orml_tokens::GenesisConfig:: { - balances: vec![(PAYMENT_CREATOR, CURRENCY_ID, 100)], + balances: vec![ + (PAYMENT_CREATOR, CURRENCY_ID, 100), + (PAYMENT_CREATOR_TWO, CURRENCY_ID, 100), + ], } .assimilate_storage(&mut t) .unwrap(); @@ -147,10 +163,24 @@ pub fn new_test_ext() -> sp_io::TestExternalities { ext } -pub fn run_to_block(n: u64) { - while System::block_number() < n { - System::on_finalize(System::block_number()); - System::set_block_number(System::block_number() + 1); - System::on_initialize(System::block_number()); +pub fn run_n_blocks(n: u64) -> u64 { + const IDLE_WEIGHT: u64 = 10_000_000_000; + const BUSY_WEIGHT: u64 = IDLE_WEIGHT / 1000; + + let start_block = System::block_number(); + + for block_number in (0..=n).map(|n| n + start_block) { + System::set_block_number(block_number); + + // Odd blocks gets busy + let idle_weight = if block_number % 2 == 0 { IDLE_WEIGHT } else { BUSY_WEIGHT }; + // ensure the on_idle is executed + >::register_extra_weight_unchecked( + Payment::on_idle(block_number, idle_weight), + DispatchClass::Mandatory, + ); + + as OnFinalize>::on_finalize(block_number); } + System::block_number() } diff --git a/payments/src/tests.rs b/payments/src/tests.rs index 225b11549..3a58ceefe 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -1,12 +1,14 @@ use crate::{ mock::*, types::{PaymentDetail, PaymentState}, - Payment as PaymentStore, PaymentHandler, + Payment as PaymentStore, PaymentHandler, ScheduledTask, ScheduledTasks, Task, }; use frame_support::{assert_noop, assert_ok, storage::with_transaction}; use orml_traits::MultiCurrency; use sp_runtime::{Percent, TransactionOutcome}; +type Error = crate::Error; + fn last_event() -> Event { System::events().pop().expect("Event expected").event } @@ -14,8 +16,12 @@ fn last_event() -> Event { #[test] fn test_pay_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should be able to create a payment with available balance @@ -23,41 +29,55 @@ fn test_pay_works() { Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); assert_eq!( last_event(), crate::Event::::PaymentCreated { from: PAYMENT_CREATOR, asset: CURRENCY_ID, - amount: 20 + amount: payment_amount, + remark: None } .into() ); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); // the payment amount should be reserved correctly // the amount + incentive should be removed from the sender account - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); // the incentive amount should be reserved in the sender account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // the transferred amount should be reserved in the recipent account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // the payment should not be overwritten assert_noop!( - Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), crate::Error::::PaymentAlreadyInProcess ); @@ -65,12 +85,11 @@ fn test_pay_works() { PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, + amount: payment_amount, incentive_amount: 2, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); }); @@ -79,49 +98,47 @@ fn test_pay_works() { #[test] fn test_cancel_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 40, - incentive_amount: 4, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - - // cancel should fail when called by user - assert_noop!( - Payment::cancel(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::InvalidPayment + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // cancel should succeed when caller is the recipent assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); assert_eq!( last_event(), - crate::Event::::PaymentCancelled { - from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT - } - .into() + crate::Event::::PaymentCancelled { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } + .into() ); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); // should be released from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -131,43 +148,49 @@ fn test_cancel_works() { #[test] fn test_release_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 40, - incentive_amount: 4, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should succeed for valid payment assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); assert_eq!( last_event(), - crate::Event::::PaymentReleased { - from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT - } - .into() + crate::Event::::PaymentReleased { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } + .into() ); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be deleted from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -177,50 +200,67 @@ fn test_release_works() { Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 16); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - (payment_amount * 2) - expected_incentive_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); }); } #[test] fn test_set_state_payment_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); // should fail for non whitelisted caller assert_noop!( - Payment::resolve_cancel_payment(Origin::signed(PAYMENT_CREATOR), PAYMENT_CREATOR, PAYMENT_RECIPENT,), - crate::Error::::InvalidAction + Payment::resolve_payment( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_CREATOR, + PAYMENT_RECIPENT, + Percent::from_percent(100) + ), + Error::InvalidAction ); // should be able to release a payment - assert_ok!(Payment::resolve_release_payment( + assert_ok!(Payment::resolve_payment( Origin::signed(RESOLVER_ACCOUNT), PAYMENT_CREATOR, PAYMENT_RECIPENT, + Percent::from_percent(100) )); assert_eq!( last_event(), - crate::Event::::PaymentReleased { + crate::Event::::PaymentResolved { from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT + to: PAYMENT_RECIPENT, + recipient_share: Percent::from_percent(100) } .into() ); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be removed from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -229,28 +269,33 @@ fn test_set_state_payment_works() { Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 40, + payment_amount, + None )); // should be able to cancel a payment - assert_ok!(Payment::resolve_cancel_payment( + assert_ok!(Payment::resolve_payment( Origin::signed(RESOLVER_ACCOUNT), PAYMENT_CREATOR, PAYMENT_RECIPENT, + Percent::from_percent(0) )); assert_eq!( last_event(), - crate::Event::::PaymentCancelled { + crate::Event::::PaymentResolved { from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT + to: PAYMENT_RECIPENT, + recipient_share: Percent::from_percent(0) } .into() ); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 60); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 40); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be released from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -260,118 +305,154 @@ fn test_set_state_payment_works() { #[test] fn test_charging_fee_payment_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, CURRENCY_ID, - 40, + payment_amount, + None )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 40, - incentive_amount: 4, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 4)), - remark: None + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - + payment_amount - expected_fee_amount - + expected_incentive_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); // should succeed for valid payment - assert_ok!(Payment::release( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT_FEE_CHARGED - )); + assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED)); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 56); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 40); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 4); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount + ); + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); }); } #[test] fn test_charging_fee_payment_works_when_canceled() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + // should be able to create a payment with available balance assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, CURRENCY_ID, - 40, + payment_amount, + None )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 40, - incentive_amount: 4, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 4)), - remark: None + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); // the payment amount should be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 52); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - + payment_amount - expected_fee_amount - + expected_incentive_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); // should succeed for valid payment - assert_ok!(Payment::cancel( - Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), - PAYMENT_CREATOR - )); + assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), PAYMENT_CREATOR)); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 0); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); }); } #[test] fn test_pay_with_remark_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + // should be able to create a payment with available balance - assert_ok!(Payment::pay_with_remark( + assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, - vec![1u8; 10].try_into().unwrap() + payment_amount, + Some(vec![1u8; 10].try_into().unwrap()) )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()) }) ); // the payment amount should be reserved correctly // the amount + incentive should be removed from the sender account - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); // the incentive amount should be reserved in the sender account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // the transferred amount should be reserved in the recipent account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // the payment should not be overwritten assert_noop!( - Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), crate::Error::::PaymentAlreadyInProcess ); @@ -380,7 +461,8 @@ fn test_pay_with_remark_works() { crate::Event::::PaymentCreated { from: PAYMENT_CREATOR, asset: CURRENCY_ID, - amount: 20 + amount: payment_amount, + remark: Some(vec![1u8; 10].try_into().unwrap()) } .into() ); @@ -390,15 +472,25 @@ fn test_pay_with_remark_works() { #[test] fn test_do_not_overwrite_logic_works() { new_test_ext().execute_with(|| { + let payment_amount = 40; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); assert_noop!( - Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), crate::Error::::PaymentAlreadyInProcess ); @@ -408,18 +500,23 @@ fn test_do_not_overwrite_logic_works() { PAYMENT_RECIPENT, PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::NeedsReview, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None, }, ); // the payment should not be overwritten assert_noop!( - Payment::pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, 20,), + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), crate::Error::::PaymentNeedsReview ); }); @@ -428,28 +525,29 @@ fn test_do_not_overwrite_logic_works() { #[test] fn test_request_refund() { new_test_ext().execute_with(|| { + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_cancel_block = CANCEL_BLOCK_BUFFER + 1; + assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); - assert_ok!(Payment::request_refund( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT - )); + assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, - state: PaymentState::RefundRequested(601u64.into()), + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::RefundRequested { cancel_block: expected_cancel_block }, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); @@ -458,102 +556,53 @@ fn test_request_refund() { crate::Event::::PaymentCreatorRequestedRefund { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT, - expiry: 601u64.into() - } - .into() - ); - }); -} - -#[test] -fn test_claim_refund() { - new_test_ext().execute_with(|| { - assert_ok!(Payment::pay( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT, - CURRENCY_ID, - 20, - )); - - // cannot claim refund unless payment is in requested refund state - assert_noop!( - Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::RefundNotRequested - ); - - assert_ok!(Payment::request_refund( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT - )); - - // cannot cancel before the dispute period has passed - assert_noop!( - Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::DisputePeriodNotPassed - ); - - run_to_block(700); - assert_ok!(Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); - - assert_eq!( - last_event(), - crate::Event::::PaymentCancelled { - from: PAYMENT_CREATOR, - to: PAYMENT_RECIPENT + expiry: expected_cancel_block } .into() ); - // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); - - // should be released from storage - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); }); } #[test] fn test_dispute_refund() { new_test_ext().execute_with(|| { + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_cancel_block = CANCEL_BLOCK_BUFFER + 1; + assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); // cannot dispute if refund is not requested assert_noop!( Payment::dispute_refund(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR), - crate::Error::::InvalidAction + Error::InvalidAction ); // creator requests a refund - assert_ok!(Payment::request_refund( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT - )); - // recipient disputes the refund request - assert_ok!(Payment::dispute_refund( - Origin::signed(PAYMENT_RECIPENT), - PAYMENT_CREATOR - )); - // payment cannot be claimed after disputed - assert_noop!( - Payment::claim_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::PaymentNeedsReview + assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + // ensure the request is added to the refund queue + assert_eq!( + ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), + ScheduledTask { task: Task::Cancel, when: expected_cancel_block } ); + // recipient disputes the refund request + assert_ok!(Payment::dispute_refund(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::NeedsReview, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); @@ -565,29 +614,34 @@ fn test_dispute_refund() { } .into() ); + + // ensure the request is added to the refund queue + assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); }); } #[test] fn test_request_payment() { new_test_ext().execute_with(|| { + let payment_amount = 20; + let expected_incentive_amount = 0; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 0_u128, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::PaymentRequested, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); @@ -605,17 +659,19 @@ fn test_request_payment() { #[test] fn test_requested_payment_cannot_be_released() { new_test_ext().execute_with(|| { + let payment_amount = 20; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); // requested payment cannot be released assert_noop!( Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), - crate::Error::::InvalidAction + Error::InvalidAction ); }); } @@ -623,11 +679,13 @@ fn test_requested_payment_cannot_be_released() { #[test] fn test_requested_payment_can_be_cancelled_by_requestor() { new_test_ext().execute_with(|| { + let payment_amount = 20; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); @@ -640,35 +698,37 @@ fn test_requested_payment_can_be_cancelled_by_requestor() { #[test] fn test_accept_and_pay() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = 0; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 0_u128, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::PaymentRequested, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: None }) ); - assert_ok!(Payment::accept_and_pay( - Origin::signed(PAYMENT_CREATOR), - PAYMENT_RECIPENT, - )); + assert_ok!(Payment::accept_and_pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT,)); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be deleted from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -692,11 +752,12 @@ fn test_accept_and_pay_should_fail_for_non_payment_requested() { PAYMENT_RECIPENT, CURRENCY_ID, 20, + None )); assert_noop!( Payment::accept_and_pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT,), - crate::Error::::InvalidAction + Error::InvalidAction ); }); } @@ -704,23 +765,27 @@ fn test_accept_and_pay_should_fail_for_non_payment_requested() { #[test] fn test_accept_and_pay_should_charge_fee_correctly() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = 0; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + assert_ok!(Payment::request_payment( Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), PAYMENT_CREATOR, CURRENCY_ID, - 20, + payment_amount, )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 0_u128, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::PaymentRequested, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 2)), - remark: None + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); @@ -730,16 +795,18 @@ fn test_accept_and_pay_should_charge_fee_correctly() { )); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 20); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 2); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); - - // should be deleted from storage assert_eq!( - PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), - None + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_fee_amount ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); assert_eq!( last_event(), @@ -771,21 +838,25 @@ fn test_create_payment_does_not_work_without_transaction() { #[test] fn test_create_payment_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = 0; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( PAYMENT_CREATOR, PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, PaymentState::Created, - Percent::from_percent(10), - Some(vec![1u8; 10].try_into().unwrap()), + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&vec![1u8; 10]), ) }))); @@ -793,12 +864,11 @@ fn test_create_payment_works() { PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()), + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); @@ -809,25 +879,24 @@ fn test_create_payment_works() { PAYMENT_CREATOR, PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, PaymentState::Created, - Percent::from_percent(10), - Some(vec![1u8; 10].try_into().unwrap()), + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&vec![1u8; 10]), ) })), - crate::Error::::PaymentAlreadyInProcess + Error::PaymentAlreadyInProcess ); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()), + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); }); @@ -836,21 +905,25 @@ fn test_create_payment_works() { #[test] fn test_reserve_payment_amount_works() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + let expected_fee_amount = 0; + // the payment amount should not be reserved assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( PAYMENT_CREATOR, PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, PaymentState::Created, - Percent::from_percent(10), - Some(vec![1u8; 10].try_into().unwrap()), + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&vec![1u8; 10]), ) }))); @@ -858,12 +931,11 @@ fn test_reserve_payment_amount_works() { PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()), + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); @@ -876,12 +948,18 @@ fn test_reserve_payment_amount_works() { }))); // the payment amount should be reserved correctly // the amount + incentive should be removed from the sender account - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 78); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount - expected_incentive_amount + ); // the incentive amount should be reserved in the sender account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); + assert_eq!( + Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // the transferred amount should be reserved in the recipent account - assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); + assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // the payment should not be overwritten assert_noop!( @@ -890,25 +968,24 @@ fn test_reserve_payment_amount_works() { PAYMENT_CREATOR, PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, PaymentState::Created, - Percent::from_percent(10), - Some(vec![1u8; 10].try_into().unwrap()), + Percent::from_percent(INCENTIVE_PERCENTAGE), + Some(&vec![1u8; 10]), ) })), - crate::Error::::PaymentAlreadyInProcess + Error::PaymentAlreadyInProcess ); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { asset: CURRENCY_ID, - amount: 20, - incentive_amount: 2, + amount: payment_amount, + incentive_amount: expected_incentive_amount, state: PaymentState::Created, resolver_account: RESOLVER_ACCOUNT, - fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), - remark: Some(vec![1u8; 10].try_into().unwrap()), + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, expected_fee_amount)), }) ); }); @@ -917,17 +994,20 @@ fn test_reserve_payment_amount_works() { #[test] fn test_settle_payment_works_for_cancel() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); assert_ok!(with_transaction(|| TransactionOutcome::Commit({ @@ -939,9 +1019,8 @@ fn test_settle_payment_works_for_cancel() { }))); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); // should be released from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -951,17 +1030,20 @@ fn test_settle_payment_works_for_cancel() { #[test] fn test_settle_payment_works_for_release() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, CURRENCY_ID, - 20, + payment_amount, + None )); assert_ok!(with_transaction(|| TransactionOutcome::Commit({ @@ -973,9 +1055,11 @@ fn test_settle_payment_works_for_release() { }))); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 80); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 20); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance - payment_amount + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), payment_amount); // should be deleted from storage assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); @@ -985,17 +1069,21 @@ fn test_settle_payment_works_for_release() { #[test] fn test_settle_payment_works_for_70_30() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 10; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, CURRENCY_ID, - 10, + payment_amount, + None )); assert_ok!(with_transaction(|| TransactionOutcome::Commit({ @@ -1006,34 +1094,45 @@ fn test_settle_payment_works_for_70_30() { ) }))); - // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 92); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 7); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 1); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + let expected_amount_for_creator = + creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(30) * payment_amount); + let expected_amount_for_recipient = Percent::from_percent(70) * payment_amount; - // should be deleted from storage + // the payment amount should be transferred assert_eq!( - PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), - None + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + expected_amount_for_creator + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + expected_amount_for_recipient ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + + // should be deleted from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); }); } #[test] fn test_settle_payment_works_for_50_50() { new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 10; + let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; + // the payment amount should not be reserved assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); - // should be able to create a payment with available balance within a - // transaction + // should be able to create a payment with available balance within a transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, CURRENCY_ID, - 10, + payment_amount, + None )); assert_ok!(with_transaction(|| TransactionOutcome::Commit({ @@ -1044,16 +1143,142 @@ fn test_settle_payment_works_for_50_50() { ) }))); + let expected_amount_for_creator = + creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(50) * payment_amount); + let expected_amount_for_recipient = Percent::from_percent(50) * payment_amount; + // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 94); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 5); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 1); - assert_eq!(Tokens::total_issuance(CURRENCY_ID), 100); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + expected_amount_for_creator + ); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), + expected_amount_for_recipient + ); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); // should be deleted from storage - assert_eq!( - PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); + }); +} + +#[test] +fn test_automatic_refund_works() { + new_test_ext().execute_with(|| { + let creator_initial_balance = 100; + let payment_amount = 20; + let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; + const CANCEL_PERIOD: u64 = 600; + const CANCEL_BLOCK: u64 = CANCEL_PERIOD + 1; + + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, None + )); + + assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), + Some(PaymentDetail { + asset: CURRENCY_ID, + amount: payment_amount, + incentive_amount: expected_incentive_amount, + state: PaymentState::RefundRequested { cancel_block: CANCEL_BLOCK }, + resolver_account: RESOLVER_ACCOUNT, + fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), + }) + ); + + assert_eq!( + ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), + ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } + ); + + // run to one block before cancel and make sure data is same + assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 600); + assert_eq!( + ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), + ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } + ); + + // run to after cancel block but odd blocks are busy + assert_eq!(run_n_blocks(1), 601); + // the payment is still not processed since the block was busy + assert!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).is_some()); + + // next block has spare weight to process the payment + assert_eq!(run_n_blocks(1), 602); + // the payment should be removed from storage + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + // the scheduled storage should be cleared + assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + + // test that the refund happened correctly + assert_eq!( + last_event(), + crate::Event::::PaymentCancelled { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } + .into() ); + // the payment amount should be released back to creator + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + }); +} + +#[test] +fn test_automatic_refund_works_for_multiple_payments() { + new_test_ext().execute_with(|| { + const CANCEL_PERIOD: u64 = 600; + + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + 20, + None + )); + + assert_ok!(Payment::pay( + Origin::signed(PAYMENT_CREATOR_TWO), + PAYMENT_RECIPENT_TWO, + CURRENCY_ID, + 20, + None + )); + + assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + run_n_blocks(1); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR_TWO), + PAYMENT_RECIPENT_TWO + )); + + assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 601); + + // Odd block 601 was busy so we still haven't processed the first payment + assert_ok!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).ok_or(())); + + // Even block 602 has enough room to process both pending payments + assert_eq!(run_n_blocks(1), 602); + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + assert_eq!(PaymentStore::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), None); + + // the scheduled storage should be cleared + assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), None); + + // test that the refund happened correctly + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); + + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR_TWO), 100); + assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_TWO), 0); }); } diff --git a/payments/src/types.rs b/payments/src/types.rs index 20b9c094b..290c67ce8 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -1,14 +1,13 @@ #![allow(unused_qualifications)] -use crate::{pallet, AssetIdOf, BalanceOf, BoundedDataOf}; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use crate::{pallet, AssetIdOf, BalanceOf}; +use parity_scale_codec::{Decode, Encode, HasCompact, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::{DispatchResult, Percent}; /// The PaymentDetail struct stores information about the payment/escrow -/// A "payment" in virto network is similar to an escrow, it is used to -/// guarantee proof of funds and can be released once an agreed upon condition -/// has reached between the payment creator and recipient. The payment lifecycle -/// is tracked using the state field. +/// A "payment" in virto network is similar to an escrow, it is used to guarantee proof of funds +/// and can be released once an agreed upon condition has reached between the payment creator +/// and recipient. The payment lifecycle is tracked using the state field. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound(T: pallet::Config))] @@ -16,23 +15,21 @@ pub struct PaymentDetail { /// type of asset used for payment pub asset: AssetIdOf, /// amount of asset used for payment + #[codec(compact)] pub amount: BalanceOf, /// incentive amount that is credited to creator for resolving + #[codec(compact)] pub incentive_amount: BalanceOf, - /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, - /// Requested] + /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, Requested] pub state: PaymentState, /// 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)>, - /// remarks to give context to payment - pub remark: Option>, } /// The `PaymentState` enum tracks the possible states that a payment can be in. -/// When a payment is 'completed' or 'cancelled' it is removed from storage and -/// hence not tracked by a state. +/// When a payment is 'completed' or 'cancelled' it is removed from storage and hence not tracked by a state. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum PaymentState { @@ -41,7 +38,7 @@ pub enum PaymentState { /// A judge needs to review and release manually NeedsReview, /// The user has requested refund and will be processed by `BlockNumber` - RefundRequested(BlockNumber), + RefundRequested { cancel_block: BlockNumber }, /// The recipient of this transaction has created a request PaymentRequested, } @@ -59,38 +56,66 @@ pub trait PaymentHandler { amount: BalanceOf, payment_state: PaymentState, incentive_percentage: Percent, - remark: Option>, + remark: Option<&[u8]>, ) -> Result, sp_runtime::DispatchError>; /// Attempt to reserve an amount of the given asset from the caller /// If not possible then return Error. Possible reasons for failure include: /// - User does not have enough balance. - fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult; + fn reserve_payment_amount( + from: &T::AccountId, + to: &T::AccountId, + payment: PaymentDetail, + ) -> DispatchResult; - // Settle a payment of `from` to `to`. To release a payment, the - // recipient_share=100, to cancel a payment recipient_share=0 + // Settle a payment of `from` to `to`. To release a payment, the recipient_share=100, + // to cancel a payment recipient_share=0 // Possible reasonse for failure include /// - The payment does not exist /// - The unreserve operation fails /// - The transfer operation fails - fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult; + fn settle_payment( + from: T::AccountId, + to: T::AccountId, + recipient_share: Percent, + ) -> DispatchResult; /// Attempt to fetch the details of a payment from the given payment_id /// Possible reasons for failure include: /// - The payment does not exist - fn get_payment_details(from: T::AccountId, to: T::AccountId) -> Option>; + fn get_payment_details(from: &T::AccountId, to: &T::AccountId) -> Option>; } -/// DisputeResolver trait defines how to create/assing judges for solving -/// payment disputes +/// DisputeResolver trait defines how to create/assign judges for solving payment disputes pub trait DisputeResolver { - /// Get a DisputeResolver (Judge) account - fn get_origin() -> Account; + /// Returns an `Account` + fn get_resolver_account() -> Account; } -/// Fee Handler trait that defines how to handle marketplace fees to every -/// payment/swap +/// Fee Handler trait that defines how to handle marketplace fees to every payment/swap pub trait FeeHandler { /// Get the distribution of fees to marketplace participants - fn apply_fees(from: &T::AccountId, to: &T::AccountId, detail: &PaymentDetail) -> (T::AccountId, Percent); + fn apply_fees( + from: &T::AccountId, + to: &T::AccountId, + detail: &PaymentDetail, + remark: Option<&[u8]>, + ) -> (T::AccountId, Percent); } + +/// Types of Tasks that can be scheduled in the pallet +#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)] +pub enum Task { + // payment `from` to `to` has to be cancelled + Cancel, +} + +/// The details of a scheduled task +#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)] +pub struct ScheduledTask { + /// the type of scheduled task + pub task: Task, + /// the 'time' at which the task should be executed + #[codec(compact)] + pub when: Time, +} \ No newline at end of file diff --git a/payments/src/weights.rs b/payments/src/weights.rs index 153ed4ae5..df20c5312 100644 --- a/payments/src/weights.rs +++ b/payments/src/weights.rs @@ -1,7 +1,7 @@ //! Autogenerated weights for virto_payment //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-02-18, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-03-11, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: @@ -16,7 +16,6 @@ // --pallet=virto-payment // --steps=20 // --repeat=10 -// --raw // --heap-pages=4096 // --output // ./pallets/payment/src/weights.rs @@ -32,43 +31,108 @@ use sp_std::marker::PhantomData; /// Weight functions needed for virto_payment. pub trait WeightInfo { - fn pay() -> Weight; - fn pay_with_remark(x: u32, ) -> Weight; + fn pay(x: u32, ) -> Weight; fn release() -> Weight; fn cancel() -> Weight; - fn resolve_cancel_payment() -> Weight; - fn resolve_release_payment() -> Weight; + fn resolve_payment() -> Weight; fn request_refund() -> Weight; - fn claim_refund() -> Weight; fn dispute_refund() -> Weight; fn request_payment() -> Weight; fn accept_and_pay() -> Weight; + fn read_task() -> Weight; + fn remove_task() -> Weight; } -// For backwards compatibility and tests -impl WeightInfo for () { +/// Weights for virto_payment using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:1) - fn pay() -> Weight { - (54_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(5 as Weight)) - .saturating_add(RocksDbWeight::get().writes(4 as Weight)) + fn pay(_x: u32, ) -> Weight { + (63_805_000 as Weight) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn release() -> Weight { + (33_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:0) + fn cancel() -> Weight { + (51_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Assets Accounts (r:2 w:2) + fn resolve_payment() -> Weight { + (39_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:0 w:1) + fn request_refund() -> Weight { + (18_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:0 w:1) + fn dispute_refund() -> Weight { + (19_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) + fn request_payment() -> Weight { + (20_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } + // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:1) - fn pay_with_remark(_x: u32, ) -> Weight { - (54_397_000 as Weight) + fn accept_and_pay() -> Weight { + (58_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + // Storage: Payment ScheduledTasks (r:2 w:0) + fn read_task() -> Weight { + (12_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + } + // Storage: Payment ScheduledTasks (r:0 w:1) + fn remove_task() -> Weight { + (2_000_000 as Weight) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + // Storage: Payment Payment (r:1 w:1) + // Storage: Sudo Key (r:1 w:0) + // Storage: Assets Accounts (r:2 w:2) + // Storage: System Account (r:1 w:1) + fn pay(_x: u32, ) -> Weight { + (63_805_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn release() -> Weight { - (34_000_000 as Weight) + (33_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } @@ -76,49 +140,35 @@ impl WeightInfo for () { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:0) fn cancel() -> Weight { - (46_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(3 as Weight)) - } - // Storage: Payment Payment (r:1 w:1) - // Storage: Assets Accounts (r:2 w:2) - // Storage: System Account (r:1 w:0) - fn resolve_cancel_payment() -> Weight { - (46_000_000 as Weight) + (51_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) - fn resolve_release_payment() -> Weight { - (35_000_000 as Weight) + fn resolve_payment() -> Weight { + (39_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:0 w:1) fn request_refund() -> Weight { - (17_000_000 as Weight) + (18_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) - } - // Storage: Payment Payment (r:1 w:1) - // Storage: Assets Accounts (r:2 w:2) - // Storage: System Account (r:1 w:0) - fn claim_refund() -> Weight { - (47_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(4 as Weight)) - .saturating_add(RocksDbWeight::get().writes(3 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) + // Storage: Payment ScheduledTasks (r:0 w:1) fn dispute_refund() -> Weight { - (16_000_000 as Weight) + (19_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(1 as Weight)) - .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) fn request_payment() -> Weight { - (18_000_000 as Weight) + (20_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } @@ -130,4 +180,14 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } + // Storage: Payment ScheduledTasks (r:2 w:0) + fn read_task() -> Weight { + (12_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) + } + // Storage: Payment ScheduledTasks (r:0 w:1) + fn remove_task() -> Weight { + (2_000_000 as Weight) + .saturating_add(RocksDbWeight::get().writes(1 as Weight)) + } } \ No newline at end of file From e7a5bd01b4623bf16ba809e91b184a4e75959e7c Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sat, 12 Mar 2022 20:36:43 +0400 Subject: [PATCH 17/28] remove virto deps --- payments/Cargo.toml | 3 +- payments/src/lib.rs | 330 +++++++++++++++++++----------------------- payments/src/mock.rs | 16 +- payments/src/tests.rs | 210 ++++++++++++++++++++------- payments/src/types.rs | 37 +++-- 5 files changed, 333 insertions(+), 263 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index de32bbf39..0bf88ebe3 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -6,7 +6,7 @@ version = "0.4.1-dev" license = "Apache-2.0" homepage = "https://github.com/virto-network/virto-node" repository = "https://github.com/virto-network/virto-node" -description = "Allows users to post payment on-chain" +description = "Allows users to post escrow payment on-chain" readme = "README.md" [package.metadata.docs.rs] @@ -39,6 +39,7 @@ std = [ 'scale-info/std', 'orml-traits/std', 'frame-benchmarking/std', + 'orml-tokens/std' ] runtime-benchmarks = [ "frame-benchmarking", diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 8e7f88bf5..63dfa7469 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -8,19 +8,13 @@ mod mock; #[cfg(test)] mod tests; -#[cfg(feature = "runtime-benchmarks")] -mod benchmarking; - pub mod types; pub mod weights; #[frame_support::pallet] pub mod pallet { pub use crate::{ - types::{ - DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, - ScheduledTask, Task, - }, + types::{DisputeResolver, FeeHandler, PaymentDetail, PaymentHandler, PaymentState, ScheduledTask, Task}, weights::WeightInfo, }; use frame_support::{ @@ -35,16 +29,15 @@ pub mod pallet { }; use sp_std::vec::Vec; - pub type BalanceOf = - <::Asset as MultiCurrency<::AccountId>>::Balance; - pub type AssetIdOf = - <::Asset as MultiCurrency<::AccountId>>::CurrencyId; + pub type BalanceOf = <::Asset as MultiCurrency<::AccountId>>::Balance; + pub type AssetIdOf = <::Asset as MultiCurrency<::AccountId>>::CurrencyId; pub type BoundedDataOf = BoundedVec::MaxRemarkLength>; pub type ScheduledTaskOf = ScheduledTask<::BlockNumber>; #[pallet::config] pub trait Config: frame_system::Config { - /// Because this pallet emits events, it depends on the runtime's definition of an event. + /// Because this pallet emits events, it depends on the runtime's + /// definition of an event. type Event: From> + IsType<::Event>; /// the type of assets this pallet can hold in payment type Asset: MultiReservableCurrency; @@ -58,7 +51,8 @@ pub mod pallet { /// Maximum permitted size of `Remark` #[pallet::constant] type MaxRemarkLength: Get; - /// Buffer period - number of blocks to wait before user can claim canceled payment + /// Buffer period - number of blocks to wait before user can claim + /// canceled payment #[pallet::constant] type CancelBufferBlockLength: Get; //// Type representing the weight of this pallet @@ -71,11 +65,12 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn payment)] - /// Payments created by a user, this method of storageDoubleMap is chosen since there is no usecase for - /// listing payments by provider/currency. The payment will only be referenced by the creator in - /// any transaction of interest. - /// The storage map keys are the creator and the recipient, this also ensures - /// that for any (sender,recipient) combo, only a single payment is active. The history of payment is not stored. + /// Payments created by a user, this method of storageDoubleMap is chosen + /// since there is no usecase for listing payments by provider/currency. The + /// payment will only be referenced by the creator in any transaction of + /// interest. The storage map keys are the creator and the recipient, this + /// also ensures that for any (sender,recipient) combo, only a single + /// payment is active. The history of payment is not stored. pub(super) type Payment = StorageDoubleMap< _, Blake2_128Concat, @@ -112,7 +107,11 @@ pub mod pallet { /// Payment has been cancelled by the creator PaymentCancelled { from: T::AccountId, to: T::AccountId }, /// A payment that NeedsReview has been resolved by Judge - PaymentResolved { from: T::AccountId, to: T::AccountId, recipient_share: Percent }, + PaymentResolved { + from: T::AccountId, + to: T::AccountId, + recipient_share: Percent, + }, /// the payment creator has created a refund request PaymentCreatorRequestedRefund { from: T::AccountId, @@ -155,20 +154,18 @@ pub mod pallet { /// This function will look for any pending scheduled tasks that can /// be executed and will process them. fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight { - let mut task_list: Vec<(T::AccountId, T::AccountId, ScheduledTaskOf)> = - ScheduledTasks::::iter() - // leave out tasks in the future - .filter(|(_, _, ScheduledTask { when, .. })| when <= &now) - .collect(); + let mut task_list: Vec<(T::AccountId, T::AccountId, ScheduledTaskOf)> = ScheduledTasks::::iter() + // leave out tasks in the future + .filter(|(_, _, ScheduledTask { when, .. })| when <= &now) + .collect(); if task_list.is_empty() { - return remaining_weight + return remaining_weight; } else { task_list.sort_by(|(_, _, t), (_, _, x)| x.when.partial_cmp(&t.when).unwrap()); } - let cancel_weight = - T::WeightInfo::cancel().saturating_add(T::WeightInfo::remove_task()); + let cancel_weight = T::WeightInfo::cancel().saturating_add(T::WeightInfo::remove_task()); while remaining_weight >= cancel_weight { match task_list.pop() { @@ -189,7 +186,7 @@ pub mod pallet { from: from.clone(), to: to.clone(), }); - }, + } _ => return remaining_weight, } } @@ -200,11 +197,12 @@ pub mod pallet { #[pallet::call] impl Pallet { - /// This allows any user to create a new payment, that releases only to specified recipient - /// The only action is to store the details of this payment in storage and reserve - /// the specified amount. User also has the option to add a remark, this remark - /// can then be used to run custom logic and trigger alternate payment flows. - /// the specified amount. + /// This allows any user to create a new payment, that releases only to + /// specified recipient The only action is to store the details of this + /// payment in storage and reserve the specified amount. User also has + /// the option to add a remark, this remark can then be used to run + /// custom logic and trigger alternate payment flows. the specified + /// amount. #[transactional] #[pallet::weight(T::WeightInfo::pay(T::MaxRemarkLength::get()))] pub fn pay( @@ -229,12 +227,17 @@ pub mod pallet { // reserve funds for payment >::reserve_payment_amount(&who, &recipient, payment_detail)?; // emit paymentcreated event - Self::deposit_event(Event::PaymentCreated { from: who, asset, amount, remark }); + Self::deposit_event(Event::PaymentCreated { + from: who, + asset, + amount, + remark, + }); Ok(().into()) } - /// Release any created payment, this will transfer the reserved amount from the - /// creator of the payment to the assigned recipient + /// Release any created payment, this will transfer the reserved amount + /// from the creator of the payment to the assigned recipient #[transactional] #[pallet::weight(T::WeightInfo::release())] pub fn release(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { @@ -248,19 +251,15 @@ pub mod pallet { } // release is a settle_payment with 100% recipient_share - >::settle_payment( - from.clone(), - to.clone(), - Percent::from_percent(100), - )?; + >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; Self::deposit_event(Event::PaymentReleased { from, to }); Ok(().into()) } - /// Cancel a payment in created state, this will release the reserved back to - /// creator of the payment. This extrinsic can only be called by the recipient - /// of the payment + /// Cancel a payment in created state, this will release the reserved + /// back to creator of the payment. This extrinsic can only be called by + /// the recipient of the payment #[transactional] #[pallet::weight(T::WeightInfo::cancel())] pub fn cancel(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { @@ -275,7 +274,7 @@ pub mod pallet { Percent::from_percent(0), )?; Self::deposit_event(Event::PaymentCancelled { from: creator, to: who }); - }, + } // if the payment is in state PaymentRequested, remove from storage PaymentState::PaymentRequested => Payment::::remove(&creator, &who), _ => fail!(Error::::InvalidAction), @@ -286,8 +285,8 @@ pub mod pallet { /// Allow judge to set state of a payment /// This extrinsic is used to resolve disputes between the creator and - /// recipient of the payment. This extrinsic allows the assigned judge to cancel/release/partial_release - /// the payment. + /// recipient of the payment. This extrinsic allows the assigned judge + /// to cancel/release/partial_release the payment. #[transactional] #[pallet::weight(T::WeightInfo::resolve_payment())] pub fn resolve_payment( @@ -302,74 +301,69 @@ pub mod pallet { ensure!(who == payment.resolver_account, Error::::InvalidAction) } // try to update the payment to new state - >::settle_payment( - from.clone(), - recipient.clone(), + >::settle_payment(from.clone(), recipient.clone(), recipient_share)?; + Self::deposit_event(Event::PaymentResolved { + from, + to: recipient, recipient_share, - )?; - Self::deposit_event(Event::PaymentResolved { from, to: recipient, recipient_share }); + }); Ok(().into()) } /// Allow payment creator to set payment to NeedsReview - /// This extrinsic is used to mark the payment as disputed so the assigned judge can tigger a resolution - /// and that the funds are no longer locked. + /// This extrinsic is used to mark the payment as disputed so the + /// assigned judge can tigger a resolution and that the funds are no + /// longer locked. #[transactional] #[pallet::weight(T::WeightInfo::request_refund())] - pub fn request_refund( - origin: OriginFor, - recipient: T::AccountId, - ) -> DispatchResultWithPostInfo { + pub fn request_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; - Payment::::try_mutate( - who.clone(), - recipient.clone(), - |maybe_payment| -> DispatchResult { - // ensure the payment exists - let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; - // ensure the payment is not in needsreview state - ensure!( - payment.state != PaymentState::NeedsReview, - Error::::PaymentNeedsReview - ); - - // set the payment to requested refund - let current_block = frame_system::Pallet::::block_number(); - let cancel_block = current_block - .checked_add(&T::CancelBufferBlockLength::get()) - .ok_or(Error::::MathError)?; - - ScheduledTasks::::insert( - who.clone(), - recipient.clone(), - ScheduledTask { task: Task::Cancel, when: cancel_block }, - ); - - payment.state = PaymentState::RefundRequested { cancel_block }; - - Self::deposit_event(Event::PaymentCreatorRequestedRefund { - from: who, - to: recipient, - expiry: cancel_block, - }); + Payment::::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult { + // ensure the payment exists + let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; + // ensure the payment is not in needsreview state + ensure!( + payment.state != PaymentState::NeedsReview, + Error::::PaymentNeedsReview + ); + + // set the payment to requested refund + let current_block = frame_system::Pallet::::block_number(); + let cancel_block = current_block + .checked_add(&T::CancelBufferBlockLength::get()) + .ok_or(Error::::MathError)?; + + ScheduledTasks::::insert( + who.clone(), + recipient.clone(), + ScheduledTask { + task: Task::Cancel, + when: cancel_block, + }, + ); - Ok(()) - }, - )?; + payment.state = PaymentState::RefundRequested { cancel_block }; + + Self::deposit_event(Event::PaymentCreatorRequestedRefund { + from: who, + to: recipient, + expiry: cancel_block, + }); + + Ok(()) + })?; Ok(().into()) } - /// Allow payment recipient to dispute the refund request from the payment creator - /// This does not cancel the request, instead sends the payment to a NeedsReview state - /// The assigned resolver account can then change the state of the payment after review. + /// Allow payment recipient to dispute the refund request from the + /// payment creator This does not cancel the request, instead sends the + /// payment to a NeedsReview state The assigned resolver account can + /// then change the state of the payment after review. #[transactional] #[pallet::weight(T::WeightInfo::dispute_refund())] - pub fn dispute_refund( - origin: OriginFor, - creator: T::AccountId, - ) -> DispatchResultWithPostInfo { + pub fn dispute_refund(origin: OriginFor, creator: T::AccountId) -> DispatchResultWithPostInfo { use PaymentState::*; let who = ensure_signed(origin)?; @@ -392,11 +386,8 @@ pub mod pallet { // remove the payment from scheduled tasks ScheduledTasks::::remove(creator.clone(), who.clone()); - Self::deposit_event(Event::PaymentRefundDisputed { - from: creator, - to: who, - }); - }, + Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who }); + } _ => fail!(Error::::InvalidAction), } @@ -407,9 +398,10 @@ pub mod pallet { Ok(().into()) } - // Creates a new payment with the given details. This can be called by the recipient of the payment - // to create a payment and then completed by the sender using the `accept_and_pay` extrinsic. - // The payment will be in PaymentRequested State and can only be modified by the `accept_and_pay` extrinsic. + // Creates a new payment with the given details. This can be called by the + // recipient of the payment to create a payment and then completed by the sender + // using the `accept_and_pay` extrinsic. The payment will be in PaymentRequested + // State and can only be modified by the `accept_and_pay` extrinsic. #[transactional] #[pallet::weight(T::WeightInfo::request_payment())] pub fn request_payment( @@ -436,29 +428,26 @@ pub mod pallet { Ok(().into()) } - // This extrinsic allows the sender to fulfill a payment request created by a recipient. - // The amount will be transferred to the recipient and payment removed from storage + // This extrinsic allows the sender to fulfill a payment request created by a + // recipient. The amount will be transferred to the recipient and payment + // removed from storage #[transactional] #[pallet::weight(T::WeightInfo::accept_and_pay())] - pub fn accept_and_pay( - origin: OriginFor, - to: T::AccountId, - ) -> DispatchResultWithPostInfo { + pub fn accept_and_pay(origin: OriginFor, to: T::AccountId) -> DispatchResultWithPostInfo { let from = ensure_signed(origin)?; let payment = Payment::::get(&from, &to).ok_or(Error::::InvalidPayment)?; - ensure!(payment.state == PaymentState::PaymentRequested, Error::::InvalidAction); + ensure!( + payment.state == PaymentState::PaymentRequested, + Error::::InvalidAction + ); // reserve all the fees from the sender >::reserve_payment_amount(&from, &to, payment)?; // release the payment and delete the payment from storage - >::settle_payment( - from.clone(), - to.clone(), - Percent::from_percent(100), - )?; + >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; Self::deposit_event(Event::PaymentRequestCompleted { from, to }); @@ -467,8 +456,9 @@ pub mod pallet { } impl PaymentHandler for Pallet { - /// The function will create a new payment. The fee and incentive amounts will be calculated and the - /// `PaymentDetail` will be added to storage. + /// The function will create a new payment. The fee and incentive + /// amounts will be calculated and the `PaymentDetail` will be added to + /// storage. #[require_transactional] fn create_payment( from: T::AccountId, @@ -523,14 +513,11 @@ pub mod pallet { ) } - /// The function will reserve the fees+transfer amount from the `from` account. After reserving - /// the payment.amount will be transferred to the recipient but will stay in Reserve state. + /// The function will reserve the fees+transfer amount from the `from` + /// account. After reserving the payment.amount will be transferred to + /// the recipient but will stay in Reserve state. #[require_transactional] - fn reserve_payment_amount( - from: &T::AccountId, - to: &T::AccountId, - payment: PaymentDetail, - ) -> DispatchResult { + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult { let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or(0u32.into()); let total_fee_amount = payment.incentive_amount.saturating_add(fee_amount); @@ -539,67 +526,50 @@ pub mod pallet { // reserve the total amount from payment creator T::Asset::reserve(payment.asset, from, total_amount)?; // transfer payment amount to recipient -- keeping reserve status - T::Asset::repatriate_reserved( - payment.asset, - from, - to, - payment.amount, - BalanceStatus::Reserved, - )?; + T::Asset::repatriate_reserved(payment.asset, from, to, payment.amount, BalanceStatus::Reserved)?; Ok(()) } - /// This function allows the caller to settle the payment by specifying a recipient_share - /// this will unreserve the fee+incentive to sender and unreserve transferred amount to recipient - /// if the settlement is a release (ie recipient_share=100), the fee is transferred to fee_recipient - /// For cancelling a payment, recipient_share = 0 + /// This function allows the caller to settle the payment by specifying + /// a recipient_share this will unreserve the fee+incentive to sender + /// and unreserve transferred amount to recipient if the settlement is a + /// release (ie recipient_share=100), the fee is transferred to + /// fee_recipient For cancelling a payment, recipient_share = 0 /// For releasing a payment, recipient_share = 100 /// In other cases, the custom recipient_share can be specified - fn settle_payment( - from: T::AccountId, - to: T::AccountId, - recipient_share: Percent, - ) -> DispatchResult { - Payment::::try_mutate( - from.clone(), - to.clone(), - |maybe_payment| -> DispatchResult { - let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; - - // unreserve the incentive amount and fees from the owner account - match payment.fee_detail { - Some((fee_recipient, fee_amount)) => { - T::Asset::unreserve( + fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult { + Payment::::try_mutate(from.clone(), to.clone(), |maybe_payment| -> DispatchResult { + let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; + + // unreserve the incentive amount and fees from the owner account + match payment.fee_detail { + Some((fee_recipient, fee_amount)) => { + T::Asset::unreserve(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, - 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 - )?; - } - }, - None => { - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); - }, - }; + &from, // fee is paid by payment creator + &fee_recipient, // account of fee recipient + fee_amount, // amount of fee + )?; + } + } + None => { + T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + } + }; - // Unreserve the transfer amount - T::Asset::unreserve(payment.asset, &to, payment.amount); + // Unreserve the transfer 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); - // send share to recipient - T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; + let amount_to_recipient = recipient_share.mul_floor(payment.amount); + let amount_to_sender = payment.amount.saturating_sub(amount_to_recipient); + // send share to recipient + T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; - Ok(()) - }, - )?; + Ok(()) + })?; Ok(()) } diff --git a/payments/src/mock.rs b/payments/src/mock.rs index 0a5646ab5..dce2b40f1 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -13,7 +13,6 @@ use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, Percent, }; -use virto_primitives::{Asset, NetworkAsset}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -24,7 +23,7 @@ pub const PAYMENT_CREATOR: AccountId = 10; pub const PAYMENT_RECIPENT: AccountId = 11; pub const PAYMENT_CREATOR_TWO: AccountId = 30; pub const PAYMENT_RECIPENT_TWO: AccountId = 31; -pub const CURRENCY_ID: Asset = Asset::Network(NetworkAsset::KSM); +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; @@ -77,7 +76,7 @@ impl system::Config for Test { } parameter_type_with_key! { - pub ExistentialDeposits: |_currency_id: Asset| -> Balance { + pub ExistentialDeposits: |_currency_id: u32| -> Balance { 0u128 }; } @@ -95,7 +94,7 @@ impl Contains for MockDustRemovalWhitelist { impl orml_tokens::Config for Test { type Amount = i64; type Balance = Balance; - type CurrencyId = Asset; + type CurrencyId = u32; type Event = Event; type ExistentialDeposits = ExistentialDeposits; type OnDust = (); @@ -120,8 +119,7 @@ impl crate::types::FeeHandler for MockFeeHandler { _remark: Option<&[u8]>, ) -> (AccountId, Percent) { match to { - &PAYMENT_RECIPENT_FEE_CHARGED => - (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE)), + &PAYMENT_RECIPENT_FEE_CHARGED => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(MARKETPLACE_FEE_PERCENTAGE)), _ => (FEE_RECIPIENT_ACCOUNT, Percent::from_percent(0)), } } @@ -173,7 +171,11 @@ pub fn run_n_blocks(n: u64) -> u64 { System::set_block_number(block_number); // Odd blocks gets busy - let idle_weight = if block_number % 2 == 0 { IDLE_WEIGHT } else { BUSY_WEIGHT }; + let idle_weight = if block_number % 2 == 0 { + IDLE_WEIGHT + } else { + BUSY_WEIGHT + }; // ensure the on_idle is executed >::register_extra_weight_unchecked( Payment::on_idle(block_number, idle_weight), diff --git a/payments/src/tests.rs b/payments/src/tests.rs index 3a58ceefe..666b2fd21 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -21,7 +21,10 @@ fn test_pay_works() { let expected_incentive_amount = payment_amount / INCENTIVE_PERCENTAGE as u128; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should be able to create a payment with available balance @@ -133,11 +136,17 @@ fn test_cancel_works() { assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); assert_eq!( last_event(), - crate::Event::::PaymentCancelled { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } - .into() + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() ); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should be released from storage @@ -182,8 +191,11 @@ fn test_release_works() { assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); assert_eq!( last_event(), - crate::Event::::PaymentReleased { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } - .into() + crate::Event::::PaymentReleased { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() ); // the payment amount should be transferred assert_eq!( @@ -332,14 +344,15 @@ fn test_charging_fee_payment_works() { // the payment amount should be reserved assert_eq!( Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), - creator_initial_balance - - payment_amount - expected_fee_amount - - expected_incentive_amount + creator_initial_balance - payment_amount - expected_fee_amount - expected_incentive_amount ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); // should succeed for valid payment - assert_ok!(Payment::release(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED)); + assert_ok!(Payment::release( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT_FEE_CHARGED + )); // the payment amount should be transferred assert_eq!( Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), @@ -353,7 +366,10 @@ fn test_charging_fee_payment_works() { Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), payment_amount ); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); }); } @@ -387,16 +403,20 @@ fn test_charging_fee_payment_works_when_canceled() { // the payment amount should be reserved assert_eq!( Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), - creator_initial_balance - - payment_amount - expected_fee_amount - - expected_incentive_amount + creator_initial_balance - payment_amount - expected_fee_amount - expected_incentive_amount ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); // should succeed for valid payment - assert_ok!(Payment::cancel(Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), PAYMENT_CREATOR)); + assert_ok!(Payment::cancel( + Origin::signed(PAYMENT_RECIPENT_FEE_CHARGED), + PAYMENT_CREATOR + )); // the payment amount should be transferred - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::total_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), 0); @@ -537,7 +557,10 @@ fn test_request_refund() { None )); - assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), @@ -545,7 +568,9 @@ fn test_request_refund() { asset: CURRENCY_ID, amount: payment_amount, incentive_amount: expected_incentive_amount, - state: PaymentState::RefundRequested { cancel_block: expected_cancel_block }, + state: PaymentState::RefundRequested { + cancel_block: expected_cancel_block + }, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), }) @@ -584,15 +609,24 @@ fn test_dispute_refund() { Error::InvalidAction ); // creator requests a refund - assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); // ensure the request is added to the refund queue assert_eq!( ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { task: Task::Cancel, when: expected_cancel_block } + ScheduledTask { + task: Task::Cancel, + when: expected_cancel_block + } ); // recipient disputes the refund request - assert_ok!(Payment::dispute_refund(Origin::signed(PAYMENT_RECIPENT), PAYMENT_CREATOR)); + assert_ok!(Payment::dispute_refund( + Origin::signed(PAYMENT_RECIPENT), + PAYMENT_CREATOR + )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), @@ -721,7 +755,10 @@ fn test_accept_and_pay() { }) ); - assert_ok!(Payment::accept_and_pay(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT,)); + assert_ok!(Payment::accept_and_pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + )); // the payment amount should be transferred assert_eq!( @@ -803,10 +840,16 @@ fn test_accept_and_pay_should_charge_fee_correctly() { Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), payment_amount ); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); // should be deleted from storage - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); assert_eq!( last_event(), @@ -844,10 +887,14 @@ fn test_create_payment_works() { let expected_fee_amount = 0; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( PAYMENT_CREATOR, @@ -914,7 +961,8 @@ fn test_reserve_payment_amount_works() { assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( PAYMENT_CREATOR, @@ -998,10 +1046,14 @@ fn test_settle_payment_works_for_cancel() { let payment_amount = 20; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, @@ -1019,7 +1071,10 @@ fn test_settle_payment_works_for_cancel() { }))); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); // should be released from storage @@ -1034,10 +1089,14 @@ fn test_settle_payment_works_for_release() { let payment_amount = 20; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT, @@ -1074,10 +1133,14 @@ fn test_settle_payment_works_for_70_30() { let expected_fee_amount = payment_amount / MARKETPLACE_FEE_PERCENTAGE as u128; // the payment amount should not be reserved - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, @@ -1094,9 +1157,8 @@ fn test_settle_payment_works_for_70_30() { ) }))); - let expected_amount_for_creator = - creator_initial_balance - payment_amount - expected_fee_amount + - (Percent::from_percent(30) * payment_amount); + let expected_amount_for_creator = creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(30) * payment_amount); let expected_amount_for_recipient = Percent::from_percent(70) * payment_amount; // the payment amount should be transferred @@ -1108,10 +1170,16 @@ fn test_settle_payment_works_for_70_30() { Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), expected_amount_for_recipient ); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); // should be deleted from storage - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); }); } @@ -1126,7 +1194,8 @@ fn test_settle_payment_works_for_50_50() { assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), 0); - // should be able to create a payment with available balance within a transaction + // should be able to create a payment with available balance within a + // transaction assert_ok!(Payment::pay( Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT_FEE_CHARGED, @@ -1143,9 +1212,8 @@ fn test_settle_payment_works_for_50_50() { ) }))); - let expected_amount_for_creator = - creator_initial_balance - payment_amount - expected_fee_amount + - (Percent::from_percent(50) * payment_amount); + let expected_amount_for_creator = creator_initial_balance - payment_amount - expected_fee_amount + + (Percent::from_percent(50) * payment_amount); let expected_amount_for_recipient = Percent::from_percent(50) * payment_amount; // the payment amount should be transferred @@ -1157,10 +1225,16 @@ fn test_settle_payment_works_for_50_50() { Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT_FEE_CHARGED), expected_amount_for_recipient ); - assert_eq!(Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), expected_fee_amount); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &FEE_RECIPIENT_ACCOUNT), + expected_fee_amount + ); // should be deleted from storage - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), None); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT_FEE_CHARGED), + None + ); }); } @@ -1181,7 +1255,10 @@ fn test_automatic_refund_works() { None )); - assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), @@ -1189,7 +1266,9 @@ fn test_automatic_refund_works() { asset: CURRENCY_ID, amount: payment_amount, incentive_amount: expected_incentive_amount, - state: PaymentState::RefundRequested { cancel_block: CANCEL_BLOCK }, + state: PaymentState::RefundRequested { + cancel_block: CANCEL_BLOCK + }, resolver_account: RESOLVER_ACCOUNT, fee_detail: Some((FEE_RECIPIENT_ACCOUNT, 0)), }) @@ -1197,14 +1276,20 @@ fn test_automatic_refund_works() { assert_eq!( ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } + ScheduledTask { + task: Task::Cancel, + when: CANCEL_BLOCK + } ); // run to one block before cancel and make sure data is same assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 600); assert_eq!( ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } + ScheduledTask { + task: Task::Cancel, + when: CANCEL_BLOCK + } ); // run to after cancel block but odd blocks are busy @@ -1223,11 +1308,17 @@ fn test_automatic_refund_works() { // test that the refund happened correctly assert_eq!( last_event(), - crate::Event::::PaymentCancelled { from: PAYMENT_CREATOR, to: PAYMENT_RECIPENT } - .into() + crate::Event::::PaymentCancelled { + from: PAYMENT_CREATOR, + to: PAYMENT_RECIPENT + } + .into() ); // the payment amount should be released back to creator - assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), creator_initial_balance); + assert_eq!( + Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), + creator_initial_balance + ); assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_RECIPENT), 0); }); } @@ -1253,7 +1344,10 @@ fn test_automatic_refund_works_for_multiple_payments() { None )); - assert_ok!(Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT)); + assert_ok!(Payment::request_refund( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT + )); run_n_blocks(1); assert_ok!(Payment::request_refund( Origin::signed(PAYMENT_CREATOR_TWO), @@ -1268,11 +1362,17 @@ fn test_automatic_refund_works_for_multiple_payments() { // Even block 602 has enough room to process both pending payments assert_eq!(run_n_blocks(1), 602); assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); - assert_eq!(PaymentStore::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), None); + assert_eq!( + PaymentStore::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), + None + ); // the scheduled storage should be cleared assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); - assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), None); + assert_eq!( + ScheduledTasks::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), + None + ); // test that the refund happened correctly assert_eq!(Tokens::free_balance(CURRENCY_ID, &PAYMENT_CREATOR), 100); diff --git a/payments/src/types.rs b/payments/src/types.rs index 290c67ce8..60991e607 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -5,9 +5,10 @@ use scale_info::TypeInfo; use sp_runtime::{DispatchResult, Percent}; /// The PaymentDetail struct stores information about the payment/escrow -/// A "payment" in virto network is similar to an escrow, it is used to guarantee proof of funds -/// and can be released once an agreed upon condition has reached between the payment creator -/// and recipient. The payment lifecycle is tracked using the state field. +/// A "payment" in virto network is similar to an escrow, it is used to +/// guarantee proof of funds and can be released once an agreed upon condition +/// has reached between the payment creator and recipient. The payment lifecycle +/// is tracked using the state field. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound(T: pallet::Config))] @@ -20,7 +21,8 @@ pub struct PaymentDetail { /// incentive amount that is credited to creator for resolving #[codec(compact)] pub incentive_amount: BalanceOf, - /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, Requested] + /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, + /// Requested] pub state: PaymentState, /// account that can settle any disputes created in the payment pub resolver_account: T::AccountId, @@ -29,7 +31,8 @@ pub struct PaymentDetail { } /// The `PaymentState` enum tracks the possible states that a payment can be in. -/// When a payment is 'completed' or 'cancelled' it is removed from storage and hence not tracked by a state. +/// When a payment is 'completed' or 'cancelled' it is removed from storage and +/// hence not tracked by a state. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum PaymentState { @@ -62,23 +65,15 @@ pub trait PaymentHandler { /// Attempt to reserve an amount of the given asset from the caller /// If not possible then return Error. Possible reasons for failure include: /// - User does not have enough balance. - fn reserve_payment_amount( - from: &T::AccountId, - to: &T::AccountId, - payment: PaymentDetail, - ) -> DispatchResult; + fn reserve_payment_amount(from: &T::AccountId, to: &T::AccountId, payment: PaymentDetail) -> DispatchResult; - // Settle a payment of `from` to `to`. To release a payment, the recipient_share=100, - // to cancel a payment recipient_share=0 + // Settle a payment of `from` to `to`. To release a payment, the + // recipient_share=100, to cancel a payment recipient_share=0 // Possible reasonse for failure include /// - The payment does not exist /// - The unreserve operation fails /// - The transfer operation fails - fn settle_payment( - from: T::AccountId, - to: T::AccountId, - recipient_share: Percent, - ) -> DispatchResult; + fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult; /// Attempt to fetch the details of a payment from the given payment_id /// Possible reasons for failure include: @@ -86,13 +81,15 @@ pub trait PaymentHandler { fn get_payment_details(from: &T::AccountId, to: &T::AccountId) -> Option>; } -/// DisputeResolver trait defines how to create/assign judges for solving payment disputes +/// DisputeResolver trait defines how to create/assign judges for solving +/// payment disputes pub trait DisputeResolver { /// Returns an `Account` fn get_resolver_account() -> Account; } -/// Fee Handler trait that defines how to handle marketplace fees to every payment/swap +/// Fee Handler trait that defines how to handle marketplace fees to every +/// payment/swap pub trait FeeHandler { /// Get the distribution of fees to marketplace participants fn apply_fees( @@ -118,4 +115,4 @@ pub struct ScheduledTask { /// the 'time' at which the task should be executed #[codec(compact)] pub when: Time, -} \ No newline at end of file +} From 7dfbd51bbe464866994e00d6b5772a79f20f067b Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Fri, 18 Mar 2022 10:59:07 +0400 Subject: [PATCH 18/28] trigger ci From 669d8c331ddb9bda2be46dba61ba40280acf7a79 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Fri, 18 Mar 2022 11:15:37 +0400 Subject: [PATCH 19/28] cleanup cargo.toml --- payments/Cargo.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 0bf88ebe3..55dca6654 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -9,9 +9,6 @@ repository = "https://github.com/virto-network/virto-node" description = "Allows users to post escrow payment on-chain" readme = "README.md" -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - [dependencies] parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } @@ -41,6 +38,3 @@ std = [ 'frame-benchmarking/std', 'orml-tokens/std' ] -runtime-benchmarks = [ - "frame-benchmarking", -] From c85d1ec906dce42353b84322492719c3be90425f Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sat, 19 Mar 2022 15:19:32 +0400 Subject: [PATCH 20/28] sync latest version --- payments/Cargo.toml | 2 + payments/src/lib.rs | 112 ++++++++++++++++++++++++---------------- payments/src/mock.rs | 2 + payments/src/tests.rs | 36 +++++++------ payments/src/weights.rs | 68 +++++++++++------------- 5 files changed, 123 insertions(+), 97 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 55dca6654..820252d5d 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -11,6 +11,7 @@ readme = "README.md" [dependencies] parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } +log = { version = "0.4.14", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } @@ -31,6 +32,7 @@ std = [ 'parity-scale-codec/std', 'frame-support/std', 'frame-system/std', + 'log/std', 'sp-runtime/std', 'sp-std/std', 'scale-info/std', diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 63dfa7469..7f9e2a55f 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -19,7 +19,7 @@ pub mod pallet { }; use frame_support::{ dispatch::DispatchResultWithPostInfo, fail, pallet_prelude::*, require_transactional, - traits::tokens::BalanceStatus, transactional, + storage::bounded_btree_map::BoundedBTreeMap, traits::tokens::BalanceStatus, transactional, }; use frame_system::pallet_prelude::*; use orml_traits::{MultiCurrency, MultiReservableCurrency}; @@ -32,7 +32,17 @@ pub mod pallet { pub type BalanceOf = <::Asset as MultiCurrency<::AccountId>>::Balance; pub type AssetIdOf = <::Asset as MultiCurrency<::AccountId>>::CurrencyId; pub type BoundedDataOf = BoundedVec::MaxRemarkLength>; + /// type of ScheduledTask used by the pallet pub type ScheduledTaskOf = ScheduledTask<::BlockNumber>; + /// list of ScheduledTasks, stored as a BoundedBTreeMap + pub type ScheduledTaskList = BoundedBTreeMap< + ( + ::AccountId, + ::AccountId, + ), + ScheduledTaskOf, + ::MaxRemarkLength, + >; #[pallet::config] pub trait Config: frame_system::Config { @@ -55,6 +65,10 @@ pub mod pallet { /// canceled payment #[pallet::constant] type CancelBufferBlockLength: Get; + /// Buffer period - number of blocks to wait before user can claim + /// canceled payment + #[pallet::constant] + type MaxScheduledTaskListLength: Get; //// Type representing the weight of this pallet type WeightInfo: WeightInfo; } @@ -83,14 +97,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn tasks)] /// Store the list of tasks to be executed in the on_idle function - pub(super) type ScheduledTasks = StorageDoubleMap< - _, - Blake2_128Concat, - T::AccountId, // payment creator - Blake2_128Concat, - T::AccountId, // payment recipient - ScheduledTaskOf, - >; + pub(super) type ScheduledTasks = StorageValue<_, ScheduledTaskList, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -154,43 +161,51 @@ pub mod pallet { /// This function will look for any pending scheduled tasks that can /// be executed and will process them. fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight { - let mut task_list: Vec<(T::AccountId, T::AccountId, ScheduledTaskOf)> = ScheduledTasks::::iter() - // leave out tasks in the future - .filter(|(_, _, ScheduledTask { when, .. })| when <= &now) - .collect(); + // reduce the weight used to read the task list + remaining_weight = remaining_weight.saturating_sub(T::WeightInfo::remove_task()); - if task_list.is_empty() { - return remaining_weight; - } else { - task_list.sort_by(|(_, _, t), (_, _, x)| x.when.partial_cmp(&t.when).unwrap()); - } + ScheduledTasks::::mutate(|tasks| { + let mut task_list: Vec<_> = tasks + .clone() + .into_iter() + // leave out tasks in the future + .filter(|(_, ScheduledTask { when, .. })| when <= &now) + .collect(); - let cancel_weight = T::WeightInfo::cancel().saturating_add(T::WeightInfo::remove_task()); + // order by oldest task to process + task_list.sort_by(|(_, t), (_, x)| x.when.partial_cmp(&t.when).unwrap()); - while remaining_weight >= cancel_weight { - match task_list.pop() { - Some((from, to, ScheduledTask { task: Task::Cancel, .. })) => { + let cancel_weight = T::WeightInfo::cancel(); + + while !task_list.is_empty() && remaining_weight >= cancel_weight { + if let Some(((from, to), ScheduledTask { task: Task::Cancel, .. })) = task_list.pop() { remaining_weight = remaining_weight.saturating_sub(cancel_weight); + // remove the task form the tasks + tasks.remove(&(from.clone(), to.clone())); // process the cancel payment - if let Err(_) = >::settle_payment( + if >::settle_payment( from.clone(), to.clone(), Percent::from_percent(0), - ) { - // panic!("{:?}", e); + ) + .is_err() + { + // log the payment refund failure + log::warn!( + target: "runtime::payments", + "Warning: Unable to process payment refund!" + ); + } else { + // emit the cancel event if the refund was successful + Self::deposit_event(Event::PaymentCancelled { + from: from.clone(), + to: to.clone(), + }); } - ScheduledTasks::::remove(from.clone(), to.clone()); - // emit the cancel event - Self::deposit_event(Event::PaymentCancelled { - from: from.clone(), - to: to.clone(), - }); } - _ => return remaining_weight, } - } - + }); remaining_weight } } @@ -334,14 +349,18 @@ pub mod pallet { .checked_add(&T::CancelBufferBlockLength::get()) .ok_or(Error::::MathError)?; - ScheduledTasks::::insert( - who.clone(), - recipient.clone(), - ScheduledTask { - task: Task::Cancel, - when: cancel_block, - }, - ); + ScheduledTasks::::try_mutate(|task_list| -> DispatchResult { + task_list + .try_insert( + (who.clone(), recipient.clone()), + ScheduledTask { + task: Task::Cancel, + when: cancel_block, + }, + ) + .map_err(|_| Error::::RefundQueueFull)?; + Ok(()) + })?; payment.state = PaymentState::RefundRequested { cancel_block }; @@ -384,7 +403,12 @@ pub mod pallet { payment.state = PaymentState::NeedsReview; // remove the payment from scheduled tasks - ScheduledTasks::::remove(creator.clone(), who.clone()); + ScheduledTasks::::try_mutate(|task_list| -> DispatchResult { + task_list + .remove(&(creator.clone(), who.clone())) + .ok_or(Error::::InvalidAction)?; + Ok(()) + })?; Self::deposit_event(Event::PaymentRefundDisputed { from: creator, to: who }); } @@ -518,7 +542,7 @@ 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) -> DispatchResult { - let fee_amount = payment.fee_detail.map(|(_, f)| f).unwrap_or(0u32.into()); + 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); diff --git a/payments/src/mock.rs b/payments/src/mock.rs index dce2b40f1..ca2aaea49 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -129,6 +129,7 @@ parameter_types! { pub const IncentivePercentage: Percent = Percent::from_percent(INCENTIVE_PERCENTAGE); pub const MaxRemarkLength: u32 = 50; pub const CancelBufferBlockLength: u64 = CANCEL_BLOCK_BUFFER; + pub const MaxScheduledTaskListLength : u32 = 5; } impl payment::Config for Test { @@ -139,6 +140,7 @@ impl payment::Config for Test { type FeeHandler = MockFeeHandler; type MaxRemarkLength = MaxRemarkLength; type CancelBufferBlockLength = CancelBufferBlockLength; + type MaxScheduledTaskListLength = MaxScheduledTaskListLength; type WeightInfo = (); } diff --git a/payments/src/tests.rs b/payments/src/tests.rs index 666b2fd21..3f996d4f7 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -614,9 +614,10 @@ fn test_dispute_refund() { PAYMENT_RECIPENT )); // ensure the request is added to the refund queue + let scheduled_tasks_list = ScheduledTasks::::get(); assert_eq!( - ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { task: Task::Cancel, when: expected_cancel_block } @@ -649,8 +650,9 @@ fn test_dispute_refund() { .into() ); - // ensure the request is added to the refund queue - assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + // ensure the request is removed from the refund queue + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)), None); }); } @@ -903,7 +905,7 @@ fn test_create_payment_works() { payment_amount, PaymentState::Created, Percent::from_percent(INCENTIVE_PERCENTAGE), - Some(&vec![1u8; 10]), + Some(&[1u8; 10]), ) }))); @@ -929,7 +931,7 @@ fn test_create_payment_works() { payment_amount, PaymentState::Created, Percent::from_percent(INCENTIVE_PERCENTAGE), - Some(&vec![1u8; 10]), + Some(&[1u8; 10]), ) })), Error::PaymentAlreadyInProcess @@ -971,7 +973,7 @@ fn test_reserve_payment_amount_works() { payment_amount, PaymentState::Created, Percent::from_percent(INCENTIVE_PERCENTAGE), - Some(&vec![1u8; 10]), + Some(&[1u8; 10]), ) }))); @@ -1019,7 +1021,7 @@ fn test_reserve_payment_amount_works() { payment_amount, PaymentState::Created, Percent::from_percent(INCENTIVE_PERCENTAGE), - Some(&vec![1u8; 10]), + Some(&[1u8; 10]), ) })), Error::PaymentAlreadyInProcess @@ -1274,9 +1276,10 @@ fn test_automatic_refund_works() { }) ); + let scheduled_tasks_list = ScheduledTasks::::get(); assert_eq!( - ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } @@ -1284,9 +1287,10 @@ fn test_automatic_refund_works() { // run to one block before cancel and make sure data is same assert_eq!(run_n_blocks(CANCEL_PERIOD - 1), 600); + let scheduled_tasks_list = ScheduledTasks::::get(); assert_eq!( - ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT).unwrap(), - ScheduledTask { + scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)).unwrap(), + &ScheduledTask { task: Task::Cancel, when: CANCEL_BLOCK } @@ -1303,7 +1307,8 @@ fn test_automatic_refund_works() { assert_eq!(PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); // the scheduled storage should be cleared - assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)), None); // test that the refund happened correctly assert_eq!( @@ -1368,9 +1373,10 @@ fn test_automatic_refund_works_for_multiple_payments() { ); // the scheduled storage should be cleared - assert_eq!(ScheduledTasks::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), None); + let scheduled_tasks_list = ScheduledTasks::::get(); + assert_eq!(scheduled_tasks_list.get(&(PAYMENT_CREATOR, PAYMENT_RECIPENT)), None); assert_eq!( - ScheduledTasks::::get(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO), + scheduled_tasks_list.get(&(PAYMENT_CREATOR_TWO, PAYMENT_RECIPENT_TWO)), None ); diff --git a/payments/src/weights.rs b/payments/src/weights.rs index df20c5312..941293c60 100644 --- a/payments/src/weights.rs +++ b/payments/src/weights.rs @@ -1,7 +1,7 @@ //! Autogenerated weights for virto_payment //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-03-11, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2022-03-19, STEPS: `20`, REPEAT: 10, LOW RANGE: `[]`, HIGH RANGE: `[]` //! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: @@ -25,6 +25,7 @@ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] #![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; use sp_std::marker::PhantomData; @@ -39,7 +40,6 @@ pub trait WeightInfo { fn dispute_refund() -> Weight; fn request_payment() -> Weight; fn accept_and_pay() -> Weight; - fn read_task() -> Weight; fn remove_task() -> Weight; } @@ -51,14 +51,14 @@ impl WeightInfo for SubstrateWeight { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:1) fn pay(_x: u32, ) -> Weight { - (63_805_000 as Weight) + (55_900_000 as Weight) .saturating_add(T::DbWeight::get().reads(5 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn release() -> Weight { - (33_000_000 as Weight) + (36_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } @@ -66,35 +66,35 @@ impl WeightInfo for SubstrateWeight { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:0) fn cancel() -> Weight { - (51_000_000 as Weight) + (48_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn resolve_payment() -> Weight { - (39_000_000 as Weight) + (35_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(3 as Weight)) .saturating_add(T::DbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn request_refund() -> Weight { - (18_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(1 as Weight)) + (20_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn dispute_refund() -> Weight { - (19_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(1 as Weight)) + (21_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) fn request_payment() -> Weight { - (20_000_000 as Weight) + (17_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } @@ -106,14 +106,10 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(4 as Weight)) .saturating_add(T::DbWeight::get().writes(4 as Weight)) } - // Storage: Payment ScheduledTasks (r:2 w:0) - fn read_task() -> Weight { - (12_000_000 as Weight) - .saturating_add(T::DbWeight::get().reads(2 as Weight)) - } - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn remove_task() -> Weight { - (2_000_000 as Weight) + (4_000_000 as Weight) + .saturating_add(T::DbWeight::get().reads(1 as Weight)) .saturating_add(T::DbWeight::get().writes(1 as Weight)) } } @@ -125,14 +121,14 @@ impl WeightInfo for () { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:1) fn pay(_x: u32, ) -> Weight { - (63_805_000 as Weight) + (55_900_000 as Weight) .saturating_add(RocksDbWeight::get().reads(5 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn release() -> Weight { - (33_000_000 as Weight) + (36_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } @@ -140,35 +136,35 @@ impl WeightInfo for () { // Storage: Assets Accounts (r:2 w:2) // Storage: System Account (r:1 w:0) fn cancel() -> Weight { - (51_000_000 as Weight) + (48_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Assets Accounts (r:2 w:2) fn resolve_payment() -> Weight { - (39_000_000 as Weight) + (35_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(3 as Weight)) .saturating_add(RocksDbWeight::get().writes(3 as Weight)) } // Storage: Payment Payment (r:1 w:1) - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn request_refund() -> Weight { - (18_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + (20_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn dispute_refund() -> Weight { - (19_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(1 as Weight)) + (21_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } // Storage: Payment Payment (r:1 w:1) // Storage: Sudo Key (r:1 w:0) fn request_payment() -> Weight { - (20_000_000 as Weight) + (17_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } @@ -180,14 +176,10 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4 as Weight)) .saturating_add(RocksDbWeight::get().writes(4 as Weight)) } - // Storage: Payment ScheduledTasks (r:2 w:0) - fn read_task() -> Weight { - (12_000_000 as Weight) - .saturating_add(RocksDbWeight::get().reads(2 as Weight)) - } - // Storage: Payment ScheduledTasks (r:0 w:1) + // Storage: Payment ScheduledTasks (r:1 w:1) fn remove_task() -> Weight { - (2_000_000 as Weight) + (4_000_000 as Weight) + .saturating_add(RocksDbWeight::get().reads(1 as Weight)) .saturating_add(RocksDbWeight::get().writes(1 as Weight)) } } \ No newline at end of file From 44b802f3594f44f73b55eb9e8ba4c5289ea6de9a Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Sun, 20 Mar 2022 13:00:30 +0400 Subject: [PATCH 21/28] cargo fmt --- payments/src/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/payments/src/types.rs b/payments/src/types.rs index 60991e607..826d1a169 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -70,6 +70,7 @@ pub trait PaymentHandler { // Settle a payment of `from` to `to`. To release a payment, the // recipient_share=100, to cancel a payment recipient_share=0 // Possible reasonse for failure include + /// /// - The payment does not exist /// - The unreserve operation fails /// - The transfer operation fails From dcf30fe1bdc265aba21b521dff8705a3033ef4db Mon Sep 17 00:00:00 2001 From: Daniel Olano Date: Mon, 21 Mar 2022 09:37:41 +0100 Subject: [PATCH 22/28] Reduce clones & Handle state change inconsistencies --- payments/src/lib.rs | 115 ++++++++++++++++++++---------------------- payments/src/tests.rs | 57 +++++++++++++-------- payments/src/types.rs | 17 ++++--- 3 files changed, 103 insertions(+), 86 deletions(-) diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 7f9e2a55f..4c02ef76d 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -169,7 +169,7 @@ pub mod pallet { .clone() .into_iter() // leave out tasks in the future - .filter(|(_, ScheduledTask { when, .. })| when <= &now) + .filter(|(_, ScheduledTask { when, task })| when <= &now && matches!(task, Task::Cancel)) .collect(); // order by oldest task to process @@ -178,15 +178,15 @@ pub mod pallet { let cancel_weight = T::WeightInfo::cancel(); while !task_list.is_empty() && remaining_weight >= cancel_weight { - if let Some(((from, to), ScheduledTask { task: Task::Cancel, .. })) = task_list.pop() { + if let Some((account_pair, _)) = task_list.pop() { remaining_weight = remaining_weight.saturating_sub(cancel_weight); // remove the task form the tasks - tasks.remove(&(from.clone(), to.clone())); + tasks.remove(&account_pair); // process the cancel payment if >::settle_payment( - from.clone(), - to.clone(), + &account_pair.0, + &account_pair.1, Percent::from_percent(0), ) .is_err() @@ -199,8 +199,8 @@ pub mod pallet { } else { // emit the cancel event if the refund was successful Self::deposit_event(Event::PaymentCancelled { - from: from.clone(), - to: to.clone(), + from: account_pair.0, + to: account_pair.1, }); } } @@ -231,8 +231,8 @@ pub mod pallet { // create PaymentDetail and add to storage let payment_detail = >::create_payment( - who.clone(), - recipient.clone(), + &who, + &recipient, asset, amount, PaymentState::Created, @@ -266,7 +266,7 @@ pub mod pallet { } // release is a settle_payment with 100% recipient_share - >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + >::settle_payment(&from, &to, Percent::from_percent(100))?; Self::deposit_event(Event::PaymentReleased { from, to }); Ok(().into()) @@ -283,11 +283,7 @@ pub mod pallet { match payment.state { // call settle payment with recipient_share=0, this refunds the sender PaymentState::Created => { - >::settle_payment( - creator.clone(), - who.clone(), - Percent::from_percent(0), - )?; + >::settle_payment(&creator, &who, Percent::from_percent(0))?; Self::deposit_event(Event::PaymentCancelled { from: creator, to: who }); } // if the payment is in state PaymentRequested, remove from storage @@ -298,10 +294,10 @@ pub mod pallet { Ok(().into()) } - /// Allow judge to set state of a payment /// This extrinsic is used to resolve disputes between the creator and - /// recipient of the payment. This extrinsic allows the assigned judge - /// to cancel/release/partial_release the payment. + /// recipient of the payment. + /// This extrinsic allows the assigned judge to + /// cancel/release/partial_release the payment. #[transactional] #[pallet::weight(T::WeightInfo::resolve_payment())] pub fn resolve_payment( @@ -311,24 +307,33 @@ pub mod pallet { recipient_share: Percent, ) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; + let account_pair = (from, recipient); // ensure the caller is the assigned resolver - if let Some(payment) = Payment::::get(&from, &recipient) { - ensure!(who == payment.resolver_account, Error::::InvalidAction) + if let Some(payment) = Payment::::get(&account_pair.0, &account_pair.1) { + ensure!(who == payment.resolver_account, Error::::InvalidAction); + ensure!( + payment.state != PaymentState::PaymentRequested, + Error::::InvalidAction + ); + if matches!(payment.state, PaymentState::RefundRequested { .. }) { + ScheduledTasks::::mutate(|tasks| { + tasks.remove(&account_pair); + }) + } } // try to update the payment to new state - >::settle_payment(from.clone(), recipient.clone(), recipient_share)?; + >::settle_payment(&account_pair.0, &account_pair.1, recipient_share)?; Self::deposit_event(Event::PaymentResolved { - from, - to: recipient, + from: account_pair.0, + to: account_pair.1, recipient_share, }); Ok(().into()) } - /// Allow payment creator to set payment to NeedsReview - /// This extrinsic is used to mark the payment as disputed so the - /// assigned judge can tigger a resolution and that the funds are no - /// longer locked. + /// Allow the creator of a payment to initiate a refund that will return + /// the funds after a configured amount of time that the reveiver has to + /// react and opose the request #[transactional] #[pallet::weight(T::WeightInfo::request_refund())] pub fn request_refund(origin: OriginFor, recipient: T::AccountId) -> DispatchResultWithPostInfo { @@ -337,11 +342,8 @@ pub mod pallet { Payment::::try_mutate(who.clone(), recipient.clone(), |maybe_payment| -> DispatchResult { // ensure the payment exists let payment = maybe_payment.as_mut().ok_or(Error::::InvalidPayment)?; - // ensure the payment is not in needsreview state - ensure!( - payment.state != PaymentState::NeedsReview, - Error::::PaymentNeedsReview - ); + // refunds only possible for payments in created state + ensure!(payment.state == PaymentState::Created, Error::::InvalidAction); // set the payment to requested refund let current_block = frame_system::Pallet::::block_number(); @@ -424,8 +426,9 @@ pub mod pallet { // Creates a new payment with the given details. This can be called by the // recipient of the payment to create a payment and then completed by the sender - // using the `accept_and_pay` extrinsic. The payment will be in PaymentRequested - // State and can only be modified by the `accept_and_pay` extrinsic. + // using the `accept_and_pay` extrinsic. The payment will be in + // PaymentRequested State and can only be modified by the `accept_and_pay` + // extrinsic. #[transactional] #[pallet::weight(T::WeightInfo::request_payment())] pub fn request_payment( @@ -438,8 +441,8 @@ pub mod pallet { // create PaymentDetail and add to storage >::create_payment( - from.clone(), - to.clone(), + &from, + &to, asset, amount, PaymentState::PaymentRequested, @@ -471,7 +474,7 @@ pub mod pallet { >::reserve_payment_amount(&from, &to, payment)?; // release the payment and delete the payment from storage - >::settle_payment(from.clone(), to.clone(), Percent::from_percent(100))?; + >::settle_payment(&from, &to, Percent::from_percent(100))?; Self::deposit_event(Event::PaymentRequestCompleted { from, to }); @@ -485,29 +488,24 @@ pub mod pallet { /// storage. #[require_transactional] fn create_payment( - from: T::AccountId, - recipient: T::AccountId, + from: &T::AccountId, + recipient: &T::AccountId, asset: AssetIdOf, amount: BalanceOf, - payment_state: PaymentState, + payment_state: PaymentState, incentive_percentage: Percent, remark: Option<&[u8]>, ) -> Result, sp_runtime::DispatchError> { Payment::::try_mutate( - from.clone(), - recipient.clone(), + from, + recipient, |maybe_payment| -> Result, sp_runtime::DispatchError> { - if maybe_payment.is_some() { - // ensure the payment is not in created/needsreview state - let current_state = maybe_payment.clone().unwrap().state; + // only payment requests can be overwritten + if let Some(payment) = maybe_payment { ensure!( - current_state != PaymentState::Created, + payment.state == PaymentState::PaymentRequested, Error::::PaymentAlreadyInProcess ); - ensure!( - current_state != PaymentState::NeedsReview, - Error::::PaymentNeedsReview - ); } // Calculate incentive amount - this is to insentivise the user to release @@ -525,8 +523,7 @@ 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_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)); @@ -561,36 +558,36 @@ pub mod pallet { /// fee_recipient For cancelling a payment, recipient_share = 0 /// For releasing a payment, recipient_share = 100 /// In other cases, the custom recipient_share can be specified - fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult { - Payment::::try_mutate(from.clone(), to.clone(), |maybe_payment| -> DispatchResult { + fn settle_payment(from: &T::AccountId, to: &T::AccountId, recipient_share: Percent) -> DispatchResult { + Payment::::try_mutate(from, to, |maybe_payment| -> DispatchResult { let payment = maybe_payment.take().ok_or(Error::::InvalidPayment)?; // unreserve the incentive amount and fees from the owner account match payment.fee_detail { Some((fee_recipient, fee_amount)) => { - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount + fee_amount); + T::Asset::unreserve(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 + from, // fee is paid by payment creator &fee_recipient, // account of fee recipient fee_amount, // amount of fee )?; } } None => { - T::Asset::unreserve(payment.asset, &from, payment.incentive_amount); + T::Asset::unreserve(payment.asset, from, payment.incentive_amount); } }; // Unreserve the transfer amount - T::Asset::unreserve(payment.asset, &to, payment.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); // send share to recipient - T::Asset::transfer(payment.asset, &to, &from, amount_to_sender)?; + T::Asset::transfer(payment.asset, to, from, amount_to_sender)?; Ok(()) })?; diff --git a/payments/src/tests.rs b/payments/src/tests.rs index 3f996d4f7..abe3ae93a 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -225,7 +225,7 @@ fn test_release_works() { } #[test] -fn test_set_state_payment_works() { +fn test_resolve_payment_works() { new_test_ext().execute_with(|| { let creator_initial_balance = 100; let payment_amount = 40; @@ -537,7 +537,7 @@ fn test_do_not_overwrite_logic_works() { payment_amount, None ), - crate::Error::::PaymentNeedsReview + crate::Error::::PaymentAlreadyInProcess ); }); } @@ -562,6 +562,18 @@ fn test_request_refund() { PAYMENT_RECIPENT )); + // do not overwrite payment + assert_noop!( + Payment::pay( + Origin::signed(PAYMENT_CREATOR), + PAYMENT_RECIPENT, + CURRENCY_ID, + payment_amount, + None + ), + crate::Error::::PaymentAlreadyInProcess + ); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { @@ -669,6 +681,11 @@ fn test_request_payment() { payment_amount, )); + assert_noop!( + Payment::request_refund(Origin::signed(PAYMENT_CREATOR), PAYMENT_RECIPENT), + crate::Error::::InvalidAction + ); + assert_eq!( PaymentStore::::get(PAYMENT_CREATOR, PAYMENT_RECIPENT), Some(PaymentDetail { @@ -869,8 +886,8 @@ fn test_accept_and_pay_should_charge_fee_correctly() { fn test_create_payment_does_not_work_without_transaction() { new_test_ext().execute_with(|| { assert_ok!(>::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, 20, PaymentState::Created, @@ -899,8 +916,8 @@ fn test_create_payment_works() { // transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, payment_amount, PaymentState::Created, @@ -925,8 +942,8 @@ fn test_create_payment_works() { assert_noop!( with_transaction(|| TransactionOutcome::Commit({ >::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, payment_amount, PaymentState::Created, @@ -967,8 +984,8 @@ fn test_reserve_payment_amount_works() { // transaction assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, payment_amount, PaymentState::Created, @@ -1015,8 +1032,8 @@ fn test_reserve_payment_amount_works() { assert_noop!( with_transaction(|| TransactionOutcome::Commit({ >::create_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, CURRENCY_ID, payment_amount, PaymentState::Created, @@ -1066,8 +1083,8 @@ fn test_settle_payment_works_for_cancel() { assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::settle_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, Percent::from_percent(0), ) }))); @@ -1109,8 +1126,8 @@ fn test_settle_payment_works_for_release() { assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::settle_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT, Percent::from_percent(100), ) }))); @@ -1153,8 +1170,8 @@ fn test_settle_payment_works_for_70_30() { assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::settle_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT_FEE_CHARGED, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT_FEE_CHARGED, Percent::from_percent(70), ) }))); @@ -1208,8 +1225,8 @@ fn test_settle_payment_works_for_50_50() { assert_ok!(with_transaction(|| TransactionOutcome::Commit({ >::settle_payment( - PAYMENT_CREATOR, - PAYMENT_RECIPENT_FEE_CHARGED, + &PAYMENT_CREATOR, + &PAYMENT_RECIPENT_FEE_CHARGED, Percent::from_percent(50), ) }))); diff --git a/payments/src/types.rs b/payments/src/types.rs index 826d1a169..90a4e58c3 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -12,6 +12,7 @@ use sp_runtime::{DispatchResult, Percent}; #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] #[scale_info(skip_type_params(T))] #[codec(mel_bound(T: pallet::Config))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PaymentDetail { /// type of asset used for payment pub asset: AssetIdOf, @@ -23,7 +24,7 @@ pub struct PaymentDetail { pub incentive_amount: BalanceOf, /// enum to track payment lifecycle [Created, NeedsReview, RefundRequested, /// Requested] - pub state: PaymentState, + pub state: PaymentState, /// account that can settle any disputes created in the payment pub resolver_account: T::AccountId, /// fee charged and recipient account details @@ -34,14 +35,16 @@ pub struct PaymentDetail { /// When a payment is 'completed' or 'cancelled' it is removed from storage and /// hence not tracked by a state. #[derive(Encode, Decode, Debug, Clone, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +#[scale_info(skip_type_params(T))] +#[codec(mel_bound(T: pallet::Config))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum PaymentState { +pub enum PaymentState { /// Amounts have been reserved and waiting for release/cancel Created, /// A judge needs to review and release manually NeedsReview, /// The user has requested refund and will be processed by `BlockNumber` - RefundRequested { cancel_block: BlockNumber }, + RefundRequested { cancel_block: T::BlockNumber }, /// The recipient of this transaction has created a request PaymentRequested, } @@ -53,11 +56,11 @@ pub trait PaymentHandler { /// Possible reasons for failure include: /// - Payment already exists and cannot be overwritten fn create_payment( - from: T::AccountId, - to: T::AccountId, + from: &T::AccountId, + to: &T::AccountId, asset: AssetIdOf, amount: BalanceOf, - payment_state: PaymentState, + payment_state: PaymentState, incentive_percentage: Percent, remark: Option<&[u8]>, ) -> Result, sp_runtime::DispatchError>; @@ -74,7 +77,7 @@ pub trait PaymentHandler { /// - The payment does not exist /// - The unreserve operation fails /// - The transfer operation fails - fn settle_payment(from: T::AccountId, to: T::AccountId, recipient_share: Percent) -> DispatchResult; + fn settle_payment(from: &T::AccountId, to: &T::AccountId, recipient_share: Percent) -> DispatchResult; /// Attempt to fetch the details of a payment from the given payment_id /// Possible reasons for failure include: From 19742bcefee85fc31ee4d60958c0b9c80fa294d2 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Wed, 6 Apr 2022 21:11:56 +0400 Subject: [PATCH 23/28] udpate to 0.9.18 --- payments/Cargo.toml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 820252d5d..9f684d963 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -10,20 +10,19 @@ description = "Allows users to post escrow payment on-chain" readme = "README.md" [dependencies] -parity-scale-codec = { default-features = false, features = ['derive'], version = "2.0.0" } +parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } log = { version = "0.4.14", default-features = false } -frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false, optional = true } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } orml-traits = {path = "../traits", version = "0.4.1-dev", default-features = false } -scale-info = { version = "1.0.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } [dev-dependencies] -serde = { version = "1.0.101" } -sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } -sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.17", default-features = false } +serde = { version = "1.0.136" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } orml-tokens = { path = "../tokens", version = "0.4.1-dev", default-features = false } [features] @@ -37,6 +36,5 @@ std = [ 'sp-std/std', 'scale-info/std', 'orml-traits/std', - 'frame-benchmarking/std', 'orml-tokens/std' ] From 29d10b1b6f00ad9ad64a09a93d6b00da9e1650a0 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Mon, 11 Apr 2022 17:34:52 +0400 Subject: [PATCH 24/28] review fixes - docs and formatting --- payments/src/lib.rs | 56 ++++++++++++++++++++++++++++++++++++++++++- payments/src/types.rs | 1 - 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 4c02ef76d..9bb11dfa0 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -1,4 +1,58 @@ -#![allow(clippy::unused_unit, unused_qualifications, missing_debug_implementations)] +//!This pallet allows users to create secure reversible payments that keep +//! funds locked in a merchant's account until the off-chain goods are confirmed +//! to be received. Each payment gets assigned its own *judge* that can help +//! resolve any disputes between the two parties. + +//! ## Terminology +//! +//! - Created: A payment has been created and the amount arrived to its +//! destination but it's locked. +//! - NeedsReview: The payment has bee disputed and is awaiting settlement by a +//! judge. +//! - IncentivePercentage: A small share of the payment amount is held in escrow +//! until a payment is completed/cancelled. The Incentive Percentage +//! represents this value. +//! - Resolver Account: A resolver account is assigned to every payment created, +//! this account has the privilege to cancel/release a payment that has been +//! disputed. +//! - Remark: The pallet allows to create payments by optionally providing some +//! extra(limited) amount of bytes, this is reffered to as Remark. This can be +//! used by a marketplace to seperate/tag payments. +//! - CancelBufferBlockLength: This is the time window where the recipient can +//! dispute a cancellation request from the payment creator. + +//! Extrinsics +//! +//! - `pay` - Create an payment for the given currencyid/amount +//! - `pay_with_remark` - Create a payment with a remark, can be used to tag +//! payments +//! - `release` - Release the payment amount to recipent +//! - `cancel` - Allows the recipient to cancel the payment and release the +//! payment amount to creator +//! - `resolve_release_payment` - Allows assigned judge to release a payment +//! - `resolve_cancel_payment` - Allows assigned judge to cancel a payment +//! - `request_refund` - Allows the creator of the payment to trigger cancel +//! with a buffer time. +//! - `claim_refund` - Allows the creator to claim payment refund after buffer +//! time +//! - `dispute_refund` - Allows the recipient to dispute the payment request of +//! sender +//! - `request_payment` - Create a payment that can be completed by the sender +//! using the `accept_and_pay` extrinsic. +//! - `accept_and_pay` - Allows the sender to fulfill a payment request created +//! by a recipient + +//! Types +//! +//! The `PaymentDetail` struct stores information about the payment/escrow. A +//! "payment" in virto network is similar to an escrow, it is used to guarantee +//! proof of funds and can be released once an agreed upon condition has reached +//! between the payment creator and recipient. The payment lifecycle is tracked +//! using the state field. + +//! The `PaymentState` enum tracks the possible states that a payment can be in. +//! When a payment is 'completed' or 'cancelled' it is removed from storage and +//! hence not tracked by a state. #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; diff --git a/payments/src/types.rs b/payments/src/types.rs index 90a4e58c3..a4e5058d4 100644 --- a/payments/src/types.rs +++ b/payments/src/types.rs @@ -117,6 +117,5 @@ pub struct ScheduledTask { /// the type of scheduled task pub task: Task, /// the 'time' at which the task should be executed - #[codec(compact)] pub when: Time, } From 6150284cfa0af4cd5d931ea6c701c160460a2454 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Thu, 28 Apr 2022 23:30:10 +0400 Subject: [PATCH 25/28] update to 0.9.19 --- payments/Cargo.toml | 12 ++++++------ payments/src/mock.rs | 3 +++ payments/src/tests.rs | 16 ---------------- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 76ab1587e..66e171420 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -12,18 +12,18 @@ readme = "README.md" [dependencies] parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } log = { version = "0.4.14", default-features = false } -frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } orml-traits = {path = "../traits", version = "0.4.1-dev", default-features = false } orml-tokens = { path = "../tokens", version = "0.4.1-dev", default-features = false } scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } [dev-dependencies] serde = { version = "1.0.136" } -sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } [features] default = ['std'] diff --git a/payments/src/mock.rs b/payments/src/mock.rs index ca2aaea49..207f2e0ee 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -82,6 +82,7 @@ parameter_type_with_key! { } parameter_types! { pub const MaxLocks: u32 = 50; + pub const MaxReserves: u32 = 50; } pub struct MockDustRemovalWhitelist; @@ -100,6 +101,8 @@ impl orml_tokens::Config for Test { type OnDust = (); type WeightInfo = (); type MaxLocks = MaxLocks; + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; type DustRemovalWhitelist = MockDustRemovalWhitelist; } diff --git a/payments/src/tests.rs b/payments/src/tests.rs index abe3ae93a..19bc67cb3 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -881,22 +881,6 @@ fn test_accept_and_pay_should_charge_fee_correctly() { }); } -#[test] -#[should_panic(expected = "Require transaction not called within with_transaction")] -fn test_create_payment_does_not_work_without_transaction() { - new_test_ext().execute_with(|| { - assert_ok!(>::create_payment( - &PAYMENT_CREATOR, - &PAYMENT_RECIPENT, - CURRENCY_ID, - 20, - PaymentState::Created, - Percent::from_percent(0), - None, - )); - }); -} - #[test] fn test_create_payment_works() { new_test_ext().execute_with(|| { From 16be33026fddbef327e030a93f8cd190f6491366 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Thu, 28 Apr 2022 23:54:50 +0400 Subject: [PATCH 26/28] limit tasks to sort --- payments/src/lib.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/payments/src/lib.rs b/payments/src/lib.rs index 142d29d64..7f9d2b9f7 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -161,6 +161,7 @@ pub mod pallet { /// This function will look for any pending scheduled tasks that can /// be executed and will process them. fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight { + const MAX_TASKS_TO_PROCESS: usize = 10; // reduce the weight used to read the task list remaining_weight = remaining_weight.saturating_sub(T::DbWeight::get().reads_writes(1, 1)); @@ -172,8 +173,11 @@ pub mod pallet { .filter(|(_, ScheduledTask { when, task })| when <= &now && matches!(task, Task::Cancel)) .collect(); + // limit the max tasks to reduce computation + task_list.truncate(MAX_TASKS_TO_PROCESS); + // order by oldest task to process - task_list.sort_by(|(_, t), (_, x)| x.when.partial_cmp(&t.when).unwrap()); + task_list.sort_by(|(_, t), (_, x)| x.when.cmp(&t.when)); let cancel_weight = T::WeightInfo::cancel(); @@ -259,11 +263,8 @@ pub mod pallet { let from = ensure_signed(origin)?; // ensure the payment is in Created state - if let Some(payment) = Payment::::get(&from, &to) { - ensure!(payment.state == PaymentState::Created, Error::::InvalidAction) - } else { - fail!(Error::::InvalidPayment); - } + let payment = Payment::::get(&from, &to).ok_or(Error::::InvalidPayment)?; + ensure!(payment.state == PaymentState::Created, Error::::InvalidAction); // release is a settle_payment with 100% recipient_share >::settle_payment(&from, &to, Percent::from_percent(100))?; From dce1f466153ec3815c31383255b1e4a860de0a42 Mon Sep 17 00:00:00 2001 From: Daniel Olano Date: Thu, 28 Apr 2022 23:20:55 +0200 Subject: [PATCH 27/28] Update to polkadot-v0.9.19 --- payments/Cargo.toml | 16 ++++++++-------- payments/src/mock.rs | 5 ++++- payments/src/tests.rs | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/payments/Cargo.toml b/payments/Cargo.toml index 9f684d963..93b6f7131 100644 --- a/payments/Cargo.toml +++ b/payments/Cargo.toml @@ -10,19 +10,19 @@ description = "Allows users to post escrow payment on-chain" readme = "README.md" [dependencies] -parity-scale-codec = { version = "3.0.0", default-features = false, features = ["max-encoded-len"] } +parity-scale-codec = { version = "3.1.2", default-features = false, features = ["max-encoded-len"] } log = { version = "0.4.14", default-features = false } -frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } orml-traits = {path = "../traits", version = "0.4.1-dev", default-features = false } -scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } [dev-dependencies] serde = { version = "1.0.136" } -sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } -sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.18", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.19", default-features = false } orml-tokens = { path = "../tokens", version = "0.4.1-dev", default-features = false } [features] diff --git a/payments/src/mock.rs b/payments/src/mock.rs index ca2aaea49..e26626e89 100644 --- a/payments/src/mock.rs +++ b/payments/src/mock.rs @@ -2,7 +2,7 @@ use crate as payment; use crate::PaymentDetail; use frame_support::{ parameter_types, - traits::{Contains, Everything, GenesisBuild, Hooks, OnFinalize}, + traits::{ConstU32, Contains, Everything, GenesisBuild, Hooks, OnFinalize}, weights::DispatchClass, }; use frame_system as system; @@ -83,6 +83,7 @@ parameter_type_with_key! { parameter_types! { pub const MaxLocks: u32 = 50; } +pub type ReserveIdentifier = [u8; 8]; pub struct MockDustRemovalWhitelist; impl Contains for MockDustRemovalWhitelist { @@ -101,6 +102,8 @@ impl orml_tokens::Config for Test { type WeightInfo = (); type MaxLocks = MaxLocks; type DustRemovalWhitelist = MockDustRemovalWhitelist; + type MaxReserves = ConstU32<2>; + type ReserveIdentifier = ReserveIdentifier; } pub struct MockDisputeResolver; diff --git a/payments/src/tests.rs b/payments/src/tests.rs index abe3ae93a..febdd083e 100644 --- a/payments/src/tests.rs +++ b/payments/src/tests.rs @@ -882,7 +882,7 @@ fn test_accept_and_pay_should_charge_fee_correctly() { } #[test] -#[should_panic(expected = "Require transaction not called within with_transaction")] +#[should_panic] fn test_create_payment_does_not_work_without_transaction() { new_test_ext().execute_with(|| { assert_ok!(>::create_payment( From d6502bc4d5f0811a0292fdb38a343031cdb562c1 Mon Sep 17 00:00:00 2001 From: Stanly Johnson Date: Fri, 29 Apr 2022 09:28:39 +0400 Subject: [PATCH 28/28] fix take() limit on read --- payments/src/lib.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/payments/src/lib.rs b/payments/src/lib.rs index f075f40b1..c48be9e97 100644 --- a/payments/src/lib.rs +++ b/payments/src/lib.rs @@ -215,30 +215,33 @@ pub mod pallet { /// This function will look for any pending scheduled tasks that can /// be executed and will process them. fn on_idle(now: T::BlockNumber, mut remaining_weight: Weight) -> Weight { - const MAX_TASKS_TO_PROCESS: usize = 10; + const MAX_TASKS_TO_PROCESS: usize = 5; // reduce the weight used to read the task list remaining_weight = remaining_weight.saturating_sub(T::WeightInfo::remove_task()); + let cancel_weight = T::WeightInfo::cancel(); + + // calculate count of tasks that can be processed with remaining weight + let possible_task_count: usize = remaining_weight + .saturating_div(cancel_weight) + .try_into() + .unwrap_or(MAX_TASKS_TO_PROCESS); ScheduledTasks::::mutate(|tasks| { let mut task_list: Vec<_> = tasks .clone() .into_iter() + .take(possible_task_count) // leave out tasks in the future .filter(|(_, ScheduledTask { when, task })| when <= &now && matches!(task, Task::Cancel)) .collect(); - // limit the max tasks to reduce computation - task_list.truncate(MAX_TASKS_TO_PROCESS); - // order by oldest task to process task_list.sort_by(|(_, t), (_, x)| x.when.cmp(&t.when)); - let cancel_weight = T::WeightInfo::cancel(); - while !task_list.is_empty() && remaining_weight >= cancel_weight { if let Some((account_pair, _)) = task_list.pop() { remaining_weight = remaining_weight.saturating_sub(cancel_weight); - // remove the task form the tasks + // remove the task form the tasks storage tasks.remove(&account_pair); // process the cancel payment