From ac3e5a4b10226ffc5ea20c3fd3cb3b42b8bf6915 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Fri, 17 Jan 2020 19:10:04 +0800 Subject: [PATCH 1/2] feature/accounts --- Cargo.lock | 20 ++++ modules/accounts/Cargo.toml | 40 +++++++ modules/accounts/src/lib.rs | 214 ++++++++++++++++++++++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 modules/accounts/Cargo.toml create mode 100644 modules/accounts/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 036143b676..e9bbfc1a87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2630,6 +2630,26 @@ dependencies = [ "ws2_32-sys", ] +[[package]] +name = "module-accounts" +version = "0.0.1" +dependencies = [ + "frame-support", + "frame-system", + "module-support", + "orml-currencies", + "orml-tokens", + "orml-traits", + "pallet-balances", + "pallet-transaction-payment", + "parity-scale-codec", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "module-auction-manager" version = "0.0.1" diff --git a/modules/accounts/Cargo.toml b/modules/accounts/Cargo.toml new file mode 100644 index 0000000000..c95884203a --- /dev/null +++ b/modules/accounts/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "module-accounts" +version = "0.0.1" +authors = ["Acala Developers"] +edition = "2018" + +[dependencies] +serde = { version = "1.0", optional = true } +codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate.git", default-features = false } +frame-support = { package = "frame-support", git = "https://github.com/paritytech/substrate.git", default-features = false } +system = { package = "frame-system", git = "https://github.com/paritytech/substrate.git", default-features = false } +rstd = { package = "sp-std", git = "https://github.com/paritytech/substrate.git", default-features = false } +orml-traits = { package = "orml-traits", path = "../../orml/traits", default-features = false } +orml-tokens = { package = "orml-tokens", path = "../../orml/tokens", default-features = false } +orml-currencies = { package = "orml-currencies", path = "../../orml/currencies", default-features = false } +support = { package = "module-support", path = "../support", default-features = false } +pallet-transaction-payment = { git = "https://github.com/paritytech/substrate.git", default-features = false } + +[dev-dependencies] +primitives = { package = "sp-core", git = "https://github.com/paritytech/substrate.git", default-features = false } +runtime-io = { package = "sp-io", git = "https://github.com/paritytech/substrate.git", default-features = false } +pallet-balances= { package = "pallet-balances", git = "https://github.com/paritytech/substrate.git", default-features = false } + + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "sp-runtime/std", + "frame-support/std", + "system/std", + "rstd/std", + "orml-traits/std", + "orml-tokens/std", + "orml-currencies/std", + "support/std", + "pallet-transaction-payment/std", +] diff --git a/modules/accounts/src/lib.rs b/modules/accounts/src/lib.rs new file mode 100644 index 0000000000..6f4888d7d0 --- /dev/null +++ b/modules/accounts/src/lib.rs @@ -0,0 +1,214 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode}; +use frame_support::{ + decl_module, decl_storage, + dispatch::Dispatchable, + traits::{Currency, ExistenceRequirement, Get, OnReapAccount, OnUnbalanced, Time, WithdrawReason}, + weights::DispatchInfo, + IsSubType, Parameter, +}; +use orml_traits::MultiCurrency; +use sp_runtime::{ + traits::{Convert, SaturatedConversion, Saturating, SignedExtension, Zero}, + transaction_validity::{ + InvalidTransaction, TransactionPriority, TransactionValidity, TransactionValidityError, ValidTransaction, + }, +}; +//use support::{Ratio}; +use rstd::prelude::*; + +//type BalanceOf = <::Currency as MultiCurrency<::AccountId>>::Balance; +type MomentOf = <::Time as Time>::Moment; +type PalletBalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + +pub trait Trait: system::Trait + pallet_transaction_payment::Trait + orml_currencies::Trait { + type FreeTransferCount: Get; + type FreeTransferPeriod: Get>; + type Time: Time; + type Currency: MultiCurrency + Send + Sync; + type Call: Parameter + + Dispatchable::Origin> + + IsSubType, Self>; +} + +decl_storage! { + trait Store for Module as Accounts { + LastFreeTransfers get(fn last_free_transfers): map T::AccountId => Vec>; + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + const FreeTransferCount: u8 = T::FreeTransferCount::get(); + const FreeTransferPeriod: MomentOf = T::FreeTransferPeriod::get(); + } +} + +impl Module { + pub fn try_free_transfer(who: &T::AccountId) -> bool { + let mut last_free_transfer = Self::last_free_transfers(who); + let now = T::Time::now(); + let free_transfer_period = T::FreeTransferPeriod::get(); + + // remove all the expired entries + last_free_transfer.retain(|&x| x.saturating_add(free_transfer_period) > now); + + // check if can transfer for free + if last_free_transfer.len() < T::FreeTransferCount::get() as usize { + // add entry to last_free_transfer + last_free_transfer.push(now); + >::insert(who, last_free_transfer); + true + } else { + false + } + } +} + +impl OnReapAccount for Module { + fn on_reap_account(who: &T::AccountId) { + >::remove(who); + } +} + +/// Require the transactor pay for themselves and maybe include a tip to gain additional priority +/// in the queue. +#[derive(Encode, Decode, Clone, Eq, PartialEq)] +pub struct ChargeTransactionPayment(#[codec(compact)] PalletBalanceOf); + +impl ChargeTransactionPayment { + /// utility constructor. Used only in client/factory code. + pub fn from(fee: PalletBalanceOf) -> Self { + Self(fee) + } + + /// Compute the final fee value for a particular transaction. + /// + /// The final fee is composed of: + /// - _base_fee_: This is the minimum amount a user pays for a transaction. + /// - _len_fee_: This is the amount paid merely to pay for size of the transaction. + /// - _weight_fee_: This amount is computed based on the weight of the transaction. Unlike + /// size-fee, this is not input dependent and reflects the _complexity_ of the execution + /// and the time it consumes. + /// - _targeted_fee_adjustment_: This is a multiplier that can tune the final fee based on + /// the congestion of the network. + /// - (optional) _tip_: if included in the transaction, it will be added on top. Only signed + /// transactions can have a tip. + /// + /// final_fee = base_fee + targeted_fee_adjustment(len_fee + weight_fee) + tip; + fn compute_fee( + len: u32, + info: ::DispatchInfo, + tip: PalletBalanceOf, + ) -> PalletBalanceOf + where + PalletBalanceOf: Sync + Send, + { + if info.pays_fee { + let len = >::from(len); + let per_byte = ::TransactionByteFee::get(); + let len_fee = per_byte.saturating_mul(len); + + let weight_fee = { + // cap the weight to the maximum defined in runtime, otherwise it will be the `Bounded` + // maximum of its data type, which is not desired. + let capped_weight = info.weight.min(::MaximumBlockWeight::get()); + ::WeightToFee::convert(capped_weight) + }; + + // the adjustable part of the fee + let adjustable_fee = len_fee.saturating_add(weight_fee); + let targeted_fee_adjustment = >::next_fee_multiplier(); + // adjusted_fee = adjustable_fee + (adjustable_fee * targeted_fee_adjustment) + let adjusted_fee = targeted_fee_adjustment.saturated_multiply_accumulate(adjustable_fee); + + let base_fee = ::TransactionBaseFee::get(); + let final_fee = base_fee.saturating_add(adjusted_fee).saturating_add(tip); + + final_fee + } else { + tip + } + } +} + +impl rstd::fmt::Debug for ChargeTransactionPayment { + #[cfg(feature = "std")] + fn fmt(&self, f: &mut rstd::fmt::Formatter) -> rstd::fmt::Result { + write!(f, "ChargeTransactionPayment<{:?}>", self.0) + } + #[cfg(not(feature = "std"))] + fn fmt(&self, _: &mut rstd::fmt::Formatter) -> rstd::fmt::Result { + Ok(()) + } +} + +impl SignedExtension for ChargeTransactionPayment +where + PalletBalanceOf: Send + Sync, +{ + type AccountId = T::AccountId; + type Call = ::Call; + type AdditionalSigned = (); + type DispatchInfo = DispatchInfo; + type Pre = (); + fn additional_signed(&self) -> rstd::result::Result<(), TransactionValidityError> { + Ok(()) + } + + fn validate( + &self, + who: &Self::AccountId, + call: &Self::Call, + info: Self::DispatchInfo, + len: usize, + ) -> TransactionValidity { + let call = match call.is_sub_type() { + Some(call) => call, + None => return Ok(ValidTransaction::default()), + }; + + // check call type + let skip_pay_fee = match call { + orml_currencies::Call::transfer(..) => { + // call try_free_transfer + if >::try_free_transfer(who) { + true + } else { + false + } + } + _ => false, + }; + + // pay any fees. + let tip = self.0; + let fee: PalletBalanceOf = Self::compute_fee(len as u32, info, tip); + + // skip payment withdraw if match conditions + if !skip_pay_fee { + let imbalance = match ::Currency::withdraw( + who, + fee, + if tip.is_zero() { + WithdrawReason::TransactionPayment.into() + } else { + WithdrawReason::TransactionPayment | WithdrawReason::Tip + }, + ExistenceRequirement::KeepAlive, + ) { + Ok(imbalance) => imbalance, + Err(_) => return InvalidTransaction::Payment.into(), + }; + ::OnTransactionPayment::on_unbalanced(imbalance); + } + + let mut r = ValidTransaction::default(); + // NOTE: we probably want to maximize the _fee (of any type) per weight unit_ here, which + // will be a bit more than setting the priority to tip. For now, this is enough. + r.priority = fee.saturated_into::(); + Ok(r) + } +} From eaef70e9d49cc2cabb1cd55c07def0a7d71c91bf Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Mon, 20 Jan 2020 13:49:03 +0800 Subject: [PATCH 2/2] add tests for accounts module --- Cargo.lock | 1 + modules/accounts/Cargo.toml | 1 + modules/accounts/src/lib.rs | 69 ++----------- modules/accounts/src/mock.rs | 168 +++++++++++++++++++++++++++++++ modules/accounts/src/tests.rs | 40 ++++++++ modules/cdp_treasury/src/mock.rs | 5 - 6 files changed, 221 insertions(+), 63 deletions(-) create mode 100644 modules/accounts/src/mock.rs create mode 100644 modules/accounts/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 625cb8d19d..742c133be4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2638,6 +2638,7 @@ dependencies = [ "orml-tokens", "orml-traits", "pallet-balances", + "pallet-timestamp", "pallet-transaction-payment", "parity-scale-codec", "serde", diff --git a/modules/accounts/Cargo.toml b/modules/accounts/Cargo.toml index c95884203a..cf04fe8afd 100644 --- a/modules/accounts/Cargo.toml +++ b/modules/accounts/Cargo.toml @@ -21,6 +21,7 @@ pallet-transaction-payment = { git = "https://github.com/paritytech/substrate.gi primitives = { package = "sp-core", git = "https://github.com/paritytech/substrate.git", default-features = false } runtime-io = { package = "sp-io", git = "https://github.com/paritytech/substrate.git", default-features = false } pallet-balances= { package = "pallet-balances", git = "https://github.com/paritytech/substrate.git", default-features = false } +pallet-timestamp= { package = "pallet-timestamp", git = "https://github.com/paritytech/substrate.git", default-features = false } [features] diff --git a/modules/accounts/src/lib.rs b/modules/accounts/src/lib.rs index 6f4888d7d0..1032902339 100644 --- a/modules/accounts/src/lib.rs +++ b/modules/accounts/src/lib.rs @@ -9,14 +9,16 @@ use frame_support::{ IsSubType, Parameter, }; use orml_traits::MultiCurrency; +use rstd::prelude::*; use sp_runtime::{ - traits::{Convert, SaturatedConversion, Saturating, SignedExtension, Zero}, + traits::{SaturatedConversion, Saturating, SignedExtension, Zero}, transaction_validity::{ InvalidTransaction, TransactionPriority, TransactionValidity, TransactionValidityError, ValidTransaction, }, }; -//use support::{Ratio}; -use rstd::prelude::*; + +mod mock; +mod tests; //type BalanceOf = <::Currency as MultiCurrency<::AccountId>>::Balance; type MomentOf = <::Time as Time>::Moment; @@ -83,55 +85,6 @@ impl ChargeTransactionPayment { pub fn from(fee: PalletBalanceOf) -> Self { Self(fee) } - - /// Compute the final fee value for a particular transaction. - /// - /// The final fee is composed of: - /// - _base_fee_: This is the minimum amount a user pays for a transaction. - /// - _len_fee_: This is the amount paid merely to pay for size of the transaction. - /// - _weight_fee_: This amount is computed based on the weight of the transaction. Unlike - /// size-fee, this is not input dependent and reflects the _complexity_ of the execution - /// and the time it consumes. - /// - _targeted_fee_adjustment_: This is a multiplier that can tune the final fee based on - /// the congestion of the network. - /// - (optional) _tip_: if included in the transaction, it will be added on top. Only signed - /// transactions can have a tip. - /// - /// final_fee = base_fee + targeted_fee_adjustment(len_fee + weight_fee) + tip; - fn compute_fee( - len: u32, - info: ::DispatchInfo, - tip: PalletBalanceOf, - ) -> PalletBalanceOf - where - PalletBalanceOf: Sync + Send, - { - if info.pays_fee { - let len = >::from(len); - let per_byte = ::TransactionByteFee::get(); - let len_fee = per_byte.saturating_mul(len); - - let weight_fee = { - // cap the weight to the maximum defined in runtime, otherwise it will be the `Bounded` - // maximum of its data type, which is not desired. - let capped_weight = info.weight.min(::MaximumBlockWeight::get()); - ::WeightToFee::convert(capped_weight) - }; - - // the adjustable part of the fee - let adjustable_fee = len_fee.saturating_add(weight_fee); - let targeted_fee_adjustment = >::next_fee_multiplier(); - // adjusted_fee = adjustable_fee + (adjustable_fee * targeted_fee_adjustment) - let adjusted_fee = targeted_fee_adjustment.saturated_multiply_accumulate(adjustable_fee); - - let base_fee = ::TransactionBaseFee::get(); - let final_fee = base_fee.saturating_add(adjusted_fee).saturating_add(tip); - - final_fee - } else { - tip - } - } } impl rstd::fmt::Debug for ChargeTransactionPayment { @@ -165,12 +118,16 @@ where info: Self::DispatchInfo, len: usize, ) -> TransactionValidity { + // pay any fees. + let tip = self.0; + let fee: PalletBalanceOf = + pallet_transaction_payment::ChargeTransactionPayment::::compute_fee(len as u32, info, tip); + + // check call type let call = match call.is_sub_type() { Some(call) => call, None => return Ok(ValidTransaction::default()), }; - - // check call type let skip_pay_fee = match call { orml_currencies::Call::transfer(..) => { // call try_free_transfer @@ -183,10 +140,6 @@ where _ => false, }; - // pay any fees. - let tip = self.0; - let fee: PalletBalanceOf = Self::compute_fee(len as u32, info, tip); - // skip payment withdraw if match conditions if !skip_pay_fee { let imbalance = match ::Currency::withdraw( diff --git a/modules/accounts/src/mock.rs b/modules/accounts/src/mock.rs new file mode 100644 index 0000000000..23324513e9 --- /dev/null +++ b/modules/accounts/src/mock.rs @@ -0,0 +1,168 @@ +//! Mocks for the cdp treasury module. + +#![cfg(test)] + +use super::*; +use frame_support::{impl_outer_dispatch, impl_outer_origin, parameter_types}; +use primitives::H256; +use sp_runtime::{ + testing::Header, + traits::{ConvertInto, IdentityLookup}, + Perbill, +}; + +impl_outer_origin! { + pub enum Origin for Runtime {} +} + +impl_outer_dispatch! { + pub enum Call for Runtime where origin: Origin { + orml_currencies::Currencies, + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: u32 = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const ExistentialDeposit: u64 = 0; + pub const TransferFee: u64 = 0; + pub const CreationFee: u64 = 2; + pub const GetNativeCurrencyId: CurrencyId = ACA; +} + +pub type AccountId = u64; +pub type BlockNumber = u64; +pub type Balance = u64; +pub type Amount = i64; +pub type CurrencyId = u32; +pub type Moment = u64; + +pub const ALICE: AccountId = 0; +pub const BOB: AccountId = 1; +pub const ACA: CurrencyId = 0; +pub const AUSD: CurrencyId = 1; +pub const BTC: CurrencyId = 2; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Runtime; + +impl system::Trait for Runtime { + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Call = (); + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = (); + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); + type ModuleToIndex = (); +} + +impl orml_tokens::Trait for Runtime { + type Event = (); + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type ExistentialDeposit = ExistentialDeposit; + type DustRemoval = (); +} +pub type Tokens = orml_tokens::Module; + +impl pallet_balances::Trait for Runtime { + type Balance = Balance; + type OnFreeBalanceZero = (); + type OnNewAccount = (); + type OnReapAccount = (); + type TransferPayment = (); + type DustRemoval = (); + type Event = (); + type ExistentialDeposit = ExistentialDeposit; + type TransferFee = TransferFee; + type CreationFee = CreationFee; +} +pub type PalletBalances = pallet_balances::Module; + +pub type AdaptedBasicCurrency = orml_currencies::BasicCurrencyAdapter; + +impl orml_currencies::Trait for Runtime { + type Event = (); + type MultiCurrency = Tokens; + type NativeCurrency = AdaptedBasicCurrency; + type GetNativeCurrencyId = GetNativeCurrencyId; +} +pub type Currencies = orml_currencies::Module; + +parameter_types! { + pub const MinimumPeriod: Moment = 5; +} + +impl pallet_timestamp::Trait for Runtime { + type Moment = Moment; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; +} +pub type TimeModule = pallet_timestamp::Module; + +parameter_types! { + pub const TransactionBaseFee: Balance = 0; + pub const TransactionByteFee: Balance = 2; +} + +impl pallet_transaction_payment::Trait for Runtime { + type Currency = PalletBalances; + type OnTransactionPayment = (); + type TransactionBaseFee = TransactionBaseFee; + type TransactionByteFee = TransactionByteFee; + type WeightToFee = ConvertInto; + type FeeMultiplierUpdate = (); +} +pub type PalletTransactionPayment = pallet_transaction_payment::Module; + +parameter_types! { + pub const FreeTransferCount: u8 = 3; + pub const FreeTransferPeriod: Moment = 100; +} + +impl Trait for Runtime { + type FreeTransferCount = FreeTransferCount; + type FreeTransferPeriod = FreeTransferPeriod; + type Time = TimeModule; + type Currency = Currencies; + type Call = Call; +} +pub type Accounts = Module; + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, CurrencyId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { + endowed_accounts: vec![(ALICE, ACA, 1000), (ALICE, AUSD, 1000), (ALICE, BTC, 1000)], + } + } +} + +impl ExtBuilder { + pub fn build(self) -> runtime_io::TestExternalities { + let mut t = system::GenesisConfig::default().build_storage::().unwrap(); + + orml_tokens::GenesisConfig:: { + endowed_accounts: self.endowed_accounts, + } + .assimilate_storage(&mut t) + .unwrap(); + + t.into() + } +} diff --git a/modules/accounts/src/tests.rs b/modules/accounts/src/tests.rs new file mode 100644 index 0000000000..724840a867 --- /dev/null +++ b/modules/accounts/src/tests.rs @@ -0,0 +1,40 @@ +//! Unit tests for the accounts module. + +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok}; +use mock::{Accounts, Currencies, ExtBuilder, Origin, Runtime, TimeModule, ALICE, AUSD, BOB, BTC}; +use sp_runtime::traits::OnFinalize; + +#[test] +fn try_free_transfer_over_cap() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(TimeModule::now(), 0); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![]); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0]); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0, 0]); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0, 0, 0]); + assert_eq!(Accounts::try_free_transfer(&ALICE), false); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0, 0, 0]); + }); +} + +#[test] +fn remove_expired_entry() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(TimeModule::now(), 0); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![]); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![0, 0, 0]); + assert_ok!(TimeModule::dispatch(pallet_timestamp::Call::set(100), Origin::NONE)); + assert_eq!(TimeModule::now(), 100); + assert_eq!(Accounts::try_free_transfer(&ALICE), true); + assert_eq!(Accounts::last_free_transfers(ALICE), vec![100]); + }); +} diff --git a/modules/cdp_treasury/src/mock.rs b/modules/cdp_treasury/src/mock.rs index bbff415bdf..8e9938f0b5 100644 --- a/modules/cdp_treasury/src/mock.rs +++ b/modules/cdp_treasury/src/mock.rs @@ -6,7 +6,6 @@ use super::*; use frame_support::{impl_outer_origin, parameter_types}; use primitives::H256; use sp_runtime::{testing::Header, traits::IdentityLookup, Perbill}; -use support::Rate; impl_outer_origin! { pub enum Origin for Runtime {} @@ -22,10 +21,6 @@ parameter_types! { pub const CreationFee: u64 = 2; pub const GetStableCurrencyId: CurrencyId = AUSD; pub const GetNativeCurrencyId: CurrencyId = ACA; - pub const MinimumIncrementSize: Rate = Rate::from_rational(1, 20); - pub const AuctionTimeToClose: u64 = 100; - pub const AuctionDurationSoftCap: u64 = 2000; - pub const GetAmountAdjustment: Rate = Rate::from_rational(1, 2); } pub type AccountId = u64;