From 90b89dfc58cdbe34adc6b2d55a7563f44d1e60d1 Mon Sep 17 00:00:00 2001 From: wangjj9219 <183318287@qq.com> Date: Wed, 13 Nov 2019 08:15:24 +0800 Subject: [PATCH] fix fmt error and bug and add tests (#20) * add auction module * remove redundant traits definition * add tests * get reference instead of ownership * optimized code --- Cargo.dev.toml | 1 + auction/Cargo.toml | 34 ++++++++++ auction/src/lib.rs | 147 ++++++++++++++++++++++++++++++++++++++++++ auction/src/mock.rs | 103 +++++++++++++++++++++++++++++ auction/src/tests.rs | 57 ++++++++++++++++ traits/src/auction.rs | 37 +++++++++-- 6 files changed, 373 insertions(+), 6 deletions(-) create mode 100644 auction/Cargo.toml create mode 100644 auction/src/lib.rs create mode 100644 auction/src/mock.rs create mode 100644 auction/src/tests.rs diff --git a/Cargo.dev.toml b/Cargo.dev.toml index be8d02b..d3408d2 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -4,4 +4,5 @@ members = [ "tokens", "traits", "utilities", + "auction", ] diff --git a/auction/Cargo.toml b/auction/Cargo.toml new file mode 100644 index 0000000..adc41b6 --- /dev/null +++ b/auction/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "orml-auction" +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 } +sr-primitives = { git = "https://github.com/paritytech/substrate.git", default-features = false } +srml-support = { package = "srml-support", git = "https://github.com/paritytech/substrate.git", default-features = false } +srml-system = { package = "srml-system", git = "https://github.com/paritytech/substrate.git", default-features = false } +runtime_io = { package = "sr-io", git = "https://github.com/paritytech/substrate.git", default-features = false } +rstd = { package = "sr-std", git = "https://github.com/paritytech/substrate.git", default-features = false } + +traits = { package = "orml-traits", path = "../traits", default-features = false } +utilities = { package = "orml-utilities", path = "../utilities", default-features = false } + +[dev-dependencies] +primitives = { package = "substrate-primitives", git = "https://github.com/paritytech/substrate.git", default-features = false } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "sr-primitives/std", + "srml-support/std", + "srml-system/std", + "runtime_io/std", + "rstd/std", + "traits/std", + "utilities/std", +] \ No newline at end of file diff --git a/auction/src/lib.rs b/auction/src/lib.rs new file mode 100644 index 0000000..bf87dcb --- /dev/null +++ b/auction/src/lib.rs @@ -0,0 +1,147 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use rstd::result; +use sr_primitives::traits::{MaybeSerializeDeserialize, Member, SimpleArithmetic}; +use srml_support::{ + decl_error, decl_event, decl_module, decl_storage, dispatch::Result, ensure, Parameter, StorageMap, StorageValue, +}; +use srml_system::{self as system, ensure_signed}; +use utilities::{LinkedItem, LinkedList}; + +use traits::{Auction, AuctionHandler, AuctionInfo}; + +mod mock; +mod tests; + +pub trait Trait: srml_system::Trait { + type Event: From<Event<Self>> + Into<<Self as srml_system::Trait>::Event>; + type Balance: Parameter + Member + SimpleArithmetic + Default + Copy + MaybeSerializeDeserialize; + type AuctionId: Parameter + Member + SimpleArithmetic + Default + Copy + MaybeSerializeDeserialize; + type Handler: AuctionHandler<Self::AccountId, Self::Balance, Self::BlockNumber, Self::AuctionId>; +} + +type AuctionEndTimeList<T> = + LinkedList<AuctionEndTime<T>, <T as srml_system::Trait>::BlockNumber, <T as Trait>::AuctionId>; + +decl_event!( + pub enum Event<T> where + <T as srml_system::Trait>::AccountId, + <T as Trait>::Balance, + <T as Trait>::AuctionId, + { + Bid(AuctionId, AccountId, Balance), + } +); + +decl_storage! { + trait Store for Module<T: Trait> as Auction { + pub Auctions get(fn auctions): map T::AuctionId => Option<AuctionInfo<T::AccountId, T::Balance, T::BlockNumber>>; + pub AuctionsCount get(fn auctions_count): T::AuctionId; + pub AuctionEndTime get(fn auction_end_time): map(T::BlockNumber, Option<T::AuctionId>) => Option<LinkedItem<T::AuctionId>>; + } +} + +decl_module! { + pub struct Module<T: Trait> for enum Call where origin: T::Origin { + fn deposit_event() = default; + + pub fn bid(origin, id: T::AuctionId, value: T::Balance) -> Result { + let from = ensure_signed(origin)?; + + let mut auction = <Auctions<T>>::get(id).ok_or(Error::AuctionNotExist)?; + if let Some(ref current_bid) = auction.bid { + ensure!(value > current_bid.1, Error::InvalidBidPrice.into()); + } else { + ensure!(value > 0.into(), Error::InvalidBidPrice.into()); + } + let bid_result = T::Handler::on_new_bid( + <srml_system::Module<T>>::block_number(), + id, + (from.clone(), value), + auction.bid.clone(), + ); + + ensure!(bid_result.accept_bid, Error::BidNotAccepted.into()); + if let Some(new_end) = bid_result.auction_end { + if let Some(old_end_block) = auction.end { + <AuctionEndTimeList<T>>::remove(&old_end_block, id); + } + if let Some(new_end_block) = new_end { + <AuctionEndTimeList<T>>::append(&new_end_block, id); + } + auction.end = new_end; + } + auction.bid = Some((from.clone(), value)); + <Auctions<T>>::insert(id, auction); + Self::deposit_event(RawEvent::Bid(id, from, value)); + Ok(()) + } + + fn on_finalize(now: T::BlockNumber) { + let head_key: Option<T::AuctionId> = None; + if let Some(mut head_item) = <AuctionEndTime<T>>::get((now, head_key)) { + while let Some(auction_id) = head_item.next { + if let Some(auction) = Self::auctions(auction_id) { + T::Handler::on_auction_ended(auction_id, auction.bid); + <Auctions<T>>::remove(auction_id); + } + head_item = <AuctionEndTime<T>>::get((now, Some(auction_id))).unwrap_or_else(|| LinkedItem { + prev: None, + next: None, + }); + <AuctionEndTime<T>>::remove((now, Some(auction_id))); + } + + <AuctionEndTime<T>>::remove((now, head_key)); + } + } + } +} + +decl_error! { + /// Error for auction module. + pub enum Error { + AuctionNotExist, + BidNotAccepted, + InvalidBidPrice, + } +} + +impl<T: Trait> Module<T> {} + +impl<T: Trait> Auction<T::AccountId, T::BlockNumber> for Module<T> { + type AuctionId = T::AuctionId; + type Balance = T::Balance; + type Error = Error; + + fn auction_info(id: Self::AuctionId) -> Option<AuctionInfo<T::AccountId, Self::Balance, T::BlockNumber>> { + Self::auctions(id) + } + + fn update_auction( + id: Self::AuctionId, + info: AuctionInfo<T::AccountId, Self::Balance, T::BlockNumber>, + ) -> result::Result<(), Self::Error> { + let auction = <Auctions<T>>::get(id).ok_or(Error::AuctionNotExist)?; + if let Some(old_end) = auction.end { + <AuctionEndTimeList<T>>::remove(&old_end, id); + } + if let Some(new_end) = info.end { + <AuctionEndTimeList<T>>::append(&new_end, id); + } + <Auctions<T>>::insert(id, info); + Ok(()) + } + + fn new_auction(_start: T::BlockNumber, end: Option<T::BlockNumber>) -> Self::AuctionId { + let auction = AuctionInfo { bid: None, end: end }; + let auction_id = Self::auctions_count(); + <AuctionsCount<T>>::mutate(|n| *n += Self::AuctionId::from(1)); + <Auctions<T>>::insert(auction_id, auction); + if let Some(end_block) = end { + <AuctionEndTimeList<T>>::append(&end_block, auction_id); + } + + auction_id + } +} diff --git a/auction/src/mock.rs b/auction/src/mock.rs new file mode 100644 index 0000000..57d4d9a --- /dev/null +++ b/auction/src/mock.rs @@ -0,0 +1,103 @@ +//! Mocks for the auction module. + +#![cfg(test)] + +use primitives::H256; +use sr_primitives::{testing::Header, traits::IdentityLookup, Perbill}; +use srml_support::{impl_outer_event, impl_outer_origin, parameter_types}; +use traits::OnNewBidResult; + +use super::*; + +impl_outer_origin! { + pub enum Origin for Runtime {} +} + +mod auction { + pub use crate::Event; +} + +impl_outer_event! { + pub enum TestEvent for Runtime { + auction<T>, + } +} + +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Runtime; +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 type AccountId = u64; +pub type Balance = u64; +pub type BlockNumber = u64; +pub type AuctionId = u64; + +impl system::Trait for Runtime { + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Call = (); + type Hash = H256; + type Hashing = ::sr_primitives::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup<Self::AccountId>; + type Header = Header; + type Event = TestEvent; + type BlockHashCount = BlockHashCount; + type MaximumBlockWeight = MaximumBlockWeight; + type MaximumBlockLength = MaximumBlockLength; + type AvailableBlockRatio = AvailableBlockRatio; + type Version = (); +} + +pub type System = system::Module<Runtime>; + +pub struct Handler; + +impl AuctionHandler<AccountId, Balance, BlockNumber, AuctionId> for Handler { + fn on_new_bid( + _now: BlockNumber, + _id: AuctionId, + _new_bid: (AccountId, Balance), + _last_bid: Option<(AccountId, Balance)>, + ) -> OnNewBidResult<BlockNumber> { + OnNewBidResult { + accept_bid: true, + auction_end: None, + } + } + + fn on_auction_ended(_id: AuctionId, _winner: Option<(AccountId, Balance)>) {} +} + +impl Trait for Runtime { + type Event = TestEvent; + type Balance = Balance; + type AuctionId = AccountId; + type Handler = Handler; +} +pub type AuctionModule = Module<Runtime>; + +pub const ALICE: AccountId = 1; + +pub struct ExtBuilder; + +impl Default for ExtBuilder { + fn default() -> Self { + ExtBuilder + } +} + +impl ExtBuilder { + pub fn build(self) -> runtime_io::TestExternalities { + let t = system::GenesisConfig::default().build_storage::<Runtime>().unwrap(); + + t.into() + } +} diff --git a/auction/src/tests.rs b/auction/src/tests.rs new file mode 100644 index 0000000..c44f97a --- /dev/null +++ b/auction/src/tests.rs @@ -0,0 +1,57 @@ +//! Unit tests for the tokens module. + +#![cfg(test)] + +use super::*; +use mock::{AuctionModule, ExtBuilder, ALICE}; +use srml_support::assert_ok; + +#[test] +fn new_auction_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(AuctionModule::new_auction(10, Some(100)), 0); + }); +} + +#[test] +fn update_auction_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(AuctionModule::new_auction(10, Some(100)), 0); + assert_ok!(AuctionModule::update_auction( + 0, + AuctionInfo { + bid: Some((ALICE, 100)), + end: Some(100) + } + )); + }); +} + +#[test] +fn auction_info_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(AuctionModule::new_auction(10, Some(100)), 0); + assert_eq!( + AuctionModule::auction_info(0), + Some(AuctionInfo { + bid: None, + end: Some(100) + }) + ); + }); +} + +#[test] +fn bid_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_eq!(AuctionModule::new_auction(10, Some(100)), 0); + assert_ok!(AuctionModule::bid(Some(ALICE).into(), 0, 20)); + assert_eq!( + AuctionModule::auction_info(0), + Some(AuctionInfo { + bid: Some((ALICE, 20)), + end: Some(100) + }) + ); + }); +} diff --git a/traits/src/auction.rs b/traits/src/auction.rs index 0bbac34..5ceb4f1 100644 --- a/traits/src/auction.rs +++ b/traits/src/auction.rs @@ -1,26 +1,50 @@ use codec::FullCodec; -use rstd::fmt::Debug; -use sr_primitives::traits::MaybeSerializeDeserialize; +use codec::{Decode, Encode}; +use rstd::{fmt::Debug, result}; +use sr_primitives::{ + traits::{MaybeSerializeDeserialize, SimpleArithmetic}, + RuntimeDebug, +}; +/// Auction info. +#[cfg_attr(feature = "std", derive(PartialEq, Eq))] +#[derive(Encode, Decode, RuntimeDebug)] pub struct AuctionInfo<AccountId, Balance, BlockNumber> { + /// Current bidder and bid price. pub bid: Option<(AccountId, Balance)>, + /// Define which block this auction will be ended. pub end: Option<BlockNumber>, } -pub trait Auction<AccountId, Balance, BlockNumber> { - type AuctionId: FullCodec + Copy + MaybeSerializeDeserialize + Debug; +/// Abstraction over a simple auction system. +pub trait Auction<AccountId, BlockNumber> { + /// The id of an AuctionInfo + type AuctionId: FullCodec + Default + Copy + MaybeSerializeDeserialize + Debug; + /// The price to bid. + type Balance: SimpleArithmetic + FullCodec + Copy + MaybeSerializeDeserialize + Debug + Default; + /// The error type. + type Error: Into<&'static str>; - fn auction_info(id: Self::AuctionId) -> Option<AuctionInfo<AccountId, Balance, BlockNumber>>; - fn update_auction(id: Self::AuctionId, info: AuctionInfo<AccountId, Balance, BlockNumber>); + /// The auction info of `id` + fn auction_info(id: Self::AuctionId) -> Option<AuctionInfo<AccountId, Self::Balance, BlockNumber>>; + /// Update the auction info of `id` with `info` + fn update_auction( + id: Self::AuctionId, + info: AuctionInfo<AccountId, Self::Balance, BlockNumber>, + ) -> result::Result<(), Self::Error>; + /// Create new auction with specific startblock and endblock, return the id of the auction fn new_auction(start: BlockNumber, end: Option<BlockNumber>) -> Self::AuctionId; } +/// The result of bid handling. pub struct OnNewBidResult<BlockNumber> { + /// Indicates if the bid was accepted pub accept_bid: bool, /// `None` means don't change, `Some(None)` means no more auction end time, `Some(Some(number))` means set auction end time to this block pub auction_end: Option<Option<BlockNumber>>, } +/// Hooks for auction to handle bids. pub trait AuctionHandler<AccountId, Balance, BlockNumber, AuctionId> { /// Called when new bid is received. /// The return value deteermine if the bid should be accepted and update auction end time. @@ -31,5 +55,6 @@ pub trait AuctionHandler<AccountId, Balance, BlockNumber, AuctionId> { new_bid: (AccountId, Balance), last_bid: Option<(AccountId, Balance)>, ) -> OnNewBidResult<BlockNumber>; + /// End an auction with `winner` fn on_auction_ended(id: AuctionId, winner: Option<(AccountId, Balance)>); } -- GitLab