From bac8dc3a3803d2c0efa0ccadfab972401143b0d5 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Thu, 1 Aug 2024 11:51:52 -0300 Subject: [PATCH 01/14] Dmarket pallet for mythical review and testing --- Cargo.toml | 1 + pallets/dmarket/Cargo.toml | 59 +++ pallets/dmarket/README.md | 3 + pallets/dmarket/src/benchmarking.rs | 179 +++++++++ pallets/dmarket/src/lib.rs | 311 ++++++++++++++++ pallets/dmarket/src/mock.rs | 144 ++++++++ pallets/dmarket/src/tests.rs | 547 ++++++++++++++++++++++++++++ pallets/dmarket/src/types.rs | 84 +++++ pallets/dmarket/src/weights.rs | 202 ++++++++++ 9 files changed, 1530 insertions(+) create mode 100644 pallets/dmarket/Cargo.toml create mode 100644 pallets/dmarket/README.md create mode 100644 pallets/dmarket/src/benchmarking.rs create mode 100644 pallets/dmarket/src/lib.rs create mode 100644 pallets/dmarket/src/mock.rs create mode 100644 pallets/dmarket/src/tests.rs create mode 100644 pallets/dmarket/src/types.rs create mode 100644 pallets/dmarket/src/weights.rs diff --git a/Cargo.toml b/Cargo.toml index 86c39a7b..e16c3c75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ enumflags2 = { version = "0.7.7" } # Local dependencies testnet-runtime = { path = "runtime/testnet" } mainnet-runtime = { path = "runtime/mainnet" } +pallet-dmarket = { path = "pallets/dmarket", default-features = false } pallet-marketplace = { path = "pallets/marketplace", default-features = false } pallet-multibatching = { path = "pallets/multibatching", default-features = false } runtime-common = { path = "runtime/common", default-features = false } diff --git a/pallets/dmarket/Cargo.toml b/pallets/dmarket/Cargo.toml new file mode 100644 index 00000000..c34facd9 --- /dev/null +++ b/pallets/dmarket/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "pallet-dmarket" +version = "0.0.1" +description = "DMarket FRAME pallet to provide Nft trading" +authors = { workspace = true } +homepage = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +parity-scale-codec = { workspace = true, default-features = false, features = [ + "derive", +] } +log = { workspace = true, default-features = false } +scale-info = { workspace = true, default-features = false, features = [ + "derive", +] } + +# Primitives +account = { workspace = true } + +frame-benchmarking = { workspace = true, default-features = false, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-std = { workspace = true } +sp-api = { workspace = true, default-features = false } +sp-io = { workspace = true } +sp-runtime = { workspace = true, default-features = false } +pallet-nfts = { workspace = true, default-features = false } +sp-core = { workspace = true, default-features = false } +sp-keystore = { workspace = true, default-features = false } +pallet-balances = { workspace = true, default-features = false } +pallet-timestamp = { workspace = true, default-features = false } + +[dev-dependencies] +sp-io = { workspace = true } + +[features] +default = ["std"] +std = [ + "account/std", + "parity-scale-codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-io/std", + "sp-api/std", + "sp-std/std", + "pallet-balances/std", + "pallet-nfts/std", + "pallet-timestamp/std", +] +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/dmarket/README.md b/pallets/dmarket/README.md new file mode 100644 index 00000000..368e02d6 --- /dev/null +++ b/pallets/dmarket/README.md @@ -0,0 +1,3 @@ +# Dmarket Pallet + +The Dmarket pallet provides a market to buy and sell Nfts from pallet-nft, based on the dmarket Smart contracts from Mythical. diff --git a/pallets/dmarket/src/benchmarking.rs b/pallets/dmarket/src/benchmarking.rs new file mode 100644 index 00000000..df65bde4 --- /dev/null +++ b/pallets/dmarket/src/benchmarking.rs @@ -0,0 +1,179 @@ +#![cfg(feature = "runtime-benchmarks")] +use super::*; +use crate::Pallet as Dmarket; +use frame_benchmarking::v2::*; +use frame_support::{ + assert_ok, + dispatch::RawOrigin, + traits::{ + fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, + tokens::nonfungibles_v2::{Create, Mutate}, + }, +}; +use pallet_nfts::ItemId; +use pallet_nfts::{CollectionConfig, CollectionSettings, ItemConfig, MintSettings, Pallet as Nfts}; +use sp_core::ecdsa::Public; +use sp_io::{ + crypto::{ecdsa_generate, ecdsa_sign_prehashed}, + hashing::keccak_256, +}; + +use crate::BenchmarkHelper; + +const SEED: u32 = 0; + +type BalanceOf = + <::Currency as InspectFungible<::AccountId>>::Balance; + +impl BenchmarkHelper for () +where + CollectionId: From, + ItemId: From, + Moment: From, +{ + fn collection(id: u16) -> CollectionId { + id.into() + } + fn timestamp(value: u64) -> Moment { + value.into() + } +} + +fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn funded_and_whitelisted_account(name: &'static str, index: u32) -> T::AccountId { + let caller: T::AccountId = account(name, index, SEED); + // Give the account half of the maximum value of the `Balance` type. + // Otherwise some transfers will fail with an overflow error. + let ed = ::Currency::minimum_balance(); + let multiplier = BalanceOf::::from(1000000u32); + + ::Currency::set_balance(&caller, ed * multiplier); + whitelist_account!(caller); + caller +} + +fn mint_nft(nft_id: ItemId, caller: T::AccountId) { + let default_config = CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: Some(u128::MAX), + mint_settings: MintSettings::default(), + }; + + assert_ok!(Nfts::::create_collection(&caller, &caller, &default_config)); + let collection = T::BenchmarkHelper::collection(0); + assert_ok!(Nfts::::mint_into(&collection, &nft_id, &caller, &ItemConfig::default(), true)); +} + +#[benchmarks(where T::AccountId: From, T::Signature: From)] +pub mod benchmarks { + use super::*; + use account::{AccountId20, EthereumSignature, EthereumSigner}; + use pallet_timestamp::Pallet as Timestamp; + + use sp_runtime::traits::IdentifyAccount; + + fn sign_trade( + sender: &T::AccountId, + fee_address: &T::AccountId, + trade: &TradeParamsOf, + seller_signer: Public, + buyer_signer: Public, + ) -> TradeSignatures + where + T::Signature: From, + { + let ask_message: Vec = Dmarket::::get_ask_message(sender, fee_address, trade); + let ask_hashed = keccak_256(&ask_message); + + let bid_message: Vec = Dmarket::::get_bid_message(sender, fee_address, trade); + let bid_hashed = keccak_256(&bid_message); + + TradeSignatures { + ask_signature: EthereumSignature::from( + ecdsa_sign_prehashed(0.into(), &seller_signer, &ask_hashed).unwrap(), + ) + .into(), + bid_signature: EthereumSignature::from( + ecdsa_sign_prehashed(1.into(), &buyer_signer, &bid_hashed).unwrap(), + ) + .into(), + } + } + + fn trade_participants() -> (T::AccountId, Public, T::AccountId, Public) + where + T::AccountId: From, + { + let ed = ::Currency::minimum_balance(); + let multiplier = BalanceOf::::from(10000u16); + + let seller_public = ecdsa_generate(0.into(), None); + let seller_signer: EthereumSigner = seller_public.into(); + let seller: T::AccountId = seller_signer.clone().into_account().into(); + whitelist_account!(seller); + + let buyer_public = ecdsa_generate(1.into(), None); + let buyer_signer: EthereumSigner = buyer_public.into(); + let buyer: T::AccountId = buyer_signer.clone().into_account().into(); + whitelist_account!(buyer); + + ::Currency::set_balance(&seller, ed * multiplier); + ::Currency::set_balance(&buyer, ed * multiplier); + + (seller, seller_public, buyer, buyer_public) + } + + #[benchmark] + fn force_set_collection() { + let collection_id = T::BenchmarkHelper::collection(0); + let caller: T::AccountId = funded_and_whitelisted_account::("caller", 0); + let _ = mint_nft::(1, caller); + + #[extrinsic_call] + _(RawOrigin::Root, collection_id); + + assert_last_event::(Event::CollectionUpdated { collection_id }.into()); + } + + #[benchmark] + fn execute_trade() { + let sender: T::AccountId = funded_and_whitelisted_account::("sender", 0); + let (seller, seller_public, buyer, buyer_public) = trade_participants::(); + let fee_address: T::AccountId = funded_and_whitelisted_account::("fee_address", 0); + + let collection_id = T::BenchmarkHelper::collection(0); + let item = 1; + mint_nft::(item, seller.clone()); + assert_ok!(Dmarket::::force_set_collection(RawOrigin::Root.into(), collection_id)); + + let expiration = Timestamp::::get() + T::BenchmarkHelper::timestamp(1000); + let trade = TradeParams { + price: BalanceOf::::from(100u16), + fee: BalanceOf::::from(1u8), + ask_expiration: expiration, + bid_expiration: expiration, + item, + }; + let signatures = + sign_trade::(&sender, &fee_address, &trade, seller_public, buyer_public); + + #[extrinsic_call] + _( + RawOrigin::Signed(sender), + seller.clone(), + buyer.clone(), + trade.clone(), + signatures, + fee_address, + ); + + assert_last_event::( + Event::Trade { seller, buyer, item, price: trade.price, fee: trade.fee }.into(), + ); + } + + impl_benchmark_test_suite!(Dmarket, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/dmarket/src/lib.rs b/pallets/dmarket/src/lib.rs new file mode 100644 index 00000000..baf7c9a8 --- /dev/null +++ b/pallets/dmarket/src/lib.rs @@ -0,0 +1,311 @@ +//! # Marketplace Module +//! +//! A module that facilitates trading of non-fungible items (NFTs) through the creation and management of orders. +//! +//! ## Related Modules +//! - NFTs: Provides functionalities for managing non-fungible tokens. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +mod types; +use parity_scale_codec::Codec; +pub use types::*; + +pub mod weights; +pub use weights::*; + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use core::usize; + + use crate::Item; + + use super::*; + use frame_support::{ + ensure, + pallet_prelude::*, + traits::{ + fungible::{Inspect, Mutate}, + nonfungibles_v2::{Inspect as NftInspect, Transfer}, + tokens::Preservation::Preserve, + }, + }; + use frame_system::{ensure_signed, pallet_prelude::*}; + + use frame_support::{dispatch::GetDispatchInfo, traits::UnfilteredDispatchable}; + + use sp_runtime::traits::Hash; + use sp_runtime::{ + traits::{IdentifyAccount, Verify}, + DispatchError, + }; + use sp_std::vec::Vec; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_nfts::Config + pallet_timestamp::Config + { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type RuntimeCall: Parameter + + UnfilteredDispatchable + + GetDispatchInfo; + + /// The currency trait. + type Currency: Inspect + Mutate; + + /// Off-Chain signature type. + /// + /// Can verify whether a `Self::Signer` created a signature. + type Signature: Verify + Parameter; + + /// Off-Chain public key. + /// + /// Must identify as an on-chain `Self::AccountId`. + type Signer: IdentifyAccount; + + ///Chain Domain + #[pallet::constant] + type Domain: Get; + + #[cfg(feature = "runtime-benchmarks")] + /// A set of helper functions for benchmarking. + type BenchmarkHelper: BenchmarkHelper; + } + + #[pallet::storage] + pub type ClosedAsks = StorageMap<_, Blake2_128Concat, T::Hash, OrderDataOf>; + + #[pallet::storage] + pub type ClosedBids = StorageMap<_, Blake2_128Concat, T::Hash, OrderDataOf>; + + #[pallet::storage] + pub type DmarketCollection = StorageValue<_, T::CollectionId, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// The fee signer account was updated. + CollectionUpdated { collection_id: T::CollectionId }, + /// + Trade { + buyer: T::AccountId, + seller: T::AccountId, + item: Item, + price: BalanceOf, + fee: BalanceOf, + }, + } + + #[pallet::error] + pub enum Error { + /// The item was not found. + ItemNotFound, + /// Item can only be operated by the Item owner. + SellerNotItemOwner, + /// + BidAlreadyExecuted, + /// + AskAlreadyExecuted, + /// + BuyerBalanceTooLow, + /// + BidExpired, + /// + AskExpired, + /// + InvalidBuyerSignature, + /// + InvalidSellerSignature, + /// Same buyer and seller not allowed. + BuyerIsSeller, + /// + BadSignedMessage, + /// + CollectionAlreadyInUse, + /// + CollectionNotSet, + /// + CollectionNotFound, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight({0})] + pub fn force_set_collection( + origin: OriginFor, + collection_id: T::CollectionId, + ) -> DispatchResult { + ensure_root(origin)?; + + as NftInspect>::collection_owner(&collection_id) + .ok_or(Error::::CollectionNotFound)?; + + ensure!( + DmarketCollection::::get().as_ref() != Some(&collection_id), + Error::::CollectionAlreadyInUse + ); + + DmarketCollection::::put(collection_id.clone()); + Self::deposit_event(Event::CollectionUpdated { collection_id }); + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight({0})] + pub fn execute_trade( + origin: OriginFor, + seller: T::AccountId, + buyer: T::AccountId, + trade: TradeParamsOf, + signatures: TradeSignatures<::Signature>, + fee_address: T::AccountId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!(seller != buyer, Error::::BuyerIsSeller); + + let timestamp = pallet_timestamp::Pallet::::get(); + ensure!(trade.ask_expiration > timestamp, Error::::AskExpired); + ensure!(trade.bid_expiration > timestamp, Error::::BidExpired); + + // Pay fees to FeeAddress + let collection = DmarketCollection::::get().ok_or(Error::::CollectionNotSet)?; + + let item_owner = pallet_nfts::Pallet::::owner(collection, trade.item) + .ok_or(Error::::ItemNotFound)?; + ensure!(seller == item_owner, Error::::SellerNotItemOwner); + + let (ask_hash, bid_hash) = Self::hash_ask_bid_data(&trade); + ensure!(!ClosedAsks::::contains_key(ask_hash), Error::::AskAlreadyExecuted); + ensure!(!ClosedBids::::contains_key(bid_hash), Error::::BidAlreadyExecuted); + + Self::verify_signature( + &seller, + &Self::get_ask_message(&who, &fee_address, &trade), + signatures.ask_signature, + ) + .map_err(|_| Error::::InvalidSellerSignature)?; + + Self::verify_signature( + &buyer, + &Self::get_bid_message(&who, &fee_address, &trade), + signatures.bid_signature, + ) + .map_err(|_| Error::::InvalidBuyerSignature)?; + + as Transfer>::transfer( + &collection, + &trade.item, + &buyer, + )?; + ::Currency::transfer(&buyer, &seller, trade.price, Preserve) + .map_err(|_| Error::::BuyerBalanceTooLow)?; + ::Currency::transfer(&seller, &fee_address, trade.fee, Preserve)?; + + let order_data: OrderDataOf = OrderData { caller: who, fee_address }; + + //Store closed trades + ClosedAsks::::insert(ask_hash, order_data.clone()); + ClosedBids::::insert(bid_hash, order_data); + + Self::deposit_event(Event::Trade { + seller, + buyer, + item: trade.item, + price: trade.price, + fee: trade.fee, + }); + + Ok(()) + } + } + + impl Pallet { + fn verify_signature( + who: &T::AccountId, + message: &Vec, + signature: T::Signature, + ) -> Result<(), DispatchError> { + if !signature.verify(message.as_ref(), &who) { + return Err(Error::::BadSignedMessage.into()); + } + + Ok(()) + } + + pub fn hash_ask_bid_data(trade: &TradeParamsOf) -> (T::Hash, T::Hash) { + let ask_hash = + T::Hashing::hash(&(trade.item, trade.price, trade.ask_expiration).encode()); + let bid_hash = T::Hashing::hash( + &(trade.item, trade.price, trade.fee, trade.bid_expiration).encode(), + ); + + (ask_hash, bid_hash) + } + + pub fn get_ask_message( + caller: &T::AccountId, + fee_address: &T::AccountId, + trade: &TradeParamsOf, + ) -> Vec { + AskMessage { + domain: T::Domain::get(), + sender: caller.clone(), + fee_address: fee_address.clone(), + item: trade.item, + price: trade.price, + expiration: trade.ask_expiration, + } + .encode() + } + + pub fn get_bid_message( + caller: &T::AccountId, + fee_address: &T::AccountId, + trade: &TradeParamsOf, + ) -> Vec { + BidMessage { + domain: T::Domain::get(), + sender: caller.clone(), + fee_address: fee_address.clone(), + item: trade.item, + price: trade.price, + fee: trade.fee, + expiration: trade.bid_expiration, + } + .encode() + } + } +} + +sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); + +use sp_std::vec::Vec; +sp_api::decl_runtime_apis! { + pub trait DmarketApi + where + AccountId: Codec, + Balance: Codec, + Moment: Codec, + Hash: Codec, + { + fn hash_ask_bid_data(trade: TradeParams)-> (Hash, Hash); + fn get_ask_message(caller: AccountId, fee_address: AccountId, trade: TradeParams) -> Vec; + fn get_bid_message(caller: AccountId, fee_address: AccountId, trade: TradeParams) -> Vec; + } +} diff --git a/pallets/dmarket/src/mock.rs b/pallets/dmarket/src/mock.rs new file mode 100644 index 00000000..dd6457f1 --- /dev/null +++ b/pallets/dmarket/src/mock.rs @@ -0,0 +1,144 @@ +use frame_support::{ + derive_impl, parameter_types, + traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64}, +}; +use frame_system as system; +use sp_core::H256; +use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, +}; + +use account::EthereumSignature; + +use crate::{self as pallet_dmarket}; +use pallet_nfts::PalletFeatures; + +type Signature = EthereumSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Dmarket: pallet_dmarket, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + Nfts: pallet_nfts, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub storage Features: PalletFeatures = PalletFeatures::all_enabled(); +} + +impl pallet_nfts::Config for Test { + type RuntimeEvent = RuntimeEvent; + type CollectionId = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type Locker = (); + type CollectionDeposit = ConstU128<0>; + type ItemDeposit = ConstU128<0>; + type MetadataDepositBase = ConstU128<1>; + type AttributeDepositBase = ConstU128<1>; + type DepositPerByte = ConstU128<1>; + type StringLimit = ConstU32<50>; + type KeyLimit = ConstU32<50>; + type ValueLimit = ConstU32<50>; + type ApprovalsLimit = ConstU32<10>; + type ItemAttributesApprovalsLimit = ConstU32<2>; + type MaxTips = ConstU32<10>; + type MaxDeadlineDuration = ConstU64<10000>; + type MaxAttributesPerCall = ConstU32<2>; + type Features = Features; + type OffchainSignature = Signature; + type OffchainPublic = ::Signer; + type WeightInfo = (); + pallet_nfts::runtime_benchmarks_enabled! { + type Helper = (); + } +} + +impl pallet_balances::Config for Test { + type MaxLocks = (); + type MaxReserves = ConstU32<2>; + type ReserveIdentifier = [u8; 8]; + type Balance = u128; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<3>; + type WeightInfo = (); +} + +parameter_types! { + pub const DOMAIN: [u8;8] = *b"MYTH_NET"; +} + +impl crate::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type Signature = Signature; + type Signer = ::Signer; + type Domain = DOMAIN; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = system::GenesisConfig::::default().build_storage().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.register_extension(KeystoreExt::new(MemoryKeystore::new())); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/pallets/dmarket/src/tests.rs b/pallets/dmarket/src/tests.rs new file mode 100644 index 00000000..5e7c2bbb --- /dev/null +++ b/pallets/dmarket/src/tests.rs @@ -0,0 +1,547 @@ +use account::{AccountId20, EthereumSignature, EthereumSigner}; + +use frame_support::{ + assert_noop, assert_ok, + error::BadOrigin, + traits::fungible::{Inspect as InspectFungible, Mutate}, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_nfts::{CollectionConfig, CollectionSettings, MintSettings, NextCollectionId}; +use sp_core::{ + ecdsa::{Pair as KeyPair, Signature}, + keccak_256, Pair, +}; + +use sp_runtime::traits::IdentifyAccount; + +use self::mock::Timestamp; +use crate::{mock::*, *}; + +type AccountIdOf = ::AccountId; +type CollectionId = ::CollectionId; +type Balance = ::Balance; + +fn account(id: u8) -> AccountIdOf { + [id; 20].into() +} + +fn create_collection(creator: &AccountIdOf) -> CollectionId { + let collection_id = NextCollectionId::::get().unwrap_or_default(); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + *creator, + collection_config_with_all_settings_enabled() + )); + + collection_id +} + +fn collection_config_with_all_settings_enabled( +) -> CollectionConfig, BlockNumberFor, CollectionId> { + CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: Some(1000000), + mint_settings: MintSettings::default(), + } +} + +mod force_set_collection { + use super::*; + + #[test] + fn force_set_collection_works() { + new_test_ext().execute_with(|| { + let collection_id = create_collection(&account(0)); + + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), collection_id)); + assert!(DmarketCollection::::get() == Some(collection_id)); + + assert_noop!( + Dmarket::force_set_collection(RuntimeOrigin::root(), collection_id), + Error::::CollectionAlreadyInUse + ); + + let other_collection = create_collection(&account(0)); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), other_collection)); + assert!(DmarketCollection::::get() == Some(other_collection)); + }) + } + + #[test] + fn fails_no_root() { + new_test_ext().execute_with(|| { + let collection_id = create_collection(&account(0)); + + assert_noop!( + Dmarket::force_set_collection(RuntimeOrigin::signed(account(1)), collection_id), + BadOrigin + ); + }) + } + + #[test] + fn collection_not_found() { + new_test_ext().execute_with(|| { + assert_noop!( + Dmarket::force_set_collection(RuntimeOrigin::root(), 0), + Error::::CollectionNotFound + ); + }) + } +} + +mod execute_trade { + use super::*; + + fn get_trade_accounts() -> (AccountIdOf, AccountIdOf, KeyPair, KeyPair) { + let sender = account(0); + let fee_address = account(1); + + let seller_pair = Pair::from_string("//Seller", None).unwrap(); + let buyer_pair = Pair::from_string("//Buyer", None).unwrap(); + (sender, fee_address, seller_pair, buyer_pair) + } + + fn setup_nft( + sender: &AccountIdOf, + item_owner: &AccountIdOf, + item: u128, + ) -> CollectionId { + let collection_id = create_collection(&sender); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), collection_id)); + + assert_ok!(Nfts::mint(RuntimeOrigin::signed(*sender), 0, Some(item), *item_owner, None)); + collection_id + } + + fn sign_trade( + caller: &AccountIdOf, + fee_address: &AccountIdOf, + trade: &TradeParamsOf, + seller_pair: KeyPair, + buyer_pair: KeyPair, + ) -> TradeSignaturesOf { + let ask_message: Vec = Dmarket::get_ask_message(caller, fee_address, trade); + let hashed_ask = keccak_256(&ask_message); + + let bid_message: Vec = Dmarket::get_bid_message(caller, fee_address, trade); + let hashed_bid = keccak_256(&bid_message); + + TradeSignatures { + ask_signature: EthereumSignature::from(seller_pair.sign_prehashed(&hashed_ask)), + bid_signature: EthereumSignature::from(buyer_pair.sign_prehashed(&hashed_bid)), + } + } + + #[test] + fn execute_trade_works() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let collection = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let buyer_balance = Balances::balance(&buyer); + let fee_address_balance = Balances::balance(&fee_address); + let seller_balance = Balances::balance(&seller); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_ok!(Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + )); + + assert_eq!(Nfts::owner(collection, item).unwrap(), buyer); + assert_eq!(Balances::balance(&buyer), buyer_balance - price); + assert_eq!(Balances::balance(&seller), seller_balance + price - fee); + assert_eq!(Balances::balance(&fee_address), fee_address_balance + fee); + + let (ask_hash, bid_hash) = Dmarket::hash_ask_bid_data(&trade); + assert!(ClosedAsks::::contains_key(ask_hash)); + assert!(ClosedBids::::contains_key(bid_hash)); + }) + } + + #[test] + fn buyer_is_seller() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = seller.clone(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::BuyerIsSeller + ); + }) + } + + #[test] + fn ask_or_bid_expired() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let mut trade = + TradeParams { price, fee, item, ask_expiration: 0, bid_expiration: expiration }; + let signatures = + sign_trade(&sender, &fee_address, &trade, seller_pair.clone(), buyer_pair.clone()); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::AskExpired + ); + + trade.ask_expiration = expiration; + trade.bid_expiration = 0; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(seller), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::BidExpired + ); + }) + } + + #[test] + fn collection_not_set() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::CollectionNotSet + ); + }) + } + + #[test] + fn item_not_found() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item: item + 1, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::ItemNotFound + ); + }) + } + + #[test] + fn already_executed() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let collection = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_ok!(Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures.clone(), + fee_address + )); + + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(buyer), collection, item, seller)); + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures.clone(), + fee_address + ), + Error::::AskAlreadyExecuted + ); + + let (ask_hash, _) = Dmarket::hash_ask_bid_data(&trade); + ClosedAsks::::remove(ask_hash); + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::BidAlreadyExecuted + ); + }) + } + + #[test] + fn invalid_signatures() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let mut signatures = + sign_trade(&sender, &fee_address, &trade, seller_pair.clone(), buyer_pair.clone()); + signatures.ask_signature = EthereumSignature::from(Signature::from_raw([0; 65])); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::InvalidSellerSignature + ); + + let mut signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + signatures.bid_signature = EthereumSignature::from(Signature::from_raw([0; 65])); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::InvalidBuyerSignature + ); + }) + } + + #[test] + fn seller_not_owner() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &account(1), item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::SellerNotItemOwner + ); + }) + } + + #[test] + fn buyer_not_enough_funds() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price - 10); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::BuyerBalanceTooLow + ); + }) + } +} diff --git a/pallets/dmarket/src/types.rs b/pallets/dmarket/src/types.rs new file mode 100644 index 00000000..e1f3ccb7 --- /dev/null +++ b/pallets/dmarket/src/types.rs @@ -0,0 +1,84 @@ +use crate::Config; +use frame_support::traits::fungible::Inspect; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +pub type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; + +pub type Item = u128; +pub type Domain = [u8; 8]; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct TradeParams { + pub price: Amount, + pub fee: Amount, + pub item: ItemId, + pub ask_expiration: Expiration, + pub bid_expiration: Expiration, +} + +pub type TradeParamsOf = TradeParams< + <::Currency as Inspect<::AccountId>>::Balance, + Item, + ::Moment, +>; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct TradeSignatures { + pub ask_signature: OffchainSignature, + pub bid_signature: OffchainSignature, +} + +pub type TradeSignaturesOf = TradeSignatures<::OffchainSignature>; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct AskMessage { + pub domain: Domain, + pub sender: Account, + pub fee_address: Account, + pub item: ItemId, + pub price: Amount, + pub expiration: Expiration, +} + +pub type AskMessageOf = AskMessage< + ::AccountId, + <::Currency as Inspect<::AccountId>>::Balance, + Item, + ::Moment, +>; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo, MaxEncodedLen)] +pub struct OrderData { + pub caller: Account, + pub fee_address: Account, +} + +pub type OrderDataOf = OrderData<::AccountId>; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct BidMessage { + pub domain: Domain, + pub sender: Account, + pub fee_address: Account, + pub item: ItemId, + pub price: Amount, + pub fee: Amount, + pub expiration: Expiration, +} + +pub type BidMessageOf = BidMessage< + ::AccountId, + <::Currency as Inspect<::AccountId>>::Balance, + Item, + ::Moment, +>; + +#[cfg(feature = "runtime-benchmarks")] +pub trait BenchmarkHelper { + /// Returns a collection id from a given integer. + fn collection(id: u16) -> CollectionId; + /// Returns an nft id from a given integer. + fn timestamp(value: u64) -> Moment; +} diff --git a/pallets/dmarket/src/weights.rs b/pallets/dmarket/src/weights.rs new file mode 100644 index 00000000..442e2064 --- /dev/null +++ b/pallets/dmarket/src/weights.rs @@ -0,0 +1,202 @@ + +//! Autogenerated weights for `pallet_dmarket` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-06-05, STEPS: `10`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `pop-os`, CPU: `AMD Ryzen 9 7900X 12-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/mythos-node +// benchmark +// pallet +// --steps +// 10 +// --template +// ./.maintain/template.hbs +// --repeat +// 20 +// --extrinsic +// * +// --wasm-execution +// compiled +// --heap-pages +// 4096 +// --pallet +// pallet_dmarket +// --output +// ./pallets/dmarket/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_dmarket`. +pub trait WeightInfo { + fn force_set_balance_manager() -> Weight; + fn force_set_collection() -> Weight; + fn set_fee_address() -> Weight; + fn execute_trade() -> Weight; +} + +/// Weights for `pallet_dmarket` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); + impl WeightInfo for SubstrateWeight { + /// Storage: `Dmarket::BalanceManager` (r:1 w:1) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn force_set_balance_manager() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `1505` + // Minimum execution time: 4_650_000 picoseconds. + Weight::from_parts(4_920_000, 1505) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:1) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + fn force_set_collection() -> Weight { + // Proof Size summary in bytes: + // Measured: `420` + // Estimated: `3610` + // Minimum execution time: 10_150_000 picoseconds. + Weight::from_parts(10_390_000, 3610) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::FeeAddress` (r:1 w:1) + /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn set_fee_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `120` + // Estimated: `1505` + // Minimum execution time: 6_770_000 picoseconds. + Weight::from_parts(7_020_000, 1505) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::FeeAddress` (r:1 w:0) + /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:0) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(637), added: 3112, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedAsks` (r:1 w:1) + /// Proof: `Dmarket::ClosedAsks` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedBids` (r:1 w:1) + /// Proof: `Dmarket::ClosedBids` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(495), added: 2970, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(117), added: 2592, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(151), added: 2626, mode: `MaxEncodedLen`) + fn execute_trade() -> Weight { + // Proof Size summary in bytes: + // Measured: `1228` + // Estimated: `4102` + // Minimum execution time: 172_460_000 picoseconds. + Weight::from_parts(174_840_000, 4102) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Dmarket::BalanceManager` (r:1 w:1) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn force_set_balance_manager() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `1505` + // Minimum execution time: 4_650_000 picoseconds. + Weight::from_parts(4_920_000, 1505) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:1) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + fn force_set_collection() -> Weight { + // Proof Size summary in bytes: + // Measured: `420` + // Estimated: `3610` + // Minimum execution time: 10_150_000 picoseconds. + Weight::from_parts(10_390_000, 3610) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::FeeAddress` (r:1 w:1) + /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn set_fee_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `120` + // Estimated: `1505` + // Minimum execution time: 6_770_000 picoseconds. + Weight::from_parts(7_020_000, 1505) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::FeeAddress` (r:1 w:0) + /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:0) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(637), added: 3112, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedAsks` (r:1 w:1) + /// Proof: `Dmarket::ClosedAsks` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedBids` (r:1 w:1) + /// Proof: `Dmarket::ClosedBids` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(495), added: 2970, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(117), added: 2592, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(151), added: 2626, mode: `MaxEncodedLen`) + fn execute_trade() -> Weight { + // Proof Size summary in bytes: + // Measured: `1228` + // Estimated: `4102` + // Minimum execution time: 172_460_000 picoseconds. + Weight::from_parts(174_840_000, 4102) + .saturating_add(RocksDbWeight::get().reads(11_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } +} From c5f67c3a223451c56048463406198b67306b4d9f Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Fri, 2 Aug 2024 09:39:53 -0300 Subject: [PATCH 02/14] Add dmarket to testnet --- Cargo.lock | 23 +++++++++++++++++++++++ runtime/testnet/Cargo.toml | 4 ++++ runtime/testnet/src/lib.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 07a78df0..9145cd42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6758,6 +6758,28 @@ dependencies = [ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?tag=polkadot-v1.13.0)", ] +[[package]] +name = "pallet-dmarket" +version = "0.0.1" +dependencies = [ + "account", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-nfts", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-core", + "sp-io", + "sp-keystore", + "sp-runtime", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?tag=polkadot-v1.13.0)", +] + [[package]] name = "pallet-election-provider-multi-phase" version = "27.0.0" @@ -13591,6 +13613,7 @@ dependencies = [ "pallet-balances", "pallet-collator-selection", "pallet-collective", + "pallet-dmarket", "pallet-escrow", "pallet-marketplace", "pallet-message-queue", diff --git a/runtime/testnet/Cargo.toml b/runtime/testnet/Cargo.toml index 0f400315..94b4557c 100644 --- a/runtime/testnet/Cargo.toml +++ b/runtime/testnet/Cargo.toml @@ -23,6 +23,7 @@ smallvec = { workspace = true } # Local runtime-common = { workspace = true, default-features = false } +pallet-dmarket = { workspace = true, default-features = false } pallet-marketplace = { workspace = true, default-features = false } pallet-multibatching = { workspace = true, default-features = false } xcm-primitives = { path = "../../primitives/xcm", default-features = false } @@ -120,6 +121,7 @@ std = [ "pallet-balances/std", "pallet-collator-selection/std", "pallet-collective/std", + "pallet-dmarket/std", "pallet-multibatching/std", "pallet-marketplace/std", "pallet-multisig/std", @@ -170,6 +172,7 @@ runtime-benchmarks = [ "pallet-collator-selection/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", + "pallet-dmarket/runtime-benchmarks", "pallet-marketplace/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-multibatching/runtime-benchmarks", @@ -204,6 +207,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", "pallet-collective/try-runtime", + "pallet-dmarket/try-runtime", "pallet-multibatching/try-runtime", "pallet-marketplace/try-runtime", "pallet-multisig/try-runtime", diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index f4381e03..e2e047d1 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -41,9 +41,11 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, }; +use pallet_dmarket::{Item, TradeParams}; use pallet_nfts::PalletFeatures; use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use polkadot_primitives::Moment; pub use runtime_common::{ AccountId, Balance, BlockNumber, DealWithFees, Hash, IncrementableU256, Nonce, Signature, AVERAGE_ON_INITIALIZE_RATIO, DAYS, HOURS, MAXIMUM_BLOCK_WEIGHT, MINUTES, NORMAL_DISPATCH_RATIO, @@ -654,6 +656,17 @@ impl pallet_marketplace::Config for Runtime { type BenchmarkHelper = (); } +impl pallet_dmarket::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type Signature = Signature; + type Signer = ::Signer; + type Domain = DOMAIN; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + parameter_types! { pub const ProxyDepositBase: Balance = deposit(1, 8); pub const ProxyDepositFactor: Balance = deposit(0, 33); @@ -850,6 +863,7 @@ construct_runtime!( Escrow: pallet_escrow = 50, MythProxy: pallet_myth_proxy = 51, + Dmarket: pallet_dmarket = 52, } ); @@ -926,6 +940,7 @@ mod benches { [pallet_vesting, Vesting] [pallet_collective, Council] [pallet_myth_proxy, MythProxy] + [pallet_dmarket, Dmarket] ); } @@ -1081,6 +1096,18 @@ impl_runtime_apis! { } } + impl pallet_dmarket::DmarketApi for Runtime { + fn hash_ask_bid_data(trade: TradeParams)-> (Hash, Hash) { + Dmarket::hash_ask_bid_data(&trade) + } + fn get_ask_message(caller: AccountId, fee_address: AccountId, trade: TradeParams) -> Vec { + Dmarket::get_ask_message(&caller, &fee_address, &trade) + } + fn get_bid_message(caller: AccountId, fee_address: AccountId, trade: TradeParams) -> Vec { + Dmarket::get_bid_message(&caller, &fee_address, &trade) + } + } + impl cumulus_primitives_aura::AuraUnincludedSegmentApi for Runtime { fn can_build_upon( included_hash: ::Hash, From 9c9f944b96d1dde133e225a10315cc6952bf3776 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Fri, 2 Aug 2024 10:44:32 -0300 Subject: [PATCH 03/14] Add WeighInfo --- pallets/dmarket/src/lib.rs | 7 +++++-- pallets/dmarket/src/mock.rs | 1 + runtime/testnet/src/lib.rs | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pallets/dmarket/src/lib.rs b/pallets/dmarket/src/lib.rs index baf7c9a8..f3b9efaf 100644 --- a/pallets/dmarket/src/lib.rs +++ b/pallets/dmarket/src/lib.rs @@ -81,6 +81,9 @@ pub mod pallet { #[pallet::constant] type Domain: Get; + /// Type representing the weight of this pallet + type WeightInfo: WeightInfo; + #[cfg(feature = "runtime-benchmarks")] /// A set of helper functions for benchmarking. type BenchmarkHelper: BenchmarkHelper; @@ -145,7 +148,7 @@ pub mod pallet { #[pallet::call] impl Pallet { #[pallet::call_index(0)] - #[pallet::weight({0})] + #[pallet::weight(::WeightInfo::force_set_collection())] pub fn force_set_collection( origin: OriginFor, collection_id: T::CollectionId, @@ -166,7 +169,7 @@ pub mod pallet { } #[pallet::call_index(1)] - #[pallet::weight({0})] + #[pallet::weight(::WeightInfo::execute_trade())] pub fn execute_trade( origin: OriginFor, seller: T::AccountId, diff --git a/pallets/dmarket/src/mock.rs b/pallets/dmarket/src/mock.rs index dd6457f1..5b58cd38 100644 --- a/pallets/dmarket/src/mock.rs +++ b/pallets/dmarket/src/mock.rs @@ -130,6 +130,7 @@ impl crate::Config for Test { type Signature = Signature; type Signer = ::Signer; type Domain = DOMAIN; + type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index e2e047d1..d9432e45 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -663,6 +663,7 @@ impl pallet_dmarket::Config for Runtime { type Signature = Signature; type Signer = ::Signer; type Domain = DOMAIN; + type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } From 628c36362b66b00af240a98bf25666f8288b25c8 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Thu, 1 Aug 2024 11:51:52 -0300 Subject: [PATCH 04/14] Dmarket pallet for mythical review and testing --- Cargo.toml | 1 + pallets/dmarket/Cargo.toml | 59 +++ pallets/dmarket/README.md | 3 + pallets/dmarket/src/benchmarking.rs | 179 +++++++++ pallets/dmarket/src/lib.rs | 311 ++++++++++++++++ pallets/dmarket/src/mock.rs | 144 ++++++++ pallets/dmarket/src/tests.rs | 547 ++++++++++++++++++++++++++++ pallets/dmarket/src/types.rs | 84 +++++ pallets/dmarket/src/weights.rs | 202 ++++++++++ 9 files changed, 1530 insertions(+) create mode 100644 pallets/dmarket/Cargo.toml create mode 100644 pallets/dmarket/README.md create mode 100644 pallets/dmarket/src/benchmarking.rs create mode 100644 pallets/dmarket/src/lib.rs create mode 100644 pallets/dmarket/src/mock.rs create mode 100644 pallets/dmarket/src/tests.rs create mode 100644 pallets/dmarket/src/types.rs create mode 100644 pallets/dmarket/src/weights.rs diff --git a/Cargo.toml b/Cargo.toml index 86c39a7b..e16c3c75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ enumflags2 = { version = "0.7.7" } # Local dependencies testnet-runtime = { path = "runtime/testnet" } mainnet-runtime = { path = "runtime/mainnet" } +pallet-dmarket = { path = "pallets/dmarket", default-features = false } pallet-marketplace = { path = "pallets/marketplace", default-features = false } pallet-multibatching = { path = "pallets/multibatching", default-features = false } runtime-common = { path = "runtime/common", default-features = false } diff --git a/pallets/dmarket/Cargo.toml b/pallets/dmarket/Cargo.toml new file mode 100644 index 00000000..c34facd9 --- /dev/null +++ b/pallets/dmarket/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "pallet-dmarket" +version = "0.0.1" +description = "DMarket FRAME pallet to provide Nft trading" +authors = { workspace = true } +homepage = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +parity-scale-codec = { workspace = true, default-features = false, features = [ + "derive", +] } +log = { workspace = true, default-features = false } +scale-info = { workspace = true, default-features = false, features = [ + "derive", +] } + +# Primitives +account = { workspace = true } + +frame-benchmarking = { workspace = true, default-features = false, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-std = { workspace = true } +sp-api = { workspace = true, default-features = false } +sp-io = { workspace = true } +sp-runtime = { workspace = true, default-features = false } +pallet-nfts = { workspace = true, default-features = false } +sp-core = { workspace = true, default-features = false } +sp-keystore = { workspace = true, default-features = false } +pallet-balances = { workspace = true, default-features = false } +pallet-timestamp = { workspace = true, default-features = false } + +[dev-dependencies] +sp-io = { workspace = true } + +[features] +default = ["std"] +std = [ + "account/std", + "parity-scale-codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-io/std", + "sp-api/std", + "sp-std/std", + "pallet-balances/std", + "pallet-nfts/std", + "pallet-timestamp/std", +] +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/dmarket/README.md b/pallets/dmarket/README.md new file mode 100644 index 00000000..368e02d6 --- /dev/null +++ b/pallets/dmarket/README.md @@ -0,0 +1,3 @@ +# Dmarket Pallet + +The Dmarket pallet provides a market to buy and sell Nfts from pallet-nft, based on the dmarket Smart contracts from Mythical. diff --git a/pallets/dmarket/src/benchmarking.rs b/pallets/dmarket/src/benchmarking.rs new file mode 100644 index 00000000..df65bde4 --- /dev/null +++ b/pallets/dmarket/src/benchmarking.rs @@ -0,0 +1,179 @@ +#![cfg(feature = "runtime-benchmarks")] +use super::*; +use crate::Pallet as Dmarket; +use frame_benchmarking::v2::*; +use frame_support::{ + assert_ok, + dispatch::RawOrigin, + traits::{ + fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, + tokens::nonfungibles_v2::{Create, Mutate}, + }, +}; +use pallet_nfts::ItemId; +use pallet_nfts::{CollectionConfig, CollectionSettings, ItemConfig, MintSettings, Pallet as Nfts}; +use sp_core::ecdsa::Public; +use sp_io::{ + crypto::{ecdsa_generate, ecdsa_sign_prehashed}, + hashing::keccak_256, +}; + +use crate::BenchmarkHelper; + +const SEED: u32 = 0; + +type BalanceOf = + <::Currency as InspectFungible<::AccountId>>::Balance; + +impl BenchmarkHelper for () +where + CollectionId: From, + ItemId: From, + Moment: From, +{ + fn collection(id: u16) -> CollectionId { + id.into() + } + fn timestamp(value: u64) -> Moment { + value.into() + } +} + +fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn funded_and_whitelisted_account(name: &'static str, index: u32) -> T::AccountId { + let caller: T::AccountId = account(name, index, SEED); + // Give the account half of the maximum value of the `Balance` type. + // Otherwise some transfers will fail with an overflow error. + let ed = ::Currency::minimum_balance(); + let multiplier = BalanceOf::::from(1000000u32); + + ::Currency::set_balance(&caller, ed * multiplier); + whitelist_account!(caller); + caller +} + +fn mint_nft(nft_id: ItemId, caller: T::AccountId) { + let default_config = CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: Some(u128::MAX), + mint_settings: MintSettings::default(), + }; + + assert_ok!(Nfts::::create_collection(&caller, &caller, &default_config)); + let collection = T::BenchmarkHelper::collection(0); + assert_ok!(Nfts::::mint_into(&collection, &nft_id, &caller, &ItemConfig::default(), true)); +} + +#[benchmarks(where T::AccountId: From, T::Signature: From)] +pub mod benchmarks { + use super::*; + use account::{AccountId20, EthereumSignature, EthereumSigner}; + use pallet_timestamp::Pallet as Timestamp; + + use sp_runtime::traits::IdentifyAccount; + + fn sign_trade( + sender: &T::AccountId, + fee_address: &T::AccountId, + trade: &TradeParamsOf, + seller_signer: Public, + buyer_signer: Public, + ) -> TradeSignatures + where + T::Signature: From, + { + let ask_message: Vec = Dmarket::::get_ask_message(sender, fee_address, trade); + let ask_hashed = keccak_256(&ask_message); + + let bid_message: Vec = Dmarket::::get_bid_message(sender, fee_address, trade); + let bid_hashed = keccak_256(&bid_message); + + TradeSignatures { + ask_signature: EthereumSignature::from( + ecdsa_sign_prehashed(0.into(), &seller_signer, &ask_hashed).unwrap(), + ) + .into(), + bid_signature: EthereumSignature::from( + ecdsa_sign_prehashed(1.into(), &buyer_signer, &bid_hashed).unwrap(), + ) + .into(), + } + } + + fn trade_participants() -> (T::AccountId, Public, T::AccountId, Public) + where + T::AccountId: From, + { + let ed = ::Currency::minimum_balance(); + let multiplier = BalanceOf::::from(10000u16); + + let seller_public = ecdsa_generate(0.into(), None); + let seller_signer: EthereumSigner = seller_public.into(); + let seller: T::AccountId = seller_signer.clone().into_account().into(); + whitelist_account!(seller); + + let buyer_public = ecdsa_generate(1.into(), None); + let buyer_signer: EthereumSigner = buyer_public.into(); + let buyer: T::AccountId = buyer_signer.clone().into_account().into(); + whitelist_account!(buyer); + + ::Currency::set_balance(&seller, ed * multiplier); + ::Currency::set_balance(&buyer, ed * multiplier); + + (seller, seller_public, buyer, buyer_public) + } + + #[benchmark] + fn force_set_collection() { + let collection_id = T::BenchmarkHelper::collection(0); + let caller: T::AccountId = funded_and_whitelisted_account::("caller", 0); + let _ = mint_nft::(1, caller); + + #[extrinsic_call] + _(RawOrigin::Root, collection_id); + + assert_last_event::(Event::CollectionUpdated { collection_id }.into()); + } + + #[benchmark] + fn execute_trade() { + let sender: T::AccountId = funded_and_whitelisted_account::("sender", 0); + let (seller, seller_public, buyer, buyer_public) = trade_participants::(); + let fee_address: T::AccountId = funded_and_whitelisted_account::("fee_address", 0); + + let collection_id = T::BenchmarkHelper::collection(0); + let item = 1; + mint_nft::(item, seller.clone()); + assert_ok!(Dmarket::::force_set_collection(RawOrigin::Root.into(), collection_id)); + + let expiration = Timestamp::::get() + T::BenchmarkHelper::timestamp(1000); + let trade = TradeParams { + price: BalanceOf::::from(100u16), + fee: BalanceOf::::from(1u8), + ask_expiration: expiration, + bid_expiration: expiration, + item, + }; + let signatures = + sign_trade::(&sender, &fee_address, &trade, seller_public, buyer_public); + + #[extrinsic_call] + _( + RawOrigin::Signed(sender), + seller.clone(), + buyer.clone(), + trade.clone(), + signatures, + fee_address, + ); + + assert_last_event::( + Event::Trade { seller, buyer, item, price: trade.price, fee: trade.fee }.into(), + ); + } + + impl_benchmark_test_suite!(Dmarket, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/dmarket/src/lib.rs b/pallets/dmarket/src/lib.rs new file mode 100644 index 00000000..baf7c9a8 --- /dev/null +++ b/pallets/dmarket/src/lib.rs @@ -0,0 +1,311 @@ +//! # Marketplace Module +//! +//! A module that facilitates trading of non-fungible items (NFTs) through the creation and management of orders. +//! +//! ## Related Modules +//! - NFTs: Provides functionalities for managing non-fungible tokens. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +mod types; +use parity_scale_codec::Codec; +pub use types::*; + +pub mod weights; +pub use weights::*; + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use core::usize; + + use crate::Item; + + use super::*; + use frame_support::{ + ensure, + pallet_prelude::*, + traits::{ + fungible::{Inspect, Mutate}, + nonfungibles_v2::{Inspect as NftInspect, Transfer}, + tokens::Preservation::Preserve, + }, + }; + use frame_system::{ensure_signed, pallet_prelude::*}; + + use frame_support::{dispatch::GetDispatchInfo, traits::UnfilteredDispatchable}; + + use sp_runtime::traits::Hash; + use sp_runtime::{ + traits::{IdentifyAccount, Verify}, + DispatchError, + }; + use sp_std::vec::Vec; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_nfts::Config + pallet_timestamp::Config + { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type RuntimeCall: Parameter + + UnfilteredDispatchable + + GetDispatchInfo; + + /// The currency trait. + type Currency: Inspect + Mutate; + + /// Off-Chain signature type. + /// + /// Can verify whether a `Self::Signer` created a signature. + type Signature: Verify + Parameter; + + /// Off-Chain public key. + /// + /// Must identify as an on-chain `Self::AccountId`. + type Signer: IdentifyAccount; + + ///Chain Domain + #[pallet::constant] + type Domain: Get; + + #[cfg(feature = "runtime-benchmarks")] + /// A set of helper functions for benchmarking. + type BenchmarkHelper: BenchmarkHelper; + } + + #[pallet::storage] + pub type ClosedAsks = StorageMap<_, Blake2_128Concat, T::Hash, OrderDataOf>; + + #[pallet::storage] + pub type ClosedBids = StorageMap<_, Blake2_128Concat, T::Hash, OrderDataOf>; + + #[pallet::storage] + pub type DmarketCollection = StorageValue<_, T::CollectionId, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// The fee signer account was updated. + CollectionUpdated { collection_id: T::CollectionId }, + /// + Trade { + buyer: T::AccountId, + seller: T::AccountId, + item: Item, + price: BalanceOf, + fee: BalanceOf, + }, + } + + #[pallet::error] + pub enum Error { + /// The item was not found. + ItemNotFound, + /// Item can only be operated by the Item owner. + SellerNotItemOwner, + /// + BidAlreadyExecuted, + /// + AskAlreadyExecuted, + /// + BuyerBalanceTooLow, + /// + BidExpired, + /// + AskExpired, + /// + InvalidBuyerSignature, + /// + InvalidSellerSignature, + /// Same buyer and seller not allowed. + BuyerIsSeller, + /// + BadSignedMessage, + /// + CollectionAlreadyInUse, + /// + CollectionNotSet, + /// + CollectionNotFound, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight({0})] + pub fn force_set_collection( + origin: OriginFor, + collection_id: T::CollectionId, + ) -> DispatchResult { + ensure_root(origin)?; + + as NftInspect>::collection_owner(&collection_id) + .ok_or(Error::::CollectionNotFound)?; + + ensure!( + DmarketCollection::::get().as_ref() != Some(&collection_id), + Error::::CollectionAlreadyInUse + ); + + DmarketCollection::::put(collection_id.clone()); + Self::deposit_event(Event::CollectionUpdated { collection_id }); + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight({0})] + pub fn execute_trade( + origin: OriginFor, + seller: T::AccountId, + buyer: T::AccountId, + trade: TradeParamsOf, + signatures: TradeSignatures<::Signature>, + fee_address: T::AccountId, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!(seller != buyer, Error::::BuyerIsSeller); + + let timestamp = pallet_timestamp::Pallet::::get(); + ensure!(trade.ask_expiration > timestamp, Error::::AskExpired); + ensure!(trade.bid_expiration > timestamp, Error::::BidExpired); + + // Pay fees to FeeAddress + let collection = DmarketCollection::::get().ok_or(Error::::CollectionNotSet)?; + + let item_owner = pallet_nfts::Pallet::::owner(collection, trade.item) + .ok_or(Error::::ItemNotFound)?; + ensure!(seller == item_owner, Error::::SellerNotItemOwner); + + let (ask_hash, bid_hash) = Self::hash_ask_bid_data(&trade); + ensure!(!ClosedAsks::::contains_key(ask_hash), Error::::AskAlreadyExecuted); + ensure!(!ClosedBids::::contains_key(bid_hash), Error::::BidAlreadyExecuted); + + Self::verify_signature( + &seller, + &Self::get_ask_message(&who, &fee_address, &trade), + signatures.ask_signature, + ) + .map_err(|_| Error::::InvalidSellerSignature)?; + + Self::verify_signature( + &buyer, + &Self::get_bid_message(&who, &fee_address, &trade), + signatures.bid_signature, + ) + .map_err(|_| Error::::InvalidBuyerSignature)?; + + as Transfer>::transfer( + &collection, + &trade.item, + &buyer, + )?; + ::Currency::transfer(&buyer, &seller, trade.price, Preserve) + .map_err(|_| Error::::BuyerBalanceTooLow)?; + ::Currency::transfer(&seller, &fee_address, trade.fee, Preserve)?; + + let order_data: OrderDataOf = OrderData { caller: who, fee_address }; + + //Store closed trades + ClosedAsks::::insert(ask_hash, order_data.clone()); + ClosedBids::::insert(bid_hash, order_data); + + Self::deposit_event(Event::Trade { + seller, + buyer, + item: trade.item, + price: trade.price, + fee: trade.fee, + }); + + Ok(()) + } + } + + impl Pallet { + fn verify_signature( + who: &T::AccountId, + message: &Vec, + signature: T::Signature, + ) -> Result<(), DispatchError> { + if !signature.verify(message.as_ref(), &who) { + return Err(Error::::BadSignedMessage.into()); + } + + Ok(()) + } + + pub fn hash_ask_bid_data(trade: &TradeParamsOf) -> (T::Hash, T::Hash) { + let ask_hash = + T::Hashing::hash(&(trade.item, trade.price, trade.ask_expiration).encode()); + let bid_hash = T::Hashing::hash( + &(trade.item, trade.price, trade.fee, trade.bid_expiration).encode(), + ); + + (ask_hash, bid_hash) + } + + pub fn get_ask_message( + caller: &T::AccountId, + fee_address: &T::AccountId, + trade: &TradeParamsOf, + ) -> Vec { + AskMessage { + domain: T::Domain::get(), + sender: caller.clone(), + fee_address: fee_address.clone(), + item: trade.item, + price: trade.price, + expiration: trade.ask_expiration, + } + .encode() + } + + pub fn get_bid_message( + caller: &T::AccountId, + fee_address: &T::AccountId, + trade: &TradeParamsOf, + ) -> Vec { + BidMessage { + domain: T::Domain::get(), + sender: caller.clone(), + fee_address: fee_address.clone(), + item: trade.item, + price: trade.price, + fee: trade.fee, + expiration: trade.bid_expiration, + } + .encode() + } + } +} + +sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); + +use sp_std::vec::Vec; +sp_api::decl_runtime_apis! { + pub trait DmarketApi + where + AccountId: Codec, + Balance: Codec, + Moment: Codec, + Hash: Codec, + { + fn hash_ask_bid_data(trade: TradeParams)-> (Hash, Hash); + fn get_ask_message(caller: AccountId, fee_address: AccountId, trade: TradeParams) -> Vec; + fn get_bid_message(caller: AccountId, fee_address: AccountId, trade: TradeParams) -> Vec; + } +} diff --git a/pallets/dmarket/src/mock.rs b/pallets/dmarket/src/mock.rs new file mode 100644 index 00000000..dd6457f1 --- /dev/null +++ b/pallets/dmarket/src/mock.rs @@ -0,0 +1,144 @@ +use frame_support::{ + derive_impl, parameter_types, + traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64}, +}; +use frame_system as system; +use sp_core::H256; +use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, +}; + +use account::EthereumSignature; + +use crate::{self as pallet_dmarket}; +use pallet_nfts::PalletFeatures; + +type Signature = EthereumSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Dmarket: pallet_dmarket, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + Nfts: pallet_nfts, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub storage Features: PalletFeatures = PalletFeatures::all_enabled(); +} + +impl pallet_nfts::Config for Test { + type RuntimeEvent = RuntimeEvent; + type CollectionId = u32; + type Currency = Balances; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type Locker = (); + type CollectionDeposit = ConstU128<0>; + type ItemDeposit = ConstU128<0>; + type MetadataDepositBase = ConstU128<1>; + type AttributeDepositBase = ConstU128<1>; + type DepositPerByte = ConstU128<1>; + type StringLimit = ConstU32<50>; + type KeyLimit = ConstU32<50>; + type ValueLimit = ConstU32<50>; + type ApprovalsLimit = ConstU32<10>; + type ItemAttributesApprovalsLimit = ConstU32<2>; + type MaxTips = ConstU32<10>; + type MaxDeadlineDuration = ConstU64<10000>; + type MaxAttributesPerCall = ConstU32<2>; + type Features = Features; + type OffchainSignature = Signature; + type OffchainPublic = ::Signer; + type WeightInfo = (); + pallet_nfts::runtime_benchmarks_enabled! { + type Helper = (); + } +} + +impl pallet_balances::Config for Test { + type MaxLocks = (); + type MaxReserves = ConstU32<2>; + type ReserveIdentifier = [u8; 8]; + type Balance = u128; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<3>; + type WeightInfo = (); +} + +parameter_types! { + pub const DOMAIN: [u8;8] = *b"MYTH_NET"; +} + +impl crate::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type Signature = Signature; + type Signer = ::Signer; + type Domain = DOMAIN; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = system::GenesisConfig::::default().build_storage().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.register_extension(KeystoreExt::new(MemoryKeystore::new())); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/pallets/dmarket/src/tests.rs b/pallets/dmarket/src/tests.rs new file mode 100644 index 00000000..5e7c2bbb --- /dev/null +++ b/pallets/dmarket/src/tests.rs @@ -0,0 +1,547 @@ +use account::{AccountId20, EthereumSignature, EthereumSigner}; + +use frame_support::{ + assert_noop, assert_ok, + error::BadOrigin, + traits::fungible::{Inspect as InspectFungible, Mutate}, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_nfts::{CollectionConfig, CollectionSettings, MintSettings, NextCollectionId}; +use sp_core::{ + ecdsa::{Pair as KeyPair, Signature}, + keccak_256, Pair, +}; + +use sp_runtime::traits::IdentifyAccount; + +use self::mock::Timestamp; +use crate::{mock::*, *}; + +type AccountIdOf = ::AccountId; +type CollectionId = ::CollectionId; +type Balance = ::Balance; + +fn account(id: u8) -> AccountIdOf { + [id; 20].into() +} + +fn create_collection(creator: &AccountIdOf) -> CollectionId { + let collection_id = NextCollectionId::::get().unwrap_or_default(); + assert_ok!(Nfts::force_create( + RuntimeOrigin::root(), + *creator, + collection_config_with_all_settings_enabled() + )); + + collection_id +} + +fn collection_config_with_all_settings_enabled( +) -> CollectionConfig, BlockNumberFor, CollectionId> { + CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: Some(1000000), + mint_settings: MintSettings::default(), + } +} + +mod force_set_collection { + use super::*; + + #[test] + fn force_set_collection_works() { + new_test_ext().execute_with(|| { + let collection_id = create_collection(&account(0)); + + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), collection_id)); + assert!(DmarketCollection::::get() == Some(collection_id)); + + assert_noop!( + Dmarket::force_set_collection(RuntimeOrigin::root(), collection_id), + Error::::CollectionAlreadyInUse + ); + + let other_collection = create_collection(&account(0)); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), other_collection)); + assert!(DmarketCollection::::get() == Some(other_collection)); + }) + } + + #[test] + fn fails_no_root() { + new_test_ext().execute_with(|| { + let collection_id = create_collection(&account(0)); + + assert_noop!( + Dmarket::force_set_collection(RuntimeOrigin::signed(account(1)), collection_id), + BadOrigin + ); + }) + } + + #[test] + fn collection_not_found() { + new_test_ext().execute_with(|| { + assert_noop!( + Dmarket::force_set_collection(RuntimeOrigin::root(), 0), + Error::::CollectionNotFound + ); + }) + } +} + +mod execute_trade { + use super::*; + + fn get_trade_accounts() -> (AccountIdOf, AccountIdOf, KeyPair, KeyPair) { + let sender = account(0); + let fee_address = account(1); + + let seller_pair = Pair::from_string("//Seller", None).unwrap(); + let buyer_pair = Pair::from_string("//Buyer", None).unwrap(); + (sender, fee_address, seller_pair, buyer_pair) + } + + fn setup_nft( + sender: &AccountIdOf, + item_owner: &AccountIdOf, + item: u128, + ) -> CollectionId { + let collection_id = create_collection(&sender); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), collection_id)); + + assert_ok!(Nfts::mint(RuntimeOrigin::signed(*sender), 0, Some(item), *item_owner, None)); + collection_id + } + + fn sign_trade( + caller: &AccountIdOf, + fee_address: &AccountIdOf, + trade: &TradeParamsOf, + seller_pair: KeyPair, + buyer_pair: KeyPair, + ) -> TradeSignaturesOf { + let ask_message: Vec = Dmarket::get_ask_message(caller, fee_address, trade); + let hashed_ask = keccak_256(&ask_message); + + let bid_message: Vec = Dmarket::get_bid_message(caller, fee_address, trade); + let hashed_bid = keccak_256(&bid_message); + + TradeSignatures { + ask_signature: EthereumSignature::from(seller_pair.sign_prehashed(&hashed_ask)), + bid_signature: EthereumSignature::from(buyer_pair.sign_prehashed(&hashed_bid)), + } + } + + #[test] + fn execute_trade_works() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let collection = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let buyer_balance = Balances::balance(&buyer); + let fee_address_balance = Balances::balance(&fee_address); + let seller_balance = Balances::balance(&seller); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_ok!(Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + )); + + assert_eq!(Nfts::owner(collection, item).unwrap(), buyer); + assert_eq!(Balances::balance(&buyer), buyer_balance - price); + assert_eq!(Balances::balance(&seller), seller_balance + price - fee); + assert_eq!(Balances::balance(&fee_address), fee_address_balance + fee); + + let (ask_hash, bid_hash) = Dmarket::hash_ask_bid_data(&trade); + assert!(ClosedAsks::::contains_key(ask_hash)); + assert!(ClosedBids::::contains_key(bid_hash)); + }) + } + + #[test] + fn buyer_is_seller() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = seller.clone(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::BuyerIsSeller + ); + }) + } + + #[test] + fn ask_or_bid_expired() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let mut trade = + TradeParams { price, fee, item, ask_expiration: 0, bid_expiration: expiration }; + let signatures = + sign_trade(&sender, &fee_address, &trade, seller_pair.clone(), buyer_pair.clone()); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::AskExpired + ); + + trade.ask_expiration = expiration; + trade.bid_expiration = 0; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(seller), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::BidExpired + ); + }) + } + + #[test] + fn collection_not_set() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::CollectionNotSet + ); + }) + } + + #[test] + fn item_not_found() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item: item + 1, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::ItemNotFound + ); + }) + } + + #[test] + fn already_executed() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let collection = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_ok!(Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures.clone(), + fee_address + )); + + assert_ok!(Nfts::transfer(RuntimeOrigin::signed(buyer), collection, item, seller)); + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures.clone(), + fee_address + ), + Error::::AskAlreadyExecuted + ); + + let (ask_hash, _) = Dmarket::hash_ask_bid_data(&trade); + ClosedAsks::::remove(ask_hash); + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::BidAlreadyExecuted + ); + }) + } + + #[test] + fn invalid_signatures() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let mut signatures = + sign_trade(&sender, &fee_address, &trade, seller_pair.clone(), buyer_pair.clone()); + signatures.ask_signature = EthereumSignature::from(Signature::from_raw([0; 65])); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::InvalidSellerSignature + ); + + let mut signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + signatures.bid_signature = EthereumSignature::from(Signature::from_raw([0; 65])); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::InvalidBuyerSignature + ); + }) + } + + #[test] + fn seller_not_owner() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &account(1), item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price * 2); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::SellerNotItemOwner + ); + }) + } + + #[test] + fn buyer_not_enough_funds() { + new_test_ext().execute_with(|| { + let (sender, fee_address, seller_pair, buyer_pair) = get_trade_accounts(); + + let seller: AccountId20 = EthereumSigner::from(seller_pair.public()).into_account(); + let buyer: AccountId20 = EthereumSigner::from(buyer_pair.public()).into_account(); + + let item = 1; + let _ = setup_nft(&sender, &seller, item); + + let expiration = Timestamp::get() + 10; + let price = 10000000; + let fee = 100; + Balances::set_balance(&buyer, price - 10); + + let trade = TradeParams { + price, + fee, + item, + ask_expiration: expiration, + bid_expiration: expiration, + }; + let signatures = sign_trade(&sender, &fee_address, &trade, seller_pair, buyer_pair); + + assert_noop!( + Dmarket::execute_trade( + RuntimeOrigin::signed(sender), + seller, + buyer, + trade.clone(), + signatures, + fee_address + ), + Error::::BuyerBalanceTooLow + ); + }) + } +} diff --git a/pallets/dmarket/src/types.rs b/pallets/dmarket/src/types.rs new file mode 100644 index 00000000..e1f3ccb7 --- /dev/null +++ b/pallets/dmarket/src/types.rs @@ -0,0 +1,84 @@ +use crate::Config; +use frame_support::traits::fungible::Inspect; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +pub type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; + +pub type Item = u128; +pub type Domain = [u8; 8]; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct TradeParams { + pub price: Amount, + pub fee: Amount, + pub item: ItemId, + pub ask_expiration: Expiration, + pub bid_expiration: Expiration, +} + +pub type TradeParamsOf = TradeParams< + <::Currency as Inspect<::AccountId>>::Balance, + Item, + ::Moment, +>; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct TradeSignatures { + pub ask_signature: OffchainSignature, + pub bid_signature: OffchainSignature, +} + +pub type TradeSignaturesOf = TradeSignatures<::OffchainSignature>; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct AskMessage { + pub domain: Domain, + pub sender: Account, + pub fee_address: Account, + pub item: ItemId, + pub price: Amount, + pub expiration: Expiration, +} + +pub type AskMessageOf = AskMessage< + ::AccountId, + <::Currency as Inspect<::AccountId>>::Balance, + Item, + ::Moment, +>; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo, MaxEncodedLen)] +pub struct OrderData { + pub caller: Account, + pub fee_address: Account, +} + +pub type OrderDataOf = OrderData<::AccountId>; + +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct BidMessage { + pub domain: Domain, + pub sender: Account, + pub fee_address: Account, + pub item: ItemId, + pub price: Amount, + pub fee: Amount, + pub expiration: Expiration, +} + +pub type BidMessageOf = BidMessage< + ::AccountId, + <::Currency as Inspect<::AccountId>>::Balance, + Item, + ::Moment, +>; + +#[cfg(feature = "runtime-benchmarks")] +pub trait BenchmarkHelper { + /// Returns a collection id from a given integer. + fn collection(id: u16) -> CollectionId; + /// Returns an nft id from a given integer. + fn timestamp(value: u64) -> Moment; +} diff --git a/pallets/dmarket/src/weights.rs b/pallets/dmarket/src/weights.rs new file mode 100644 index 00000000..442e2064 --- /dev/null +++ b/pallets/dmarket/src/weights.rs @@ -0,0 +1,202 @@ + +//! Autogenerated weights for `pallet_dmarket` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-06-05, STEPS: `10`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `pop-os`, CPU: `AMD Ryzen 9 7900X 12-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/mythos-node +// benchmark +// pallet +// --steps +// 10 +// --template +// ./.maintain/template.hbs +// --repeat +// 20 +// --extrinsic +// * +// --wasm-execution +// compiled +// --heap-pages +// 4096 +// --pallet +// pallet_dmarket +// --output +// ./pallets/dmarket/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_dmarket`. +pub trait WeightInfo { + fn force_set_balance_manager() -> Weight; + fn force_set_collection() -> Weight; + fn set_fee_address() -> Weight; + fn execute_trade() -> Weight; +} + +/// Weights for `pallet_dmarket` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); + impl WeightInfo for SubstrateWeight { + /// Storage: `Dmarket::BalanceManager` (r:1 w:1) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn force_set_balance_manager() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `1505` + // Minimum execution time: 4_650_000 picoseconds. + Weight::from_parts(4_920_000, 1505) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:1) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + fn force_set_collection() -> Weight { + // Proof Size summary in bytes: + // Measured: `420` + // Estimated: `3610` + // Minimum execution time: 10_150_000 picoseconds. + Weight::from_parts(10_390_000, 3610) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::FeeAddress` (r:1 w:1) + /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn set_fee_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `120` + // Estimated: `1505` + // Minimum execution time: 6_770_000 picoseconds. + Weight::from_parts(7_020_000, 1505) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::FeeAddress` (r:1 w:0) + /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:0) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(637), added: 3112, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedAsks` (r:1 w:1) + /// Proof: `Dmarket::ClosedAsks` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedBids` (r:1 w:1) + /// Proof: `Dmarket::ClosedBids` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(495), added: 2970, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(117), added: 2592, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(151), added: 2626, mode: `MaxEncodedLen`) + fn execute_trade() -> Weight { + // Proof Size summary in bytes: + // Measured: `1228` + // Estimated: `4102` + // Minimum execution time: 172_460_000 picoseconds. + Weight::from_parts(174_840_000, 4102) + .saturating_add(T::DbWeight::get().reads(11_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Dmarket::BalanceManager` (r:1 w:1) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn force_set_balance_manager() -> Weight { + // Proof Size summary in bytes: + // Measured: `76` + // Estimated: `1505` + // Minimum execution time: 4_650_000 picoseconds. + Weight::from_parts(4_920_000, 1505) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:1) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + fn force_set_collection() -> Weight { + // Proof Size summary in bytes: + // Measured: `420` + // Estimated: `3610` + // Minimum execution time: 10_150_000 picoseconds. + Weight::from_parts(10_390_000, 3610) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::FeeAddress` (r:1 w:1) + /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn set_fee_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `120` + // Estimated: `1505` + // Minimum execution time: 6_770_000 picoseconds. + Weight::from_parts(7_020_000, 1505) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) + /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::FeeAddress` (r:1 w:0) + /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:0) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(637), added: 3112, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedAsks` (r:1 w:1) + /// Proof: `Dmarket::ClosedAsks` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedBids` (r:1 w:1) + /// Proof: `Dmarket::ClosedBids` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(495), added: 2970, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(117), added: 2592, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(151), added: 2626, mode: `MaxEncodedLen`) + fn execute_trade() -> Weight { + // Proof Size summary in bytes: + // Measured: `1228` + // Estimated: `4102` + // Minimum execution time: 172_460_000 picoseconds. + Weight::from_parts(174_840_000, 4102) + .saturating_add(RocksDbWeight::get().reads(11_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + } +} From 4add66984029ef2cb87d46273ea7bef31473aa57 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Fri, 2 Aug 2024 09:39:53 -0300 Subject: [PATCH 05/14] Add dmarket to testnet --- Cargo.lock | 23 +++++++++++++++++++++++ runtime/testnet/Cargo.toml | 4 ++++ runtime/testnet/src/lib.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 07a78df0..9145cd42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6758,6 +6758,28 @@ dependencies = [ "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?tag=polkadot-v1.13.0)", ] +[[package]] +name = "pallet-dmarket" +version = "0.0.1" +dependencies = [ + "account", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-nfts", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-core", + "sp-io", + "sp-keystore", + "sp-runtime", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?tag=polkadot-v1.13.0)", +] + [[package]] name = "pallet-election-provider-multi-phase" version = "27.0.0" @@ -13591,6 +13613,7 @@ dependencies = [ "pallet-balances", "pallet-collator-selection", "pallet-collective", + "pallet-dmarket", "pallet-escrow", "pallet-marketplace", "pallet-message-queue", diff --git a/runtime/testnet/Cargo.toml b/runtime/testnet/Cargo.toml index 0f400315..94b4557c 100644 --- a/runtime/testnet/Cargo.toml +++ b/runtime/testnet/Cargo.toml @@ -23,6 +23,7 @@ smallvec = { workspace = true } # Local runtime-common = { workspace = true, default-features = false } +pallet-dmarket = { workspace = true, default-features = false } pallet-marketplace = { workspace = true, default-features = false } pallet-multibatching = { workspace = true, default-features = false } xcm-primitives = { path = "../../primitives/xcm", default-features = false } @@ -120,6 +121,7 @@ std = [ "pallet-balances/std", "pallet-collator-selection/std", "pallet-collective/std", + "pallet-dmarket/std", "pallet-multibatching/std", "pallet-marketplace/std", "pallet-multisig/std", @@ -170,6 +172,7 @@ runtime-benchmarks = [ "pallet-collator-selection/runtime-benchmarks", "pallet-collective/runtime-benchmarks", "pallet-message-queue/runtime-benchmarks", + "pallet-dmarket/runtime-benchmarks", "pallet-marketplace/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-multibatching/runtime-benchmarks", @@ -204,6 +207,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-collator-selection/try-runtime", "pallet-collective/try-runtime", + "pallet-dmarket/try-runtime", "pallet-multibatching/try-runtime", "pallet-marketplace/try-runtime", "pallet-multisig/try-runtime", diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index f4381e03..e2e047d1 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -41,9 +41,11 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, }; +use pallet_dmarket::{Item, TradeParams}; use pallet_nfts::PalletFeatures; use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use polkadot_primitives::Moment; pub use runtime_common::{ AccountId, Balance, BlockNumber, DealWithFees, Hash, IncrementableU256, Nonce, Signature, AVERAGE_ON_INITIALIZE_RATIO, DAYS, HOURS, MAXIMUM_BLOCK_WEIGHT, MINUTES, NORMAL_DISPATCH_RATIO, @@ -654,6 +656,17 @@ impl pallet_marketplace::Config for Runtime { type BenchmarkHelper = (); } +impl pallet_dmarket::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type Signature = Signature; + type Signer = ::Signer; + type Domain = DOMAIN; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + parameter_types! { pub const ProxyDepositBase: Balance = deposit(1, 8); pub const ProxyDepositFactor: Balance = deposit(0, 33); @@ -850,6 +863,7 @@ construct_runtime!( Escrow: pallet_escrow = 50, MythProxy: pallet_myth_proxy = 51, + Dmarket: pallet_dmarket = 52, } ); @@ -926,6 +940,7 @@ mod benches { [pallet_vesting, Vesting] [pallet_collective, Council] [pallet_myth_proxy, MythProxy] + [pallet_dmarket, Dmarket] ); } @@ -1081,6 +1096,18 @@ impl_runtime_apis! { } } + impl pallet_dmarket::DmarketApi for Runtime { + fn hash_ask_bid_data(trade: TradeParams)-> (Hash, Hash) { + Dmarket::hash_ask_bid_data(&trade) + } + fn get_ask_message(caller: AccountId, fee_address: AccountId, trade: TradeParams) -> Vec { + Dmarket::get_ask_message(&caller, &fee_address, &trade) + } + fn get_bid_message(caller: AccountId, fee_address: AccountId, trade: TradeParams) -> Vec { + Dmarket::get_bid_message(&caller, &fee_address, &trade) + } + } + impl cumulus_primitives_aura::AuraUnincludedSegmentApi for Runtime { fn can_build_upon( included_hash: ::Hash, From 4eb75876b040f21dd0580bc89bc54df75000d794 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Fri, 2 Aug 2024 10:44:32 -0300 Subject: [PATCH 06/14] Add WeighInfo --- pallets/dmarket/src/lib.rs | 7 +++++-- pallets/dmarket/src/mock.rs | 1 + runtime/testnet/src/lib.rs | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/pallets/dmarket/src/lib.rs b/pallets/dmarket/src/lib.rs index baf7c9a8..f3b9efaf 100644 --- a/pallets/dmarket/src/lib.rs +++ b/pallets/dmarket/src/lib.rs @@ -81,6 +81,9 @@ pub mod pallet { #[pallet::constant] type Domain: Get; + /// Type representing the weight of this pallet + type WeightInfo: WeightInfo; + #[cfg(feature = "runtime-benchmarks")] /// A set of helper functions for benchmarking. type BenchmarkHelper: BenchmarkHelper; @@ -145,7 +148,7 @@ pub mod pallet { #[pallet::call] impl Pallet { #[pallet::call_index(0)] - #[pallet::weight({0})] + #[pallet::weight(::WeightInfo::force_set_collection())] pub fn force_set_collection( origin: OriginFor, collection_id: T::CollectionId, @@ -166,7 +169,7 @@ pub mod pallet { } #[pallet::call_index(1)] - #[pallet::weight({0})] + #[pallet::weight(::WeightInfo::execute_trade())] pub fn execute_trade( origin: OriginFor, seller: T::AccountId, diff --git a/pallets/dmarket/src/mock.rs b/pallets/dmarket/src/mock.rs index dd6457f1..5b58cd38 100644 --- a/pallets/dmarket/src/mock.rs +++ b/pallets/dmarket/src/mock.rs @@ -130,6 +130,7 @@ impl crate::Config for Test { type Signature = Signature; type Signer = ::Signer; type Domain = DOMAIN; + type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index e2e047d1..d9432e45 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -663,6 +663,7 @@ impl pallet_dmarket::Config for Runtime { type Signature = Signature; type Signer = ::Signer; type Domain = DOMAIN; + type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } From aaeb39fe11a0dff82179a38ce989e086b411ba81 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Tue, 13 Aug 2024 13:00:39 -0300 Subject: [PATCH 07/14] Include dmarket weights on testnet --- pallets/dmarket/src/weights.rs | 52 +---------- runtime/testnet/src/weights/mod.rs | 1 + runtime/testnet/src/weights/pallet_dmarket.rs | 88 +++++++++++++++++++ 3 files changed, 91 insertions(+), 50 deletions(-) create mode 100644 runtime/testnet/src/weights/pallet_dmarket.rs diff --git a/pallets/dmarket/src/weights.rs b/pallets/dmarket/src/weights.rs index 442e2064..19b64d69 100644 --- a/pallets/dmarket/src/weights.rs +++ b/pallets/dmarket/src/weights.rs @@ -38,26 +38,13 @@ use core::marker::PhantomData; /// Weight functions needed for `pallet_dmarket`. pub trait WeightInfo { - fn force_set_balance_manager() -> Weight; fn force_set_collection() -> Weight; - fn set_fee_address() -> Weight; fn execute_trade() -> Weight; } /// Weights for `pallet_dmarket` using the Substrate node and recommended hardware. pub struct SubstrateWeight(PhantomData); impl WeightInfo for SubstrateWeight { - /// Storage: `Dmarket::BalanceManager` (r:1 w:1) - /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) - fn force_set_balance_manager() -> Weight { - // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `1505` - // Minimum execution time: 4_650_000 picoseconds. - Weight::from_parts(4_920_000, 1505) - .saturating_add(T::DbWeight::get().reads(1_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } /// Storage: `Nfts::Collection` (r:1 w:0) /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) /// Storage: `Dmarket::DmarketCollection` (r:1 w:1) @@ -71,19 +58,7 @@ pub struct SubstrateWeight(PhantomData); .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } - /// Storage: `Dmarket::BalanceManager` (r:1 w:0) - /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) - /// Storage: `Dmarket::FeeAddress` (r:1 w:1) - /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) - fn set_fee_address() -> Weight { - // Proof Size summary in bytes: - // Measured: `120` - // Estimated: `1505` - // Minimum execution time: 6_770_000 picoseconds. - Weight::from_parts(7_020_000, 1505) - .saturating_add(T::DbWeight::get().reads(2_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) /// Storage: `Timestamp::Now` (r:1 w:0) @@ -125,17 +100,6 @@ pub struct SubstrateWeight(PhantomData); // For backwards compatibility and tests. impl WeightInfo for () { - /// Storage: `Dmarket::BalanceManager` (r:1 w:1) - /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) - fn force_set_balance_manager() -> Weight { - // Proof Size summary in bytes: - // Measured: `76` - // Estimated: `1505` - // Minimum execution time: 4_650_000 picoseconds. - Weight::from_parts(4_920_000, 1505) - .saturating_add(RocksDbWeight::get().reads(1_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } /// Storage: `Nfts::Collection` (r:1 w:0) /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(145), added: 2620, mode: `MaxEncodedLen`) /// Storage: `Dmarket::DmarketCollection` (r:1 w:1) @@ -149,19 +113,7 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } - /// Storage: `Dmarket::BalanceManager` (r:1 w:0) - /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) - /// Storage: `Dmarket::FeeAddress` (r:1 w:1) - /// Proof: `Dmarket::FeeAddress` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) - fn set_fee_address() -> Weight { - // Proof Size summary in bytes: - // Measured: `120` - // Estimated: `1505` - // Minimum execution time: 6_770_000 picoseconds. - Weight::from_parts(7_020_000, 1505) - .saturating_add(RocksDbWeight::get().reads(2_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - } + /// Storage: `Dmarket::BalanceManager` (r:1 w:0) /// Proof: `Dmarket::BalanceManager` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) /// Storage: `Timestamp::Now` (r:1 w:0) diff --git a/runtime/testnet/src/weights/mod.rs b/runtime/testnet/src/weights/mod.rs index c3fe409e..b80dbd31 100644 --- a/runtime/testnet/src/weights/mod.rs +++ b/runtime/testnet/src/weights/mod.rs @@ -33,6 +33,7 @@ pub mod frame_system; pub mod pallet_balances; pub mod pallet_collator_selection; pub mod pallet_collective; +pub mod pallet_dmarket; pub mod pallet_escrow; pub mod pallet_marketplace; pub mod pallet_message_queue; diff --git a/runtime/testnet/src/weights/pallet_dmarket.rs b/runtime/testnet/src/weights/pallet_dmarket.rs new file mode 100644 index 00000000..5a354b89 --- /dev/null +++ b/runtime/testnet/src/weights/pallet_dmarket.rs @@ -0,0 +1,88 @@ + +//! Autogenerated weights for `pallet_dmarket` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-08-02, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bdl-ref-hw`, CPU: `AMD EPYC 7232P 8-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("local-v")`, DB CACHE: 1024 + +// Executed Command: +// ./target/release/mythos-node +// benchmark +// pallet +// --chain +// local-v +// --pallet +// pallet_dmarket +// --extrinsic +// * +// --wasm-execution +// compiled +// --steps +// 50 +// --repeat +// 20 +// --output +// ./runtime/testnet/src/weights/pallet_dmarket.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_dmarket`. +pub struct WeightInfo(PhantomData); +impl pallet_dmarket::WeightInfo for WeightInfo { + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(169), added: 2644, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:1) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + fn force_set_collection() -> Weight { + // Proof Size summary in bytes: + // Measured: `444` + // Estimated: `3634` + // Minimum execution time: 22_571_000 picoseconds. + Weight::from_parts(23_310_000, 0) + .saturating_add(Weight::from_parts(0, 3634)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:0) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(637), added: 3112, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedAsks` (r:1 w:1) + /// Proof: `Dmarket::ClosedAsks` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::ClosedBids` (r:1 w:1) + /// Proof: `Dmarket::ClosedBids` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(169), added: 2644, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(495), added: 2970, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(117), added: 2592, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(151), added: 2626, mode: `MaxEncodedLen`) + fn execute_trade() -> Weight { + // Proof Size summary in bytes: + // Measured: `1205` + // Estimated: `4102` + // Minimum execution time: 321_303_000 picoseconds. + Weight::from_parts(325_174_000, 0) + .saturating_add(Weight::from_parts(0, 4102)) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().writes(7)) + } +} From 08bd4c6d3f267cf172d5dfa9db489f1ee599aba9 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Tue, 13 Aug 2024 13:01:05 -0300 Subject: [PATCH 08/14] tweaks to migration pallet to match dmarket requirements --- Cargo.lock | 23 +++ Cargo.toml | 1 + pallets/migration/Cargo.toml | 56 ++++++ pallets/migration/src/benchmarking.rs | 105 ++++++++++ pallets/migration/src/lib.rs | 279 ++++++++++++++++++++++++++ pallets/migration/src/mock.rs | 162 +++++++++++++++ pallets/migration/src/tests.rs | 260 ++++++++++++++++++++++++ pallets/migration/src/weights.rs | 132 ++++++++++++ runtime/testnet/Cargo.toml | 4 + runtime/testnet/src/lib.rs | 11 + 10 files changed, 1033 insertions(+) create mode 100644 pallets/migration/Cargo.toml create mode 100644 pallets/migration/src/benchmarking.rs create mode 100644 pallets/migration/src/lib.rs create mode 100644 pallets/migration/src/mock.rs create mode 100644 pallets/migration/src/tests.rs create mode 100644 pallets/migration/src/weights.rs diff --git a/Cargo.lock b/Cargo.lock index 9145cd42..d2981fb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7006,6 +7006,28 @@ dependencies = [ "sp-weights", ] +[[package]] +name = "pallet-migration" +version = "0.0.1" +dependencies = [ + "account", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-dmarket", + "pallet-nfts", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?tag=polkadot-v1.13.0)", +] + [[package]] name = "pallet-mmr" version = "27.0.0" @@ -13617,6 +13639,7 @@ dependencies = [ "pallet-escrow", "pallet-marketplace", "pallet-message-queue", + "pallet-migration", "pallet-multibatching", "pallet-multisig", "pallet-myth-proxy", diff --git a/Cargo.toml b/Cargo.toml index e16c3c75..ac7fcbad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ testnet-runtime = { path = "runtime/testnet" } mainnet-runtime = { path = "runtime/mainnet" } pallet-dmarket = { path = "pallets/dmarket", default-features = false } pallet-marketplace = { path = "pallets/marketplace", default-features = false } +pallet-migration = { path = "pallets/migration", default-features = false } pallet-multibatching = { path = "pallets/multibatching", default-features = false } runtime-common = { path = "runtime/common", default-features = false } pallet-escrow = { path = "pallets/escrow", default-features = false } diff --git a/pallets/migration/Cargo.toml b/pallets/migration/Cargo.toml new file mode 100644 index 00000000..2471c3f3 --- /dev/null +++ b/pallets/migration/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "pallet-migration" +version = "0.0.1" +description = "Migration pallet used to recreate the state of marketplace and nfts" +authors = { workspace = true } +homepage = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +parity-scale-codec = { workspace = true, default-features = false, features = [ + "derive", +] } +log = { workspace = true, default-features = false } +scale-info = { workspace = true, default-features = false, features = [ + "derive", +] } +frame-benchmarking = { workspace = true, default-features = false, optional = true } +frame-support = { workspace = true, default-features = false } +frame-system = { workspace = true, default-features = false } +sp-std = { workspace = true, default-features = false } +sp-runtime = { workspace = true, default-features = false } +sp-api = { workspace = true, default-features = false } +pallet-nfts = { workspace = true, default-features = false } +pallet-dmarket = { workspace = true, default-features = false } +pallet-timestamp = { workspace = true, default-features = false } +sp-core = { workspace = true, default-features = false } +pallet-balances = { workspace = true, default-features = false } + +# Primitives +account = { workspace = true } + +[dev-dependencies] +sp-io = { workspace = true, default-features = false } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "sp-api/std", + "sp-std/std", + "pallet-balances/std", + "pallet-nfts/std", + "pallet-dmarket/std", + "pallet-timestamp/std", +] +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/migration/src/benchmarking.rs b/pallets/migration/src/benchmarking.rs new file mode 100644 index 00000000..0d636ad5 --- /dev/null +++ b/pallets/migration/src/benchmarking.rs @@ -0,0 +1,105 @@ +#![cfg(feature = "runtime-benchmarks")] +use super::*; +use crate::Pallet as Migration; +use frame_benchmarking::v2::*; +use frame_support::{ + assert_ok, + dispatch::RawOrigin, + traits::{ + fungible::{Inspect as InspectFungible, Mutate as MutateFungible}, + tokens::nonfungibles_v2::{Create, Mutate}, + }, +}; +use pallet_dmarket::DmarketCollection; +use pallet_dmarket::Pallet as Dmarket; +use pallet_nfts::{ + CollectionConfig, CollectionSettings, ItemConfig, ItemId, MintSettings, Pallet as Nfts, +}; +const SEED: u32 = 0; + +use crate::BenchmarkHelper; + +impl BenchmarkHelper for () +where + CollectionId: From, + ItemId: From, + Moment: From, +{ + fn collection(id: u16) -> CollectionId { + id.into() + } + fn timestamp(value: u64) -> Moment { + value.into() + } +} + +fn assert_last_event(generic_event: ::RuntimeEvent) { + frame_system::Pallet::::assert_last_event(generic_event.into()); +} + +fn get_migrator() -> T::AccountId { + let migrator: T::AccountId = account("migrator", 10, SEED); + whitelist_account!(migrator); + assert_ok!(Migration::::force_set_migrator(RawOrigin::Root.into(), migrator.clone())); + + migrator +} + +fn funded_and_whitelisted_account(name: &'static str, index: u32) -> T::AccountId { + let caller: T::AccountId = account(name, index, SEED); + // Give the account half of the maximum value of the `Balance` type. + let ed = ::Currency::minimum_balance(); + let multiplier = BalanceOf::::from(1000000u32); + + ::Currency::set_balance(&caller, ed * multiplier); + whitelist_account!(caller); + caller +} + +fn mint_nft(nft_id: ItemId) -> T::AccountId { + let caller: T::AccountId = funded_and_whitelisted_account::("tokenOwner", 0); + + let default_config = CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: Some(u128::MAX), + mint_settings: MintSettings::default(), + }; + + assert_ok!(Nfts::::create_collection(&caller, &caller, &default_config)); + let collection = ::BenchmarkHelper::collection(0); + assert_ok!(Nfts::::mint_into(&collection, &nft_id, &caller, &ItemConfig::default(), true)); + caller +} +#[benchmarks()] +pub mod benchmarks { + use super::*; + + #[benchmark] + fn force_set_migrator() { + let migrator: T::AccountId = account("migrator", 0, SEED); + + #[extrinsic_call] + _(RawOrigin::Root, migrator.clone()); + + assert_last_event::(Event::MigratorUpdated(migrator).into()); + } + + #[benchmark] + fn set_item_owner() { + let migrator: T::AccountId = get_migrator::(); + let collection: T::CollectionId = ::BenchmarkHelper::collection(0); + let item: ItemId = 1; + let _ = mint_nft::(item); + let receiver: T::AccountId = account("receiver", 0, SEED); + + assert_ok!(Dmarket::::force_set_collection(RawOrigin::Root.into(), collection)); + assert_eq!(DmarketCollection::::get().unwrap(), collection); + + #[extrinsic_call] + _(RawOrigin::Signed(migrator), item.clone(), receiver.clone()); + + assert_eq!(Nfts::::owner(collection, item), Some(receiver)); + } + + impl_benchmark_test_suite!(Migration, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/migration/src/lib.rs b/pallets/migration/src/lib.rs new file mode 100644 index 00000000..f6a18ac8 --- /dev/null +++ b/pallets/migration/src/lib.rs @@ -0,0 +1,279 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod weights; +pub use weights::*; + +use parity_scale_codec::Codec; + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + dispatch::GetDispatchInfo, + traits::{nonfungibles_v2::Transfer, Currency, UnfilteredDispatchable}, + }; + use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{Inspect, Mutate}, + SortedMembers, + }, + }; + + use frame_system::{ensure_signed, pallet_prelude::*}; + use pallet_dmarket::DmarketCollection; + use pallet_nfts::ItemId; + use pallet_nfts::{ItemConfig, WeightInfo as NftWeight}; + use sp_runtime::traits::StaticLookup; + use sp_std::{vec, vec::Vec}; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: + frame_system::Config + + pallet_nfts::Config + + pallet_dmarket::Config + + pallet_timestamp::Config + { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type RuntimeCall: Parameter + + UnfilteredDispatchable + + GetDispatchInfo; + + /// The fungible trait use for balance holds and transfers. + type Currency: Inspect + Mutate; + + /// Type representing the weight of this pallet + type WeightInfo: WeightInfo; + #[cfg(feature = "runtime-benchmarks")] + /// A set of helper functions for benchmarking. + type BenchmarkHelper: BenchmarkHelper; + } + + pub type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; + + pub type NftBalanceOf = <::Currency as Currency< + ::AccountId, + >>::Balance; + + #[cfg(feature = "runtime-benchmarks")] + pub trait BenchmarkHelper { + /// Returns a collection id from a given integer. + fn collection(id: u16) -> CollectionId; + /// Returns an nft id from a given integer. + fn timestamp(value: u64) -> Moment; + } + + type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; + + #[pallet::storage] + #[pallet::getter(fn migrator)] + pub type Migrator = StorageValue<_, T::AccountId, OptionQuery>; + pub struct MigratorProvider(sp_std::marker::PhantomData); + + impl SortedMembers for MigratorProvider { + fn sorted_members() -> Vec { + if let Some(migrator) = Migrator::::get() { + return vec![migrator]; + } + vec![] + } + + fn contains(who: &T::AccountId) -> bool { + if let Some(migrator) = Migrator::::get() { + return migrator == *who; + } + false + } + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// The pallet's migrator was updated. + MigratorUpdated(T::AccountId), + } + + #[pallet::error] + pub enum Error { + /// The caller is not the migrator account. + NotMigrator, + /// The item with the given collectionId and itemId was not found. + ItemNotFound, + /// Tried to store an account that is already set for this storage value. + AccountAlreadySet, + // Migrator is not set. + MigratorNotSet, + /// The account is already the owner of the item. + AlreadyOwner, + /// The DmarketCollection is not configured. + DmarketCollectionNotSet, + } + + #[pallet::call] + impl Pallet { + /// Sets the migrator role, granting rights to call this pallet's extrinsics. + /// + /// Only the root origin can execute this function. + /// + /// Parameters: + /// - `migrator`: The account ID to be set as the pallet's migrator. + /// + /// Emits MigratorUpdated when successful. + /// + /// Weight: `WeightInfo::force_set_migrator` (defined in the `Config` trait). + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::force_set_migrator())] + pub fn force_set_migrator(origin: OriginFor, migrator: T::AccountId) -> DispatchResult { + ensure_root(origin)?; + + ensure!( + Migrator::::get().as_ref() != Some(&migrator), + Error::::AccountAlreadySet + ); + + Migrator::::put(migrator.clone()); + Self::deposit_event(Event::MigratorUpdated(migrator)); + Ok(()) + } + + /// Transfers a given Nft to an AccountId. + /// + /// Only the migrator origin can execute this function. Migrator will not be charged fees for executing the extrinsic + /// + /// Parameters: + /// - `collection`: Id of the collection for the item. + /// - `item`: Id of the item. + /// - `transfer_to`: AccountId of the user that will receive the item + /// + /// Emits `Transferred` event upon successful execution. + /// + /// Weight: `WeightInfo::set_item_owner` (defined in the `Config` trait). + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::set_item_owner())] + pub fn set_item_owner( + origin: OriginFor, + item: ItemId, + transfer_to: T::AccountId, + ) -> DispatchResultWithPostInfo { + Self::ensure_migrator(origin)?; + let collection = Self::get_dmarket_collection()?; + + let owner = pallet_nfts::Pallet::::owner(collection.clone(), item.clone()) + .ok_or(Error::::ItemNotFound)?; + + ensure!(owner != transfer_to, Error::::AlreadyOwner); + + as Transfer>::transfer( + &collection, + &item, + &transfer_to, + )?; + + Ok(Pays::No.into()) + } + + /// Dispatches a call to pallet-nfts::set_team. + /// + /// Only the migrator origin can execute this function. Migrator will not be charged fees for executing the extrinsic + /// + /// Parameters: + /// - `collection`: The collection whose team should be changed. + /// - `issuer`: The new Issuer of this collection. + /// - `admin`: The new Admin of this collection. + /// - `freezer`: The new Freezer of this collection. + /// + /// Emits `TeamChanged`. + /// + /// Weight: `WeightInfo::set_team` (defined in the `Config` trait). + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::set_team())] + pub fn set_team( + origin: OriginFor, + issuer: Option>, + admin: Option>, + freezer: Option>, + ) -> DispatchResultWithPostInfo { + Self::ensure_migrator(origin.clone())?; + let collection = Self::get_dmarket_collection()?; + + pallet_nfts::Pallet::::set_team(origin, collection, issuer, admin, freezer)?; + + Ok(Pays::No.into()) + } + + /// Dispatches a call to pallet-nfts::force_mint. + /// + /// Only the migrator origin can execute this function. Migrator will not be charged fees for executing the extrinsic + /// + /// Parameters: + /// - `collection`: The collection of the item to be minted. + /// - `item`: An identifier of the new item. + /// - `mint_to`: Account into which the item will be minted. + /// - `item_config`: A config of the new item. + /// + /// Emits `Issued` event when successful. + /// + /// Weight: `WeightInfo::force_mint` (defined in the `Config` trait). + #[pallet::call_index(3)] + #[pallet::weight(::WeightInfo::force_mint())] + pub fn force_mint( + origin: OriginFor, + item: ItemId, + mint_to: AccountIdLookupOf, + item_config: ItemConfig, + ) -> DispatchResultWithPostInfo { + Self::ensure_migrator(origin.clone())?; + let collection = Self::get_dmarket_collection()?; + + pallet_nfts::Pallet::::force_mint( + origin, + collection, + Some(item), + mint_to, + item_config, + )?; + + Ok(Pays::No.into()) + } + } + impl Pallet { + pub fn ensure_migrator(origin: OriginFor) -> Result<(), DispatchError> { + let sender = ensure_signed(origin.clone())?; + let migrator = Migrator::::get().ok_or(Error::::MigratorNotSet)?; + ensure!(sender == migrator, Error::::NotMigrator); + Ok(()) + } + + pub fn get_dmarket_collection() -> Result { + let dmarket_collection = + DmarketCollection::::get().ok_or(Error::::DmarketCollectionNotSet)?; + Ok(dmarket_collection) + } + } +} + +sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); + +sp_api::decl_runtime_apis! { + /// This runtime api allows to query the migration pot address. + pub trait MigrationApi + where AccountId: Codec + { + /// Queries the pot account. + fn pot_account_id() -> AccountId; + } +} diff --git a/pallets/migration/src/mock.rs b/pallets/migration/src/mock.rs new file mode 100644 index 00000000..82721221 --- /dev/null +++ b/pallets/migration/src/mock.rs @@ -0,0 +1,162 @@ +use frame_support::{ + derive_impl, + pallet_prelude::DispatchResult, + parameter_types, + traits::{tokens::fungible::Mutate, ConstU128, ConstU32, ConstU64}, + PalletId, +}; +use frame_system as system; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, + BuildStorage, +}; + +use account::EthereumSignature; +use system::EnsureSignedBy; + +use crate::{self as pallet_migration}; +use pallet_nfts::PalletFeatures; + +type Signature = EthereumSignature; +type AccountPublic = ::Signer; +type AccountId = ::AccountId; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Migration: pallet_migration, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + Nfts: pallet_nfts, + Dmarket: pallet_dmarket, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub storage Features: PalletFeatures = PalletFeatures::all_enabled(); +} + +pub type MigratorOrigin = EnsureSignedBy, AccountId>; + +impl pallet_nfts::Config for Test { + type RuntimeEvent = RuntimeEvent; + type CollectionId = u32; + type Currency = Balances; + type CreateOrigin = MigratorOrigin; + type ForceOrigin = MigratorOrigin; + type Locker = (); + type CollectionDeposit = ConstU128<0>; + type ItemDeposit = ConstU128<0>; + type MetadataDepositBase = ConstU128<1>; + type AttributeDepositBase = ConstU128<1>; + type DepositPerByte = ConstU128<1>; + type StringLimit = ConstU32<50>; + type KeyLimit = ConstU32<50>; + type ValueLimit = ConstU32<50>; + type ApprovalsLimit = ConstU32<10>; + type ItemAttributesApprovalsLimit = ConstU32<2>; + type MaxTips = ConstU32<10>; + type MaxDeadlineDuration = ConstU64<10000>; + type MaxAttributesPerCall = ConstU32<2>; + type Features = Features; + type OffchainSignature = Signature; + type OffchainPublic = AccountPublic; + type WeightInfo = (); + pallet_nfts::runtime_benchmarks_enabled! { + type Helper = (); + } +} + +parameter_types! { + pub const DOMAIN: [u8;8] = *b"MYTH_NET"; +} + +impl pallet_dmarket::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type Signature = Signature; + type Signer = ::Signer; + type Domain = DOMAIN; + pallet_dmarket::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +impl pallet_balances::Config for Test { + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = u128; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<3>; + type WeightInfo = (); +} + +parameter_types! { + pub const PotId: PalletId = PalletId(*b"PotMigra"); +} + +impl pallet_migration::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type WeightInfo = (); + pallet_migration::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + system::GenesisConfig::::default().build_storage().unwrap().into() +} diff --git a/pallets/migration/src/tests.rs b/pallets/migration/src/tests.rs new file mode 100644 index 00000000..acfbcfa7 --- /dev/null +++ b/pallets/migration/src/tests.rs @@ -0,0 +1,260 @@ +use crate::{mock::*, *}; +use frame_support::{ + assert_noop, assert_ok, dispatch::Pays, error::BadOrigin, traits::fungible::Mutate, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_nfts::{CollectionConfig, CollectionSettings, ItemConfig, ItemSettings, MintSettings}; + +type AccountIdOf = ::AccountId; +type Balance = ::Balance; +type CollectionId = ::CollectionId; + +fn account(id: u8) -> AccountIdOf { + [id; 32].into() +} + +fn mint_item(item: u128, owner: AccountIdOf) { + Balances::set_balance(&account(1), 100000); + if Nfts::collection_owner(0).is_none() { + assert_ok!(Nfts::create( + RuntimeOrigin::signed(account(1)), + account(1), + collection_config_with_all_settings_enabled() + )); + }; + assert_ok!(Nfts::mint(RuntimeOrigin::signed(account(1)), 0, Some(item), owner, None)); +} + +fn collection_config_with_all_settings_enabled( +) -> CollectionConfig, BlockNumberFor, CollectionId> { + CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: Some(u128::MAX), + mint_settings: MintSettings::default(), + } +} + +fn default_item_config() -> ItemConfig { + ItemConfig { settings: ItemSettings::all_enabled() } +} + +mod force_set_migrator { + use super::*; + + #[test] + fn force_set_migrator_works() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(1))); + assert_eq!(Migration::migrator(), Some(account(1))); + }) + } + + #[test] + fn fails_no_root() { + new_test_ext().execute_with(|| { + assert_noop!( + Migration::force_set_migrator(RuntimeOrigin::signed(account(1)), account(1)), + BadOrigin + ); + }) + } +} + +mod set_item_owner { + use super::*; + + #[test] + fn sender_is_not_migrator_fails() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(1))); + mint_item(1, account(1)); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), 0)); + + assert_noop!( + Migration::set_item_owner(RuntimeOrigin::signed(account(2)), 0, account(2)), + Error::::NotMigrator + ); + }) + } + + #[test] + fn dmarket_collection_not_set() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(1))); + assert_noop!( + Migration::set_item_owner(RuntimeOrigin::signed(account(1)), 0, account(2)), + Error::::DmarketCollectionNotSet + ); + }) + } + + #[test] + fn item_not_found_fails() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(1))); + mint_item(1, account(1)); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), 0)); + + assert_noop!( + Migration::set_item_owner(RuntimeOrigin::signed(account(1)), 0, account(2)), + Error::::ItemNotFound + ); + }) + } + + #[test] + fn already_owner_fails() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(1))); + mint_item(1, account(1)); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), 0)); + + assert_noop!( + Migration::set_item_owner(RuntimeOrigin::signed(account(1)), 1, account(1)), + Error::::AlreadyOwner + ); + }) + } + + #[test] + fn set_item_owner_passes() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(1))); + mint_item(1, account(1)); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), 0)); + + let res = Migration::set_item_owner(RuntimeOrigin::signed(account(1)), 1, account(2)); + assert!(res.is_ok()); + assert_eq!(res.unwrap().pays_fee, Pays::No); + + assert_eq!(Nfts::owner(0, 1), Some(account(2))); + }) + } +} + +mod set_team { + use super::*; + + #[test] + fn sender_is_not_migrator_fails() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(1))); + assert_noop!( + Migration::set_team( + RuntimeOrigin::signed(account(2)), + Some(account(3)), + Some(account(3)), + Some(account(3)) + ), + Error::::NotMigrator + ); + }) + } + + #[test] + fn dmarket_collection_not_set() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(2))); + assert_ok!(Nfts::force_create( + RuntimeOrigin::signed(account(2)), + account(3), + collection_config_with_all_settings_enabled() + )); + + assert_noop!( + Migration::set_team( + RuntimeOrigin::signed(account(2)), + Some(account(3)), + Some(account(3)), + Some(account(3)), + ), + Error::::DmarketCollectionNotSet + ); + }) + } + + #[test] + fn set_team_redispatch_works() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(2))); + assert_ok!(Nfts::force_create( + RuntimeOrigin::signed(account(2)), + account(3), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), 0)); + + let res = Migration::set_team( + RuntimeOrigin::signed(account(2)), + Some(account(3)), + Some(account(3)), + Some(account(3)), + ); + assert!(res.is_ok()); + assert_eq!(res.unwrap().pays_fee, Pays::No) + }) + } +} + +mod force_mint { + use super::*; + + #[test] + fn sender_is_not_migrator_fails() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(1))); + assert_noop!( + Migration::force_mint( + RuntimeOrigin::signed(account(2)), + 0, + account(3), + default_item_config() + ), + Error::::NotMigrator + ); + }) + } + + #[test] + fn dmarket_collection_not_set() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(2))); + assert_ok!(Nfts::force_create( + RuntimeOrigin::signed(account(2)), + account(3), + collection_config_with_all_settings_enabled() + )); + + assert_noop!( + Migration::force_mint( + RuntimeOrigin::signed(account(2)), + 1, + account(3), + default_item_config(), + ), + Error::::DmarketCollectionNotSet + ); + }) + } + #[test] + fn force_mint_redispatch_works() { + new_test_ext().execute_with(|| { + assert_ok!(Migration::force_set_migrator(RuntimeOrigin::root(), account(2))); + assert_ok!(Nfts::force_create( + RuntimeOrigin::signed(account(2)), + account(3), + collection_config_with_all_settings_enabled() + )); + assert_ok!(Dmarket::force_set_collection(RuntimeOrigin::root(), 0)); + + let res = Migration::force_mint( + RuntimeOrigin::signed(account(2)), + 1, + account(3), + default_item_config(), + ); + assert!(res.is_ok()); + assert_eq!(res.unwrap().pays_fee, Pays::No) + }) + } +} diff --git a/pallets/migration/src/weights.rs b/pallets/migration/src/weights.rs new file mode 100644 index 00000000..efe14bea --- /dev/null +++ b/pallets/migration/src/weights.rs @@ -0,0 +1,132 @@ + +//! Autogenerated weights for `pallet_migration` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-07-25, STEPS: `10`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `pop-os`, CPU: `AMD Ryzen 9 7900X 12-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/mythos-node +// benchmark +// pallet +// --steps +// 10 +// --template +// ./.maintain/template.hbs +// --repeat +// 20 +// --extrinsic +// * +// --wasm-execution +// compiled +// --heap-pages +// 4096 +// --pallet +// pallet_migration +// --output +// ./pallets/migration/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_migration`. +pub trait WeightInfo { + fn force_set_migrator() -> Weight; + fn set_item_owner() -> Weight; +} + +/// Weights for `pallet_migration` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); + impl WeightInfo for SubstrateWeight { + /// Storage: `Migration::Migrator` (r:1 w:1) + /// Proof: `Migration::Migrator` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn force_set_migrator() -> Weight { + // Proof Size summary in bytes: + // Measured: `3` + // Estimated: `1505` + // Minimum execution time: 4_220_000 picoseconds. + Weight::from_parts(4_420_000, 1505) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Migration::Migrator` (r:1 w:0) + /// Proof: `Migration::Migrator` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:0) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(637), added: 3112, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(169), added: 2644, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(495), added: 2970, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(117), added: 2592, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(151), added: 2626, mode: `MaxEncodedLen`) + fn set_item_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `900` + // Estimated: `4102` + // Minimum execution time: 37_960_000 picoseconds. + Weight::from_parts(39_180_000, 4102) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Migration::Migrator` (r:1 w:1) + /// Proof: `Migration::Migrator` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn force_set_migrator() -> Weight { + // Proof Size summary in bytes: + // Measured: `3` + // Estimated: `1505` + // Minimum execution time: 4_220_000 picoseconds. + Weight::from_parts(4_420_000, 1505) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Migration::Migrator` (r:1 w:0) + /// Proof: `Migration::Migrator` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:0) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(637), added: 3112, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(169), added: 2644, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(495), added: 2970, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(117), added: 2592, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(151), added: 2626, mode: `MaxEncodedLen`) + fn set_item_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `900` + // Estimated: `4102` + // Minimum execution time: 37_960_000 picoseconds. + Weight::from_parts(39_180_000, 4102) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } +} diff --git a/runtime/testnet/Cargo.toml b/runtime/testnet/Cargo.toml index 94b4557c..1983e434 100644 --- a/runtime/testnet/Cargo.toml +++ b/runtime/testnet/Cargo.toml @@ -25,6 +25,7 @@ smallvec = { workspace = true } runtime-common = { workspace = true, default-features = false } pallet-dmarket = { workspace = true, default-features = false } pallet-marketplace = { workspace = true, default-features = false } +pallet-migration = { workspace = true, default-features = false } pallet-multibatching = { workspace = true, default-features = false } xcm-primitives = { path = "../../primitives/xcm", default-features = false } pallet-escrow = { workspace = true, default-features = false } @@ -124,6 +125,7 @@ std = [ "pallet-dmarket/std", "pallet-multibatching/std", "pallet-marketplace/std", + "pallet-migration/std", "pallet-multisig/std", "pallet-nfts/std", "pallet-session/std", @@ -174,6 +176,7 @@ runtime-benchmarks = [ "pallet-message-queue/runtime-benchmarks", "pallet-dmarket/runtime-benchmarks", "pallet-marketplace/runtime-benchmarks", + "pallet-migration/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", "pallet-multibatching/runtime-benchmarks", "pallet-nfts/runtime-benchmarks", @@ -210,6 +213,7 @@ try-runtime = [ "pallet-dmarket/try-runtime", "pallet-multibatching/try-runtime", "pallet-marketplace/try-runtime", + "pallet-migration/try-runtime", "pallet-multisig/try-runtime", "pallet-nfts/try-runtime", "pallet-session/try-runtime", diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index d9432e45..145242dd 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -663,6 +663,15 @@ impl pallet_dmarket::Config for Runtime { type Signature = Signature; type Signer = ::Signer; type Domain = DOMAIN; + type WeightInfo = weights::pallet_dmarket::WeightInfo; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); +} + +impl pallet_migration::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); @@ -861,6 +870,7 @@ construct_runtime!( // Other pallets Proxy: pallet_proxy = 40, Vesting: pallet_vesting = 41, + Migration: pallet_migration = 42, Escrow: pallet_escrow = 50, MythProxy: pallet_myth_proxy = 51, @@ -936,6 +946,7 @@ mod benches { [pallet_collator_selection, CollatorSelection] [pallet_nfts, Nfts] [pallet_marketplace, Marketplace] + [pallet_migration, Migration] [pallet_proxy, Proxy] [pallet_escrow, Escrow] [pallet_vesting, Vesting] From 8d86717a386abddc72859f34c0db011d95349de4 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Tue, 13 Aug 2024 13:51:29 -0300 Subject: [PATCH 09/14] Add dmarket docs --- pallets/dmarket/README.md | 29 +++++++++++++++++- pallets/dmarket/src/lib.rs | 60 +++++++++++++++++++++++++++++++------- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/pallets/dmarket/README.md b/pallets/dmarket/README.md index 368e02d6..9fcc8b5a 100644 --- a/pallets/dmarket/README.md +++ b/pallets/dmarket/README.md @@ -1,3 +1,30 @@ # Dmarket Pallet -The Dmarket pallet provides a market to buy and sell Nfts from pallet-nft, based on the dmarket Smart contracts from Mythical. +The Dmarket pallet provides a marketplace for buying and selling NFTs that are managed by the `pallet-nfts`, based on the dmarket Smart contracts developed by Mythical. + +## Overview + +This project enables users to securely trade NFTs that are part of the Dmarket collection by allowing both the seller and buyer to agree on the terms and digitally sign a message approving the trade. The signed messages include specific parameters that ensure both parties are in agreement before the trade is executed on the blockchain. + +For the seller: + +- Domain: The network domain identifier, specifying the environment in which the trade is executed. Helps to prevent transaction replay on other chains that use this very same pallet. +- Sender: The account authorized to submit the trade transaction to the blockchain. +- FeeAccount: The account designated to receive the trade fee. +- ItemId: The unique identifier of the NFT being traded. Must be part of the Dmarket Colection +- Price: The selling price set by the seller for the NFT. +- AskExpirationAt: The expiration timestamp, after which the seller's signature is no longer valid. + +For the Buyer: + +- Domain, Sender, FeeAccount, ItemId, and Price: These parameters must match those in the seller's message to ensure both parties are in agreement. + +- Fee: The amount of tokens the buyer agrees to pay as a fee for the trade. +- BiExpirationAt: The expiration timestamp, after which the buyer's signature is no longer valid. + +Once the seller and buyer have signed their respective messages, an agreed-upon sender can submit the trade to the blockchain. The transaction validates the signatures against the provided trade parameters. If the signatures are valid and the trade conditions are met, the NFT is transferred from the seller to the buyer. Simultaneously, the agreed-upon price is transferred from the buyer to the seller, and the fee is transferred to the FeeAccount. + +## Dispatchable Functions + +- `force_set_collection()`: Sets the Dmarket collection. Only callable by root. +- `execute_trade()`: Execute a trade between a seller and a buyer for a specific NFT (item) in the configured DmarketCollection. Callable by anyone as long as the origin matches the sender field inside both Ask and Bid signed messages. diff --git a/pallets/dmarket/src/lib.rs b/pallets/dmarket/src/lib.rs index f3b9efaf..fa8dc7de 100644 --- a/pallets/dmarket/src/lib.rs +++ b/pallets/dmarket/src/lib.rs @@ -119,34 +119,48 @@ pub mod pallet { ItemNotFound, /// Item can only be operated by the Item owner. SellerNotItemOwner, - /// + /// The bid with the provided parameters has already been executed BidAlreadyExecuted, - /// + /// The ask with the provided parameters has already been executed AskAlreadyExecuted, - /// + /// Buyer balance is not enough to pay for trade costs BuyerBalanceTooLow, - /// + /// Bid expiration timestamp must be in the future BidExpired, - /// + /// Ask expiration timestamp must be in the future AskExpired, - /// + /// The signature provided by the buyer is invalid InvalidBuyerSignature, - /// + /// The signature provided by the seller is invalid InvalidSellerSignature, /// Same buyer and seller not allowed. BuyerIsSeller, - /// + /// Invalid Signed message BadSignedMessage, - /// + /// Dmarket collection already set to the provided value. CollectionAlreadyInUse, - /// + /// Dmarket collection has not been set CollectionNotSet, - /// + /// The provided Dmarket collect was not found CollectionNotFound, } #[pallet::call] impl Pallet { + /// Sets the Dmarket collection. + /// + /// Only the root origin can execute this function. + /// + /// Precondition: + /// - The collection must already exist, otherwise the extrinsic will fail. + /// + /// Parameters: + /// - `collection_id`: The collectionID of the NFT collection to be set as the Dmarket Collection. + /// + /// + /// Emits CollectionUpdated when successful. + /// + /// Weight: `WeightInfo::force_set_collection` (defined in the `Config` trait). #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::force_set_collection())] pub fn force_set_collection( @@ -168,6 +182,30 @@ pub mod pallet { Ok(()) } + /// Execute a trade between a seller and a buyer for a specific NFT (item) in the configured DmarketCollection. + /// + /// Preconditions: + /// - The seller and buyer must be different accounts. + /// - The seller must be the current owner of the NFT item. + /// - The trade must not be expired, and signatures provided must be valid. + /// + /// Parameters: + /// - `origin`: The origin of the call, which must be part of the signed message of both seller and buyer. + /// - `seller`: The account ID of the seller who owns the NFT item. + /// - `buyer`: The account ID of the buyer who will purchase the NFT item. + /// - `trade`: The parameters of the trade, including item details, prices, and expiration times. + /// - `signatures`: The signatures from both the seller and buyer authorizing the trade. + /// - `fee_address`: The account ID where the transaction fee will be transferred. + /// + /// Signed message schema: + /// - Ask: (domain, sender, fee_address, item, price, expiration). + /// - Bid: (domain, sender, fee_address, item, price, fee, expiration). + /// + /// Only callable if origin matches `sender` in both Ask and Bid signed messages. + /// + /// Emits `Trade` event upon successful execution. + /// + /// Weight: `WeightInfo::execute_trade` (defined in the `Config` trait). #[pallet::call_index(1)] #[pallet::weight(::WeightInfo::execute_trade())] pub fn execute_trade( From dea0affaef3a32885fce605f99e9e5250ee54a38 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Wed, 14 Aug 2024 15:56:56 -0300 Subject: [PATCH 10/14] remove unnecessary runtime api on migration pallet --- pallets/migration/src/lib.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pallets/migration/src/lib.rs b/pallets/migration/src/lib.rs index f6a18ac8..7bd5b8df 100644 --- a/pallets/migration/src/lib.rs +++ b/pallets/migration/src/lib.rs @@ -267,13 +267,3 @@ pub mod pallet { } sp_core::generate_feature_enabled_macro!(runtime_benchmarks_enabled, feature = "runtime-benchmarks", $); - -sp_api::decl_runtime_apis! { - /// This runtime api allows to query the migration pot address. - pub trait MigrationApi - where AccountId: Codec - { - /// Queries the pot account. - fn pot_account_id() -> AccountId; - } -} From 3a9a3631ad0dca800247e58769a3af7f959b6496 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Wed, 14 Aug 2024 16:05:35 -0300 Subject: [PATCH 11/14] small cleanup --- pallets/migration/src/lib.rs | 2 -- pallets/migration/src/mock.rs | 7 +++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pallets/migration/src/lib.rs b/pallets/migration/src/lib.rs index 7bd5b8df..1123e257 100644 --- a/pallets/migration/src/lib.rs +++ b/pallets/migration/src/lib.rs @@ -10,8 +10,6 @@ mod benchmarking; pub mod weights; pub use weights::*; -use parity_scale_codec::Codec; - pub use pallet::*; #[frame_support::pallet] diff --git a/pallets/migration/src/mock.rs b/pallets/migration/src/mock.rs index 82721221..ef90bec6 100644 --- a/pallets/migration/src/mock.rs +++ b/pallets/migration/src/mock.rs @@ -1,8 +1,6 @@ use frame_support::{ - derive_impl, - pallet_prelude::DispatchResult, - parameter_types, - traits::{tokens::fungible::Mutate, ConstU128, ConstU32, ConstU64}, + derive_impl, parameter_types, + traits::{ConstU128, ConstU32, ConstU64}, PalletId, }; use frame_system as system; @@ -114,6 +112,7 @@ impl pallet_dmarket::Config for Test { type Signature = Signature; type Signer = ::Signer; type Domain = DOMAIN; + type WeightInfo = (); pallet_dmarket::runtime_benchmarks_enabled! { type BenchmarkHelper = (); } From b9e6b50ce334d02842dac017e31179452aecaf92 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Thu, 15 Aug 2024 09:28:56 -0300 Subject: [PATCH 12/14] Increase spec-version --- runtime/testnet/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index 6c5699da..ca3c943a 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -226,7 +226,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("muse"), impl_name: create_runtime_str!("muse"), authoring_version: 1, - spec_version: 1012, + spec_version: 1013, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From cca06f9214ae321ab99e8028d51dc7f56e685d50 Mon Sep 17 00:00:00 2001 From: Valentin Fernandez Date: Thu, 15 Aug 2024 12:40:29 -0300 Subject: [PATCH 13/14] Address PR comments --- pallets/dmarket/src/lib.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pallets/dmarket/src/lib.rs b/pallets/dmarket/src/lib.rs index fa8dc7de..ec073689 100644 --- a/pallets/dmarket/src/lib.rs +++ b/pallets/dmarket/src/lib.rs @@ -249,6 +249,13 @@ pub mod pallet { ) .map_err(|_| Error::::InvalidBuyerSignature)?; + let order_data: OrderDataOf = + OrderData { caller: who, fee_address: fee_address.clone() }; + + //Store closed trades + ClosedAsks::::insert(ask_hash, order_data.clone()); + ClosedBids::::insert(bid_hash, order_data); + as Transfer>::transfer( &collection, &trade.item, @@ -258,12 +265,6 @@ pub mod pallet { .map_err(|_| Error::::BuyerBalanceTooLow)?; ::Currency::transfer(&seller, &fee_address, trade.fee, Preserve)?; - let order_data: OrderDataOf = OrderData { caller: who, fee_address }; - - //Store closed trades - ClosedAsks::::insert(ask_hash, order_data.clone()); - ClosedBids::::insert(bid_hash, order_data); - Self::deposit_event(Event::Trade { seller, buyer, From 53258aafea0ddb76e658f74ceea2dc58fa17db20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Molina?= Date: Fri, 16 Aug 2024 18:11:14 +0200 Subject: [PATCH 14/14] Add weights for pallet-migration on testnet --- runtime/testnet/src/lib.rs | 3 +- runtime/testnet/src/weights/mod.rs | 1 + .../testnet/src/weights/pallet_migration.rs | 82 +++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 runtime/testnet/src/weights/pallet_migration.rs diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index ca3c943a..a93a37df 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -114,7 +114,6 @@ pub type Executive = frame_executive::Executive< >; pub mod fee { - use super::{Balance, ExtrinsicBaseWeight, MILLI_MUSE, MILLI_ROC}; use frame_support::weights::{ constants::WEIGHT_REF_TIME_PER_SECOND, FeePolynomial, Weight, WeightToFeeCoefficient, @@ -672,7 +671,7 @@ impl pallet_migration::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeCall = RuntimeCall; type Currency = Balances; - type WeightInfo = (); + type WeightInfo = weights::pallet_migration::WeightInfo; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); } diff --git a/runtime/testnet/src/weights/mod.rs b/runtime/testnet/src/weights/mod.rs index b80dbd31..538d296b 100644 --- a/runtime/testnet/src/weights/mod.rs +++ b/runtime/testnet/src/weights/mod.rs @@ -37,6 +37,7 @@ pub mod pallet_dmarket; pub mod pallet_escrow; pub mod pallet_marketplace; pub mod pallet_message_queue; +pub mod pallet_migration; pub mod pallet_multibatching; pub mod pallet_multisig; pub mod pallet_myth_proxy; diff --git a/runtime/testnet/src/weights/pallet_migration.rs b/runtime/testnet/src/weights/pallet_migration.rs new file mode 100644 index 00000000..8b0ccf2a --- /dev/null +++ b/runtime/testnet/src/weights/pallet_migration.rs @@ -0,0 +1,82 @@ + +//! Autogenerated weights for `pallet_migration` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2024-08-16, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bdl-ref`, CPU: `AMD EPYC 7232P 8-Core Processor` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("local-v")`, DB CACHE: 1024 + +// Executed Command: +// ./target/release/mythos-node +// benchmark +// pallet +// --chain +// local-v +// --pallet +// pallet_migration +// --extrinsic +// * +// --wasm-execution +// compiled +// --steps +// 50 +// --repeat +// 20 +// --output +// ./runtime/testnet/src/weights/pallet_migration.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_migration`. +pub struct WeightInfo(PhantomData); +impl pallet_migration::WeightInfo for WeightInfo { + /// Storage: `Migration::Migrator` (r:1 w:1) + /// Proof: `Migration::Migrator` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + fn force_set_migrator() -> Weight { + // Proof Size summary in bytes: + // Measured: `3` + // Estimated: `1505` + // Minimum execution time: 10_780_000 picoseconds. + Weight::from_parts(11_620_000, 0) + .saturating_add(Weight::from_parts(0, 1505)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: `Migration::Migrator` (r:1 w:0) + /// Proof: `Migration::Migrator` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `Dmarket::DmarketCollection` (r:1 w:0) + /// Proof: `Dmarket::DmarketCollection` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Item` (r:1 w:1) + /// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(637), added: 3112, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Collection` (r:1 w:0) + /// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(169), added: 2644, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Attribute` (r:1 w:0) + /// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(495), added: 2970, mode: `MaxEncodedLen`) + /// Storage: `Nfts::CollectionConfigOf` (r:1 w:0) + /// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(142), added: 2617, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemConfigOf` (r:1 w:0) + /// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`) + /// Storage: `Nfts::Account` (r:0 w:2) + /// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(116), added: 2591, mode: `MaxEncodedLen`) + /// Storage: `Nfts::ItemPriceOf` (r:0 w:1) + /// Proof: `Nfts::ItemPriceOf` (`max_values`: None, `max_size`: Some(117), added: 2592, mode: `MaxEncodedLen`) + /// Storage: `Nfts::PendingSwapOf` (r:0 w:1) + /// Proof: `Nfts::PendingSwapOf` (`max_values`: None, `max_size`: Some(151), added: 2626, mode: `MaxEncodedLen`) + fn set_item_owner() -> Weight { + // Proof Size summary in bytes: + // Measured: `900` + // Estimated: `4102` + // Minimum execution time: 82_700_000 picoseconds. + Weight::from_parts(84_051_000, 0) + .saturating_add(Weight::from_parts(0, 4102)) + .saturating_add(T::DbWeight::get().reads(7)) + .saturating_add(T::DbWeight::get().writes(5)) + } +}