difftreelog
Merge branch 'develop' into feature/NFTPAR-268_inregration_test_broken
in: master
24 files changed
node/src/chain_spec.rsdiffbeforeafterboth--- a/node/src/chain_spec.rs
+++ b/node/src/chain_spec.rs
@@ -193,7 +193,7 @@
offchain_schema: vec![],
schema_version: SchemaVersion::default(),
sponsor: get_account_id_from_seed::<sr25519::Public>("Alice"),
- unconfirmed_sponsor: get_account_id_from_seed::<sr25519::Public>("Alice"),
+ sponsor_confirmed: true,
const_on_chain_schema: vec![],
variable_on_chain_schema: vec![],
limits: CollectionLimits::default()
pallets/nft/src/lib.rsdiffbeforeafterboth1//2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.4//56#![recursion_limit = "1024"]78#![cfg_attr(not(feature = "std"), no_std)]910#[cfg(feature = "std")]11pub use std::*;1213#[cfg(feature = "std")]14pub use serde::*;1516use codec::{Decode, Encode};17pub use frame_support::{18 construct_runtime, decl_event, decl_module, decl_storage, decl_error,19 dispatch::DispatchResult,20 ensure, fail, parameter_types,21 traits::{22 Currency, ExistenceRequirement, Get, Imbalance, KeyOwnerProofSystem, OnUnbalanced,23 Randomness, WithdrawReason,24 },25 weights::{26 constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},27 DispatchInfo, GetDispatchInfo, IdentityFee, Pays, PostDispatchInfo, Weight,28 WeightToFeePolynomial,29 },30 IsSubType, StorageValue,31};3233use frame_system::{self as system, ensure_signed, ensure_root};34use sp_runtime::sp_std::prelude::Vec;35use sp_runtime::{36 traits::{37 DispatchInfoOf, Dispatchable, PostDispatchInfoOf, Saturating, SaturatedConversion, SignedExtension, Zero,38 },39 transaction_validity::{40 TransactionPriority, InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction,41 },42 FixedPointOperand, FixedU128,43};44use pallet_contracts::ContractAddressFor;45use sp_runtime::traits::StaticLookup;4647#[cfg(test)]48mod mock;4950#[cfg(test)]51mod tests;5253mod default_weights;5455pub const MAX_DECIMAL_POINTS: DecimalPoints = 30;56pub const MAX_SPONSOR_TIMEOUT: u32 = 10_368_000;57pub const MAX_TOKEN_OWNERSHIP: u32 = 10_000_000;5859// Structs60// #region6162pub type CollectionId = u32;63pub type TokenId = u32;64pub type DecimalPoints = u8;6566#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)]67#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]68pub enum CollectionMode {69 Invalid,70 NFT,71 // decimal points72 Fungible(DecimalPoints),73 // decimal points74 ReFungible(DecimalPoints),75}7677impl Into<u8> for CollectionMode {78 fn into(self) -> u8 {79 match self {80 CollectionMode::Invalid => 0,81 CollectionMode::NFT => 1,82 CollectionMode::Fungible(_) => 2,83 CollectionMode::ReFungible(_) => 3,84 }85 }86}8788#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)]89#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]90pub enum AccessMode {91 Normal,92 WhiteList,93}94impl Default for AccessMode {95 fn default() -> Self {96 Self::Normal97 }98}99100impl Default for CollectionMode {101 fn default() -> Self {102 Self::Invalid103 }104}105106#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)]107#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]108pub enum SchemaVersion {109 ImageURL,110 Unique,111}112impl Default for SchemaVersion {113 fn default() -> Self {114 Self::ImageURL115 }116}117118#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]119#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]120pub struct Ownership<AccountId> {121 pub owner: AccountId,122 pub fraction: u128,123}124125#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]126#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]127pub struct CollectionType<AccountId> {128 pub owner: AccountId,129 pub mode: CollectionMode,130 pub access: AccessMode,131 pub decimal_points: DecimalPoints,132 pub name: Vec<u16>, // 64 include null escape char133 pub description: Vec<u16>, // 256 include null escape char134 pub token_prefix: Vec<u8>, // 16 include null escape char135 pub mint_mode: bool,136 pub offchain_schema: Vec<u8>,137 pub schema_version: SchemaVersion,138 pub sponsor: AccountId, // Who pays fees. If set to default address, the fees are applied to the transaction sender139 pub unconfirmed_sponsor: AccountId, // Sponsor address that has not yet confirmed sponsorship140 pub limits: CollectionLimits, // Collection private restrictions 141 pub variable_on_chain_schema: Vec<u8>, //142 pub const_on_chain_schema: Vec<u8>, //143}144145#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]146#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]147pub struct NftItemType<AccountId> {148 pub collection: CollectionId,149 pub owner: AccountId,150 pub const_data: Vec<u8>,151 pub variable_data: Vec<u8>,152}153154#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]155#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]156pub struct FungibleItemType<AccountId> {157 pub collection: CollectionId,158 pub owner: AccountId,159 pub value: u128,160}161162#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]163#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]164pub struct ReFungibleItemType<AccountId> {165 pub collection: CollectionId,166 pub owner: Vec<Ownership<AccountId>>,167 pub const_data: Vec<u8>,168 pub variable_data: Vec<u8>,169}170171#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]172#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]173pub struct ApprovePermissions<AccountId> {174 pub approved: AccountId,175 pub amount: u128,176}177178#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]179#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]180pub struct VestingItem<AccountId, Moment> {181 pub sender: AccountId,182 pub recipient: AccountId,183 pub collection_id: CollectionId,184 pub item_id: TokenId,185 pub amount: u64,186 pub vesting_date: Moment,187}188189#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]190#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]191pub struct BasketItem<AccountId, BlockNumber> {192 pub address: AccountId,193 pub start_block: BlockNumber,194}195196#[derive(Encode, Decode, Debug, Clone, PartialEq)]197#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]198pub struct CollectionLimits {199 pub account_token_ownership_limit: u32,200 pub sponsored_data_size: u32,201 pub token_limit: u32,202203 // Timeouts for item types in passed blocks204 pub sponsor_transfer_timeout: u32,205}206207impl Default for CollectionLimits {208 fn default() -> CollectionLimits {209 CollectionLimits { 210 account_token_ownership_limit: 10_000_000, 211 token_limit: u32::max_value(),212 sponsored_data_size: u32::max_value(), 213 sponsor_transfer_timeout: 14400 }214 }215}216217#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]218#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]219pub struct ChainLimits {220 pub collection_numbers_limit: u32,221 pub account_token_ownership_limit: u32,222 pub collections_admins_limit: u64,223 pub custom_data_limit: u32,224225 // Timeouts for item types in passed blocks226 pub nft_sponsor_transfer_timeout: u32,227 pub fungible_sponsor_transfer_timeout: u32,228 pub refungible_sponsor_transfer_timeout: u32,229}230231pub trait WeightInfo {232 fn create_collection() -> Weight;233 fn destroy_collection() -> Weight;234 fn add_to_white_list() -> Weight;235 fn remove_from_white_list() -> Weight;236 fn set_public_access_mode() -> Weight;237 fn set_mint_permission() -> Weight;238 fn change_collection_owner() -> Weight;239 fn add_collection_admin() -> Weight;240 fn remove_collection_admin() -> Weight;241 fn set_collection_sponsor() -> Weight;242 fn confirm_sponsorship() -> Weight;243 fn remove_collection_sponsor() -> Weight;244 fn create_item(s: usize) -> Weight;245 fn burn_item() -> Weight;246 fn transfer() -> Weight;247 fn approve() -> Weight;248 fn transfer_from() -> Weight;249 fn set_offchain_schema() -> Weight;250 fn set_const_on_chain_schema() -> Weight;251 fn set_variable_on_chain_schema() -> Weight;252 fn set_variable_meta_data() -> Weight;253 fn enable_contract_sponsoring() -> Weight;254 fn set_schema_version() -> Weight;255 fn set_chain_limits() -> Weight;256 fn set_contract_sponsoring_rate_limit() -> Weight;257 fn toggle_contract_white_list() -> Weight;258 fn add_to_contract_white_list() -> Weight;259 fn remove_from_contract_white_list() -> Weight;260 fn set_collection_limits() -> Weight;261}262263#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]264#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]265pub struct CreateNftData {266 pub const_data: Vec<u8>,267 pub variable_data: Vec<u8>,268}269270#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]271#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]272pub struct CreateFungibleData {273}274275#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]276#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]277pub struct CreateReFungibleData {278 pub const_data: Vec<u8>,279 pub variable_data: Vec<u8>,280}281282#[derive(Encode, Decode, Debug, Clone, PartialEq)]283#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]284pub enum CreateItemData {285 NFT(CreateNftData),286 Fungible(CreateFungibleData),287 ReFungible(CreateReFungibleData),288}289290impl CreateItemData {291 pub fn len(&self) -> usize {292 let len = match self {293 CreateItemData::NFT(data) => data.variable_data.len() + data.const_data.len(),294 CreateItemData::ReFungible(data) => data.variable_data.len() + data.const_data.len(),295 _ => 0296 };297 298 return len;299 }300}301302impl From<CreateNftData> for CreateItemData {303 fn from(item: CreateNftData) -> Self {304 CreateItemData::NFT(item)305 }306}307308impl From<CreateReFungibleData> for CreateItemData {309 fn from(item: CreateReFungibleData) -> Self {310 CreateItemData::ReFungible(item)311 }312}313314impl From<CreateFungibleData> for CreateItemData {315 fn from(item: CreateFungibleData) -> Self {316 CreateItemData::Fungible(item)317 }318}319320321decl_error! {322 /// Error for non-fungible-token module.323 pub enum Error for Module<T: Trait> {324 /// Total collections bound exceeded.325 TotalCollectionsLimitExceeded,326 /// Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.327 CollectionDecimalPointLimitExceeded, 328 /// Collection name can not be longer than 63 char.329 CollectionNameLimitExceeded, 330 /// Collection description can not be longer than 255 char.331 CollectionDescriptionLimitExceeded, 332 /// Token prefix can not be longer than 15 char.333 CollectionTokenPrefixLimitExceeded,334 /// This collection does not exist.335 CollectionNotFound,336 /// Item not exists.337 TokenNotFound,338 /// Arithmetic calculation overflow.339 NumOverflow, 340 /// Account already has admin role.341 AlreadyAdmin, 342 /// You do not own this collection.343 NoPermission,344 /// This address is not set as sponsor, use setCollectionSponsor first.345 ConfirmUnsetSponsorFail,346 /// Collection is not in mint mode.347 PublicMintingNotAllowed,348 /// Sender parameter and item owner must be equal.349 MustBeTokenOwner,350 /// Item balance not enough.351 TokenValueTooLow,352 /// Size of item is too large.353 NftSizeLimitExceeded,354 /// No approve found355 ApproveNotFound,356 /// Requested value more than approved.357 TokenValueNotEnough,358 /// Only approved addresses can call this method.359 ApproveRequired,360 /// Address is not in white list.361 AddresNotInWhiteList,362 /// Number of collection admins bound exceeded.363 CollectionAdminsLimitExceeded,364 /// Owned tokens by a single address bound exceeded.365 AddressOwnershipLimitExceeded,366 /// Length of items properties must be greater than 0.367 EmptyArgument,368 /// const_data exceeded data limit.369 TokenConstDataLimitExceeded,370 /// variable_data exceeded data limit.371 TokenVariableDataLimitExceeded,372 /// Not NFT item data used to mint in NFT collection.373 NotNftDataUsedToMintNftCollectionToken,374 /// Not Fungible item data used to mint in Fungible collection.375 NotFungibleDataUsedToMintFungibleCollectionToken,376 /// Not Re Fungible item data used to mint in Re Fungible collection.377 NotReFungibleDataUsedToMintReFungibleCollectionToken,378 /// Unexpected collection type.379 UnexpectedCollectionType,380 /// Can't store metadata in fungible tokens.381 CantStoreMetadataInFungibleTokens,382 /// Collection token limit exceeded383 CollectionTokenLimitExceeded,384 /// Account token limit exceeded per collection385 AccountTokenLimitExceeded,386 /// Collection limit bounds per collection exceeded387 CollectionLimitBoundsExceeded388 }389}390391pub trait Trait: system::Trait + Sized + transaction_payment::Trait + pallet_contracts::Trait {392 type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;393394 /// Weight information for extrinsics in this pallet.395 type WeightInfo: WeightInfo;396}397398#[cfg(feature = "runtime-benchmarks")]399mod benchmarking;400401// #endregion402403decl_storage! {404 trait Store for Module<T: Trait> as Nft {405406 // Private members407 NextCollectionID: CollectionId;408 CreatedCollectionCount: u32;409 ChainVersion: u64;410 ItemListIndex: map hasher(identity) CollectionId => TokenId;411412 // Chain limits struct413 pub ChainLimit get(fn chain_limit) config(): ChainLimits;414415 // Bound counters416 CollectionCount: u32;417 pub AccountItemCount get(fn account_item_count): map hasher(twox_64_concat) T::AccountId => u32;418419 // Basic collections420 pub Collection get(fn collection) config(): map hasher(identity) CollectionId => CollectionType<T::AccountId>;421 pub AdminList get(fn admin_list_collection): map hasher(identity) CollectionId => Vec<T::AccountId>;422 pub WhiteList get(fn white_list): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => bool;423424 /// Balance owner per collection map425 pub Balance get(fn balance_count): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => u128;426427 /// second parameter: item id + owner account id428 pub ApprovedList get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId) => Vec<ApprovePermissions<T::AccountId>>;429430 /// Item collections431 pub NftItemList get(fn nft_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => NftItemType<T::AccountId>;432 pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => FungibleItemType<T::AccountId>;433 pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => ReFungibleItemType<T::AccountId>;434435 /// Index list436 pub AddressTokens get(fn address_tokens): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => Vec<TokenId>;437438 /// Tokens transfer baskets439 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;440 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => Vec<BasketItem<T::AccountId, T::BlockNumber>>;441 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;442443 // Contract Sponsorship and Ownership444 pub ContractOwner get(fn contract_owner): map hasher(twox_64_concat) T::AccountId => T::AccountId;445 pub ContractSelfSponsoring get(fn contract_self_sponsoring): map hasher(twox_64_concat) T::AccountId => bool;446 pub ContractSponsorBasket get(fn contract_sponsor_basket): map hasher(twox_64_concat) (T::AccountId, T::AccountId) => T::BlockNumber;447 pub ContractSponsoringRateLimit get(fn contract_sponsoring_rate_limit): map hasher(twox_64_concat) T::AccountId => T::BlockNumber;448 pub ContractWhiteListEnabled get(fn contract_white_list_enabled): map hasher(twox_64_concat) T::AccountId => bool; 449 pub ContractWhiteList get(fn contract_white_list): double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) T::AccountId => bool; 450 }451 add_extra_genesis {452 build(|config: &GenesisConfig<T>| {453 // Modification of storage454 for (_num, _c) in &config.collection {455 <Module<T>>::init_collection(_c);456 }457458 for (_num, _q, _i) in &config.nft_item_id {459 <Module<T>>::init_nft_token(_i);460 }461462 for (_num, _q, _i) in &config.fungible_item_id {463 <Module<T>>::init_fungible_token(_i);464 }465466 for (_num, _q, _i) in &config.refungible_item_id {467 <Module<T>>::init_refungible_token(_i);468 }469 })470 }471}472473decl_event!(474 pub enum Event<T>475 where476 AccountId = <T as system::Trait>::AccountId,477 {478 /// New collection was created479 /// 480 /// # Arguments481 /// 482 /// * collection_id: Globally unique identifier of newly created collection.483 /// 484 /// * mode: [CollectionMode] converted into u8.485 /// 486 /// * account_id: Collection owner.487 Created(CollectionId, u8, AccountId),488489 /// New item was created.490 /// 491 /// # Arguments492 /// 493 /// * collection_id: Id of the collection where item was created.494 /// 495 /// * item_id: Id of an item. Unique within the collection.496 ItemCreated(CollectionId, TokenId),497498 /// Collection item was burned.499 /// 500 /// # Arguments501 /// 502 /// collection_id.503 /// 504 /// item_id: Identifier of burned NFT.505 ItemDestroyed(CollectionId, TokenId),506 }507);508509decl_module! {510 pub struct Module<T: Trait> for enum Call where origin: T::Origin {511512 fn deposit_event() = default;513 type Error = Error<T>;514515 fn on_initialize(now: T::BlockNumber) -> Weight {516517 if ChainVersion::get() < 2518 {519 let value = NextCollectionID::get();520 CreatedCollectionCount::put(value);521 ChainVersion::put(2);522 }523524 0525 }526527 /// This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner and admin of the collection are set to the address that signed the transaction. Both addresses can be changed later.528 /// 529 /// # Permissions530 /// 531 /// * Anyone.532 /// 533 /// # Arguments534 /// 535 /// * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.536 /// 537 /// * collection_description: UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.538 /// 539 /// * token_prefix: UTF-8 string with token prefix.540 /// 541 /// * mode: [CollectionMode] collection type and type dependent data.542 // returns collection ID543 #[weight = T::WeightInfo::create_collection()]544 pub fn create_collection(origin,545 collection_name: Vec<u16>,546 collection_description: Vec<u16>,547 token_prefix: Vec<u8>,548 mode: CollectionMode) -> DispatchResult {549550 // Anyone can create a collection551 let who = ensure_signed(origin)?;552553 let decimal_points = match mode {554 CollectionMode::Fungible(points) => points,555 CollectionMode::ReFungible(points) => points,556 _ => 0557 };558559 // bound Total number of collections560 ensure!(CollectionCount::get() < ChainLimit::get().collection_numbers_limit, Error::<T>::TotalCollectionsLimitExceeded);561562 // check params563 ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);564 ensure!(collection_name.len() <= 64, Error::<T>::CollectionNameLimitExceeded);565 ensure!(collection_description.len() <= 256, Error::<T>::CollectionDescriptionLimitExceeded);566 ensure!(token_prefix.len() <= 16, Error::<T>::CollectionTokenPrefixLimitExceeded);567568 // Generate next collection ID569 let next_id = CreatedCollectionCount::get()570 .checked_add(1)571 .ok_or(Error::<T>::NumOverflow)?;572573 // bound counter574 let total = CollectionCount::get()575 .checked_add(1)576 .ok_or(Error::<T>::NumOverflow)?;577578 CreatedCollectionCount::put(next_id);579 CollectionCount::put(total);580581 // Create new collection582 let new_collection = CollectionType {583 owner: who.clone(),584 name: collection_name,585 mode: mode.clone(),586 mint_mode: false,587 access: AccessMode::Normal,588 description: collection_description,589 decimal_points: decimal_points,590 token_prefix: token_prefix,591 offchain_schema: Vec::new(),592 schema_version: SchemaVersion::ImageURL,593 sponsor: T::AccountId::default(),594 unconfirmed_sponsor: T::AccountId::default(),595 variable_on_chain_schema: Vec::new(),596 const_on_chain_schema: Vec::new(),597 limits: CollectionLimits::default(),598 };599600 // Add new collection to map601 <Collection<T>>::insert(next_id, new_collection);602603 // call event604 Self::deposit_event(RawEvent::Created(next_id, mode.into(), who.clone()));605606 Ok(())607 }608609 /// **DANGEROUS**: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.610 /// 611 /// # Permissions612 /// 613 /// * Collection Owner.614 /// 615 /// # Arguments616 /// 617 /// * collection_id: collection to destroy.618 #[weight = T::WeightInfo::destroy_collection()]619 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {620621 let sender = ensure_signed(origin)?;622 Self::check_owner_permissions(collection_id, sender)?;623624 <AddressTokens<T>>::remove_prefix(collection_id);625 <ApprovedList<T>>::remove_prefix(collection_id);626 <Balance<T>>::remove_prefix(collection_id);627 <ItemListIndex>::remove(collection_id);628 <AdminList<T>>::remove(collection_id);629 <Collection<T>>::remove(collection_id);630 <WhiteList<T>>::remove_prefix(collection_id);631632 <NftItemList<T>>::remove_prefix(collection_id);633 <FungibleItemList<T>>::remove_prefix(collection_id);634 <ReFungibleItemList<T>>::remove_prefix(collection_id);635636 <NftTransferBasket<T>>::remove_prefix(collection_id);637 <FungibleTransferBasket<T>>::remove_prefix(collection_id);638 <ReFungibleTransferBasket<T>>::remove_prefix(collection_id);639640 if CollectionCount::get() > 0641 {642 // bound couter643 let total = CollectionCount::get()644 .checked_sub(1)645 .ok_or(Error::<T>::NumOverflow)?;646647 CollectionCount::put(total);648 }649650 Ok(())651 }652653 /// Add an address to white list.654 /// 655 /// # Permissions656 /// 657 /// * Collection Owner658 /// * Collection Admin659 /// 660 /// # Arguments661 /// 662 /// * collection_id.663 /// 664 /// * address.665 #[weight = T::WeightInfo::add_to_white_list()]666 pub fn add_to_white_list(origin, collection_id: CollectionId, address: T::AccountId) -> DispatchResult{667668 let sender = ensure_signed(origin)?;669 Self::check_owner_or_admin_permissions(collection_id, sender)?;670671 <WhiteList<T>>::insert(collection_id, address, true);672 673 Ok(())674 }675676 /// Remove an address from white list.677 /// 678 /// # Permissions679 /// 680 /// * Collection Owner681 /// * Collection Admin682 /// 683 /// # Arguments684 /// 685 /// * collection_id.686 /// 687 /// * address.688 #[weight = T::WeightInfo::remove_from_white_list()]689 pub fn remove_from_white_list(origin, collection_id: CollectionId, address: T::AccountId) -> DispatchResult{690691 let sender = ensure_signed(origin)?;692 Self::check_owner_or_admin_permissions(collection_id, sender)?;693694 <WhiteList<T>>::remove(collection_id, address);695696 Ok(())697 }698699 /// Toggle between normal and white list access for the methods with access for `Anyone`.700 /// 701 /// # Permissions702 /// 703 /// * Collection Owner.704 /// 705 /// # Arguments706 /// 707 /// * collection_id.708 /// 709 /// * mode: [AccessMode]710 #[weight = T::WeightInfo::set_public_access_mode()]711 pub fn set_public_access_mode(origin, collection_id: CollectionId, mode: AccessMode) -> DispatchResult712 {713 let sender = ensure_signed(origin)?;714715 Self::check_owner_permissions(collection_id, sender)?;716 let mut target_collection = <Collection<T>>::get(collection_id);717 target_collection.access = mode;718 <Collection<T>>::insert(collection_id, target_collection);719720 Ok(())721 }722723 /// Allows Anyone to create tokens if:724 /// * White List is enabled, and725 /// * Address is added to white list, and726 /// * This method was called with True parameter727 /// 728 /// # Permissions729 /// * Collection Owner730 ///731 /// # Arguments732 /// 733 /// * collection_id.734 /// 735 /// * mint_permission: Boolean parameter. If True, allows minting to Anyone with conditions above.736 #[weight = T::WeightInfo::set_mint_permission()]737 pub fn set_mint_permission(origin, collection_id: CollectionId, mint_permission: bool) -> DispatchResult738 {739 let sender = ensure_signed(origin)?;740741 Self::check_owner_permissions(collection_id, sender)?;742 let mut target_collection = <Collection<T>>::get(collection_id);743 target_collection.mint_mode = mint_permission;744 <Collection<T>>::insert(collection_id, target_collection);745746 Ok(())747 }748749 /// Change the owner of the collection.750 /// 751 /// # Permissions752 /// 753 /// * Collection Owner.754 /// 755 /// # Arguments756 /// 757 /// * collection_id.758 /// 759 /// * new_owner.760 #[weight = T::WeightInfo::change_collection_owner()]761 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {762763 let sender = ensure_signed(origin)?;764 Self::check_owner_permissions(collection_id, sender)?;765 let mut target_collection = <Collection<T>>::get(collection_id);766 target_collection.owner = new_owner;767 <Collection<T>>::insert(collection_id, target_collection);768769 Ok(())770 }771772 /// Adds an admin of the Collection.773 /// NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership. 774 /// 775 /// # Permissions776 /// 777 /// * Collection Owner.778 /// * Collection Admin.779 /// 780 /// # Arguments781 /// 782 /// * collection_id: ID of the Collection to add admin for.783 /// 784 /// * new_admin_id: Address of new admin to add.785 #[weight = T::WeightInfo::add_collection_admin()]786 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::AccountId) -> DispatchResult {787788 let sender = ensure_signed(origin)?;789 Self::check_owner_or_admin_permissions(collection_id, sender)?;790 let mut admin_arr: Vec<T::AccountId> = Vec::new();791792 if <AdminList<T>>::contains_key(collection_id)793 {794 admin_arr = <AdminList<T>>::get(collection_id);795 ensure!(!admin_arr.contains(&new_admin_id), Error::<T>::AlreadyAdmin);796 }797798 // Number of collection admins799 ensure!((admin_arr.len() as u64) < ChainLimit::get().collections_admins_limit, Error::<T>::CollectionAdminsLimitExceeded);800801 admin_arr.push(new_admin_id);802 <AdminList<T>>::insert(collection_id, admin_arr);803804 Ok(())805 }806807 /// Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.808 ///809 /// # Permissions810 /// 811 /// * Collection Owner.812 /// * Collection Admin.813 /// 814 /// # Arguments815 /// 816 /// * collection_id: ID of the Collection to remove admin for.817 /// 818 /// * account_id: Address of admin to remove.819 #[weight = T::WeightInfo::remove_collection_admin()]820 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::AccountId) -> DispatchResult {821822 let sender = ensure_signed(origin)?;823 Self::check_owner_or_admin_permissions(collection_id, sender)?;824825 if <AdminList<T>>::contains_key(collection_id)826 {827 let mut admin_arr = <AdminList<T>>::get(collection_id);828 admin_arr.retain(|i| *i != account_id);829 <AdminList<T>>::insert(collection_id, admin_arr);830 }831832 Ok(())833 }834835 /// # Permissions836 /// 837 /// * Collection Owner838 /// 839 /// # Arguments840 /// 841 /// * collection_id.842 /// 843 /// * new_sponsor.844 #[weight = T::WeightInfo::set_collection_sponsor()]845 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {846847 let sender = ensure_signed(origin)?;848 ensure!(<Collection<T>>::contains_key(collection_id), Error::<T>::CollectionNotFound);849850 let mut target_collection = <Collection<T>>::get(collection_id);851 ensure!(sender == target_collection.owner, Error::<T>::NoPermission);852853 target_collection.unconfirmed_sponsor = new_sponsor;854 <Collection<T>>::insert(collection_id, target_collection);855856 Ok(())857 }858859 /// # Permissions860 /// 861 /// * Sponsor.862 /// 863 /// # Arguments864 /// 865 /// * collection_id.866 #[weight = T::WeightInfo::confirm_sponsorship()]867 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {868869 let sender = ensure_signed(origin)?;870 ensure!(<Collection<T>>::contains_key(collection_id), Error::<T>::CollectionNotFound);871872 let mut target_collection = <Collection<T>>::get(collection_id);873 ensure!(sender == target_collection.unconfirmed_sponsor, Error::<T>::ConfirmUnsetSponsorFail);874875 target_collection.sponsor = target_collection.unconfirmed_sponsor;876 target_collection.unconfirmed_sponsor = T::AccountId::default();877 <Collection<T>>::insert(collection_id, target_collection);878879 Ok(())880 }881882 /// Switch back to pay-per-own-transaction model.883 ///884 /// # Permissions885 ///886 /// * Collection owner.887 /// 888 /// # Arguments889 /// 890 /// * collection_id.891 #[weight = T::WeightInfo::remove_collection_sponsor()]892 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {893894 let sender = ensure_signed(origin)?;895 ensure!(<Collection<T>>::contains_key(collection_id), Error::<T>::CollectionNotFound);896897 let mut target_collection = <Collection<T>>::get(collection_id);898 ensure!(sender == target_collection.owner, Error::<T>::NoPermission);899900 target_collection.sponsor = T::AccountId::default();901 <Collection<T>>::insert(collection_id, target_collection);902903 Ok(())904 }905906 /// This method creates a concrete instance of NFT Collection created with CreateCollection method.907 /// 908 /// # Permissions909 /// 910 /// * Collection Owner.911 /// * Collection Admin.912 /// * Anyone if913 /// * White List is enabled, and914 /// * Address is added to white list, and915 /// * MintPermission is enabled (see SetMintPermission method)916 /// 917 /// # Arguments918 /// 919 /// * collection_id: ID of the collection.920 /// 921 /// * owner: Address, initial owner of the NFT.922 ///923 /// * data: Token data to store on chain.924 // #[weight =925 // (130_000_000 as Weight)926 // .saturating_add((2135 as Weight).saturating_mul((properties.len() as u64) as Weight))927 // .saturating_add(RocksDbWeight::get().reads(10 as Weight))928 // .saturating_add(RocksDbWeight::get().writes(8 as Weight))]929930 #[weight = T::WeightInfo::create_item(data.len())]931 pub fn create_item(origin, collection_id: CollectionId, owner: T::AccountId, data: CreateItemData) -> DispatchResult {932933 let sender = ensure_signed(origin)?;934935 Self::collection_exists(collection_id)?;936937 let target_collection = <Collection<T>>::get(collection_id);938939 Self::can_create_items_in_collection(collection_id, &target_collection, &sender, &owner)?;940 Self::validate_create_item_args(&target_collection, &data)?;941 Self::create_item_no_validation(collection_id, &target_collection, owner, data)?;942943 Ok(())944 }945946 /// This method creates multiple instances of NFT Collection created with CreateCollection method.947 /// 948 /// # Permissions949 /// 950 /// * Collection Owner.951 /// * Collection Admin.952 /// * Anyone if953 /// * White List is enabled, and954 /// * Address is added to white list, and955 /// * MintPermission is enabled (see SetMintPermission method)956 /// 957 /// # Arguments958 /// 959 /// * collection_id: ID of the collection.960 /// 961 /// * itemsData: Array items properties. Each property is an array of bytes itself, see [create_item].962 /// 963 /// * owner: Address, initial owner of the NFT.964 #[weight = T::WeightInfo::create_item(items_data.into_iter()965 .map(|data| { data.len() })966 .sum())]967 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::AccountId, items_data: Vec<CreateItemData>) -> DispatchResult {968969 ensure!(items_data.len() > 0, Error::<T>::EmptyArgument);970 let sender = ensure_signed(origin)?;971972 Self::collection_exists(collection_id)?;973 let target_collection = <Collection<T>>::get(collection_id);974975 Self::can_create_items_in_collection(collection_id, &target_collection, &sender, &owner)?;976977 for data in &items_data {978 Self::validate_create_item_args(&target_collection, data)?;979 }980 for data in &items_data {981 Self::create_item_no_validation(collection_id, &target_collection, owner.clone(), data.clone())?;982 }983984 Ok(())985 }986987 /// Destroys a concrete instance of NFT.988 /// 989 /// # Permissions990 /// 991 /// * Collection Owner.992 /// * Collection Admin.993 /// * Current NFT Owner.994 /// 995 /// # Arguments996 /// 997 /// * collection_id: ID of the collection.998 /// 999 /// * item_id: ID of NFT to burn.1000 #[weight = T::WeightInfo::burn_item()]1001 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId) -> DispatchResult {10021003 let sender = ensure_signed(origin)?;1004 Self::collection_exists(collection_id)?;10051006 // Transfer permissions check1007 let target_collection = <Collection<T>>::get(collection_id);1008 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||1009 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1010 Error::<T>::NoPermission);10111012 if target_collection.access == AccessMode::WhiteList {1013 Self::check_white_list(collection_id, &sender)?;1014 }10151016 match target_collection.mode1017 {1018 CollectionMode::NFT => Self::burn_nft_item(collection_id, item_id)?,1019 CollectionMode::Fungible(_) => Self::burn_fungible_item(collection_id, item_id)?,1020 CollectionMode::ReFungible(_) => Self::burn_refungible_item(collection_id, item_id, sender.clone())?,1021 _ => ()1022 };10231024 // call event1025 Self::deposit_event(RawEvent::ItemDestroyed(collection_id, item_id));10261027 Ok(())1028 }10291030 /// Change ownership of the token.1031 /// 1032 /// # Permissions1033 /// 1034 /// * Collection Owner1035 /// * Collection Admin1036 /// * Current NFT owner1037 ///1038 /// # Arguments1039 /// 1040 /// * recipient: Address of token recipient.1041 /// 1042 /// * collection_id.1043 /// 1044 /// * item_id: ID of the item1045 /// * Non-Fungible Mode: Required.1046 /// * Fungible Mode: Ignored.1047 /// * Re-Fungible Mode: Required.1048 /// 1049 /// * value: Amount to transfer.1050 /// * Non-Fungible Mode: Ignored1051 /// * Fungible Mode: Must specify transferred amount1052 /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)1053 #[weight = T::WeightInfo::transfer()]1054 pub fn transfer(origin, recipient: T::AccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult {10551056 let sender = ensure_signed(origin)?;1057 let target_collection = <Collection<T>>::get(collection_id);10581059 // Limits check1060 Self::is_correct_transfer(collection_id, &target_collection, &recipient)?;10611062 // Transfer permissions check1063 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||1064 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1065 Error::<T>::NoPermission);10661067 if target_collection.access == AccessMode::WhiteList {1068 Self::check_white_list(collection_id, &sender)?;1069 Self::check_white_list(collection_id, &recipient)?;1070 }10711072 match target_collection.mode1073 {1074 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient)?,1075 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, item_id, value, sender.clone(), recipient)?,1076 CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient)?,1077 _ => ()1078 };10791080 Ok(())1081 }10821083 /// Set, change, or remove approved address to transfer the ownership of the NFT.1084 /// 1085 /// # Permissions1086 /// 1087 /// * Collection Owner1088 /// * Collection Admin1089 /// * Current NFT owner1090 /// 1091 /// # Arguments1092 /// 1093 /// * approved: Address that is approved to transfer this NFT or zero (if needed to remove approval).1094 /// 1095 /// * collection_id.1096 /// 1097 /// * item_id: ID of the item.1098 #[weight = T::WeightInfo::approve()]1099 pub fn approve(origin, approved: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> DispatchResult {11001101 let sender = ensure_signed(origin)?;11021103 // Transfer permissions check1104 let target_collection = <Collection<T>>::get(collection_id);1105 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||1106 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1107 Error::<T>::NoPermission);11081109 if target_collection.access == AccessMode::WhiteList {1110 Self::check_white_list(collection_id, &sender)?;1111 Self::check_white_list(collection_id, &approved)?;1112 }11131114 // amount param stub1115 let amount = 100000000;11161117 let list_exists = <ApprovedList<T>>::contains_key(collection_id, (item_id, sender.clone()));1118 if list_exists {11191120 let mut list = <ApprovedList<T>>::get(collection_id, (item_id, sender.clone()));1121 let item_contains = list.iter().any(|i| i.approved == approved);11221123 if !item_contains {1124 list.push(ApprovePermissions { approved: approved.clone(), amount: amount });1125 <ApprovedList<T>>::insert(collection_id, (item_id, sender.clone()), list);1126 }1127 } else {11281129 let mut list = Vec::new();1130 list.push(ApprovePermissions { approved: approved.clone(), amount: amount });1131 <ApprovedList<T>>::insert(collection_id, (item_id, sender.clone()), list);1132 }11331134 Ok(())1135 }1136 1137 /// Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.1138 /// 1139 /// # Permissions1140 /// * Collection Owner1141 /// * Collection Admin1142 /// * Current NFT owner1143 /// * Address approved by current NFT owner1144 /// 1145 /// # Arguments1146 /// 1147 /// * from: Address that owns token.1148 /// 1149 /// * recipient: Address of token recipient.1150 /// 1151 /// * collection_id.1152 /// 1153 /// * item_id: ID of the item.1154 /// 1155 /// * value: Amount to transfer.1156 #[weight = T::WeightInfo::transfer_from()]1157 pub fn transfer_from(origin, from: T::AccountId, recipient: T::AccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResult {11581159 let sender = ensure_signed(origin)?;1160 let mut appoved_transfer = false;11611162 // Check approve1163 if <ApprovedList<T>>::contains_key(collection_id, (item_id, from.clone())) {1164 let list_itm = <ApprovedList<T>>::get(collection_id, (item_id, from.clone()));1165 let opt_item = list_itm.iter().find(|i| i.approved == sender.clone());1166 if opt_item.is_some()1167 {1168 appoved_transfer = true;1169 ensure!(opt_item.unwrap().amount >= value, Error::<T>::TokenValueNotEnough);1170 }1171 }11721173 let target_collection = <Collection<T>>::get(collection_id);11741175 // Limits check1176 Self::is_correct_transfer(collection_id, &target_collection, &recipient)?;11771178 // Transfer permissions check 1179 ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1180 Error::<T>::NoPermission);11811182 if target_collection.access == AccessMode::WhiteList {1183 Self::check_white_list(collection_id, &sender)?;1184 Self::check_white_list(collection_id, &recipient)?;1185 }11861187 // remove approve1188 let approve_list: Vec<ApprovePermissions<T::AccountId>> = <ApprovedList<T>>::get(collection_id, (item_id, from.clone()))1189 .into_iter().filter(|i| i.approved != sender.clone()).collect();1190 <ApprovedList<T>>::insert(collection_id, (item_id, from.clone()), approve_list);119111921193 match target_collection.mode1194 {1195 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, from, recipient)?,1196 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, item_id, value, from.clone(), recipient)?,1197 CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, from.clone(), recipient)?,1198 _ => ()1199 };12001201 Ok(())1202 }12031204 #[weight = 0]1205 pub fn safe_transfer_from(origin, collection_id: CollectionId, item_id: TokenId, new_owner: T::AccountId) -> DispatchResult {12061207 // let no_perm_mes = "You do not have permissions to modify this collection";1208 // ensure!(<ApprovedList<T>>::contains_key((collection_id, item_id)), no_perm_mes);1209 // let list_itm = <ApprovedList<T>>::get((collection_id, item_id));1210 // ensure!(list_itm.contains(&new_owner.clone()), no_perm_mes);12111212 // // on_nft_received call12131214 // Self::transfer(origin, collection_id, item_id, new_owner)?;12151216 Ok(())1217 }12181219 /// Set off-chain data schema.1220 /// 1221 /// # Permissions1222 /// 1223 /// * Collection Owner1224 /// * Collection Admin1225 /// 1226 /// # Arguments1227 /// 1228 /// * collection_id.1229 /// 1230 /// * schema: String representing the offchain data schema.1231 #[weight = T::WeightInfo::set_variable_meta_data()]1232 pub fn set_variable_meta_data (1233 origin,1234 collection_id: CollectionId,1235 item_id: TokenId,1236 data: Vec<u8>1237 ) -> DispatchResult {1238 let sender = ensure_signed(origin)?;1239 1240 Self::collection_exists(collection_id)?;1241 1242 ensure!(ChainLimit::get().custom_data_limit >= data.len() as u32, Error::<T>::TokenVariableDataLimitExceeded);12431244 // Modify permissions check1245 let target_collection = <Collection<T>>::get(collection_id);1246 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||1247 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1248 Error::<T>::NoPermission);12491250 Self::item_exists(collection_id, item_id, &target_collection.mode)?;12511252 match target_collection.mode1253 {1254 CollectionMode::NFT => Self::set_nft_variable_data(collection_id, item_id, data)?,1255 CollectionMode::ReFungible(_) => Self::set_re_fungible_variable_data(collection_id, item_id, data)?,1256 CollectionMode::Fungible(_) => fail!(Error::<T>::CantStoreMetadataInFungibleTokens),1257 _ => fail!(Error::<T>::UnexpectedCollectionType)1258 };12591260 Ok(())1261 }1262 1263 /// Set schema standard1264 /// ImageURL1265 /// Unique1266 /// 1267 /// # Permissions1268 /// 1269 /// * Collection Owner1270 /// * Collection Admin1271 /// 1272 /// # Arguments1273 /// 1274 /// * collection_id.1275 /// 1276 /// * schema: SchemaVersion: enum1277 #[weight = T::WeightInfo::set_schema_version()]1278 pub fn set_schema_version(1279 origin,1280 collection_id: CollectionId,1281 version: SchemaVersion1282 ) -> DispatchResult {1283 let sender = ensure_signed(origin)?;1284 Self::check_owner_or_admin_permissions(collection_id, sender.clone())?;1285 let mut target_collection = <Collection<T>>::get(collection_id);1286 target_collection.schema_version = version;1287 <Collection<T>>::insert(collection_id, target_collection);12881289 Ok(())1290 }12911292 /// Set off-chain data schema.1293 /// 1294 /// # Permissions1295 /// 1296 /// * Collection Owner1297 /// * Collection Admin1298 /// 1299 /// # Arguments1300 /// 1301 /// * collection_id.1302 /// 1303 /// * schema: String representing the offchain data schema.1304 #[weight = T::WeightInfo::set_offchain_schema()]1305 pub fn set_offchain_schema(1306 origin,1307 collection_id: CollectionId,1308 schema: Vec<u8>1309 ) -> DispatchResult {1310 let sender = ensure_signed(origin)?;1311 Self::check_owner_or_admin_permissions(collection_id, sender.clone())?;13121313 let mut target_collection = <Collection<T>>::get(collection_id);1314 target_collection.offchain_schema = schema;1315 <Collection<T>>::insert(collection_id, target_collection);13161317 Ok(())1318 }13191320 /// Set const on-chain data schema.1321 /// 1322 /// # Permissions1323 /// 1324 /// * Collection Owner1325 /// * Collection Admin1326 /// 1327 /// # Arguments1328 /// 1329 /// * collection_id.1330 /// 1331 /// * schema: String representing the const on-chain data schema.1332 #[weight = T::WeightInfo::set_const_on_chain_schema()]1333 pub fn set_const_on_chain_schema (1334 origin,1335 collection_id: CollectionId,1336 schema: Vec<u8>1337 ) -> DispatchResult {1338 let sender = ensure_signed(origin)?;1339 Self::check_owner_or_admin_permissions(collection_id, sender.clone())?;13401341 let mut target_collection = <Collection<T>>::get(collection_id);1342 target_collection.const_on_chain_schema = schema;1343 <Collection<T>>::insert(collection_id, target_collection);13441345 Ok(())1346 }13471348 /// Set variable on-chain data schema.1349 /// 1350 /// # Permissions1351 /// 1352 /// * Collection Owner1353 /// * Collection Admin1354 /// 1355 /// # Arguments1356 /// 1357 /// * collection_id.1358 /// 1359 /// * schema: String representing the variable on-chain data schema.1360 #[weight = T::WeightInfo::set_const_on_chain_schema()]1361 pub fn set_variable_on_chain_schema (1362 origin,1363 collection_id: CollectionId,1364 schema: Vec<u8>1365 ) -> DispatchResult {1366 let sender = ensure_signed(origin)?;1367 Self::check_owner_or_admin_permissions(collection_id, sender.clone())?;13681369 let mut target_collection = <Collection<T>>::get(collection_id);1370 target_collection.variable_on_chain_schema = schema;1371 <Collection<T>>::insert(collection_id, target_collection);13721373 Ok(())1374 }13751376 // Sudo permissions function1377 #[weight = T::WeightInfo::set_chain_limits()]1378 pub fn set_chain_limits(1379 origin,1380 limits: ChainLimits1381 ) -> DispatchResult {13821383 #[cfg(not(feature = "runtime-benchmarks"))]1384 ensure_root(origin)?;13851386 <ChainLimit>::put(limits);1387 Ok(())1388 }13891390 /// Enable smart contract self-sponsoring.1391 /// 1392 /// # Permissions1393 /// 1394 /// * Contract Owner1395 /// 1396 /// # Arguments1397 /// 1398 /// * contract address1399 /// * enable flag1400 /// 1401 #[weight = T::WeightInfo::enable_contract_sponsoring()]1402 pub fn enable_contract_sponsoring(1403 origin,1404 contract_address: T::AccountId,1405 enable: bool1406 ) -> DispatchResult {14071408 let sender = ensure_signed(origin)?;14091410 #[cfg(feature = "runtime-benchmarks")]1411 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());14121413 Self::ensure_contract_owned(sender, &contract_address)?;14141415 <ContractSelfSponsoring<T>>::insert(contract_address, enable);1416 Ok(())1417 }14181419 /// Set the rate limit for contract sponsoring to specified number of blocks.1420 /// 1421 /// If not set (has the default value of 0 blocks), the sponsoring will be disabled. 1422 /// If set to the number B (for blocks), the transactions will be sponsored with a rate 1423 /// limit of B, i.e. fees for every transaction sent to this smart contract will be paid 1424 /// from contract endowment if there are at least B blocks between such transactions. 1425 /// Nonetheless, if transactions are sent more frequently, the fees are paid by the sender.1426 /// 1427 /// # Permissions1428 /// 1429 /// * Contract Owner1430 /// 1431 /// # Arguments1432 /// 1433 /// -`contract_address`: Address of the contract to sponsor1434 /// -`rate_limit`: Number of blocks to wait until the next sponsored transaction is allowed1435 /// 1436 #[weight = T::WeightInfo::set_contract_sponsoring_rate_limit()]1437 pub fn set_contract_sponsoring_rate_limit(1438 origin,1439 contract_address: T::AccountId,1440 rate_limit: T::BlockNumber1441 ) -> DispatchResult {1442 let sender = ensure_signed(origin)?;14431444 #[cfg(feature = "runtime-benchmarks")]1445 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());14461447 Self::ensure_contract_owned(sender, &contract_address)?;1448 <ContractSponsoringRateLimit<T>>::insert(contract_address, rate_limit);1449 Ok(())1450 }14511452 /// Enable the white list for a contract. Only addresses added to the white list with addToContractWhiteList will be able to call this smart contract.1453 /// 1454 /// # Permissions1455 /// 1456 /// * Address that deployed smart contract.1457 /// 1458 /// # Arguments1459 /// 1460 /// -`contract_address`: Address of the contract.1461 /// 1462 /// - `enable`: . 1463 #[weight = T::WeightInfo::toggle_contract_white_list()]1464 pub fn toggle_contract_white_list(1465 origin,1466 contract_address: T::AccountId,1467 enable: bool1468 ) -> DispatchResult {1469 let sender = ensure_signed(origin)?;14701471 #[cfg(feature = "runtime-benchmarks")]1472 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());14731474 Self::ensure_contract_owned(sender, &contract_address)?;1475 <ContractWhiteListEnabled<T>>::insert(contract_address, enable);1476 Ok(())1477 }1478 1479 /// Add an address to smart contract white list.1480 /// 1481 /// # Permissions1482 /// 1483 /// * Address that deployed smart contract.1484 /// 1485 /// # Arguments1486 /// 1487 /// -`contract_address`: Address of the contract.1488 ///1489 /// -`account_address`: Address to add.1490 #[weight = T::WeightInfo::add_to_contract_white_list()]1491 pub fn add_to_contract_white_list(1492 origin,1493 contract_address: T::AccountId,1494 account_address: T::AccountId1495 ) -> DispatchResult {1496 let sender = ensure_signed(origin)?;14971498 #[cfg(feature = "runtime-benchmarks")]1499 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());1500 1501 Self::ensure_contract_owned(sender, &contract_address)?; 1502 <ContractWhiteList<T>>::insert(contract_address, account_address, true);1503 Ok(())1504 }15051506 /// Remove an address from smart contract white list.1507 /// 1508 /// # Permissions1509 /// 1510 /// * Address that deployed smart contract.1511 /// 1512 /// # Arguments1513 /// 1514 /// -`contract_address`: Address of the contract.1515 ///1516 /// -`account_address`: Address to remove.1517 #[weight = T::WeightInfo::remove_from_contract_white_list()]1518 pub fn remove_from_contract_white_list(1519 origin,1520 contract_address: T::AccountId,1521 account_address: T::AccountId1522 ) -> DispatchResult {1523 let sender = ensure_signed(origin)?;15241525 #[cfg(feature = "runtime-benchmarks")]1526 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());15271528 Self::ensure_contract_owned(sender, &contract_address)?;1529 <ContractWhiteList<T>>::remove(contract_address, account_address);1530 Ok(())1531 }15321533 #[weight = T::WeightInfo::set_collection_limits()]1534 pub fn set_collection_limits(1535 origin,1536 collection_id: u32,1537 limits: CollectionLimits,1538 ) -> DispatchResult {1539 let sender = ensure_signed(origin)?;1540 Self::check_owner_permissions(collection_id, sender.clone())?;1541 let mut target_collection = <Collection<T>>::get(collection_id);1542 let chain_limits = ChainLimit::get();1543 let climits = target_collection.limits;15441545 // collection bounds1546 ensure!(limits.sponsor_transfer_timeout <= MAX_SPONSOR_TIMEOUT &&1547 limits.account_token_ownership_limit <= MAX_TOKEN_OWNERSHIP, 1548 Error::<T>::CollectionLimitBoundsExceeded);15491550 // token_limit check prev1551 ensure!(climits.token_limit > limits.token_limit && 1552 limits.token_limit <= chain_limits.account_token_ownership_limit, 1553 Error::<T>::AccountTokenLimitExceeded);15541555 target_collection.limits = limits;1556 <Collection<T>>::insert(collection_id, target_collection);15571558 Ok(())1559 } 1560 }1561}15621563impl<T: Trait> Module<T> {15641565 fn is_correct_transfer(collection_id: CollectionId, collection: &CollectionType<T::AccountId>, recipient: &T::AccountId) -> DispatchResult {15661567 // check token limit and account token limit1568 let account_items: u32 = <AddressTokens<T>>::get(collection_id, recipient).len() as u32;1569 ensure!(collection.limits.account_token_ownership_limit > account_items, Error::<T>::AccountTokenLimitExceeded);1570 1571 Ok(())1572 }15731574 fn can_create_items_in_collection(collection_id: CollectionId, collection: &CollectionType<T::AccountId>, sender: &T::AccountId, owner: &T::AccountId) -> DispatchResult {15751576 // check token limit and account token limit1577 let total_items: u32 = ItemListIndex::get(collection_id);1578 let account_items: u32 = <AddressTokens<T>>::get(collection_id, owner).len() as u32;1579 ensure!(collection.limits.token_limit > total_items, Error::<T>::CollectionTokenLimitExceeded);1580 ensure!(collection.limits.account_token_ownership_limit > account_items, Error::<T>::AccountTokenLimitExceeded);15811582 if !Self::is_owner_or_admin_permissions(collection_id, sender.clone()) {1583 ensure!(collection.mint_mode == true, Error::<T>::PublicMintingNotAllowed);1584 Self::check_white_list(collection_id, owner)?;1585 Self::check_white_list(collection_id, sender)?;1586 }15871588 Ok(())1589 }15901591 fn validate_create_item_args(target_collection: &CollectionType<T::AccountId>, data: &CreateItemData) -> DispatchResult {1592 match target_collection.mode1593 {1594 CollectionMode::NFT => {1595 if let CreateItemData::NFT(data) = data {1596 // check sizes1597 ensure!(ChainLimit::get().custom_data_limit >= data.const_data.len() as u32, Error::<T>::TokenConstDataLimitExceeded);1598 ensure!(ChainLimit::get().custom_data_limit >= data.variable_data.len() as u32, Error::<T>::TokenVariableDataLimitExceeded);1599 } else {1600 fail!(Error::<T>::NotNftDataUsedToMintNftCollectionToken);1601 }1602 },1603 CollectionMode::Fungible(_) => {1604 if let CreateItemData::Fungible(_) = data {1605 } else {1606 fail!(Error::<T>::NotFungibleDataUsedToMintFungibleCollectionToken);1607 }1608 },1609 CollectionMode::ReFungible(_) => {1610 if let CreateItemData::ReFungible(data) = data {16111612 // check sizes1613 ensure!(ChainLimit::get().custom_data_limit >= data.const_data.len() as u32, Error::<T>::TokenConstDataLimitExceeded);1614 ensure!(ChainLimit::get().custom_data_limit >= data.variable_data.len() as u32, Error::<T>::TokenVariableDataLimitExceeded);1615 } else {1616 fail!(Error::<T>::NotReFungibleDataUsedToMintReFungibleCollectionToken);1617 }1618 },1619 _ => { fail!(Error::<T>::UnexpectedCollectionType); }1620 };16211622 Ok(())1623 }16241625 fn create_item_no_validation(collection_id: CollectionId, collection: &CollectionType<T::AccountId>, owner: T::AccountId, data: CreateItemData) -> DispatchResult {1626 match data1627 {1628 CreateItemData::NFT(data) => {1629 let item = NftItemType {1630 collection: collection_id,1631 owner,1632 const_data: data.const_data,1633 variable_data: data.variable_data1634 };16351636 Self::add_nft_item(item)?;1637 },1638 CreateItemData::Fungible(_) => {1639 let item = FungibleItemType {1640 collection: collection_id,1641 owner,1642 value: (10 as u128).pow(collection.decimal_points as u32)1643 };16441645 Self::add_fungible_item(item)?;1646 },1647 CreateItemData::ReFungible(data) => {1648 let mut owner_list = Vec::new();1649 let value = (10 as u128).pow(collection.decimal_points as u32);1650 owner_list.push(Ownership {owner: owner.clone(), fraction: value});16511652 let item = ReFungibleItemType {1653 collection: collection_id,1654 owner: owner_list,1655 const_data: data.const_data,1656 variable_data: data.variable_data1657 };16581659 Self::add_refungible_item(item)?;1660 }1661 };16621663 // call event1664 Self::deposit_event(RawEvent::ItemCreated(collection_id, <ItemListIndex>::get(collection_id)));16651666 Ok(())1667 }16681669 fn add_fungible_item(item: FungibleItemType<T::AccountId>) -> DispatchResult {1670 let current_index = <ItemListIndex>::get(item.collection)1671 .checked_add(1)1672 .ok_or(Error::<T>::NumOverflow)?;1673 let itemcopy = item.clone();1674 let owner = item.owner.clone();16751676 Self::add_token_index(item.collection, current_index, owner.clone())?;16771678 <ItemListIndex>::insert(item.collection, current_index);1679 <FungibleItemList<T>>::insert(item.collection, current_index, itemcopy);16801681 // Add current block1682 let v: Vec<BasketItem<T::AccountId, T::BlockNumber>> = Vec::new();1683 <FungibleTransferBasket<T>>::insert(item.collection, current_index, v);1684 1685 // Update balance1686 let new_balance = <Balance<T>>::get(item.collection, owner.clone())1687 .checked_add(item.value)1688 .ok_or(Error::<T>::NumOverflow)?;1689 <Balance<T>>::insert(item.collection, owner.clone(), new_balance);16901691 Ok(())1692 }16931694 fn add_refungible_item(item: ReFungibleItemType<T::AccountId>) -> DispatchResult {1695 let current_index = <ItemListIndex>::get(item.collection)1696 .checked_add(1)1697 .ok_or(Error::<T>::NumOverflow)?;1698 let itemcopy = item.clone();16991700 let value = item.owner.first().unwrap().fraction;1701 let owner = item.owner.first().unwrap().owner.clone();17021703 Self::add_token_index(item.collection, current_index, owner.clone())?;17041705 <ItemListIndex>::insert(item.collection, current_index);1706 <ReFungibleItemList<T>>::insert(item.collection, current_index, itemcopy);17071708 // Add current block1709 let block_number: T::BlockNumber = 0.into();1710 <ReFungibleTransferBasket<T>>::insert(item.collection, current_index, block_number);17111712 // Update balance1713 let new_balance = <Balance<T>>::get(item.collection, owner.clone())1714 .checked_add(value)1715 .ok_or(Error::<T>::NumOverflow)?;1716 <Balance<T>>::insert(item.collection, owner.clone(), new_balance);17171718 Ok(())1719 }17201721 fn add_nft_item(item: NftItemType<T::AccountId>) -> DispatchResult {1722 let current_index = <ItemListIndex>::get(item.collection)1723 .checked_add(1)1724 .ok_or(Error::<T>::NumOverflow)?;17251726 let item_owner = item.owner.clone();1727 let collection_id = item.collection.clone();1728 Self::add_token_index(collection_id, current_index, item.owner.clone())?;17291730 <ItemListIndex>::insert(collection_id, current_index);1731 <NftItemList<T>>::insert(collection_id, current_index, item);17321733 // Add current block1734 let block_number: T::BlockNumber = 0.into();1735 <NftTransferBasket<T>>::insert(collection_id, current_index, block_number);17361737 // Update balance1738 let new_balance = <Balance<T>>::get(collection_id, item_owner.clone())1739 .checked_add(1)1740 .ok_or(Error::<T>::NumOverflow)?;1741 <Balance<T>>::insert(collection_id, item_owner.clone(), new_balance);17421743 Ok(())1744 }17451746 fn burn_refungible_item(1747 collection_id: CollectionId,1748 item_id: TokenId,1749 owner: T::AccountId,1750 ) -> DispatchResult {1751 ensure!(1752 <ReFungibleItemList<T>>::contains_key(collection_id, item_id),1753 Error::<T>::TokenNotFound1754 );1755 let collection = <ReFungibleItemList<T>>::get(collection_id, item_id);1756 let item = collection1757 .owner1758 .iter()1759 .filter(|&i| i.owner == owner)1760 .next()1761 .unwrap();1762 Self::remove_token_index(collection_id, item_id, owner.clone())?;17631764 // remove approve list1765 <ApprovedList<T>>::remove(collection_id, (item_id, owner.clone()));17661767 // update balance1768 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1769 .checked_sub(item.fraction)1770 .ok_or(Error::<T>::NumOverflow)?;1771 <Balance<T>>::insert(collection_id, item.owner.clone(), new_balance);17721773 <ReFungibleItemList<T>>::remove(collection_id, item_id);17741775 Ok(())1776 }17771778 fn burn_nft_item(collection_id: CollectionId, item_id: TokenId) -> DispatchResult {1779 ensure!(1780 <NftItemList<T>>::contains_key(collection_id, item_id),1781 Error::<T>::TokenNotFound1782 );1783 let item = <NftItemList<T>>::get(collection_id, item_id);1784 Self::remove_token_index(collection_id, item_id, item.owner.clone())?;17851786 // remove approve list1787 <ApprovedList<T>>::remove(collection_id, (item_id, item.owner.clone()));17881789 // update balance1790 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1791 .checked_sub(1)1792 .ok_or(Error::<T>::NumOverflow)?;1793 <Balance<T>>::insert(collection_id, item.owner.clone(), new_balance);1794 <NftItemList<T>>::remove(collection_id, item_id);17951796 Ok(())1797 }17981799 fn burn_fungible_item(collection_id: CollectionId, item_id: TokenId) -> DispatchResult {1800 ensure!(1801 <FungibleItemList<T>>::contains_key(collection_id, item_id),1802 Error::<T>::TokenNotFound1803 );1804 let item = <FungibleItemList<T>>::get(collection_id, item_id);1805 Self::remove_token_index(collection_id, item_id, item.owner.clone())?;18061807 // remove approve list1808 <ApprovedList<T>>::remove(collection_id, (item_id, item.owner.clone()));18091810 // update balance1811 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1812 .checked_sub(item.value)1813 .ok_or(Error::<T>::NumOverflow)?;1814 <Balance<T>>::insert(collection_id, item.owner.clone(), new_balance);18151816 <FungibleItemList<T>>::remove(collection_id, item_id);18171818 Ok(())1819 }18201821 fn collection_exists(collection_id: CollectionId) -> DispatchResult {1822 ensure!(1823 <Collection<T>>::contains_key(collection_id),1824 Error::<T>::CollectionNotFound1825 );1826 Ok(())1827 }18281829 fn check_owner_permissions(collection_id: CollectionId, subject: T::AccountId) -> DispatchResult {1830 Self::collection_exists(collection_id)?;18311832 let target_collection = <Collection<T>>::get(collection_id);1833 ensure!(1834 subject == target_collection.owner,1835 Error::<T>::NoPermission1836 );18371838 Ok(())1839 }18401841 fn is_owner_or_admin_permissions(collection_id: CollectionId, subject: T::AccountId) -> bool {1842 let target_collection = <Collection<T>>::get(collection_id);1843 let mut result: bool = subject == target_collection.owner;1844 let exists = <AdminList<T>>::contains_key(collection_id);18451846 if !result & exists {1847 if <AdminList<T>>::get(collection_id).contains(&subject) {1848 result = true1849 }1850 }18511852 result1853 }18541855 fn check_owner_or_admin_permissions(1856 collection_id: CollectionId,1857 subject: T::AccountId,1858 ) -> DispatchResult {1859 Self::collection_exists(collection_id)?;1860 let result = Self::is_owner_or_admin_permissions(collection_id, subject.clone());18611862 ensure!(1863 result,1864 Error::<T>::NoPermission1865 );1866 Ok(())1867 }18681869 fn is_item_owner(subject: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> bool {1870 let target_collection = <Collection<T>>::get(collection_id);18711872 match target_collection.mode {1873 CollectionMode::NFT => {1874 <NftItemList<T>>::get(collection_id, item_id).owner == subject1875 }1876 CollectionMode::Fungible(_) => {1877 <FungibleItemList<T>>::get(collection_id, item_id).owner == subject1878 }1879 CollectionMode::ReFungible(_) => {1880 <ReFungibleItemList<T>>::get(collection_id, item_id)1881 .owner1882 .iter()1883 .any(|i| i.owner == subject)1884 }1885 CollectionMode::Invalid => false,1886 }1887 }18881889 fn check_white_list(collection_id: CollectionId, address: &T::AccountId) -> DispatchResult {1890 let mes = Error::<T>::AddresNotInWhiteList;1891 ensure!(<WhiteList<T>>::contains_key(collection_id, address), mes);18921893 Ok(())1894 }18951896 fn transfer_fungible(1897 collection_id: CollectionId,1898 item_id: TokenId,1899 value: u128,1900 owner: T::AccountId,1901 new_owner: T::AccountId,1902 ) -> DispatchResult {1903 ensure!(1904 <FungibleItemList<T>>::contains_key(collection_id, item_id),1905 Error::<T>::TokenNotFound1906 );19071908 let full_item = <FungibleItemList<T>>::get(collection_id, item_id);1909 let amount = full_item.value;19101911 ensure!(amount >= value, Error::<T>::TokenValueTooLow);19121913 // update balance1914 let balance_old_owner = <Balance<T>>::get(collection_id, owner.clone())1915 .checked_sub(value)1916 .ok_or(Error::<T>::NumOverflow)?;1917 <Balance<T>>::insert(collection_id, owner.clone(), balance_old_owner);19181919 let mut new_owner_account_id = 0;1920 let new_owner_items = <AddressTokens<T>>::get(collection_id, new_owner.clone());1921 if new_owner_items.len() > 0 {1922 new_owner_account_id = new_owner_items[0];1923 }19241925 // transfer1926 if amount == value && new_owner_account_id == 0 {1927 // change owner1928 // new owner do not have account1929 let mut new_full_item = full_item.clone();1930 new_full_item.owner = new_owner.clone();1931 <FungibleItemList<T>>::insert(collection_id, item_id, new_full_item);19321933 // update balance1934 let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.clone())1935 .checked_add(value)1936 .ok_or(Error::<T>::NumOverflow)?;1937 <Balance<T>>::insert(collection_id, new_owner.clone(), balance_new_owner);19381939 // update index collection1940 Self::move_token_index(collection_id, item_id, owner.clone(), new_owner.clone())?;1941 } else {1942 let mut new_full_item = full_item.clone();1943 new_full_item.value -= value;19441945 // separate amount1946 if new_owner_account_id > 0 {1947 // new owner has account1948 let mut item = <FungibleItemList<T>>::get(collection_id, new_owner_account_id);1949 item.value += value;19501951 // update balance1952 let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.clone())1953 .checked_add(value)1954 .ok_or(Error::<T>::NumOverflow)?;1955 <Balance<T>>::insert(collection_id, new_owner.clone(), balance_new_owner);19561957 <FungibleItemList<T>>::insert(collection_id, new_owner_account_id, item);1958 } else {1959 // new owner do not have account1960 let item = FungibleItemType {1961 collection: collection_id,1962 owner: new_owner.clone(),1963 value1964 };19651966 Self::add_fungible_item(item)?;1967 }19681969 if amount == value {1970 Self::remove_token_index(collection_id, item_id, full_item.owner.clone())?;19711972 // remove approve list1973 <ApprovedList<T>>::remove(collection_id, (item_id, full_item.owner.clone()));1974 <FungibleItemList<T>>::remove(collection_id, item_id);1975 }19761977 <FungibleItemList<T>>::insert(collection_id, item_id, new_full_item);1978 }19791980 Ok(())1981 }19821983 fn transfer_refungible(1984 collection_id: CollectionId,1985 item_id: TokenId,1986 value: u128,1987 owner: T::AccountId,1988 new_owner: T::AccountId,1989 ) -> DispatchResult {1990 ensure!(1991 <ReFungibleItemList<T>>::contains_key(collection_id, item_id),1992 Error::<T>::TokenNotFound1993 );19941995 let full_item = <ReFungibleItemList<T>>::get(collection_id, item_id);1996 let item = full_item1997 .owner1998 .iter()1999 .filter(|i| i.owner == owner)2000 .next()2001 .ok_or(Error::<T>::NumOverflow)?;2002 let amount = item.fraction;20032004 ensure!(amount >= value, Error::<T>::TokenValueTooLow);20052006 // update balance2007 let balance_old_owner = <Balance<T>>::get(collection_id, item.owner.clone())2008 .checked_sub(value)2009 .ok_or(Error::<T>::NumOverflow)?;2010 <Balance<T>>::insert(collection_id, item.owner.clone(), balance_old_owner);20112012 let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.clone())2013 .checked_add(value)2014 .ok_or(Error::<T>::NumOverflow)?;2015 <Balance<T>>::insert(collection_id, new_owner.clone(), balance_new_owner);20162017 let old_owner = item.owner.clone();2018 let new_owner_has_account = full_item.owner.iter().any(|i| i.owner == new_owner);20192020 // transfer2021 if amount == value && !new_owner_has_account {2022 // change owner2023 // new owner do not have account2024 let mut new_full_item = full_item.clone();2025 new_full_item2026 .owner2027 .iter_mut()2028 .find(|i| i.owner == owner)2029 .unwrap()2030 .owner = new_owner.clone();2031 <ReFungibleItemList<T>>::insert(collection_id, item_id, new_full_item);20322033 // update index collection2034 Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?;2035 } else {2036 let mut new_full_item = full_item.clone();2037 new_full_item2038 .owner2039 .iter_mut()2040 .find(|i| i.owner == owner)2041 .unwrap()2042 .fraction -= value;20432044 // separate amount2045 if new_owner_has_account {2046 // new owner has account2047 new_full_item2048 .owner2049 .iter_mut()2050 .find(|i| i.owner == new_owner)2051 .unwrap()2052 .fraction += value;2053 } else {2054 // new owner do not have account2055 new_full_item.owner.push(Ownership {2056 owner: new_owner.clone(),2057 fraction: value,2058 });2059 Self::add_token_index(collection_id, item_id, new_owner.clone())?;2060 }20612062 <ReFungibleItemList<T>>::insert(collection_id, item_id, new_full_item);2063 }20642065 Ok(())2066 }20672068 fn transfer_nft(2069 collection_id: CollectionId,2070 item_id: TokenId,2071 sender: T::AccountId,2072 new_owner: T::AccountId,2073 ) -> DispatchResult {2074 ensure!(2075 <NftItemList<T>>::contains_key(collection_id, item_id),2076 Error::<T>::TokenNotFound2077 );20782079 let mut item = <NftItemList<T>>::get(collection_id, item_id);20802081 ensure!(2082 sender == item.owner,2083 Error::<T>::MustBeTokenOwner2084 );20852086 // update balance2087 let balance_old_owner = <Balance<T>>::get(collection_id, item.owner.clone())2088 .checked_sub(1)2089 .ok_or(Error::<T>::NumOverflow)?;2090 <Balance<T>>::insert(collection_id, item.owner.clone(), balance_old_owner);20912092 let balance_new_owner = <Balance<T>>::get(collection_id, new_owner.clone())2093 .checked_add(1)2094 .ok_or(Error::<T>::NumOverflow)?;2095 <Balance<T>>::insert(collection_id, new_owner.clone(), balance_new_owner);20962097 // change owner2098 let old_owner = item.owner.clone();2099 item.owner = new_owner.clone();2100 <NftItemList<T>>::insert(collection_id, item_id, item);21012102 // update index collection2103 Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?;21042105 // reset approved list2106 <ApprovedList<T>>::remove(collection_id, (item_id, old_owner));2107 Ok(())2108 }2109 2110 fn item_exists(2111 collection_id: CollectionId,2112 item_id: TokenId,2113 mode: &CollectionMode2114 ) -> DispatchResult {2115 match mode {2116 CollectionMode::NFT => ensure!(<NftItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),2117 CollectionMode::ReFungible(_) => ensure!(<ReFungibleItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),2118 CollectionMode::Fungible(_) => ensure!(<FungibleItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),2119 _ => ()2120 };2121 2122 Ok(())2123 }21242125 fn set_re_fungible_variable_data(2126 collection_id: CollectionId,2127 item_id: TokenId,2128 data: Vec<u8>2129 ) -> DispatchResult {2130 let mut item = <ReFungibleItemList<T>>::get(collection_id, item_id);21312132 item.variable_data = data;21332134 <ReFungibleItemList<T>>::insert(collection_id, item_id, item);21352136 Ok(())2137 }21382139 fn set_nft_variable_data(2140 collection_id: CollectionId,2141 item_id: TokenId,2142 data: Vec<u8>2143 ) -> DispatchResult {2144 let mut item = <NftItemList<T>>::get(collection_id, item_id);2145 2146 item.variable_data = data;21472148 <NftItemList<T>>::insert(collection_id, item_id, item);2149 2150 Ok(())2151 }21522153 fn init_collection(item: &CollectionType<T::AccountId>) {2154 // check params2155 assert!(2156 item.decimal_points <= MAX_DECIMAL_POINTS,2157 "decimal_points parameter must be lower than MAX_DECIMAL_POINTS"2158 );2159 assert!(2160 item.name.len() <= 64,2161 "Collection name can not be longer than 63 char"2162 );2163 assert!(2164 item.name.len() <= 256,2165 "Collection description can not be longer than 255 char"2166 );2167 assert!(2168 item.token_prefix.len() <= 16,2169 "Token prefix can not be longer than 15 char"2170 );21712172 // Generate next collection ID2173 let next_id = CreatedCollectionCount::get()2174 .checked_add(1)2175 .unwrap();21762177 CreatedCollectionCount::put(next_id);2178 }21792180 fn init_nft_token(item: &NftItemType<T::AccountId>) {2181 let current_index = <ItemListIndex>::get(item.collection)2182 .checked_add(1)2183 .unwrap();21842185 let item_owner = item.owner.clone();2186 let collection_id = item.collection.clone();2187 Self::add_token_index(collection_id, current_index, item.owner.clone()).unwrap();21882189 <ItemListIndex>::insert(collection_id, current_index);21902191 // Update balance2192 let new_balance = <Balance<T>>::get(collection_id, item_owner.clone())2193 .checked_add(1)2194 .unwrap();2195 <Balance<T>>::insert(collection_id, item_owner.clone(), new_balance);2196 }21972198 fn init_fungible_token(item: &FungibleItemType<T::AccountId>) {2199 let current_index = <ItemListIndex>::get(item.collection)2200 .checked_add(1)2201 .unwrap();2202 let owner = item.owner.clone();22032204 Self::add_token_index(item.collection, current_index, owner.clone()).unwrap();22052206 <ItemListIndex>::insert(item.collection, current_index);22072208 // Update balance2209 let new_balance = <Balance<T>>::get(item.collection, owner.clone())2210 .checked_add(item.value)2211 .unwrap();2212 <Balance<T>>::insert(item.collection, owner.clone(), new_balance);2213 }22142215 fn init_refungible_token(item: &ReFungibleItemType<T::AccountId>) {2216 let current_index = <ItemListIndex>::get(item.collection)2217 .checked_add(1)2218 .unwrap();22192220 let value = item.owner.first().unwrap().fraction;2221 let owner = item.owner.first().unwrap().owner.clone();22222223 Self::add_token_index(item.collection, current_index, owner.clone()).unwrap();22242225 <ItemListIndex>::insert(item.collection, current_index);22262227 // Update balance2228 let new_balance = <Balance<T>>::get(item.collection, owner.clone())2229 .checked_add(value)2230 .unwrap();2231 <Balance<T>>::insert(item.collection, owner.clone(), new_balance);2232 }22332234 fn add_token_index(collection_id: CollectionId, item_index: TokenId, owner: T::AccountId) -> DispatchResult {22352236 // add to account limit2237 if <AccountItemCount<T>>::contains_key(owner.clone()) {22382239 // bound Owned tokens by a single address2240 let count = <AccountItemCount<T>>::get(owner.clone());2241 ensure!(count < ChainLimit::get().account_token_ownership_limit, Error::<T>::AddressOwnershipLimitExceeded);22422243 <AccountItemCount<T>>::insert(owner.clone(), count2244 .checked_add(1)2245 .ok_or(Error::<T>::NumOverflow)?);2246 }2247 else {2248 <AccountItemCount<T>>::insert(owner.clone(), 1);2249 }22502251 let list_exists = <AddressTokens<T>>::contains_key(collection_id, owner.clone());2252 if list_exists {2253 let mut list = <AddressTokens<T>>::get(collection_id, owner.clone());2254 let item_contains = list.contains(&item_index.clone());22552256 if !item_contains {2257 list.push(item_index.clone());2258 }22592260 <AddressTokens<T>>::insert(collection_id, owner.clone(), list);2261 } else {2262 let mut itm = Vec::new();2263 itm.push(item_index.clone());2264 <AddressTokens<T>>::insert(collection_id, owner, itm);2265 2266 }22672268 Ok(())2269 }22702271 fn remove_token_index(2272 collection_id: CollectionId,2273 item_index: TokenId,2274 owner: T::AccountId,2275 ) -> DispatchResult {22762277 // update counter2278 <AccountItemCount<T>>::insert(owner.clone(), 2279 <AccountItemCount<T>>::get(owner.clone())2280 .checked_sub(1)2281 .ok_or(Error::<T>::NumOverflow)?);228222832284 let list_exists = <AddressTokens<T>>::contains_key(collection_id, owner.clone());2285 if list_exists {2286 let mut list = <AddressTokens<T>>::get(collection_id, owner.clone());2287 let item_contains = list.contains(&item_index.clone());22882289 if item_contains {2290 list.retain(|&item| item != item_index);2291 <AddressTokens<T>>::insert(collection_id, owner, list);2292 }2293 }22942295 Ok(())2296 }22972298 fn move_token_index(2299 collection_id: CollectionId,2300 item_index: TokenId,2301 old_owner: T::AccountId,2302 new_owner: T::AccountId,2303 ) -> DispatchResult {2304 Self::remove_token_index(collection_id, item_index, old_owner)?;2305 Self::add_token_index(collection_id, item_index, new_owner)?;23062307 Ok(())2308 }2309 2310 fn ensure_contract_owned(account: T::AccountId, contract: &T::AccountId) -> DispatchResult {2311 if <ContractOwner<T>>::contains_key(contract.clone()) {2312 let owner = <ContractOwner<T>>::get(contract);2313 ensure!(account == owner, Error::<T>::NoPermission);2314 } else {2315 fail!(Error::<T>::NoPermission);2316 }23172318 Ok(())2319 }2320}23212322////////////////////////////////////////////////////////////////////////////////////////////////////2323// Economic models2324// #region23252326/// Fee multiplier.2327pub type Multiplier = FixedU128;23282329type BalanceOf<T> = <<T as transaction_payment::Trait>::Currency as Currency<2330 <T as system::Trait>::AccountId,2331>>::Balance;2332type NegativeImbalanceOf<T> = <<T as transaction_payment::Trait>::Currency as Currency<2333 <T as system::Trait>::AccountId,2334>>::NegativeImbalance;23352336/// Require the transactor pay for themselves and maybe include a tip to gain additional priority2337/// in the queue.2338#[derive(Encode, Decode, Clone, Eq, PartialEq)]2339pub struct ChargeTransactionPayment<T: Trait + Send + Sync>(2340 #[codec(compact)] BalanceOf<T>2341);23422343impl<T: Trait + Send + Sync> sp_std::fmt::Debug2344 for ChargeTransactionPayment<T>2345{2346 #[cfg(feature = "std")]2347 fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {2348 write!(f, "ChargeTransactionPayment<{:?}>", self.0)2349 }2350 #[cfg(not(feature = "std"))]2351 fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {2352 Ok(())2353 }2354}23552356impl<T: Trait + Send + Sync> ChargeTransactionPayment<T>2357where2358 T::Call: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> + IsSubType<Call<T>> + IsSubType<pallet_contracts::Call<T>>,2359 BalanceOf<T>: Send + Sync + FixedPointOperand,2360{2361 /// utility constructor. Used only in client/factory code.2362 pub fn from(fee: BalanceOf<T>) -> Self {2363 Self(fee)2364 }23652366 pub fn traditional_fee(2367 len: usize,2368 info: &DispatchInfoOf<T::Call>,2369 tip: BalanceOf<T>,2370 ) -> BalanceOf<T>2371 where2372 T::Call: Dispatchable<Info = DispatchInfo>,2373 {2374 <transaction_payment::Module<T>>::compute_fee(len as u32, info, tip)2375 }23762377 fn get_priority(len: usize, info: &DispatchInfoOf<T::Call>, final_fee: BalanceOf<T>) -> TransactionPriority {2378 let weight_saturation = T::MaximumBlockWeight::get() / info.weight.max(1);2379 let len_saturation = T::MaximumBlockLength::get() as u64 / (len as u64).max(1);2380 let coefficient: BalanceOf<T> = weight_saturation.min(len_saturation).saturated_into::<BalanceOf<T>>();2381 final_fee.saturating_mul(coefficient).saturated_into::<TransactionPriority>()2382 }23832384 fn withdraw_fee(2385 &self,2386 who: &T::AccountId,2387 call: &T::Call,2388 info: &DispatchInfoOf<T::Call>,2389 len: usize,2390 ) -> Result<(BalanceOf<T>, Option<NegativeImbalanceOf<T>>), TransactionValidityError> {2391 let tip = self.0;23922393 // Set fee based on call type. Creating collection costs 1 Unique.2394 // All other transactions have traditional fees so far2395 // let fee = match call.is_sub_type() {2396 // Some(Call::create_collection(..)) => <BalanceOf<T>>::from(1_000_000_000),2397 // _ => Self::traditional_fee(len, info, tip), // Flat fee model, use only for testing purposes2398 // // _ => <BalanceOf<T>>::from(100)2399 // };2400 let fee = Self::traditional_fee(len, info, tip);24012402 // Determine who is paying transaction fee based on ecnomic model2403 // Parse call to extract collection ID and access collection sponsor2404 let mut sponsor: T::AccountId = match IsSubType::<Call<T>>::is_sub_type(call) {2405 Some(Call::create_item(collection_id, _owner, _properties)) => {24062407 // check free create limit2408 if <Collection<T>>::get(collection_id).limits.sponsored_data_size >= (_properties.len() as u32)2409 {2410 <Collection<T>>::get(collection_id).sponsor2411 } else {2412 T::AccountId::default()2413 }2414 }2415 Some(Call::transfer(_new_owner, collection_id, _item_id, _value)) => {2416 2417 let _collection_limits = <Collection<T>>::get(collection_id).limits;2418 let _collection_mode = <Collection<T>>::get(collection_id).mode;24192420 // sponsor timeout2421 let sponsor_transfer = match _collection_mode {2422 CollectionMode::NFT => {24232424 // get correct limit2425 let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 {2426 _collection_limits.sponsor_transfer_timeout2427 } else {2428 ChainLimit::get().nft_sponsor_transfer_timeout2429 };24302431 let basket = <NftTransferBasket<T>>::get(collection_id, _item_id);2432 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2433 let limit_time = basket + limit.into();2434 if block_number >= limit_time {2435 <NftTransferBasket<T>>::insert(collection_id, _item_id, block_number);2436 true2437 }2438 else {2439 false2440 }2441 }2442 CollectionMode::Fungible(_) => {24432444 // get correct limit2445 let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 {2446 _collection_limits.sponsor_transfer_timeout2447 } else {2448 ChainLimit::get().fungible_sponsor_transfer_timeout2449 };24502451 let mut basket = <FungibleTransferBasket<T>>::get(collection_id, _item_id);2452 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2453 if basket.iter().any(|i| i.address == _new_owner.clone())2454 {2455 let item = basket.iter_mut().find(|i| i.address == _new_owner.clone()).unwrap().clone();2456 let limit_time = item.start_block + limit.into();2457 if block_number >= limit_time {2458 basket.retain(|x| x.address == item.address);2459 basket.push(BasketItem { start_block: block_number, address: _new_owner.clone() });2460 <FungibleTransferBasket<T>>::insert(collection_id, _item_id, basket);2461 true2462 }2463 else {2464 false2465 }2466 }2467 else {2468 basket.push(BasketItem { start_block: block_number, address: _new_owner.clone()});2469 true2470 }2471 }2472 CollectionMode::ReFungible(_) => {24732474 // get correct limit2475 let limit: u32 = if _collection_limits.sponsor_transfer_timeout > 0 {2476 _collection_limits.sponsor_transfer_timeout2477 } else {2478 ChainLimit::get().refungible_sponsor_transfer_timeout2479 };24802481 let basket = <ReFungibleTransferBasket<T>>::get(collection_id, _item_id);2482 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2483 let limit_time = basket + limit.into();2484 if block_number >= limit_time {2485 <ReFungibleTransferBasket<T>>::insert(collection_id, _item_id, block_number);2486 true2487 } else {2488 false2489 }2490 }2491 _ => {2492 false2493 },2494 };24952496 if !sponsor_transfer {2497 T::AccountId::default()2498 } else {2499 <Collection<T>>::get(collection_id).sponsor2500 }2501 }25022503 _ => T::AccountId::default(),2504 };25052506 // Sponsor smart contracts2507 sponsor = match IsSubType::<pallet_contracts::Call<T>>::is_sub_type(call) {25082509 // On instantiation: set the contract owner2510 Some(pallet_contracts::Call::instantiate(_endowment, _gas_limit, code_hash, data)) => {25112512 let new_contract_address = <T as pallet_contracts::Trait>::DetermineContractAddress::contract_address_for(2513 code_hash,2514 &data,2515 &who,2516 );2517 <ContractOwner<T>>::insert(new_contract_address.clone(), who.clone());25182519 T::AccountId::default()2520 },25212522 // When the contract is called, check if the sponsoring is enabled and pay fees from contract endowment if it is2523 Some(pallet_contracts::Call::call(dest, _value, _gas_limit, _data)) => {25242525 let called_contract: T::AccountId = T::Lookup::lookup((*dest).clone()).unwrap_or(T::AccountId::default());25262527 let owned_contract = <ContractOwner<T>>::contains_key(called_contract.clone())2528 && <ContractOwner<T>>::get(called_contract.clone()) == *who;2529 let white_list_enabled = <ContractWhiteListEnabled<T>>::contains_key(called_contract.clone()) && <ContractWhiteListEnabled<T>>::get(called_contract.clone());2530 2531 if !owned_contract && white_list_enabled {2532 if !<ContractWhiteList<T>>::contains_key(called_contract.clone(), who) {2533 return Err(InvalidTransaction::Call.into());2534 }2535 }25362537 let mut sponsor_transfer = false;2538 if <ContractSponsoringRateLimit<T>>::contains_key(called_contract.clone()) {2539 let last_tx_block = <ContractSponsorBasket<T>>::get((&called_contract, &who));2540 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2541 let rate_limit = <ContractSponsoringRateLimit<T>>::get(&called_contract);2542 let limit_time = last_tx_block + rate_limit;25432544 if block_number >= limit_time {2545 <ContractSponsorBasket<T>>::insert((called_contract.clone(), who.clone()), block_number);2546 sponsor_transfer = true;2547 }2548 } else {2549 sponsor_transfer = false;2550 }2551 2552 2553 let mut sp = T::AccountId::default();2554 if sponsor_transfer {2555 if <ContractSelfSponsoring<T>>::contains_key(called_contract.clone()) {2556 if <ContractSelfSponsoring<T>>::get(called_contract.clone()) {2557 sp = called_contract;2558 }2559 }2560 }25612562 sp2563 },25642565 _ => sponsor,2566 };25672568 let mut who_pays_fee: T::AccountId = sponsor.clone();2569 if sponsor == T::AccountId::default() {2570 who_pays_fee = who.clone();2571 }25722573 // Only mess with balances if fee is not zero.2574 if fee.is_zero() {2575 return Ok((fee, None));2576 }25772578 match <T as transaction_payment::Trait>::Currency::withdraw(2579 &who_pays_fee,2580 fee,2581 if tip.is_zero() {2582 WithdrawReason::TransactionPayment.into()2583 } else {2584 WithdrawReason::TransactionPayment | WithdrawReason::Tip2585 },2586 ExistenceRequirement::KeepAlive,2587 ) {2588 Ok(imbalance) => Ok((fee, Some(imbalance))),2589 Err(_) => Err(InvalidTransaction::Payment.into()),2590 }2591 }2592}259325942595impl<T: Trait + Send + Sync> SignedExtension2596 for ChargeTransactionPayment<T>2597where2598 BalanceOf<T>: Send + Sync + From<u64> + FixedPointOperand,2599 T::Call: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> + IsSubType<Call<T>> + IsSubType<pallet_contracts::Call<T>>,2600{2601 const IDENTIFIER: &'static str = "ChargeTransactionPayment";2602 type AccountId = T::AccountId;2603 type Call = T::Call;2604 type AdditionalSigned = ();2605 type Pre = (2606 BalanceOf<T>,2607 Self::AccountId,2608 Option<NegativeImbalanceOf<T>>,2609 BalanceOf<T>,2610 );2611 fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> {2612 Ok(())2613 }26142615 fn validate(2616 &self,2617 who: &Self::AccountId,2618 call: &Self::Call,2619 info: &DispatchInfoOf<Self::Call>,2620 len: usize,2621 ) -> TransactionValidity {2622 let (fee, _) = self.withdraw_fee(who, call, info, len)?;2623 Ok(ValidTransaction {2624 priority: Self::get_priority(len, info, fee),2625 ..Default::default()2626 })2627 }26282629 fn pre_dispatch(2630 self,2631 who: &Self::AccountId,2632 call: &Self::Call,2633 info: &DispatchInfoOf<Self::Call>,2634 len: usize,2635 ) -> Result<Self::Pre, TransactionValidityError> {2636 let (fee, imbalance) = self.withdraw_fee(who, call, info, len)?;2637 Ok((self.0, who.clone(), imbalance, fee))2638 }26392640 fn post_dispatch(2641 pre: Self::Pre,2642 info: &DispatchInfoOf<Self::Call>,2643 post_info: &PostDispatchInfoOf<Self::Call>,2644 len: usize,2645 _result: &DispatchResult,2646 ) -> Result<(), TransactionValidityError> {2647 let (tip, who, imbalance, fee) = pre;2648 if let Some(payed) = imbalance {2649 let actual_fee = <transaction_payment::Module<T>>::compute_actual_fee(2650 len as u32, info, post_info, tip,2651 );2652 let refund = fee.saturating_sub(actual_fee);2653 let actual_payment =2654 match <T as transaction_payment::Trait>::Currency::deposit_into_existing(2655 &who, refund,2656 ) {2657 Ok(refund_imbalance) => {2658 // The refund cannot be larger than the up front payed max weight.2659 // `PostDispatchInfo::calc_unspent` guards against such a case.2660 match payed.offset(refund_imbalance) {2661 Ok(actual_payment) => actual_payment,2662 Err(_) => return Err(InvalidTransaction::Payment.into()),2663 }2664 }2665 // We do not recreate the account using the refund. The up front payment2666 // is gone in that case.2667 Err(_) => payed,2668 };2669 let imbalances = actual_payment.split(tip);2670 <T as transaction_payment::Trait>::OnTransactionPayment::on_unbalanceds(2671 Some(imbalances.0).into_iter().chain(Some(imbalances.1)),2672 );2673 }2674 Ok(())2675 }2676}26772678// #endregion1//2// This file is subject to the terms and conditions defined in3// file 'LICENSE', which is part of this source code package.4//56#![recursion_limit = "1024"]78#![cfg_attr(not(feature = "std"), no_std)]910#[cfg(feature = "std")]11pub use std::*;1213#[cfg(feature = "std")]14pub use serde::*;1516use codec::{Decode, Encode};17pub use frame_support::{18 construct_runtime, decl_event, decl_module, decl_storage, decl_error,19 dispatch::DispatchResult,20 ensure, fail, parameter_types,21 traits::{22 Currency, ExistenceRequirement, Get, Imbalance, KeyOwnerProofSystem, OnUnbalanced,23 Randomness, WithdrawReason,24 },25 weights::{26 constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND},27 DispatchInfo, GetDispatchInfo, IdentityFee, Pays, PostDispatchInfo, Weight,28 WeightToFeePolynomial,29 },30 IsSubType, StorageValue,31};3233use frame_system::{self as system, ensure_signed, ensure_root};34use sp_runtime::sp_std::prelude::Vec;35use sp_runtime::{36 traits::{37 DispatchInfoOf, Dispatchable, PostDispatchInfoOf, Saturating, SaturatedConversion, SignedExtension, Zero,38 },39 transaction_validity::{40 TransactionPriority, InvalidTransaction, TransactionValidity, TransactionValidityError, ValidTransaction,41 },42 FixedPointOperand, FixedU128,43};44use pallet_contracts::ContractAddressFor;45use sp_runtime::traits::StaticLookup;4647#[cfg(test)]48mod mock;4950#[cfg(test)]51mod tests;5253mod default_weights;5455pub const MAX_DECIMAL_POINTS: DecimalPoints = 30;56pub const MAX_SPONSOR_TIMEOUT: u32 = 10_368_000;57pub const MAX_TOKEN_OWNERSHIP: u32 = 10_000_000;5859// Structs60// #region6162pub type CollectionId = u32;63pub type TokenId = u32;64pub type DecimalPoints = u8;6566#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)]67#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]68pub enum CollectionMode {69 Invalid,70 NFT,71 // decimal points72 Fungible(DecimalPoints),73 // decimal points74 ReFungible(DecimalPoints),75}7677impl Into<u8> for CollectionMode {78 fn into(self) -> u8 {79 match self {80 CollectionMode::Invalid => 0,81 CollectionMode::NFT => 1,82 CollectionMode::Fungible(_) => 2,83 CollectionMode::ReFungible(_) => 3,84 }85 }86}8788#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)]89#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]90pub enum AccessMode {91 Normal,92 WhiteList,93}94impl Default for AccessMode {95 fn default() -> Self {96 Self::Normal97 }98}99100impl Default for CollectionMode {101 fn default() -> Self {102 Self::Invalid103 }104}105106#[derive(Encode, Decode, Eq, Debug, Clone, PartialEq)]107#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]108pub enum SchemaVersion {109 ImageURL,110 Unique,111}112impl Default for SchemaVersion {113 fn default() -> Self {114 Self::ImageURL115 }116}117118#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]119#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]120pub struct Ownership<AccountId> {121 pub owner: AccountId,122 pub fraction: u128,123}124125#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]126#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]127pub struct CollectionType<AccountId> {128 pub owner: AccountId,129 pub mode: CollectionMode,130 pub access: AccessMode,131 pub decimal_points: DecimalPoints,132 pub name: Vec<u16>, // 64 include null escape char133 pub description: Vec<u16>, // 256 include null escape char134 pub token_prefix: Vec<u8>, // 16 include null escape char135 pub mint_mode: bool,136 pub offchain_schema: Vec<u8>,137 pub schema_version: SchemaVersion,138 pub sponsor: AccountId, // Who pays fees. If set to default address, the fees are applied to the transaction sender139 pub sponsor_confirmed: bool, // False if sponsor address has not yet confirmed sponsorship. True otherwise.140 pub limits: CollectionLimits, // Collection private restrictions 141 pub variable_on_chain_schema: Vec<u8>, //142 pub const_on_chain_schema: Vec<u8>, //143}144145#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]146#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]147pub struct NftItemType<AccountId> {148 pub owner: AccountId,149 pub const_data: Vec<u8>,150 pub variable_data: Vec<u8>,151}152153#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]154#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]155pub struct FungibleItemType {156 pub value: u128,157}158159#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]160#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]161pub struct ReFungibleItemType<AccountId> {162 pub owner: Vec<Ownership<AccountId>>,163 pub const_data: Vec<u8>,164 pub variable_data: Vec<u8>,165}166167// #[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]168// #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]169// pub struct VestingItem<AccountId, Moment> {170// pub sender: AccountId,171// pub recipient: AccountId,172// pub collection_id: CollectionId,173// pub item_id: TokenId,174// pub amount: u64,175// pub vesting_date: Moment,176// }177178#[derive(Encode, Decode, Debug, Clone, PartialEq)]179#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]180pub struct CollectionLimits {181 pub account_token_ownership_limit: u32,182 pub sponsored_data_size: u32,183 pub token_limit: u32,184185 // Timeouts for item types in passed blocks186 pub sponsor_transfer_timeout: u32,187}188189impl Default for CollectionLimits {190 fn default() -> CollectionLimits {191 CollectionLimits { 192 account_token_ownership_limit: 10_000_000, 193 token_limit: u32::max_value(),194 sponsored_data_size: u32::max_value(), 195 sponsor_transfer_timeout: 14400 }196 }197}198199#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]200#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]201pub struct ChainLimits {202 pub collection_numbers_limit: u32,203 pub account_token_ownership_limit: u32,204 pub collections_admins_limit: u64,205 pub custom_data_limit: u32,206207 // Timeouts for item types in passed blocks208 pub nft_sponsor_transfer_timeout: u32,209 pub fungible_sponsor_transfer_timeout: u32,210 pub refungible_sponsor_transfer_timeout: u32,211}212213pub trait WeightInfo {214 fn create_collection() -> Weight;215 fn destroy_collection() -> Weight;216 fn add_to_white_list() -> Weight;217 fn remove_from_white_list() -> Weight;218 fn set_public_access_mode() -> Weight;219 fn set_mint_permission() -> Weight;220 fn change_collection_owner() -> Weight;221 fn add_collection_admin() -> Weight;222 fn remove_collection_admin() -> Weight;223 fn set_collection_sponsor() -> Weight;224 fn confirm_sponsorship() -> Weight;225 fn remove_collection_sponsor() -> Weight;226 fn create_item(s: usize) -> Weight;227 fn burn_item() -> Weight;228 fn transfer() -> Weight;229 fn approve() -> Weight;230 fn transfer_from() -> Weight;231 fn set_offchain_schema() -> Weight;232 fn set_const_on_chain_schema() -> Weight;233 fn set_variable_on_chain_schema() -> Weight;234 fn set_variable_meta_data() -> Weight;235 fn enable_contract_sponsoring() -> Weight;236 fn set_schema_version() -> Weight;237 fn set_chain_limits() -> Weight;238 fn set_contract_sponsoring_rate_limit() -> Weight;239 fn toggle_contract_white_list() -> Weight;240 fn add_to_contract_white_list() -> Weight;241 fn remove_from_contract_white_list() -> Weight;242 fn set_collection_limits() -> Weight;243}244245#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]246#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]247pub struct CreateNftData {248 pub const_data: Vec<u8>,249 pub variable_data: Vec<u8>,250}251252#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]253#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]254pub struct CreateFungibleData {255 pub value: u128,256}257258#[derive(Encode, Decode, Default, Debug, Clone, PartialEq)]259#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]260pub struct CreateReFungibleData {261 pub const_data: Vec<u8>,262 pub variable_data: Vec<u8>,263}264265#[derive(Encode, Decode, Debug, Clone, PartialEq)]266#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]267pub enum CreateItemData {268 NFT(CreateNftData),269 Fungible(CreateFungibleData),270 ReFungible(CreateReFungibleData),271}272273impl CreateItemData {274 pub fn len(&self) -> usize {275 let len = match self {276 CreateItemData::NFT(data) => data.variable_data.len() + data.const_data.len(),277 CreateItemData::ReFungible(data) => data.variable_data.len() + data.const_data.len(),278 _ => 0279 };280 281 return len;282 }283}284285impl From<CreateNftData> for CreateItemData {286 fn from(item: CreateNftData) -> Self {287 CreateItemData::NFT(item)288 }289}290291impl From<CreateReFungibleData> for CreateItemData {292 fn from(item: CreateReFungibleData) -> Self {293 CreateItemData::ReFungible(item)294 }295}296297impl From<CreateFungibleData> for CreateItemData {298 fn from(item: CreateFungibleData) -> Self {299 CreateItemData::Fungible(item)300 }301}302303304decl_error! {305 /// Error for non-fungible-token module.306 pub enum Error for Module<T: Trait> {307 /// Total collections bound exceeded.308 TotalCollectionsLimitExceeded,309 /// Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30.310 CollectionDecimalPointLimitExceeded, 311 /// Collection name can not be longer than 63 char.312 CollectionNameLimitExceeded, 313 /// Collection description can not be longer than 255 char.314 CollectionDescriptionLimitExceeded, 315 /// Token prefix can not be longer than 15 char.316 CollectionTokenPrefixLimitExceeded,317 /// This collection does not exist.318 CollectionNotFound,319 /// Item not exists.320 TokenNotFound,321 /// Arithmetic calculation overflow.322 NumOverflow, 323 /// Account already has admin role.324 AlreadyAdmin, 325 /// You do not own this collection.326 NoPermission,327 /// This address is not set as sponsor, use setCollectionSponsor first.328 ConfirmUnsetSponsorFail,329 /// Collection is not in mint mode.330 PublicMintingNotAllowed,331 /// Sender parameter and item owner must be equal.332 MustBeTokenOwner,333 /// Item balance not enough.334 TokenValueTooLow,335 /// Size of item is too large.336 NftSizeLimitExceeded,337 /// No approve found338 ApproveNotFound,339 /// Requested value more than approved.340 TokenValueNotEnough,341 /// Only approved addresses can call this method.342 ApproveRequired,343 /// Address is not in white list.344 AddresNotInWhiteList,345 /// Number of collection admins bound exceeded.346 CollectionAdminsLimitExceeded,347 /// Owned tokens by a single address bound exceeded.348 AddressOwnershipLimitExceeded,349 /// Length of items properties must be greater than 0.350 EmptyArgument,351 /// const_data exceeded data limit.352 TokenConstDataLimitExceeded,353 /// variable_data exceeded data limit.354 TokenVariableDataLimitExceeded,355 /// Not NFT item data used to mint in NFT collection.356 NotNftDataUsedToMintNftCollectionToken,357 /// Not Fungible item data used to mint in Fungible collection.358 NotFungibleDataUsedToMintFungibleCollectionToken,359 /// Not Re Fungible item data used to mint in Re Fungible collection.360 NotReFungibleDataUsedToMintReFungibleCollectionToken,361 /// Unexpected collection type.362 UnexpectedCollectionType,363 /// Can't store metadata in fungible tokens.364 CantStoreMetadataInFungibleTokens,365 /// Collection token limit exceeded366 CollectionTokenLimitExceeded,367 /// Account token limit exceeded per collection368 AccountTokenLimitExceeded,369 /// Collection limit bounds per collection exceeded370 CollectionLimitBoundsExceeded371 }372}373374pub trait Trait: system::Trait + Sized + transaction_payment::Trait + pallet_contracts::Trait {375 type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;376377 /// Weight information for extrinsics in this pallet.378 type WeightInfo: WeightInfo;379}380381#[cfg(feature = "runtime-benchmarks")]382mod benchmarking;383384// #endregion385386decl_storage! {387 trait Store for Module<T: Trait> as Nft {388389 // Private members390 NextCollectionID: CollectionId;391 CreatedCollectionCount: u32;392 ChainVersion: u64;393 ItemListIndex: map hasher(identity) CollectionId => TokenId;394395 // Chain limits struct396 pub ChainLimit get(fn chain_limit) config(): ChainLimits;397398 // Bound counters399 CollectionCount: u32;400 pub AccountItemCount get(fn account_item_count): map hasher(twox_64_concat) T::AccountId => u32;401402 // Basic collections403 pub Collection get(fn collection) config(): map hasher(identity) CollectionId => CollectionType<T::AccountId>;404 pub AdminList get(fn admin_list_collection): map hasher(identity) CollectionId => Vec<T::AccountId>;405 pub WhiteList get(fn white_list): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => bool;406407 /// Balance owner per collection map408 pub Balance get(fn balance_count): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => u128;409410 /// second parameter: item id + owner account id + spender account id411 pub Allowances get(fn approved): double_map hasher(identity) CollectionId, hasher(twox_64_concat) (TokenId, T::AccountId, T::AccountId) => u128;412413 /// Item collections414 pub NftItemList get(fn nft_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => NftItemType<T::AccountId>;415 pub FungibleItemList get(fn fungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => FungibleItemType;416 pub ReFungibleItemList get(fn refungible_item_id) config(): double_map hasher(identity) CollectionId, hasher(identity) TokenId => ReFungibleItemType<T::AccountId>;417418 /// Index list419 pub AddressTokens get(fn address_tokens): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => Vec<TokenId>;420421 /// Tokens transfer baskets422 pub NftTransferBasket get(fn nft_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;423 pub FungibleTransferBasket get(fn fungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(twox_64_concat) T::AccountId => T::BlockNumber;424 pub ReFungibleTransferBasket get(fn refungible_transfer_basket): double_map hasher(identity) CollectionId, hasher(identity) TokenId => T::BlockNumber;425426 // Contract Sponsorship and Ownership427 pub ContractOwner get(fn contract_owner): map hasher(twox_64_concat) T::AccountId => T::AccountId;428 pub ContractSelfSponsoring get(fn contract_self_sponsoring): map hasher(twox_64_concat) T::AccountId => bool;429 pub ContractSponsorBasket get(fn contract_sponsor_basket): map hasher(twox_64_concat) (T::AccountId, T::AccountId) => T::BlockNumber;430 pub ContractSponsoringRateLimit get(fn contract_sponsoring_rate_limit): map hasher(twox_64_concat) T::AccountId => T::BlockNumber;431 pub ContractWhiteListEnabled get(fn contract_white_list_enabled): map hasher(twox_64_concat) T::AccountId => bool; 432 pub ContractWhiteList get(fn contract_white_list): double_map hasher(twox_64_concat) T::AccountId, hasher(twox_64_concat) T::AccountId => bool; 433 }434 add_extra_genesis {435 build(|config: &GenesisConfig<T>| {436 // Modification of storage437 for (_num, _c) in &config.collection {438 <Module<T>>::init_collection(_c);439 }440441 for (_num, _c, _i) in &config.nft_item_id {442 <Module<T>>::init_nft_token(*_c, _i);443 }444445 for (collection_id, account_id, fungible_item) in &config.fungible_item_id {446 <Module<T>>::init_fungible_token(*collection_id, account_id, fungible_item);447 }448449 for (_num, _c, _i) in &config.refungible_item_id {450 <Module<T>>::init_refungible_token(*_c, _i);451 }452 })453 }454}455456decl_event!(457 pub enum Event<T>458 where459 AccountId = <T as system::Trait>::AccountId,460 {461 /// New collection was created462 /// 463 /// # Arguments464 /// 465 /// * collection_id: Globally unique identifier of newly created collection.466 /// 467 /// * mode: [CollectionMode] converted into u8.468 /// 469 /// * account_id: Collection owner.470 Created(CollectionId, u8, AccountId),471472 /// New item was created.473 /// 474 /// # Arguments475 /// 476 /// * collection_id: Id of the collection where item was created.477 /// 478 /// * item_id: Id of an item. Unique within the collection.479 ItemCreated(CollectionId, TokenId),480481 /// Collection item was burned.482 /// 483 /// # Arguments484 /// 485 /// collection_id.486 /// 487 /// item_id: Identifier of burned NFT.488 ItemDestroyed(CollectionId, TokenId),489 }490);491492decl_module! {493 pub struct Module<T: Trait> for enum Call where origin: T::Origin {494495 fn deposit_event() = default;496 type Error = Error<T>;497498 fn on_initialize(now: T::BlockNumber) -> Weight {499500 if ChainVersion::get() < 2501 {502 let value = NextCollectionID::get();503 CreatedCollectionCount::put(value);504 ChainVersion::put(2);505 }506507 0508 }509510 /// This method creates a Collection of NFTs. Each Token may have multiple properties encoded as an array of bytes of certain length. The initial owner and admin of the collection are set to the address that signed the transaction. Both addresses can be changed later.511 /// 512 /// # Permissions513 /// 514 /// * Anyone.515 /// 516 /// # Arguments517 /// 518 /// * collection_name: UTF-16 string with collection name (limit 64 characters), will be stored as zero-terminated.519 /// 520 /// * collection_description: UTF-16 string with collection description (limit 256 characters), will be stored as zero-terminated.521 /// 522 /// * token_prefix: UTF-8 string with token prefix.523 /// 524 /// * mode: [CollectionMode] collection type and type dependent data.525 // returns collection ID526 #[weight = T::WeightInfo::create_collection()]527 pub fn create_collection(origin,528 collection_name: Vec<u16>,529 collection_description: Vec<u16>,530 token_prefix: Vec<u8>,531 mode: CollectionMode) -> DispatchResult {532533 // Anyone can create a collection534 let who = ensure_signed(origin)?;535536 let decimal_points = match mode {537 CollectionMode::Fungible(points) => points,538 CollectionMode::ReFungible(points) => points,539 _ => 0540 };541542 // bound Total number of collections543 ensure!(CollectionCount::get() < ChainLimit::get().collection_numbers_limit, Error::<T>::TotalCollectionsLimitExceeded);544545 // check params546 ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::<T>::CollectionDecimalPointLimitExceeded);547 ensure!(collection_name.len() <= 64, Error::<T>::CollectionNameLimitExceeded);548 ensure!(collection_description.len() <= 256, Error::<T>::CollectionDescriptionLimitExceeded);549 ensure!(token_prefix.len() <= 16, Error::<T>::CollectionTokenPrefixLimitExceeded);550551 // Generate next collection ID552 let next_id = CreatedCollectionCount::get()553 .checked_add(1)554 .ok_or(Error::<T>::NumOverflow)?;555556 // bound counter557 let total = CollectionCount::get()558 .checked_add(1)559 .ok_or(Error::<T>::NumOverflow)?;560561 CreatedCollectionCount::put(next_id);562 CollectionCount::put(total);563564 // Create new collection565 let new_collection = CollectionType {566 owner: who.clone(),567 name: collection_name,568 mode: mode.clone(),569 mint_mode: false,570 access: AccessMode::Normal,571 description: collection_description,572 decimal_points: decimal_points,573 token_prefix: token_prefix,574 offchain_schema: Vec::new(),575 schema_version: SchemaVersion::ImageURL,576 sponsor: T::AccountId::default(),577 sponsor_confirmed: false,578 variable_on_chain_schema: Vec::new(),579 const_on_chain_schema: Vec::new(),580 limits: CollectionLimits::default(),581 };582583 // Add new collection to map584 <Collection<T>>::insert(next_id, new_collection);585586 // call event587 Self::deposit_event(RawEvent::Created(next_id, mode.into(), who.clone()));588589 Ok(())590 }591592 /// **DANGEROUS**: Destroys collection and all NFTs within this collection. Users irrecoverably lose their assets and may lose real money.593 /// 594 /// # Permissions595 /// 596 /// * Collection Owner.597 /// 598 /// # Arguments599 /// 600 /// * collection_id: collection to destroy.601 #[weight = T::WeightInfo::destroy_collection()]602 pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {603604 let sender = ensure_signed(origin)?;605 Self::check_owner_permissions(collection_id, sender)?;606607 <AddressTokens<T>>::remove_prefix(collection_id);608 <Allowances<T>>::remove_prefix(collection_id);609 <Balance<T>>::remove_prefix(collection_id);610 <ItemListIndex>::remove(collection_id);611 <AdminList<T>>::remove(collection_id);612 <Collection<T>>::remove(collection_id);613 <WhiteList<T>>::remove_prefix(collection_id);614615 <NftItemList<T>>::remove_prefix(collection_id);616 <FungibleItemList<T>>::remove_prefix(collection_id);617 <ReFungibleItemList<T>>::remove_prefix(collection_id);618619 <NftTransferBasket<T>>::remove_prefix(collection_id);620 <FungibleTransferBasket<T>>::remove_prefix(collection_id);621 <ReFungibleTransferBasket<T>>::remove_prefix(collection_id);622623 if CollectionCount::get() > 0624 {625 // bound couter626 let total = CollectionCount::get()627 .checked_sub(1)628 .ok_or(Error::<T>::NumOverflow)?;629630 CollectionCount::put(total);631 }632633 Ok(())634 }635636 /// Add an address to white list.637 /// 638 /// # Permissions639 /// 640 /// * Collection Owner641 /// * Collection Admin642 /// 643 /// # Arguments644 /// 645 /// * collection_id.646 /// 647 /// * address.648 #[weight = T::WeightInfo::add_to_white_list()]649 pub fn add_to_white_list(origin, collection_id: CollectionId, address: T::AccountId) -> DispatchResult{650651 let sender = ensure_signed(origin)?;652 Self::check_owner_or_admin_permissions(collection_id, sender)?;653654 <WhiteList<T>>::insert(collection_id, address, true);655 656 Ok(())657 }658659 /// Remove an address from white list.660 /// 661 /// # Permissions662 /// 663 /// * Collection Owner664 /// * Collection Admin665 /// 666 /// # Arguments667 /// 668 /// * collection_id.669 /// 670 /// * address.671 #[weight = T::WeightInfo::remove_from_white_list()]672 pub fn remove_from_white_list(origin, collection_id: CollectionId, address: T::AccountId) -> DispatchResult{673674 let sender = ensure_signed(origin)?;675 Self::check_owner_or_admin_permissions(collection_id, sender)?;676677 <WhiteList<T>>::remove(collection_id, address);678679 Ok(())680 }681682 /// Toggle between normal and white list access for the methods with access for `Anyone`.683 /// 684 /// # Permissions685 /// 686 /// * Collection Owner.687 /// 688 /// # Arguments689 /// 690 /// * collection_id.691 /// 692 /// * mode: [AccessMode]693 #[weight = T::WeightInfo::set_public_access_mode()]694 pub fn set_public_access_mode(origin, collection_id: CollectionId, mode: AccessMode) -> DispatchResult695 {696 let sender = ensure_signed(origin)?;697698 Self::check_owner_permissions(collection_id, sender)?;699 let mut target_collection = <Collection<T>>::get(collection_id);700 target_collection.access = mode;701 <Collection<T>>::insert(collection_id, target_collection);702703 Ok(())704 }705706 /// Allows Anyone to create tokens if:707 /// * White List is enabled, and708 /// * Address is added to white list, and709 /// * This method was called with True parameter710 /// 711 /// # Permissions712 /// * Collection Owner713 ///714 /// # Arguments715 /// 716 /// * collection_id.717 /// 718 /// * mint_permission: Boolean parameter. If True, allows minting to Anyone with conditions above.719 #[weight = T::WeightInfo::set_mint_permission()]720 pub fn set_mint_permission(origin, collection_id: CollectionId, mint_permission: bool) -> DispatchResult721 {722 let sender = ensure_signed(origin)?;723724 Self::check_owner_permissions(collection_id, sender)?;725 let mut target_collection = <Collection<T>>::get(collection_id);726 target_collection.mint_mode = mint_permission;727 <Collection<T>>::insert(collection_id, target_collection);728729 Ok(())730 }731732 /// Change the owner of the collection.733 /// 734 /// # Permissions735 /// 736 /// * Collection Owner.737 /// 738 /// # Arguments739 /// 740 /// * collection_id.741 /// 742 /// * new_owner.743 #[weight = T::WeightInfo::change_collection_owner()]744 pub fn change_collection_owner(origin, collection_id: CollectionId, new_owner: T::AccountId) -> DispatchResult {745746 let sender = ensure_signed(origin)?;747 Self::check_owner_permissions(collection_id, sender)?;748 let mut target_collection = <Collection<T>>::get(collection_id);749 target_collection.owner = new_owner;750 <Collection<T>>::insert(collection_id, target_collection);751752 Ok(())753 }754755 /// Adds an admin of the Collection.756 /// NFT Collection can be controlled by multiple admin addresses (some which can also be servers, for example). Admins can issue and burn NFTs, as well as add and remove other admins, but cannot change NFT or Collection ownership. 757 /// 758 /// # Permissions759 /// 760 /// * Collection Owner.761 /// * Collection Admin.762 /// 763 /// # Arguments764 /// 765 /// * collection_id: ID of the Collection to add admin for.766 /// 767 /// * new_admin_id: Address of new admin to add.768 #[weight = T::WeightInfo::add_collection_admin()]769 pub fn add_collection_admin(origin, collection_id: CollectionId, new_admin_id: T::AccountId) -> DispatchResult {770771 let sender = ensure_signed(origin)?;772 Self::check_owner_or_admin_permissions(collection_id, sender)?;773 let mut admin_arr: Vec<T::AccountId> = Vec::new();774775 if <AdminList<T>>::contains_key(collection_id)776 {777 admin_arr = <AdminList<T>>::get(collection_id);778 ensure!(!admin_arr.contains(&new_admin_id), Error::<T>::AlreadyAdmin);779 }780781 // Number of collection admins782 ensure!((admin_arr.len() as u64) < ChainLimit::get().collections_admins_limit, Error::<T>::CollectionAdminsLimitExceeded);783784 admin_arr.push(new_admin_id);785 <AdminList<T>>::insert(collection_id, admin_arr);786787 Ok(())788 }789790 /// Remove admin address of the Collection. An admin address can remove itself. List of admins may become empty, in which case only Collection Owner will be able to add an Admin.791 ///792 /// # Permissions793 /// 794 /// * Collection Owner.795 /// * Collection Admin.796 /// 797 /// # Arguments798 /// 799 /// * collection_id: ID of the Collection to remove admin for.800 /// 801 /// * account_id: Address of admin to remove.802 #[weight = T::WeightInfo::remove_collection_admin()]803 pub fn remove_collection_admin(origin, collection_id: CollectionId, account_id: T::AccountId) -> DispatchResult {804805 let sender = ensure_signed(origin)?;806 Self::check_owner_or_admin_permissions(collection_id, sender)?;807808 if <AdminList<T>>::contains_key(collection_id)809 {810 let mut admin_arr = <AdminList<T>>::get(collection_id);811 admin_arr.retain(|i| *i != account_id);812 <AdminList<T>>::insert(collection_id, admin_arr);813 }814815 Ok(())816 }817818 /// # Permissions819 /// 820 /// * Collection Owner821 /// 822 /// # Arguments823 /// 824 /// * collection_id.825 /// 826 /// * new_sponsor.827 #[weight = T::WeightInfo::set_collection_sponsor()]828 pub fn set_collection_sponsor(origin, collection_id: CollectionId, new_sponsor: T::AccountId) -> DispatchResult {829830 let sender = ensure_signed(origin)?;831 ensure!(<Collection<T>>::contains_key(collection_id), Error::<T>::CollectionNotFound);832833 let mut target_collection = <Collection<T>>::get(collection_id);834 ensure!(sender == target_collection.owner, Error::<T>::NoPermission);835836 target_collection.sponsor = new_sponsor;837 target_collection.sponsor_confirmed = false;838 <Collection<T>>::insert(collection_id, target_collection);839840 Ok(())841 }842843 /// # Permissions844 /// 845 /// * Sponsor.846 /// 847 /// # Arguments848 /// 849 /// * collection_id.850 #[weight = T::WeightInfo::confirm_sponsorship()]851 pub fn confirm_sponsorship(origin, collection_id: CollectionId) -> DispatchResult {852853 let sender = ensure_signed(origin)?;854 ensure!(<Collection<T>>::contains_key(collection_id), Error::<T>::CollectionNotFound);855856 let mut target_collection = <Collection<T>>::get(collection_id);857 ensure!(sender == target_collection.sponsor, Error::<T>::ConfirmUnsetSponsorFail);858859 target_collection.sponsor_confirmed = true;860 <Collection<T>>::insert(collection_id, target_collection);861862 Ok(())863 }864865 /// Switch back to pay-per-own-transaction model.866 ///867 /// # Permissions868 ///869 /// * Collection owner.870 /// 871 /// # Arguments872 /// 873 /// * collection_id.874 #[weight = T::WeightInfo::remove_collection_sponsor()]875 pub fn remove_collection_sponsor(origin, collection_id: CollectionId) -> DispatchResult {876877 let sender = ensure_signed(origin)?;878 ensure!(<Collection<T>>::contains_key(collection_id), Error::<T>::CollectionNotFound);879880 let mut target_collection = <Collection<T>>::get(collection_id);881 ensure!(sender == target_collection.owner, Error::<T>::NoPermission);882883 target_collection.sponsor = T::AccountId::default();884 target_collection.sponsor_confirmed = false;885 <Collection<T>>::insert(collection_id, target_collection);886887 Ok(())888 }889890 /// This method creates a concrete instance of NFT Collection created with CreateCollection method.891 /// 892 /// # Permissions893 /// 894 /// * Collection Owner.895 /// * Collection Admin.896 /// * Anyone if897 /// * White List is enabled, and898 /// * Address is added to white list, and899 /// * MintPermission is enabled (see SetMintPermission method)900 /// 901 /// # Arguments902 /// 903 /// * collection_id: ID of the collection.904 /// 905 /// * owner: Address, initial owner of the NFT.906 ///907 /// * data: Token data to store on chain.908 // #[weight =909 // (130_000_000 as Weight)910 // .saturating_add((2135 as Weight).saturating_mul((properties.len() as u64) as Weight))911 // .saturating_add(RocksDbWeight::get().reads(10 as Weight))912 // .saturating_add(RocksDbWeight::get().writes(8 as Weight))]913914 #[weight = T::WeightInfo::create_item(data.len())]915 pub fn create_item(origin, collection_id: CollectionId, owner: T::AccountId, data: CreateItemData) -> DispatchResult {916917 let sender = ensure_signed(origin)?;918919 Self::collection_exists(collection_id)?;920921 let target_collection = <Collection<T>>::get(collection_id);922923 Self::can_create_items_in_collection(collection_id, &target_collection, &sender, &owner)?;924 Self::validate_create_item_args(&target_collection, &data)?;925 Self::create_item_no_validation(collection_id, &target_collection, owner, data)?;926927 Ok(())928 }929930 /// This method creates multiple instances of NFT Collection created with CreateCollection method.931 /// 932 /// # Permissions933 /// 934 /// * Collection Owner.935 /// * Collection Admin.936 /// * Anyone if937 /// * White List is enabled, and938 /// * Address is added to white list, and939 /// * MintPermission is enabled (see SetMintPermission method)940 /// 941 /// # Arguments942 /// 943 /// * collection_id: ID of the collection.944 /// 945 /// * itemsData: Array items properties. Each property is an array of bytes itself, see [create_item].946 /// 947 /// * owner: Address, initial owner of the NFT.948 #[weight = T::WeightInfo::create_item(items_data.into_iter()949 .map(|data| { data.len() })950 .sum())]951 pub fn create_multiple_items(origin, collection_id: CollectionId, owner: T::AccountId, items_data: Vec<CreateItemData>) -> DispatchResult {952953 ensure!(items_data.len() > 0, Error::<T>::EmptyArgument);954 let sender = ensure_signed(origin)?;955956 Self::collection_exists(collection_id)?;957 let target_collection = <Collection<T>>::get(collection_id);958959 Self::can_create_items_in_collection(collection_id, &target_collection, &sender, &owner)?;960961 for data in &items_data {962 Self::validate_create_item_args(&target_collection, data)?;963 }964 for data in &items_data {965 Self::create_item_no_validation(collection_id, &target_collection, owner.clone(), data.clone())?;966 }967968 Ok(())969 }970971 /// Destroys a concrete instance of NFT.972 /// 973 /// # Permissions974 /// 975 /// * Collection Owner.976 /// * Collection Admin.977 /// * Current NFT Owner.978 /// 979 /// # Arguments980 /// 981 /// * collection_id: ID of the collection.982 /// 983 /// * item_id: ID of NFT to burn.984 #[weight = T::WeightInfo::burn_item()]985 pub fn burn_item(origin, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult {986987 let sender = ensure_signed(origin)?;988 Self::collection_exists(collection_id)?;989990 // Transfer permissions check991 let target_collection = <Collection<T>>::get(collection_id);992 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||993 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),994 Error::<T>::NoPermission);995996 if target_collection.access == AccessMode::WhiteList {997 Self::check_white_list(collection_id, &sender)?;998 }9991000 match target_collection.mode1001 {1002 CollectionMode::NFT => Self::burn_nft_item(collection_id, item_id)?,1003 CollectionMode::Fungible(_) => Self::burn_fungible_item(&sender, collection_id, value)?,1004 CollectionMode::ReFungible(_) => Self::burn_refungible_item(collection_id, item_id, sender.clone())?,1005 _ => ()1006 };10071008 // call event1009 Self::deposit_event(RawEvent::ItemDestroyed(collection_id, item_id));10101011 Ok(())1012 }10131014 /// Change ownership of the token.1015 /// 1016 /// # Permissions1017 /// 1018 /// * Collection Owner1019 /// * Collection Admin1020 /// * Current NFT owner1021 ///1022 /// # Arguments1023 /// 1024 /// * recipient: Address of token recipient.1025 /// 1026 /// * collection_id.1027 /// 1028 /// * item_id: ID of the item1029 /// * Non-Fungible Mode: Required.1030 /// * Fungible Mode: Ignored.1031 /// * Re-Fungible Mode: Required.1032 /// 1033 /// * value: Amount to transfer.1034 /// * Non-Fungible Mode: Ignored1035 /// * Fungible Mode: Must specify transferred amount1036 /// * Re-Fungible Mode: Must specify transferred portion (between 0 and 1)1037 #[weight = T::WeightInfo::transfer()]1038 pub fn transfer(origin, recipient: T::AccountId, collection_id: CollectionId, item_id: TokenId, value: u128) -> DispatchResult {10391040 let sender = ensure_signed(origin)?;1041 let target_collection = <Collection<T>>::get(collection_id);10421043 // Limits check1044 Self::is_correct_transfer(collection_id, &target_collection, &recipient)?;10451046 // Transfer permissions check1047 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||1048 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1049 Error::<T>::NoPermission);10501051 if target_collection.access == AccessMode::WhiteList {1052 Self::check_white_list(collection_id, &sender)?;1053 Self::check_white_list(collection_id, &recipient)?;1054 }10551056 match target_collection.mode1057 {1058 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, sender.clone(), recipient)?,1059 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &sender, &recipient)?,1060 CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, sender.clone(), recipient)?,1061 _ => ()1062 };10631064 Ok(())1065 }10661067 /// Set, change, or remove approved address to transfer the ownership of the NFT.1068 /// 1069 /// # Permissions1070 /// 1071 /// * Collection Owner1072 /// * Collection Admin1073 /// * Current NFT owner1074 /// 1075 /// # Arguments1076 /// 1077 /// * approved: Address that is approved to transfer this NFT or zero (if needed to remove approval).1078 /// 1079 /// * collection_id.1080 /// 1081 /// * item_id: ID of the item.1082 #[weight = T::WeightInfo::approve()]1083 pub fn approve(origin, spender: T::AccountId, collection_id: CollectionId, item_id: TokenId, amount: u128) -> DispatchResult {10841085 let sender = ensure_signed(origin)?;10861087 // Transfer permissions check1088 let target_collection = <Collection<T>>::get(collection_id);1089 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||1090 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1091 Error::<T>::NoPermission);10921093 if target_collection.access == AccessMode::WhiteList {1094 Self::check_white_list(collection_id, &sender)?;1095 Self::check_white_list(collection_id, &spender)?;1096 }10971098 let allowance_exists = <Allowances<T>>::contains_key(collection_id, (item_id, &sender, &spender));1099 let mut allowance: u128 = amount;1100 if allowance_exists {1101 allowance += <Allowances<T>>::get(collection_id, (item_id, &sender, &spender));1102 }1103 <Allowances<T>>::insert(collection_id, (item_id, sender.clone(), spender.clone()), allowance);11041105 Ok(())1106 }1107 1108 /// Change ownership of a NFT on behalf of the owner. See Approve method for additional information. After this method executes, the approval is removed so that the approved address will not be able to transfer this NFT again from this owner.1109 /// 1110 /// # Permissions1111 /// * Collection Owner1112 /// * Collection Admin1113 /// * Current NFT owner1114 /// * Address approved by current NFT owner1115 /// 1116 /// # Arguments1117 /// 1118 /// * from: Address that owns token.1119 /// 1120 /// * recipient: Address of token recipient.1121 /// 1122 /// * collection_id.1123 /// 1124 /// * item_id: ID of the item.1125 /// 1126 /// * value: Amount to transfer.1127 #[weight = T::WeightInfo::transfer_from()]1128 pub fn transfer_from(origin, from: T::AccountId, recipient: T::AccountId, collection_id: CollectionId, item_id: TokenId, value: u128 ) -> DispatchResult {11291130 let sender = ensure_signed(origin)?;1131 let mut appoved_transfer = false;11321133 // Check approval1134 let mut approval: u128 = 0;1135 if <Allowances<T>>::contains_key(collection_id, (item_id, &from, &recipient)) {1136 approval = <Allowances<T>>::get(collection_id, (item_id, &from, &recipient));1137 ensure!(approval >= value, Error::<T>::TokenValueNotEnough);1138 appoved_transfer = true;1139 }11401141 let target_collection = <Collection<T>>::get(collection_id);11421143 // Limits check1144 Self::is_correct_transfer(collection_id, &target_collection, &recipient)?;11451146 // Transfer permissions check 1147 ensure!(appoved_transfer || Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1148 Error::<T>::NoPermission);11491150 if target_collection.access == AccessMode::WhiteList {1151 Self::check_white_list(collection_id, &sender)?;1152 Self::check_white_list(collection_id, &recipient)?;1153 }11541155 // Reduce approval by transferred amount or remove if remaining approval drops to 01156 if approval - value > 0 {1157 <Allowances<T>>::insert(collection_id, (item_id, &from, &recipient), approval - value);1158 }1159 else {1160 <Allowances<T>>::remove(collection_id, (item_id, &from, &recipient));1161 }11621163 match target_collection.mode1164 {1165 CollectionMode::NFT => Self::transfer_nft(collection_id, item_id, from, recipient)?,1166 CollectionMode::Fungible(_) => Self::transfer_fungible(collection_id, value, &from, &recipient)?,1167 CollectionMode::ReFungible(_) => Self::transfer_refungible(collection_id, item_id, value, from.clone(), recipient)?,1168 _ => ()1169 };11701171 Ok(())1172 }11731174 #[weight = 0]1175 pub fn safe_transfer_from(origin, collection_id: CollectionId, item_id: TokenId, new_owner: T::AccountId) -> DispatchResult {11761177 // let no_perm_mes = "You do not have permissions to modify this collection";1178 // ensure!(<ApprovedList<T>>::contains_key((collection_id, item_id)), no_perm_mes);1179 // let list_itm = <ApprovedList<T>>::get((collection_id, item_id));1180 // ensure!(list_itm.contains(&new_owner.clone()), no_perm_mes);11811182 // // on_nft_received call11831184 // Self::transfer(origin, collection_id, item_id, new_owner)?;11851186 Ok(())1187 }11881189 /// Set off-chain data schema.1190 /// 1191 /// # Permissions1192 /// 1193 /// * Collection Owner1194 /// * Collection Admin1195 /// 1196 /// # Arguments1197 /// 1198 /// * collection_id.1199 /// 1200 /// * schema: String representing the offchain data schema.1201 #[weight = T::WeightInfo::set_variable_meta_data()]1202 pub fn set_variable_meta_data (1203 origin,1204 collection_id: CollectionId,1205 item_id: TokenId,1206 data: Vec<u8>1207 ) -> DispatchResult {1208 let sender = ensure_signed(origin)?;1209 1210 Self::collection_exists(collection_id)?;1211 1212 ensure!(ChainLimit::get().custom_data_limit >= data.len() as u32, Error::<T>::TokenVariableDataLimitExceeded);12131214 // Modify permissions check1215 let target_collection = <Collection<T>>::get(collection_id);1216 ensure!(Self::is_item_owner(sender.clone(), collection_id, item_id) ||1217 Self::is_owner_or_admin_permissions(collection_id, sender.clone()),1218 Error::<T>::NoPermission);12191220 Self::item_exists(collection_id, item_id, &target_collection.mode)?;12211222 match target_collection.mode1223 {1224 CollectionMode::NFT => Self::set_nft_variable_data(collection_id, item_id, data)?,1225 CollectionMode::ReFungible(_) => Self::set_re_fungible_variable_data(collection_id, item_id, data)?,1226 CollectionMode::Fungible(_) => fail!(Error::<T>::CantStoreMetadataInFungibleTokens),1227 _ => fail!(Error::<T>::UnexpectedCollectionType)1228 };12291230 Ok(())1231 }1232 1233 /// Set schema standard1234 /// ImageURL1235 /// Unique1236 /// 1237 /// # Permissions1238 /// 1239 /// * Collection Owner1240 /// * Collection Admin1241 /// 1242 /// # Arguments1243 /// 1244 /// * collection_id.1245 /// 1246 /// * schema: SchemaVersion: enum1247 #[weight = T::WeightInfo::set_schema_version()]1248 pub fn set_schema_version(1249 origin,1250 collection_id: CollectionId,1251 version: SchemaVersion1252 ) -> DispatchResult {1253 let sender = ensure_signed(origin)?;1254 Self::check_owner_or_admin_permissions(collection_id, sender.clone())?;1255 let mut target_collection = <Collection<T>>::get(collection_id);1256 target_collection.schema_version = version;1257 <Collection<T>>::insert(collection_id, target_collection);12581259 Ok(())1260 }12611262 /// Set off-chain data schema.1263 /// 1264 /// # Permissions1265 /// 1266 /// * Collection Owner1267 /// * Collection Admin1268 /// 1269 /// # Arguments1270 /// 1271 /// * collection_id.1272 /// 1273 /// * schema: String representing the offchain data schema.1274 #[weight = T::WeightInfo::set_offchain_schema()]1275 pub fn set_offchain_schema(1276 origin,1277 collection_id: CollectionId,1278 schema: Vec<u8>1279 ) -> DispatchResult {1280 let sender = ensure_signed(origin)?;1281 Self::check_owner_or_admin_permissions(collection_id, sender.clone())?;12821283 let mut target_collection = <Collection<T>>::get(collection_id);1284 target_collection.offchain_schema = schema;1285 <Collection<T>>::insert(collection_id, target_collection);12861287 Ok(())1288 }12891290 /// Set const on-chain data schema.1291 /// 1292 /// # Permissions1293 /// 1294 /// * Collection Owner1295 /// * Collection Admin1296 /// 1297 /// # Arguments1298 /// 1299 /// * collection_id.1300 /// 1301 /// * schema: String representing the const on-chain data schema.1302 #[weight = T::WeightInfo::set_const_on_chain_schema()]1303 pub fn set_const_on_chain_schema (1304 origin,1305 collection_id: CollectionId,1306 schema: Vec<u8>1307 ) -> DispatchResult {1308 let sender = ensure_signed(origin)?;1309 Self::check_owner_or_admin_permissions(collection_id, sender.clone())?;13101311 let mut target_collection = <Collection<T>>::get(collection_id);1312 target_collection.const_on_chain_schema = schema;1313 <Collection<T>>::insert(collection_id, target_collection);13141315 Ok(())1316 }13171318 /// Set variable on-chain data schema.1319 /// 1320 /// # Permissions1321 /// 1322 /// * Collection Owner1323 /// * Collection Admin1324 /// 1325 /// # Arguments1326 /// 1327 /// * collection_id.1328 /// 1329 /// * schema: String representing the variable on-chain data schema.1330 #[weight = T::WeightInfo::set_const_on_chain_schema()]1331 pub fn set_variable_on_chain_schema (1332 origin,1333 collection_id: CollectionId,1334 schema: Vec<u8>1335 ) -> DispatchResult {1336 let sender = ensure_signed(origin)?;1337 Self::check_owner_or_admin_permissions(collection_id, sender.clone())?;13381339 let mut target_collection = <Collection<T>>::get(collection_id);1340 target_collection.variable_on_chain_schema = schema;1341 <Collection<T>>::insert(collection_id, target_collection);13421343 Ok(())1344 }13451346 // Sudo permissions function1347 #[weight = T::WeightInfo::set_chain_limits()]1348 pub fn set_chain_limits(1349 origin,1350 limits: ChainLimits1351 ) -> DispatchResult {13521353 #[cfg(not(feature = "runtime-benchmarks"))]1354 ensure_root(origin)?;13551356 <ChainLimit>::put(limits);1357 Ok(())1358 }13591360 /// Enable smart contract self-sponsoring.1361 /// 1362 /// # Permissions1363 /// 1364 /// * Contract Owner1365 /// 1366 /// # Arguments1367 /// 1368 /// * contract address1369 /// * enable flag1370 /// 1371 #[weight = T::WeightInfo::enable_contract_sponsoring()]1372 pub fn enable_contract_sponsoring(1373 origin,1374 contract_address: T::AccountId,1375 enable: bool1376 ) -> DispatchResult {13771378 let sender = ensure_signed(origin)?;13791380 #[cfg(feature = "runtime-benchmarks")]1381 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());13821383 Self::ensure_contract_owned(sender, &contract_address)?;13841385 <ContractSelfSponsoring<T>>::insert(contract_address, enable);1386 Ok(())1387 }13881389 /// Set the rate limit for contract sponsoring to specified number of blocks.1390 /// 1391 /// If not set (has the default value of 0 blocks), the sponsoring will be disabled. 1392 /// If set to the number B (for blocks), the transactions will be sponsored with a rate 1393 /// limit of B, i.e. fees for every transaction sent to this smart contract will be paid 1394 /// from contract endowment if there are at least B blocks between such transactions. 1395 /// Nonetheless, if transactions are sent more frequently, the fees are paid by the sender.1396 /// 1397 /// # Permissions1398 /// 1399 /// * Contract Owner1400 /// 1401 /// # Arguments1402 /// 1403 /// -`contract_address`: Address of the contract to sponsor1404 /// -`rate_limit`: Number of blocks to wait until the next sponsored transaction is allowed1405 /// 1406 #[weight = T::WeightInfo::set_contract_sponsoring_rate_limit()]1407 pub fn set_contract_sponsoring_rate_limit(1408 origin,1409 contract_address: T::AccountId,1410 rate_limit: T::BlockNumber1411 ) -> DispatchResult {1412 let sender = ensure_signed(origin)?;14131414 #[cfg(feature = "runtime-benchmarks")]1415 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());14161417 Self::ensure_contract_owned(sender, &contract_address)?;1418 <ContractSponsoringRateLimit<T>>::insert(contract_address, rate_limit);1419 Ok(())1420 }14211422 /// Enable the white list for a contract. Only addresses added to the white list with addToContractWhiteList will be able to call this smart contract.1423 /// 1424 /// # Permissions1425 /// 1426 /// * Address that deployed smart contract.1427 /// 1428 /// # Arguments1429 /// 1430 /// -`contract_address`: Address of the contract.1431 /// 1432 /// - `enable`: . 1433 #[weight = T::WeightInfo::toggle_contract_white_list()]1434 pub fn toggle_contract_white_list(1435 origin,1436 contract_address: T::AccountId,1437 enable: bool1438 ) -> DispatchResult {1439 let sender = ensure_signed(origin)?;14401441 #[cfg(feature = "runtime-benchmarks")]1442 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());14431444 Self::ensure_contract_owned(sender, &contract_address)?;1445 <ContractWhiteListEnabled<T>>::insert(contract_address, enable);1446 Ok(())1447 }1448 1449 /// Add an address to smart contract white list.1450 /// 1451 /// # Permissions1452 /// 1453 /// * Address that deployed smart contract.1454 /// 1455 /// # Arguments1456 /// 1457 /// -`contract_address`: Address of the contract.1458 ///1459 /// -`account_address`: Address to add.1460 #[weight = T::WeightInfo::add_to_contract_white_list()]1461 pub fn add_to_contract_white_list(1462 origin,1463 contract_address: T::AccountId,1464 account_address: T::AccountId1465 ) -> DispatchResult {1466 let sender = ensure_signed(origin)?;14671468 #[cfg(feature = "runtime-benchmarks")]1469 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());1470 1471 Self::ensure_contract_owned(sender, &contract_address)?; 1472 <ContractWhiteList<T>>::insert(contract_address, account_address, true);1473 Ok(())1474 }14751476 /// Remove an address from smart contract white list.1477 /// 1478 /// # Permissions1479 /// 1480 /// * Address that deployed smart contract.1481 /// 1482 /// # Arguments1483 /// 1484 /// -`contract_address`: Address of the contract.1485 ///1486 /// -`account_address`: Address to remove.1487 #[weight = T::WeightInfo::remove_from_contract_white_list()]1488 pub fn remove_from_contract_white_list(1489 origin,1490 contract_address: T::AccountId,1491 account_address: T::AccountId1492 ) -> DispatchResult {1493 let sender = ensure_signed(origin)?;14941495 #[cfg(feature = "runtime-benchmarks")]1496 <ContractOwner<T>>::insert(contract_address.clone(), sender.clone());14971498 Self::ensure_contract_owned(sender, &contract_address)?;1499 <ContractWhiteList<T>>::remove(contract_address, account_address);1500 Ok(())1501 }15021503 #[weight = T::WeightInfo::set_collection_limits()]1504 pub fn set_collection_limits(1505 origin,1506 collection_id: u32,1507 limits: CollectionLimits,1508 ) -> DispatchResult {1509 let sender = ensure_signed(origin)?;1510 Self::check_owner_permissions(collection_id, sender.clone())?;1511 let mut target_collection = <Collection<T>>::get(collection_id);1512 let chain_limits = ChainLimit::get();1513 let climits = target_collection.limits;15141515 // collection bounds1516 ensure!(limits.sponsor_transfer_timeout <= MAX_SPONSOR_TIMEOUT &&1517 limits.account_token_ownership_limit <= MAX_TOKEN_OWNERSHIP, 1518 Error::<T>::CollectionLimitBoundsExceeded);15191520 // token_limit check prev1521 ensure!(climits.token_limit > limits.token_limit && 1522 limits.token_limit <= chain_limits.account_token_ownership_limit, 1523 Error::<T>::AccountTokenLimitExceeded);15241525 target_collection.limits = limits;1526 <Collection<T>>::insert(collection_id, target_collection);15271528 Ok(())1529 } 1530 }1531}15321533impl<T: Trait> Module<T> {15341535 fn is_correct_transfer(collection_id: CollectionId, collection: &CollectionType<T::AccountId>, recipient: &T::AccountId) -> DispatchResult {15361537 // check token limit and account token limit1538 let account_items: u32 = <AddressTokens<T>>::get(collection_id, recipient).len() as u32;1539 ensure!(collection.limits.account_token_ownership_limit > account_items, Error::<T>::AccountTokenLimitExceeded);1540 1541 Ok(())1542 }15431544 fn can_create_items_in_collection(collection_id: CollectionId, collection: &CollectionType<T::AccountId>, sender: &T::AccountId, owner: &T::AccountId) -> DispatchResult {15451546 // check token limit and account token limit1547 let total_items: u32 = ItemListIndex::get(collection_id);1548 let account_items: u32 = <AddressTokens<T>>::get(collection_id, owner).len() as u32;1549 ensure!(collection.limits.token_limit > total_items, Error::<T>::CollectionTokenLimitExceeded);1550 ensure!(collection.limits.account_token_ownership_limit > account_items, Error::<T>::AccountTokenLimitExceeded);15511552 if !Self::is_owner_or_admin_permissions(collection_id, sender.clone()) {1553 ensure!(collection.mint_mode == true, Error::<T>::PublicMintingNotAllowed);1554 Self::check_white_list(collection_id, owner)?;1555 Self::check_white_list(collection_id, sender)?;1556 }15571558 Ok(())1559 }15601561 fn validate_create_item_args(target_collection: &CollectionType<T::AccountId>, data: &CreateItemData) -> DispatchResult {1562 match target_collection.mode1563 {1564 CollectionMode::NFT => {1565 if let CreateItemData::NFT(data) = data {1566 // check sizes1567 ensure!(ChainLimit::get().custom_data_limit >= data.const_data.len() as u32, Error::<T>::TokenConstDataLimitExceeded);1568 ensure!(ChainLimit::get().custom_data_limit >= data.variable_data.len() as u32, Error::<T>::TokenVariableDataLimitExceeded);1569 } else {1570 fail!(Error::<T>::NotNftDataUsedToMintNftCollectionToken);1571 }1572 },1573 CollectionMode::Fungible(_) => {1574 if let CreateItemData::Fungible(_) = data {1575 } else {1576 fail!(Error::<T>::NotFungibleDataUsedToMintFungibleCollectionToken);1577 }1578 },1579 CollectionMode::ReFungible(_) => {1580 if let CreateItemData::ReFungible(data) = data {15811582 // check sizes1583 ensure!(ChainLimit::get().custom_data_limit >= data.const_data.len() as u32, Error::<T>::TokenConstDataLimitExceeded);1584 ensure!(ChainLimit::get().custom_data_limit >= data.variable_data.len() as u32, Error::<T>::TokenVariableDataLimitExceeded);1585 } else {1586 fail!(Error::<T>::NotReFungibleDataUsedToMintReFungibleCollectionToken);1587 }1588 },1589 _ => { fail!(Error::<T>::UnexpectedCollectionType); }1590 };15911592 Ok(())1593 }15941595 fn create_item_no_validation(collection_id: CollectionId, collection: &CollectionType<T::AccountId>, owner: T::AccountId, data: CreateItemData) -> DispatchResult {1596 match data1597 {1598 CreateItemData::NFT(data) => {1599 let item = NftItemType {1600 owner,1601 const_data: data.const_data,1602 variable_data: data.variable_data1603 };16041605 Self::add_nft_item(collection_id, item)?;1606 },1607 CreateItemData::Fungible(data) => {1608 Self::add_fungible_item(collection_id, &owner, data.value)?;1609 },1610 CreateItemData::ReFungible(data) => {1611 let mut owner_list = Vec::new();1612 let value = (10 as u128).pow(collection.decimal_points as u32);1613 owner_list.push(Ownership {owner: owner.clone(), fraction: value});16141615 let item = ReFungibleItemType {1616 owner: owner_list,1617 const_data: data.const_data,1618 variable_data: data.variable_data1619 };16201621 Self::add_refungible_item(collection_id, item)?;1622 }1623 };16241625 // call event1626 Self::deposit_event(RawEvent::ItemCreated(collection_id, <ItemListIndex>::get(collection_id)));16271628 Ok(())1629 }16301631 fn add_fungible_item(collection_id: CollectionId, owner: &T::AccountId, value: u128) -> DispatchResult {16321633 // Does new owner already have an account?1634 let mut balance: u128 = 0;1635 if <FungibleItemList<T>>::contains_key(collection_id, owner) {1636 balance = <FungibleItemList<T>>::get(collection_id, owner).value;1637 } 16381639 // Mint 1640 let item = FungibleItemType {1641 value: balance + value1642 };1643 <FungibleItemList<T>>::insert(collection_id, (*owner).clone(), item);16441645 // Update balance1646 let new_balance = <Balance<T>>::get(collection_id, owner)1647 .checked_add(value)1648 .ok_or(Error::<T>::NumOverflow)?;1649 <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);16501651 Ok(())1652 }16531654 fn add_refungible_item(collection_id: CollectionId, item: ReFungibleItemType<T::AccountId>) -> DispatchResult {1655 let current_index = <ItemListIndex>::get(collection_id)1656 .checked_add(1)1657 .ok_or(Error::<T>::NumOverflow)?;1658 let itemcopy = item.clone();16591660 let value = item.owner.first().unwrap().fraction;1661 let owner = item.owner.first().unwrap().owner.clone();16621663 Self::add_token_index(collection_id, current_index, owner.clone())?;16641665 <ItemListIndex>::insert(collection_id, current_index);1666 <ReFungibleItemList<T>>::insert(collection_id, current_index, itemcopy);16671668 // Update balance1669 let new_balance = <Balance<T>>::get(collection_id, owner.clone())1670 .checked_add(value)1671 .ok_or(Error::<T>::NumOverflow)?;1672 <Balance<T>>::insert(collection_id, owner.clone(), new_balance);16731674 Ok(())1675 }16761677 fn add_nft_item(collection_id: CollectionId, item: NftItemType<T::AccountId>) -> DispatchResult {1678 let current_index = <ItemListIndex>::get(collection_id)1679 .checked_add(1)1680 .ok_or(Error::<T>::NumOverflow)?;16811682 let item_owner = item.owner.clone();1683 Self::add_token_index(collection_id, current_index, item.owner.clone())?;16841685 <ItemListIndex>::insert(collection_id, current_index);1686 <NftItemList<T>>::insert(collection_id, current_index, item);16871688 // Update balance1689 let new_balance = <Balance<T>>::get(collection_id, item_owner.clone())1690 .checked_add(1)1691 .ok_or(Error::<T>::NumOverflow)?;1692 <Balance<T>>::insert(collection_id, item_owner.clone(), new_balance);16931694 Ok(())1695 }16961697 fn burn_refungible_item(1698 collection_id: CollectionId,1699 item_id: TokenId,1700 owner: T::AccountId,1701 ) -> DispatchResult {1702 ensure!(1703 <ReFungibleItemList<T>>::contains_key(collection_id, item_id),1704 Error::<T>::TokenNotFound1705 );1706 let collection = <ReFungibleItemList<T>>::get(collection_id, item_id);1707 let item = collection1708 .owner1709 .iter()1710 .filter(|&i| i.owner == owner)1711 .next()1712 .unwrap();1713 Self::remove_token_index(collection_id, item_id, owner.clone())?;17141715 // update balance1716 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1717 .checked_sub(item.fraction)1718 .ok_or(Error::<T>::NumOverflow)?;1719 <Balance<T>>::insert(collection_id, item.owner.clone(), new_balance);17201721 <ReFungibleItemList<T>>::remove(collection_id, item_id);17221723 Ok(())1724 }17251726 fn burn_nft_item(collection_id: CollectionId, item_id: TokenId) -> DispatchResult {1727 ensure!(1728 <NftItemList<T>>::contains_key(collection_id, item_id),1729 Error::<T>::TokenNotFound1730 );1731 let item = <NftItemList<T>>::get(collection_id, item_id);1732 Self::remove_token_index(collection_id, item_id, item.owner.clone())?;17331734 // update balance1735 let new_balance = <Balance<T>>::get(collection_id, item.owner.clone())1736 .checked_sub(1)1737 .ok_or(Error::<T>::NumOverflow)?;1738 <Balance<T>>::insert(collection_id, item.owner.clone(), new_balance);1739 <NftItemList<T>>::remove(collection_id, item_id);17401741 Ok(())1742 }17431744 fn burn_fungible_item(owner: &T::AccountId, collection_id: CollectionId, value: u128) -> DispatchResult {1745 ensure!(1746 <FungibleItemList<T>>::contains_key(collection_id, owner),1747 Error::<T>::TokenNotFound1748 );1749 let mut balance = <FungibleItemList<T>>::get(collection_id, owner);1750 ensure!(balance.value >= value, Error::<T>::TokenValueNotEnough);17511752 // update balance1753 let new_balance = <Balance<T>>::get(collection_id, owner)1754 .checked_sub(value)1755 .ok_or(Error::<T>::NumOverflow)?;1756 <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);17571758 if balance.value - value > 0 {1759 balance.value -= value;1760 <FungibleItemList<T>>::insert(collection_id, (*owner).clone(), balance);1761 }1762 else {1763 <FungibleItemList<T>>::remove(collection_id, owner);1764 }17651766 Ok(())1767 }17681769 fn collection_exists(collection_id: CollectionId) -> DispatchResult {1770 ensure!(1771 <Collection<T>>::contains_key(collection_id),1772 Error::<T>::CollectionNotFound1773 );1774 Ok(())1775 }17761777 fn check_owner_permissions(collection_id: CollectionId, subject: T::AccountId) -> DispatchResult {1778 Self::collection_exists(collection_id)?;17791780 let target_collection = <Collection<T>>::get(collection_id);1781 ensure!(1782 subject == target_collection.owner,1783 Error::<T>::NoPermission1784 );17851786 Ok(())1787 }17881789 fn is_owner_or_admin_permissions(collection_id: CollectionId, subject: T::AccountId) -> bool {1790 let target_collection = <Collection<T>>::get(collection_id);1791 let mut result: bool = subject == target_collection.owner;1792 let exists = <AdminList<T>>::contains_key(collection_id);17931794 if !result & exists {1795 if <AdminList<T>>::get(collection_id).contains(&subject) {1796 result = true1797 }1798 }17991800 result1801 }18021803 fn check_owner_or_admin_permissions(1804 collection_id: CollectionId,1805 subject: T::AccountId,1806 ) -> DispatchResult {1807 Self::collection_exists(collection_id)?;1808 let result = Self::is_owner_or_admin_permissions(collection_id, subject.clone());18091810 ensure!(1811 result,1812 Error::<T>::NoPermission1813 );1814 Ok(())1815 }18161817 fn is_item_owner(subject: T::AccountId, collection_id: CollectionId, item_id: TokenId) -> bool {1818 let target_collection = <Collection<T>>::get(collection_id);18191820 match target_collection.mode {1821 CollectionMode::NFT => {1822 <NftItemList<T>>::get(collection_id, item_id).owner == subject1823 }1824 CollectionMode::Fungible(_) => {1825 <FungibleItemList<T>>::contains_key(collection_id, &subject)1826 }1827 CollectionMode::ReFungible(_) => {1828 <ReFungibleItemList<T>>::get(collection_id, item_id)1829 .owner1830 .iter()1831 .any(|i| i.owner == subject)1832 }1833 CollectionMode::Invalid => false,1834 }1835 }18361837 fn check_white_list(collection_id: CollectionId, address: &T::AccountId) -> DispatchResult {1838 let mes = Error::<T>::AddresNotInWhiteList;1839 ensure!(<WhiteList<T>>::contains_key(collection_id, address), mes);18401841 Ok(())1842 }18431844 fn transfer_fungible(1845 collection_id: CollectionId,1846 value: u128,1847 owner: &T::AccountId,1848 recipient: &T::AccountId,1849 ) -> DispatchResult {1850 ensure!(1851 <FungibleItemList<T>>::contains_key(collection_id, owner),1852 Error::<T>::TokenNotFound1853 );18541855 let mut balance = <FungibleItemList<T>>::get(collection_id, owner);1856 ensure!(balance.value >= value, Error::<T>::TokenValueTooLow);18571858 // Send balance to recipient (updates balanceOf of recipient)1859 Self::add_fungible_item(collection_id, recipient, value)?;18601861 // update balanceOf of sender1862 <Balance<T>>::insert(collection_id, (*owner).clone(), balance.value - value);18631864 // Reduce or remove sender1865 if balance.value == value {1866 <FungibleItemList<T>>::remove(collection_id, owner);1867 }1868 else {1869 balance.value -= value;1870 <FungibleItemList<T>>::insert(collection_id, (*owner).clone(), balance);1871 }18721873 Ok(())1874 }18751876 fn transfer_refungible(1877 collection_id: CollectionId,1878 item_id: TokenId,1879 value: u128,1880 owner: T::AccountId,1881 new_owner: T::AccountId,1882 ) -> DispatchResult {1883 ensure!(1884 <ReFungibleItemList<T>>::contains_key(collection_id, item_id),1885 Error::<T>::TokenNotFound1886 );18871888 let full_item = <ReFungibleItemList<T>>::get(collection_id, item_id);1889 let item = full_item1890 .owner1891 .iter()1892 .filter(|i| i.owner == owner)1893 .next()1894 .ok_or(Error::<T>::NumOverflow)?;1895 let amount = item.fraction;18961897 ensure!(amount >= value, Error::<T>::TokenValueTooLow);18981899 // update balance1900 let balance_old_owner = <Balance<T>>::get(collection_id, item.owner.clone())1901 .checked_sub(value)1902 .ok_or(Error::<T>::NumOverflow)?;1903 <Balance<T>>::insert(collection_id, item.owner.clone(), balance_old_owner);19041905 let balancenew_owner = <Balance<T>>::get(collection_id, new_owner.clone())1906 .checked_add(value)1907 .ok_or(Error::<T>::NumOverflow)?;1908 <Balance<T>>::insert(collection_id, new_owner.clone(), balancenew_owner);19091910 let old_owner = item.owner.clone();1911 let new_owner_has_account = full_item.owner.iter().any(|i| i.owner == new_owner);19121913 // transfer1914 if amount == value && !new_owner_has_account {1915 // change owner1916 // new owner do not have account1917 let mut new_full_item = full_item.clone();1918 new_full_item1919 .owner1920 .iter_mut()1921 .find(|i| i.owner == owner)1922 .unwrap()1923 .owner = new_owner.clone();1924 <ReFungibleItemList<T>>::insert(collection_id, item_id, new_full_item);19251926 // update index collection1927 Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?;1928 } else {1929 let mut new_full_item = full_item.clone();1930 new_full_item1931 .owner1932 .iter_mut()1933 .find(|i| i.owner == owner)1934 .unwrap()1935 .fraction -= value;19361937 // separate amount1938 if new_owner_has_account {1939 // new owner has account1940 new_full_item1941 .owner1942 .iter_mut()1943 .find(|i| i.owner == new_owner)1944 .unwrap()1945 .fraction += value;1946 } else {1947 // new owner do not have account1948 new_full_item.owner.push(Ownership {1949 owner: new_owner.clone(),1950 fraction: value,1951 });1952 Self::add_token_index(collection_id, item_id, new_owner.clone())?;1953 }19541955 <ReFungibleItemList<T>>::insert(collection_id, item_id, new_full_item);1956 }19571958 Ok(())1959 }19601961 fn transfer_nft(1962 collection_id: CollectionId,1963 item_id: TokenId,1964 sender: T::AccountId,1965 new_owner: T::AccountId,1966 ) -> DispatchResult {1967 ensure!(1968 <NftItemList<T>>::contains_key(collection_id, item_id),1969 Error::<T>::TokenNotFound1970 );19711972 let mut item = <NftItemList<T>>::get(collection_id, item_id);19731974 ensure!(1975 sender == item.owner,1976 Error::<T>::MustBeTokenOwner1977 );19781979 // update balance1980 let balance_old_owner = <Balance<T>>::get(collection_id, item.owner.clone())1981 .checked_sub(1)1982 .ok_or(Error::<T>::NumOverflow)?;1983 <Balance<T>>::insert(collection_id, item.owner.clone(), balance_old_owner);19841985 let balancenew_owner = <Balance<T>>::get(collection_id, new_owner.clone())1986 .checked_add(1)1987 .ok_or(Error::<T>::NumOverflow)?;1988 <Balance<T>>::insert(collection_id, new_owner.clone(), balancenew_owner);19891990 // change owner1991 let old_owner = item.owner.clone();1992 item.owner = new_owner.clone();1993 <NftItemList<T>>::insert(collection_id, item_id, item);19941995 // update index collection1996 Self::move_token_index(collection_id, item_id, old_owner.clone(), new_owner.clone())?;19971998 Ok(())1999 }2000 2001 fn item_exists(2002 collection_id: CollectionId,2003 item_id: TokenId,2004 mode: &CollectionMode2005 ) -> DispatchResult {2006 match mode {2007 CollectionMode::NFT => ensure!(<NftItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),2008 CollectionMode::ReFungible(_) => ensure!(<ReFungibleItemList<T>>::contains_key(collection_id, item_id), Error::<T>::TokenNotFound),2009 _ => ()2010 };2011 2012 Ok(())2013 }20142015 fn set_re_fungible_variable_data(2016 collection_id: CollectionId,2017 item_id: TokenId,2018 data: Vec<u8>2019 ) -> DispatchResult {2020 let mut item = <ReFungibleItemList<T>>::get(collection_id, item_id);20212022 item.variable_data = data;20232024 <ReFungibleItemList<T>>::insert(collection_id, item_id, item);20252026 Ok(())2027 }20282029 fn set_nft_variable_data(2030 collection_id: CollectionId,2031 item_id: TokenId,2032 data: Vec<u8>2033 ) -> DispatchResult {2034 let mut item = <NftItemList<T>>::get(collection_id, item_id);2035 2036 item.variable_data = data;20372038 <NftItemList<T>>::insert(collection_id, item_id, item);2039 2040 Ok(())2041 }20422043 fn init_collection(item: &CollectionType<T::AccountId>) {2044 // check params2045 assert!(2046 item.decimal_points <= MAX_DECIMAL_POINTS,2047 "decimal_points parameter must be lower than MAX_DECIMAL_POINTS"2048 );2049 assert!(2050 item.name.len() <= 64,2051 "Collection name can not be longer than 63 char"2052 );2053 assert!(2054 item.name.len() <= 256,2055 "Collection description can not be longer than 255 char"2056 );2057 assert!(2058 item.token_prefix.len() <= 16,2059 "Token prefix can not be longer than 15 char"2060 );20612062 // Generate next collection ID2063 let next_id = CreatedCollectionCount::get()2064 .checked_add(1)2065 .unwrap();20662067 CreatedCollectionCount::put(next_id);2068 }20692070 fn init_nft_token(collection_id: CollectionId, item: &NftItemType<T::AccountId>) {2071 let current_index = <ItemListIndex>::get(collection_id)2072 .checked_add(1)2073 .unwrap();20742075 let item_owner = item.owner.clone();2076 Self::add_token_index(collection_id, current_index, item.owner.clone()).unwrap();20772078 <ItemListIndex>::insert(collection_id, current_index);20792080 // Update balance2081 let new_balance = <Balance<T>>::get(collection_id, item_owner.clone())2082 .checked_add(1)2083 .unwrap();2084 <Balance<T>>::insert(collection_id, item_owner.clone(), new_balance);2085 }20862087 fn init_fungible_token(collection_id: CollectionId, owner: &T::AccountId, item: &FungibleItemType) {2088 let current_index = <ItemListIndex>::get(collection_id)2089 .checked_add(1)2090 .unwrap();20912092 Self::add_token_index(collection_id, current_index, (*owner).clone()).unwrap();20932094 <ItemListIndex>::insert(collection_id, current_index);20952096 // Update balance2097 let new_balance = <Balance<T>>::get(collection_id, owner)2098 .checked_add(item.value)2099 .unwrap();2100 <Balance<T>>::insert(collection_id, (*owner).clone(), new_balance);2101 }21022103 fn init_refungible_token(collection_id: CollectionId, item: &ReFungibleItemType<T::AccountId>) {2104 let current_index = <ItemListIndex>::get(collection_id)2105 .checked_add(1)2106 .unwrap();21072108 let value = item.owner.first().unwrap().fraction;2109 let owner = item.owner.first().unwrap().owner.clone();21102111 Self::add_token_index(collection_id, current_index, owner.clone()).unwrap();21122113 <ItemListIndex>::insert(collection_id, current_index);21142115 // Update balance2116 let new_balance = <Balance<T>>::get(collection_id, owner.clone())2117 .checked_add(value)2118 .unwrap();2119 <Balance<T>>::insert(collection_id, owner.clone(), new_balance);2120 }21212122 fn add_token_index(collection_id: CollectionId, item_index: TokenId, owner: T::AccountId) -> DispatchResult {21232124 // add to account limit2125 if <AccountItemCount<T>>::contains_key(owner.clone()) {21262127 // bound Owned tokens by a single address2128 let count = <AccountItemCount<T>>::get(owner.clone());2129 ensure!(count < ChainLimit::get().account_token_ownership_limit, Error::<T>::AddressOwnershipLimitExceeded);21302131 <AccountItemCount<T>>::insert(owner.clone(), count2132 .checked_add(1)2133 .ok_or(Error::<T>::NumOverflow)?);2134 }2135 else {2136 <AccountItemCount<T>>::insert(owner.clone(), 1);2137 }21382139 let list_exists = <AddressTokens<T>>::contains_key(collection_id, owner.clone());2140 if list_exists {2141 let mut list = <AddressTokens<T>>::get(collection_id, owner.clone());2142 let item_contains = list.contains(&item_index.clone());21432144 if !item_contains {2145 list.push(item_index.clone());2146 }21472148 <AddressTokens<T>>::insert(collection_id, owner.clone(), list);2149 } else {2150 let mut itm = Vec::new();2151 itm.push(item_index.clone());2152 <AddressTokens<T>>::insert(collection_id, owner, itm);2153 2154 }21552156 Ok(())2157 }21582159 fn remove_token_index(2160 collection_id: CollectionId,2161 item_index: TokenId,2162 owner: T::AccountId,2163 ) -> DispatchResult {21642165 // update counter2166 <AccountItemCount<T>>::insert(owner.clone(), 2167 <AccountItemCount<T>>::get(owner.clone())2168 .checked_sub(1)2169 .ok_or(Error::<T>::NumOverflow)?);217021712172 let list_exists = <AddressTokens<T>>::contains_key(collection_id, owner.clone());2173 if list_exists {2174 let mut list = <AddressTokens<T>>::get(collection_id, owner.clone());2175 let item_contains = list.contains(&item_index.clone());21762177 if item_contains {2178 list.retain(|&item| item != item_index);2179 <AddressTokens<T>>::insert(collection_id, owner, list);2180 }2181 }21822183 Ok(())2184 }21852186 fn move_token_index(2187 collection_id: CollectionId,2188 item_index: TokenId,2189 old_owner: T::AccountId,2190 new_owner: T::AccountId,2191 ) -> DispatchResult {2192 Self::remove_token_index(collection_id, item_index, old_owner)?;2193 Self::add_token_index(collection_id, item_index, new_owner)?;21942195 Ok(())2196 }2197 2198 fn ensure_contract_owned(account: T::AccountId, contract: &T::AccountId) -> DispatchResult {2199 if <ContractOwner<T>>::contains_key(contract.clone()) {2200 let owner = <ContractOwner<T>>::get(contract);2201 ensure!(account == owner, Error::<T>::NoPermission);2202 } else {2203 fail!(Error::<T>::NoPermission);2204 }22052206 Ok(())2207 }2208}22092210////////////////////////////////////////////////////////////////////////////////////////////////////2211// Economic models2212// #region22132214/// Fee multiplier.2215pub type Multiplier = FixedU128;22162217type BalanceOf<T> = <<T as transaction_payment::Trait>::Currency as Currency<2218 <T as system::Trait>::AccountId,2219>>::Balance;2220type NegativeImbalanceOf<T> = <<T as transaction_payment::Trait>::Currency as Currency<2221 <T as system::Trait>::AccountId,2222>>::NegativeImbalance;22232224/// Require the transactor pay for themselves and maybe include a tip to gain additional priority2225/// in the queue.2226#[derive(Encode, Decode, Clone, Eq, PartialEq)]2227pub struct ChargeTransactionPayment<T: Trait + Send + Sync>(2228 #[codec(compact)] BalanceOf<T>2229);22302231impl<T: Trait + Send + Sync> sp_std::fmt::Debug2232 for ChargeTransactionPayment<T>2233{2234 #[cfg(feature = "std")]2235 fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {2236 write!(f, "ChargeTransactionPayment<{:?}>", self.0)2237 }2238 #[cfg(not(feature = "std"))]2239 fn fmt(&self, _: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {2240 Ok(())2241 }2242}22432244impl<T: Trait + Send + Sync> ChargeTransactionPayment<T>2245where2246 T::Call: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> + IsSubType<Call<T>> + IsSubType<pallet_contracts::Call<T>>,2247 BalanceOf<T>: Send + Sync + FixedPointOperand,2248{2249 /// utility constructor. Used only in client/factory code.2250 pub fn from(fee: BalanceOf<T>) -> Self {2251 Self(fee)2252 }22532254 pub fn traditional_fee(2255 len: usize,2256 info: &DispatchInfoOf<T::Call>,2257 tip: BalanceOf<T>,2258 ) -> BalanceOf<T>2259 where2260 T::Call: Dispatchable<Info = DispatchInfo>,2261 {2262 <transaction_payment::Module<T>>::compute_fee(len as u32, info, tip)2263 }22642265 fn get_priority(len: usize, info: &DispatchInfoOf<T::Call>, final_fee: BalanceOf<T>) -> TransactionPriority {2266 let weight_saturation = T::MaximumBlockWeight::get() / info.weight.max(1);2267 let len_saturation = T::MaximumBlockLength::get() as u64 / (len as u64).max(1);2268 let coefficient: BalanceOf<T> = weight_saturation.min(len_saturation).saturated_into::<BalanceOf<T>>();2269 final_fee.saturating_mul(coefficient).saturated_into::<TransactionPriority>()2270 }22712272 fn withdraw_fee(2273 &self,2274 who: &T::AccountId,2275 call: &T::Call,2276 info: &DispatchInfoOf<T::Call>,2277 len: usize,2278 ) -> Result<(BalanceOf<T>, Option<NegativeImbalanceOf<T>>), TransactionValidityError> {2279 let tip = self.0;22802281 // Set fee based on call type. Creating collection costs 1 Unique.2282 // All other transactions have traditional fees so far2283 // let fee = match call.is_sub_type() {2284 // Some(Call::create_collection(..)) => <BalanceOf<T>>::from(1_000_000_000),2285 // _ => Self::traditional_fee(len, info, tip), // Flat fee model, use only for testing purposes2286 // // _ => <BalanceOf<T>>::from(100)2287 // };2288 let fee = Self::traditional_fee(len, info, tip);22892290 // Only mess with balances if fee is not zero.2291 if fee.is_zero() {2292 return Ok((fee, None));2293 }22942295 // Determine who is paying transaction fee based on ecnomic model2296 // Parse call to extract collection ID and access collection sponsor2297 let mut sponsor: T::AccountId = match IsSubType::<Call<T>>::is_sub_type(call) {2298 Some(Call::create_item(collection_id, _owner, _properties)) => {22992300 // check free create limit2301 if (<Collection<T>>::get(collection_id).limits.sponsored_data_size >= (_properties.len() as u32)) &&2302 (<Collection<T>>::get(collection_id).sponsor_confirmed)2303 {2304 <Collection<T>>::get(collection_id).sponsor2305 } else {2306 T::AccountId::default()2307 }2308 }2309 Some(Call::transfer(_new_owner, collection_id, item_id, _value)) => {2310 2311 let mut sponsor_transfer = false;2312 if <Collection<T>>::get(collection_id).sponsor_confirmed {23132314 let collection_limits = <Collection<T>>::get(collection_id).limits;2315 let collection_mode = <Collection<T>>::get(collection_id).mode;2316 2317 // sponsor timeout2318 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2319 sponsor_transfer = match collection_mode {2320 CollectionMode::NFT => {2321 2322 // get correct limit2323 let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {2324 collection_limits.sponsor_transfer_timeout2325 } else {2326 ChainLimit::get().nft_sponsor_transfer_timeout2327 };2328 2329 let mut sponsored = true;2330 if <NftTransferBasket<T>>::contains_key(collection_id, item_id) {2331 let last_tx_block = <NftTransferBasket<T>>::get(collection_id, item_id);2332 let limit_time = last_tx_block + limit.into();2333 if block_number <= limit_time {2334 sponsored = false;2335 }2336 }2337 if sponsored {2338 <NftTransferBasket<T>>::insert(collection_id, item_id, block_number);2339 }23402341 sponsored2342 }2343 CollectionMode::Fungible(_) => {2344 2345 // get correct limit2346 let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {2347 collection_limits.sponsor_transfer_timeout2348 } else {2349 ChainLimit::get().fungible_sponsor_transfer_timeout2350 };2351 2352 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2353 let mut sponsored = true;2354 if <FungibleTransferBasket<T>>::contains_key(collection_id, who) {2355 let last_tx_block = <FungibleTransferBasket<T>>::get(collection_id, who);2356 let limit_time = last_tx_block + limit.into();2357 if block_number <= limit_time {2358 sponsored = false;2359 }2360 }2361 if sponsored {2362 <FungibleTransferBasket<T>>::insert(collection_id, who, block_number);2363 }23642365 sponsored2366 }2367 CollectionMode::ReFungible(_) => {2368 2369 // get correct limit2370 let limit: u32 = if collection_limits.sponsor_transfer_timeout > 0 {2371 collection_limits.sponsor_transfer_timeout2372 } else {2373 ChainLimit::get().refungible_sponsor_transfer_timeout2374 };2375 2376 let mut sponsored = true;2377 if <ReFungibleTransferBasket<T>>::contains_key(collection_id, item_id) {2378 let last_tx_block = <ReFungibleTransferBasket<T>>::get(collection_id, item_id);2379 let limit_time = last_tx_block + limit.into();2380 if block_number <= limit_time {2381 sponsored = false;2382 }2383 }2384 if sponsored {2385 <ReFungibleTransferBasket<T>>::insert(collection_id, item_id, block_number);2386 }23872388 sponsored2389 }2390 _ => {2391 false2392 },2393 };2394 }23952396 if !sponsor_transfer {2397 T::AccountId::default()2398 } else {2399 <Collection<T>>::get(collection_id).sponsor2400 }2401 }24022403 _ => T::AccountId::default(),2404 };24052406 // Sponsor smart contracts2407 sponsor = match IsSubType::<pallet_contracts::Call<T>>::is_sub_type(call) {24082409 // On instantiation: set the contract owner2410 Some(pallet_contracts::Call::instantiate(_endowment, _gas_limit, code_hash, data)) => {24112412 let new_contract_address = <T as pallet_contracts::Trait>::DetermineContractAddress::contract_address_for(2413 code_hash,2414 &data,2415 &who,2416 );2417 <ContractOwner<T>>::insert(new_contract_address.clone(), who.clone());24182419 T::AccountId::default()2420 },24212422 // When the contract is called, check if the sponsoring is enabled and pay fees from contract endowment if it is2423 Some(pallet_contracts::Call::call(dest, _value, _gas_limit, _data)) => {24242425 let called_contract: T::AccountId = T::Lookup::lookup((*dest).clone()).unwrap_or(T::AccountId::default());24262427 let owned_contract = <ContractOwner<T>>::contains_key(called_contract.clone())2428 && <ContractOwner<T>>::get(called_contract.clone()) == *who;2429 let white_list_enabled = <ContractWhiteListEnabled<T>>::contains_key(called_contract.clone()) && <ContractWhiteListEnabled<T>>::get(called_contract.clone());2430 2431 if !owned_contract && white_list_enabled {2432 if !<ContractWhiteList<T>>::contains_key(called_contract.clone(), who) {2433 return Err(InvalidTransaction::Call.into());2434 }2435 }24362437 let mut sponsor_transfer = false;2438 if <ContractSponsoringRateLimit<T>>::contains_key(called_contract.clone()) {2439 let last_tx_block = <ContractSponsorBasket<T>>::get((&called_contract, &who));2440 let block_number = <system::Module<T>>::block_number() as T::BlockNumber;2441 let rate_limit = <ContractSponsoringRateLimit<T>>::get(&called_contract);2442 let limit_time = last_tx_block + rate_limit;24432444 if block_number >= limit_time {2445 <ContractSponsorBasket<T>>::insert((called_contract.clone(), who.clone()), block_number);2446 sponsor_transfer = true;2447 }2448 } else {2449 sponsor_transfer = false;2450 }2451 2452 2453 let mut sp = T::AccountId::default();2454 if sponsor_transfer {2455 if <ContractSelfSponsoring<T>>::contains_key(called_contract.clone()) {2456 if <ContractSelfSponsoring<T>>::get(called_contract.clone()) {2457 sp = called_contract;2458 }2459 }2460 }24612462 sp2463 },24642465 _ => sponsor,2466 };24672468 let mut who_pays_fee: T::AccountId = sponsor.clone();2469 if sponsor == T::AccountId::default() {2470 who_pays_fee = who.clone();2471 }24722473 match <T as transaction_payment::Trait>::Currency::withdraw(2474 &who_pays_fee,2475 fee,2476 if tip.is_zero() {2477 WithdrawReason::TransactionPayment.into()2478 } else {2479 WithdrawReason::TransactionPayment | WithdrawReason::Tip2480 },2481 ExistenceRequirement::KeepAlive,2482 ) {2483 Ok(imbalance) => Ok((fee, Some(imbalance))),2484 Err(_) => Err(InvalidTransaction::Payment.into()),2485 }2486 }2487}248824892490impl<T: Trait + Send + Sync> SignedExtension2491 for ChargeTransactionPayment<T>2492where2493 BalanceOf<T>: Send + Sync + From<u64> + FixedPointOperand,2494 T::Call: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo> + IsSubType<Call<T>> + IsSubType<pallet_contracts::Call<T>>,2495{2496 const IDENTIFIER: &'static str = "ChargeTransactionPayment";2497 type AccountId = T::AccountId;2498 type Call = T::Call;2499 type AdditionalSigned = ();2500 type Pre = (2501 BalanceOf<T>,2502 Self::AccountId,2503 Option<NegativeImbalanceOf<T>>,2504 BalanceOf<T>,2505 );2506 fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> {2507 Ok(())2508 }25092510 fn validate(2511 &self,2512 who: &Self::AccountId,2513 call: &Self::Call,2514 info: &DispatchInfoOf<Self::Call>,2515 len: usize,2516 ) -> TransactionValidity {2517 let (fee, _) = self.withdraw_fee(who, call, info, len)?;2518 Ok(ValidTransaction {2519 priority: Self::get_priority(len, info, fee),2520 ..Default::default()2521 })2522 }25232524 fn pre_dispatch(2525 self,2526 who: &Self::AccountId,2527 call: &Self::Call,2528 info: &DispatchInfoOf<Self::Call>,2529 len: usize,2530 ) -> Result<Self::Pre, TransactionValidityError> {2531 let (fee, imbalance) = self.withdraw_fee(who, call, info, len)?;2532 Ok((self.0, who.clone(), imbalance, fee))2533 }25342535 fn post_dispatch(2536 pre: Self::Pre,2537 info: &DispatchInfoOf<Self::Call>,2538 post_info: &PostDispatchInfoOf<Self::Call>,2539 len: usize,2540 _result: &DispatchResult,2541 ) -> Result<(), TransactionValidityError> {2542 let (tip, who, imbalance, fee) = pre;2543 if let Some(payed) = imbalance {2544 let actual_fee = <transaction_payment::Module<T>>::compute_actual_fee(2545 len as u32, info, post_info, tip,2546 );2547 let refund = fee.saturating_sub(actual_fee);2548 let actual_payment =2549 match <T as transaction_payment::Trait>::Currency::deposit_into_existing(2550 &who, refund,2551 ) {2552 Ok(refund_imbalance) => {2553 // The refund cannot be larger than the up front payed max weight.2554 // `PostDispatchInfo::calc_unspent` guards against such a case.2555 match payed.offset(refund_imbalance) {2556 Ok(actual_payment) => actual_payment,2557 Err(_) => return Err(InvalidTransaction::Payment.into()),2558 }2559 }2560 // We do not recreate the account using the refund. The up front payment2561 // is gone in that case.2562 Err(_) => payed,2563 };2564 let imbalances = actual_payment.split(tip);2565 <T as transaction_payment::Trait>::OnTransactionPayment::on_unbalanceds(2566 Some(imbalances.0).into_iter().chain(Some(imbalances.1)),2567 );2568 }2569 Ok(())2570 }2571}25722573// #endregionruntime_types.jsondiffbeforeafterboth--- a/runtime_types.json
+++ b/runtime_types.json
@@ -42,18 +42,14 @@
"Fraction": "u128"
},
"FungibleItemType": {
- "Collection": "CollectionId",
- "Owner": "AccountId",
"Value": "u128"
},
"NftItemType": {
- "Collection": "CollectionId",
"Owner": "AccountId",
"ConstData": "Vec<u8>",
"VariableData": "Vec<u8>"
},
"ReFungibleItemType": {
- "Collection": "CollectionId",
"Owner": "Vec<Ownership<AccountId>>",
"ConstData": "Vec<u8>",
"VariableData": "Vec<u8>"
@@ -70,14 +66,10 @@
"OffchainSchema": "Vec<u8>",
"SchemaVersion": "SchemaVersion",
"Sponsor": "AccountId",
- "UnconfirmedSponsor": "AccountId",
+ "SponsorConfirmed": "bool",
"Limits": "CollectionLimits",
"VariableOnChainSchema": "Vec<u8>",
"ConstOnChainSchema": "Vec<u8>"
- },
- "ApprovePermissions": {
- "Approved": "AccountId",
- "Amount": "u128"
},
"RawData": "Vec<u8>",
"Address": "AccountId",
@@ -87,7 +79,9 @@
"const_data": "Vec<u8>",
"variable_data": "Vec<u8>"
},
- "CreateFungibleData": {},
+ "CreateFungibleData": {
+ "value": "u128"
+ },
"CreateReFungibleData": {
"const_data": "Vec<u8>",
"variable_data": "Vec<u8>"
@@ -107,10 +101,6 @@
},
"CollectionId": "u32",
"TokenId": "u32",
- "BasketItem": {
- "Address": "AccountId",
- "start_block": "BlockNumber"
- },
"ChainLimits": {
"collection_numbers_limit": "u32",
"account_token_ownership_limit": "u32",
tests/loadtester-src/.gitignorediffbeforeafterboth--- /dev/null
+++ b/tests/loadtester-src/.gitignore
@@ -0,0 +1,9 @@
+# Ignore build artifacts from the local tests sub-crate.
+/target/
+
+# Ignore backup files creates by cargo fmt.
+**/*.rs.bk
+
+# Remove Cargo.lock when creating an executable, leave it for libraries
+# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
+Cargo.lock
\ No newline at end of file
tests/loadtester-src/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/tests/loadtester-src/Cargo.toml
@@ -0,0 +1,36 @@
+[package]
+name = "loadtester"
+version = "0.1.0"
+authors = ["[your_name] <[your_email]>"]
+edition = "2018"
+
+[dependencies]
+ink_primitives = { version = "3.0.0-rc2", default-features = false }
+ink_metadata = { version = "3.0.0-rc2", default-features = false, features = ["derive"], optional = true }
+ink_env = { version = "3.0.0-rc2", default-features = false }
+ink_storage = { version = "3.0.0-rc2", default-features = false }
+ink_lang = { version = "3.0.0-rc2", default-features = false }
+ink_prelude = { version = "3.0.0-rc2", default-features = false }
+
+scale = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] }
+scale-info = { version = "0.4.1", default-features = false, features = ["derive"], optional = true }
+
+[lib]
+name = "loadtester"
+path = "lib.rs"
+crate-type = [
+ # Used for normal contract Wasm blobs.
+ "cdylib",
+]
+
+[features]
+default = ["std"]
+std = [
+ "ink_metadata/std",
+ "ink_env/std",
+ "ink_storage/std",
+ "ink_primitives/std",
+ "scale/std",
+ "scale-info/std",
+]
+ink-as-dependency = []
tests/loadtester-src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/tests/loadtester-src/lib.rs
@@ -0,0 +1,53 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use ink_lang as ink;
+
+#[ink::contract]
+mod loadtester {
+ use ink_storage::collections::Vec as InkVec;
+
+ #[ink(storage)]
+ pub struct LoadTester {
+ vector: InkVec<u64>,
+ }
+
+ impl LoadTester {
+ #[ink(constructor)]
+ pub fn new() -> Self {
+ Self {
+ vector: InkVec::new(),
+ }
+ }
+
+ #[ink(message)]
+ pub fn bloat(&mut self, count: u64){
+ for i in 1..count+1 {
+ self.vector.push(i);
+ }
+ }
+
+ #[ink(message)]
+ pub fn get(&self) -> u128 {
+ let mut sum: u128 = 0;
+ for num in self.vector.iter() {
+ sum += *num as u128;
+ }
+ sum
+ }
+ }
+
+ #[cfg(test)]
+ mod tests {
+
+ use super::*;
+
+ #[test]
+ fn it_works() {
+ let mut lt = LoadTester::new();
+ lt.bloat(4);
+ assert_eq!(lt.get(), [1,2,3,4]);
+ lt.bloat(3);
+ assert_eq!(lt.get(), [1,2,3,4,1,2,3]);
+ }
+ }
+}
tests/package.jsondiffbeforeafterboth--- a/tests/package.json
+++ b/tests/package.json
@@ -16,7 +16,8 @@
"typescript": "^3.9.7"
},
"scripts": {
- "test": "mocha --timeout 9999999 -r ts-node/register ./**/*.test.ts"
+ "test": "mocha --timeout 9999999 -r ts-node/register ./**/*.test.ts",
+ "load": "mocha --timeout 9999999 -r ts-node/register ./**/*.load.ts"
},
"author": "",
"license": "Apache 2.0",
tests/src/change-collection-owner.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/change-collection-owner.test.ts
@@ -0,0 +1,59 @@
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import privateKey from './substrate/privateKey';
+import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api";
+import { createCollectionExpectSuccess, createCollectionExpectFailure } from "./util/helpers";
+
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+describe('Integration Test changeCollectionOwner(collection_id, new_owner):', () => {
+ it('Changing owner changes owner.', async () => {
+ await usingApi(async api => {
+ const collectionId = await createCollectionExpectSuccess();
+ const alice = privateKey('//Alice');
+ const bob = privateKey('//Bob');
+
+ const collection: any = (await api.query.nft.collection(collectionId));
+ expect(collection.Owner.toString()).to.be.eq(alice.address);
+
+ const changeOwnerTx = api.tx.nft.changeCollectionOwner(collectionId, bob.address);
+ await submitTransactionAsync(alice, changeOwnerTx);
+
+ const collectionAfterOwnerChange: any = (await api.query.nft.collection(collectionId));
+ expect(collectionAfterOwnerChange.Owner.toString()).to.be.eq(bob.address);
+ });
+ });
+});
+
+describe('Negative Integration Test changeCollectionOwner(collection_id, new_owner):', () => {
+ it(`Not owner can't change owner.`, async () => {
+ await usingApi(async api => {
+ const collectionId = await createCollectionExpectSuccess();
+ const alice = privateKey('//Alice');
+ const bob = privateKey('//Bob');
+
+ const changeOwnerTx = api.tx.nft.changeCollectionOwner(collectionId, bob.address);
+ await expect(submitTransactionExpectFailAsync(bob, changeOwnerTx)).to.be.rejected;
+
+ const collectionAfterOwnerChange: any = (await api.query.nft.collection(collectionId));
+ expect(collectionAfterOwnerChange.Owner.toString()).to.be.eq(alice.address);
+
+ // Verifying that nothing bad happened (network is live, new collections can be created, etc.)
+ await createCollectionExpectSuccess();
+ });
+ });
+ it(`Can't change owner of not existing collection.`, async () => {
+ await usingApi(async api => {
+ const collectionId = (1<<32) - 1;
+ const alice = privateKey('//Alice');
+ const bob = privateKey('//Bob');
+
+ const changeOwnerTx = api.tx.nft.changeCollectionOwner(collectionId, bob.address);
+ await expect(submitTransactionExpectFailAsync(alice, changeOwnerTx)).to.be.rejected;
+
+ // Verifying that nothing bad happened (network is live, new collections can be created, etc.)
+ await createCollectionExpectSuccess();
+ });
+ });
+});
tests/src/confirmSponsorship.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/confirmSponsorship.test.ts
@@ -0,0 +1,338 @@
+//
+// This file is subject to the terms and conditions defined in
+// file 'LICENSE', which is part of this source code package.
+//
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api";
+import {
+ createCollectionExpectSuccess,
+ setCollectionSponsorExpectSuccess,
+ destroyCollectionExpectSuccess,
+ setCollectionSponsorExpectFailure,
+ confirmSponsorshipExpectSuccess,
+ confirmSponsorshipExpectFailure,
+ createItemExpectSuccess,
+ findUnusedAddress,
+ getGenericResult,
+ enableWhiteListExpectSuccess,
+ enablePublicMintingExpectSuccess,
+ addToWhiteListExpectSuccess,
+} from "./util/helpers";
+import { Keyring } from "@polkadot/api";
+import { IKeyringPair } from "@polkadot/types/types";
+import type { AccountId } from '@polkadot/types/interfaces';
+import { BigNumber } from 'bignumber.js';
+
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+let alice: IKeyringPair;
+let bob: IKeyringPair;
+let charlie: IKeyringPair;
+
+describe('integration test: ext. confirmSponsorship():', () => {
+
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ alice = keyring.addFromUri(`//Alice`);
+ bob = keyring.addFromUri(`//Bob`);
+ charlie = keyring.addFromUri(`//Charlie`);
+ });
+ });
+
+ it('Confirm collection sponsorship', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+ });
+ it('Add sponsor to a collection after the same sponsor was already added and confirmed', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ });
+ it('Add new sponsor to a collection after another sponsor was already added and confirmed', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+ await setCollectionSponsorExpectSuccess(collectionId, charlie.address);
+ });
+
+ it('NFT: Transfer fees are paid by the sponsor after confirmation', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for unused address
+ const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', zeroBalance.address);
+
+ // Transfer this tokens from unused address to Alice
+ const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 0);
+ const events = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result = getGenericResult(events);
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ expect(result.success).to.be.true;
+ expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true;
+ });
+
+ });
+
+ it('Fungible: Transfer fees are paid by the sponsor after confirmation', async () => {
+ const collectionId = await createCollectionExpectSuccess({mode: 'Fungible'});
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for unused address
+ const itemId = await createItemExpectSuccess(alice, collectionId, 'Fungible', zeroBalance.address);
+
+ // Transfer this tokens from unused address to Alice
+ const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1);
+ const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result1 = getGenericResult(events1);
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ expect(result1.success).to.be.true;
+ expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true;
+ });
+ });
+
+ it('ReFungible: Transfer fees are paid by the sponsor after confirmation', async () => {
+ const collectionId = await createCollectionExpectSuccess({mode: 'ReFungible'});
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for unused address
+ const itemId = await createItemExpectSuccess(alice, collectionId, 'ReFungible', zeroBalance.address);
+
+ // Transfer this tokens from unused address to Alice
+ const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1);
+ const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result1 = getGenericResult(events1);
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ expect(result1.success).to.be.true;
+ expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true;
+ });
+ });
+
+ it('CreateItem fees are paid by the sponsor after confirmation', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ // Enable collection white list
+ await enableWhiteListExpectSuccess(alice, collectionId);
+
+ // Enable public minting
+ await enablePublicMintingExpectSuccess(alice, collectionId);
+
+ // Create Item
+ await usingApi(async (api) => {
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Add zeroBalance address to white list
+ await addToWhiteListExpectSuccess(alice, collectionId, zeroBalance.address);
+
+ // Mint token using unused address as signer
+ const tokenId = await createItemExpectSuccess(zeroBalance, collectionId, 'NFT', zeroBalance.address);
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ expect(BsponsorBalance.lt(AsponsorBalance)).to.be.true;
+ });
+ });
+
+ it('NFT: Sponsoring is rate limited', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for alice
+ const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', alice.address);
+
+ // Transfer this token from Alice to unused address and back
+ // Alice to Zero gets sponsored
+ const aliceToZero = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 0);
+ const events1 = await submitTransactionAsync(alice, aliceToZero);
+ const result1 = getGenericResult(events1);
+
+ // Second transfer should fail
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+ const zeroToAlice = api.tx.nft.transfer(alice.address, collectionId, itemId, 0);
+ const badTransaction = async function () {
+ await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);
+ };
+ await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees");
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Try again after Zero gets some balance - now it should succeed
+ const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15);
+ await submitTransactionAsync(alice, balancetx);
+ const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result2 = getGenericResult(events2);
+
+ expect(result1.success).to.be.true;
+ expect(result2.success).to.be.true;
+ expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true;
+ });
+ });
+
+ it('Fungible: Sponsoring is rate limited', async () => {
+ const collectionId = await createCollectionExpectSuccess({mode: 'Fungible'});
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for unused address
+ const itemId = await createItemExpectSuccess(alice, collectionId, 'Fungible', zeroBalance.address);
+
+ // Transfer this tokens in parts from unused address to Alice
+ const zeroToAlice = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1);
+ const events1 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result1 = getGenericResult(events1);
+
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ const badTransaction = async function () {
+ await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);
+ };
+
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Try again after Zero gets some balance - now it should succeed
+ const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15);
+ await submitTransactionAsync(alice, balancetx);
+ const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result2 = getGenericResult(events2);
+
+ expect(result1.success).to.be.true;
+ expect(result2.success).to.be.true;
+ expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true;
+ });
+ });
+
+ it('ReFungible: Sponsoring is rate limited', async () => {
+ const collectionId = await createCollectionExpectSuccess({mode: 'ReFungible'});
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+
+ await usingApi(async (api) => {
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for alice
+ const itemId = await createItemExpectSuccess(alice, collectionId, 'ReFungible', alice.address);
+
+ // Transfer this token from Alice to unused address and back
+ // Alice to Zero gets sponsored
+ const aliceToZero = api.tx.nft.transfer(zeroBalance.address, collectionId, itemId, 1);
+ const events1 = await submitTransactionAsync(alice, aliceToZero);
+ const result1 = getGenericResult(events1);
+
+ // Second transfer should fail
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+ const zeroToAlice = api.tx.nft.transfer(alice.address, collectionId, itemId, 1);
+ const badTransaction = async function () {
+ await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);
+ };
+ await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees");
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ // Try again after Zero gets some balance - now it should succeed
+ const balancetx = api.tx.balances.transfer(zeroBalance.address, 1e15);
+ await submitTransactionAsync(alice, balancetx);
+ const events2 = await submitTransactionAsync(zeroBalance, zeroToAlice);
+ const result2 = getGenericResult(events2);
+
+ expect(result1.success).to.be.true;
+ expect(result2.success).to.be.true;
+ expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true;
+ });
+ });
+
+});
+
+describe('(!negative test!) integration test: ext. removeCollectionSponsor():', () => {
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ alice = keyring.addFromUri(`//Alice`);
+ bob = keyring.addFromUri(`//Bob`);
+ charlie = keyring.addFromUri(`//Charlie`);
+ });
+ });
+
+ it('(!negative test!) Confirm sponsorship for a collection that never existed', async () => {
+ // Find the collection that never existed
+ const collectionId = 0;
+ await usingApi(async (api) => {
+ const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1;
+ });
+
+ await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+ });
+
+ it('(!negative test!) Confirm sponsorship using a non-sponsor address', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+
+ await usingApi(async (api) => {
+ const transfer = api.tx.balances.transfer(charlie.address, 1e15);
+ await submitTransactionAsync(alice, transfer);
+ });
+
+ await confirmSponsorshipExpectFailure(collectionId, '//Charlie');
+ });
+
+ it('(!negative test!) Confirm sponsorship using owner address', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectFailure(collectionId, '//Alice');
+ });
+
+ it('(!negative test!) Confirm sponsorship without sponsor being set with setCollectionSponsor', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+ });
+
+ it('(!negative test!) Confirm sponsorship in a collection that was destroyed', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await destroyCollectionExpectSuccess(collectionId);
+ await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+ });
+});
tests/src/connection.test.tsdiffbeforeafterboth--- a/tests/src/connection.test.ts
+++ b/tests/src/connection.test.ts
@@ -21,6 +21,8 @@
});
it('Cannot connect to 255.255.255.255', async () => {
+ const log = console.log;
+ const error = console.error;
console.log = function () {};
console.error = function () {};
@@ -31,7 +33,7 @@
}, { provider: neverConnectProvider });
})()).to.be.eventually.rejected;
- delete console.log;
- delete console.error;
+ console.log = log;
+ console.error = error;
});
});
\ No newline at end of file
tests/src/contracts.test.tsdiffbeforeafterboth--- a/tests/src/contracts.test.ts
+++ b/tests/src/contracts.test.ts
@@ -109,9 +109,6 @@
const bob = privateKey("//Bob");
const [contract, deployer] = await deployFlipper(api);
- const consoleError = console.error;
- console.error = (...data: any[]) => {
- };
let expectedFlipValue = await getFlipValue(contract, deployer);
@@ -166,7 +163,6 @@
const afterWhiteListDisabled = await getFlipValue(contract,deployer);
expect(afterWhiteListDisabled).to.be.eq(expectedFlipValue, `Anyone can call contract with disabled whitelist.`);
- console.error = consoleError;
});
});
tests/src/createCollection.test.tsdiffbeforeafterboth--- a/tests/src/createCollection.test.ts
+++ b/tests/src/createCollection.test.ts
@@ -6,37 +6,29 @@
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { default as usingApi } from "./substrate/substrate-api";
-import { createCollectionExpectSuccess, createCollectionExpectFailure } from "./util/helpers";
+import { createCollectionExpectSuccess, createCollectionExpectFailure, CollectionMode } from "./util/helpers";
chai.use(chaiAsPromised);
const expect = chai.expect;
describe('integration test: ext. createCollection():', () => {
it('Create new NFT collection', async () => {
- await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: 'NFT'});
});
it('Create new NFT collection whith collection_name of maximum length (64 bytes)', async () => {
- await createCollectionExpectSuccess(
- 'ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCD',
- '1', '1', 'NFT');
+ await createCollectionExpectSuccess({name: 'A'.repeat(64)});
});
it('Create new NFT collection whith collection_description of maximum length (256 bytes)', async () => {
- await createCollectionExpectSuccess(
- 'A',
- 'ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJabcdef',
- '1', 'NFT');
+ await createCollectionExpectSuccess({description: 'A'.repeat(256)});
});
it('Create new NFT collection whith token_prefix of maximum length (16 bytes)', async () => {
- await createCollectionExpectSuccess(
- '1',
- '1',
- 'ABCDEFGHIJABCDEF', 'NFT');
+ await createCollectionExpectSuccess({tokenPrefix: 'A'.repeat(16)});
});
it('Create new Fungible collection', async () => {
- await createCollectionExpectSuccess('1', '1', '1', 'Fungible');
+ await createCollectionExpectSuccess({mode: 'Fungible'});
});
it('Create new ReFungible collection', async () => {
- await createCollectionExpectSuccess('1', '1', '1', 'ReFungible');
+ await createCollectionExpectSuccess({mode: 'ReFungible'});
});
});
@@ -46,7 +38,7 @@
const AcollectionCount = parseInt((await api.query.nft.collectionCount()).toString());
const badTransaction = async function () {
- await createCollectionExpectSuccess('1', '1', '1', 'BadMode');
+ await createCollectionExpectSuccess({mode: 'BadMode' as CollectionMode});
};
expect(badTransaction()).to.be.rejected;
@@ -55,18 +47,12 @@
});
});
it('(!negative test!) create new NFT collection whith incorrect data (collection_name)', async () => {
- await createCollectionExpectFailure(
- 'ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDE',
- '1', '1', 'NFT');
+ await createCollectionExpectFailure({name: 'A'.repeat(65)});
});
it('(!negative test!) create new NFT collection whith incorrect data (collection_description)', async () => {
- await createCollectionExpectFailure('1',
- 'ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJabcdefg',
- '1', 'NFT');
+ await createCollectionExpectFailure({description: 'A'.repeat(257)});
});
it('(!negative test!) create new NFT collection whith incorrect data (token_prefix)', async () => {
- await createCollectionExpectFailure('1', '1',
- 'ABCDEFGHIJABCDEFG',
- 'NFT');
+ await createCollectionExpectFailure({tokenPrefix: 'A'.repeat(17)});
});
});
tests/src/createItem.test.tsdiffbeforeafterboth--- a/tests/src/createItem.test.ts
+++ b/tests/src/createItem.test.ts
@@ -1,27 +1,34 @@
-import { assert } from 'chai';
-import { alicesPublicKey } from './accounts';
-import privateKey from './substrate/privateKey';
import { default as usingApi } from './substrate/substrate-api';
-import waitNewBlocks from './substrate/wait-new-blocks';
+import { Keyring } from "@polkadot/api";
+import { IKeyringPair } from "@polkadot/types/types";
import {
createCollectionExpectSuccess,
createItemExpectSuccess
} from './util/helpers';
+let alice: IKeyringPair;
+
describe('integration test: ext. createItem():', () => {
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ alice = keyring.addFromUri(`//Alice`);
+ });
+ });
+
it('Create new item in NFT collection', async () => {
const createMode = 'NFT';
- const newCollectionID = await createCollectionExpectSuccess('0', '0', '0', createMode);
- await createItemExpectSuccess(newCollectionID, createMode, '//Alice');
+ const newCollectionID = await createCollectionExpectSuccess({mode: createMode});
+ await createItemExpectSuccess(alice, newCollectionID, createMode);
});
it('Create new item in Fungible collection', async () => {
const createMode = 'Fungible';
- const newCollectionID = await createCollectionExpectSuccess('0', '0', '0', createMode);
- await createItemExpectSuccess(newCollectionID, createMode, '//Alice');
+ const newCollectionID = await createCollectionExpectSuccess({mode: createMode});
+ await createItemExpectSuccess(alice, newCollectionID, createMode);
});
it('Create new item in ReFungible collection', async () => {
const createMode = 'ReFungible';
- const newCollectionID = await createCollectionExpectSuccess('0', '0', '0', createMode);
- await createItemExpectSuccess(newCollectionID, createMode, '//Alice');
+ const newCollectionID = await createCollectionExpectSuccess({mode: createMode});
+ await createItemExpectSuccess(alice, newCollectionID, createMode);
});
});
tests/src/createMultipleItems.test.tsdiffbeforeafterboth--- a/tests/src/createMultipleItems.test.ts
+++ b/tests/src/createMultipleItems.test.ts
@@ -11,7 +11,7 @@
const idCollection = 12;
-describe('integration test: ext. createMultipleItems():', () => {
+describe.skip('integration test: ext. createMultipleItems():', () => {
it('Create two NFT tokens in active NFT collection', async () => {
await usingApi(async (api) => {
const AitemListIndex = await api.query.nft.itemListIndex(idCollection);
tests/src/creditFeesToTreasury.test.tsdiffbeforeafterboth--- a/tests/src/creditFeesToTreasury.test.ts
+++ b/tests/src/creditFeesToTreasury.test.ts
@@ -5,7 +5,7 @@
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
-import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api";
+import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api";
import { alicesPublicKey, bobsPublicKey } from "./accounts";
import privateKey from "./substrate/privateKey";
import { BigNumber } from 'bignumber.js';
@@ -63,14 +63,13 @@
const bobBalanceBefore = new BigNumber((await api.query.system.account(bobsPublicKey)).data.free.toString());
const badTx = api.tx.balances.setBalance(alicesPublicKey, 0, 0);
- const result = getGenericResult(await submitTransactionAsync(bobPrivateKey, badTx));
+ await expect(submitTransactionExpectFailAsync(bobPrivateKey, badTx)).to.be.rejected;
const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString());
const bobBalanceAfter = new BigNumber((await api.query.system.account(bobsPublicKey)).data.free.toString());
const fee = bobBalanceBefore.minus(bobBalanceAfter);
const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore);
- expect(result.success).to.be.false;
expect(treasuryIncrease.toFixed()).to.be.equal(fee.toFixed());
});
});
@@ -80,7 +79,7 @@
const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString());
const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString());
- await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await createCollectionExpectSuccess();
const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString());
const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString());
@@ -96,7 +95,7 @@
const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString());
const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString());
- await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ await createCollectionExpectSuccess();
const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString());
const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString());
tests/src/destroyCollection.test.tsdiffbeforeafterboth--- a/tests/src/destroyCollection.test.ts
+++ b/tests/src/destroyCollection.test.ts
@@ -1,67 +1,24 @@
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api";
-import { createCollectionExpectSuccess, createCollectionExpectFailure } from "./util/helpers";
+import { createCollectionExpectSuccess, createCollectionExpectFailure, destroyCollectionExpectSuccess, destroyCollectionExpectFailure } from "./util/helpers";
import type { AccountId, EventRecord } from '@polkadot/types/interfaces';
import privateKey from './substrate/privateKey';
-import { nullPublicKey } from './accounts';
chai.use(chaiAsPromised);
const expect = chai.expect;
-
-function getDestroyResult(events: EventRecord[]): boolean {
- let success: boolean = false;
- events.forEach(({ phase, event: { data, method, section } }) => {
- // console.log(` ${phase}: ${section}.${method}:: ${data}`);
- if (method == 'ExtrinsicSuccess') {
- success = true;
- }
- });
- return success;
-}
-async function destroyCollectionExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {
- await usingApi(async (api) => {
- // Run the DestroyCollection transaction
- const alicePrivateKey = privateKey(senderSeed);
- const tx = api.tx.nft.destroyCollection(collectionId);
- const events = await submitTransactionAsync(alicePrivateKey, tx);
- const result = getDestroyResult(events);
-
- // Get the collection
- const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
-
- // What to expect
- expect(result).to.be.true;
- expect(collection).to.be.not.null;
- expect(collection.Owner).to.be.equal(nullPublicKey);
- });
-}
-
-async function destroyCollectionExpectFailure(collectionId: number, senderSeed: string = '//Alice') {
- await usingApi(async (api) => {
- // Run the DestroyCollection transaction
- const alicePrivateKey = privateKey(senderSeed);
- const tx = api.tx.nft.destroyCollection(collectionId);
- const events = await submitTransactionAsync(alicePrivateKey, tx);
- const result = getDestroyResult(events);
-
- // What to expect
- expect(result).to.be.false;
- });
-}
-
describe('integration test: ext. destroyCollection():', () => {
it('NFT collection can be destroyed', async () => {
- const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ const collectionId = await createCollectionExpectSuccess();
await destroyCollectionExpectSuccess(collectionId);
});
it('Fungible collection can be destroyed', async () => {
- const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'Fungible');
+ const collectionId = await createCollectionExpectSuccess({ mode: 'Fungible' });
await destroyCollectionExpectSuccess(collectionId);
});
it('ReFungible collection can be destroyed', async () => {
- const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'ReFungible');
+ const collectionId = await createCollectionExpectSuccess({ mode: 'ReFungible' });
await destroyCollectionExpectSuccess(collectionId);
});
});
@@ -75,12 +32,12 @@
});
});
it('(!negative test!) Destroy a collection that has already been destroyed', async () => {
- const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ const collectionId = await createCollectionExpectSuccess();
await destroyCollectionExpectSuccess(collectionId);
await destroyCollectionExpectFailure(collectionId);
});
it('(!negative test!) Destroy a collection using non-owner account', async () => {
- const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT');
+ const collectionId = await createCollectionExpectSuccess();
await destroyCollectionExpectFailure(collectionId, '//Bob');
await destroyCollectionExpectSuccess(collectionId, '//Alice');
});
tests/src/load_test_sc/loadtester.wasmdiffbeforeafterbothbinary blob — no preview
tests/src/load_test_sc/metadata.jsondiffbeforeafterboth--- /dev/null
+++ b/tests/src/load_test_sc/metadata.json
@@ -0,0 +1,125 @@
+{
+ "metadataVersion": "0.1.0",
+ "source": {
+ "hash": "0x168cc3cba9657ad3950fb506e568751f99b90fb097685107f6101675662a8303",
+ "language": "ink! 3.0.0-rc2",
+ "compiler": "rustc 1.49.0-nightly"
+ },
+ "contract": {
+ "name": "loadtester",
+ "version": "0.1.0",
+ "authors": [
+ "[your_name] <[your_email]>"
+ ]
+ },
+ "spec": {
+ "constructors": [
+ {
+ "args": [],
+ "docs": [],
+ "name": [
+ "new"
+ ],
+ "selector": "0xd183512b"
+ }
+ ],
+ "docs": [],
+ "events": [],
+ "messages": [
+ {
+ "args": [
+ {
+ "name": "count",
+ "type": {
+ "displayName": [
+ "u64"
+ ],
+ "type": 2
+ }
+ }
+ ],
+ "docs": [],
+ "mutates": true,
+ "name": [
+ "bloat"
+ ],
+ "payable": false,
+ "returnType": null,
+ "selector": "0x49891c2a"
+ },
+ {
+ "args": [],
+ "docs": [],
+ "mutates": false,
+ "name": [
+ "get"
+ ],
+ "payable": false,
+ "returnType": {
+ "displayName": [
+ "u128"
+ ],
+ "type": 3
+ },
+ "selector": "0x1e5ca456"
+ }
+ ]
+ },
+ "storage": {
+ "struct": {
+ "fields": [
+ {
+ "layout": {
+ "struct": {
+ "fields": [
+ {
+ "layout": {
+ "cell": {
+ "key": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "ty": 1
+ }
+ },
+ "name": "len"
+ },
+ {
+ "layout": {
+ "array": {
+ "cellsPerElem": 1,
+ "layout": {
+ "cell": {
+ "key": "0x0000000001000000000000000000000000000000000000000000000000000000",
+ "ty": 2
+ }
+ },
+ "len": 4294967295,
+ "offset": "0x0100000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "name": "elems"
+ }
+ ]
+ }
+ },
+ "name": "vector"
+ }
+ ]
+ }
+ },
+ "types": [
+ {
+ "def": {
+ "primitive": "u32"
+ }
+ },
+ {
+ "def": {
+ "primitive": "u64"
+ }
+ },
+ {
+ "def": {
+ "primitive": "u128"
+ }
+ }
+ ]
+}
\ No newline at end of file
tests/src/removeCollectionSponsor.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/removeCollectionSponsor.test.ts
@@ -0,0 +1,137 @@
+//
+// This file is subject to the terms and conditions defined in
+// file 'LICENSE', which is part of this source code package.
+//
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api";
+import {
+ createCollectionExpectSuccess,
+ setCollectionSponsorExpectSuccess,
+ destroyCollectionExpectSuccess,
+ setCollectionSponsorExpectFailure,
+ confirmSponsorshipExpectSuccess,
+ confirmSponsorshipExpectFailure,
+ createItemExpectSuccess,
+ findUnusedAddress,
+ getGenericResult,
+ enableWhiteListExpectSuccess,
+ enablePublicMintingExpectSuccess,
+ addToWhiteListExpectSuccess,
+ removeCollectionSponsorExpectSuccess,
+ removeCollectionSponsorExpectFailure,
+} from "./util/helpers";
+import { Keyring } from "@polkadot/api";
+import { IKeyringPair } from "@polkadot/types/types";
+import type { AccountId } from '@polkadot/types/interfaces';
+import { BigNumber } from 'bignumber.js';
+
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+let alice: IKeyringPair;
+let bob: IKeyringPair;
+let charlie: IKeyringPair;
+
+describe('integration test: ext. removeCollectionSponsor():', () => {
+
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ alice = keyring.addFromUri(`//Alice`);
+ bob = keyring.addFromUri(`//Bob`);
+ charlie = keyring.addFromUri(`//Charlie`);
+ });
+ });
+
+ it('Remove NFT collection sponsor stops sponsorship', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+ await removeCollectionSponsorExpectSuccess(collectionId);
+
+ await usingApi(async (api) => {
+ // Find unused address
+ const zeroBalance = await findUnusedAddress(api);
+
+ // Mint token for unused address
+ const itemId = await createItemExpectSuccess(alice, collectionId, 'NFT', zeroBalance.address);
+
+ // Transfer this tokens from unused address to Alice - should fail
+ const AsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+ const zeroToAlice = api.tx.nft.transfer(alice.address, collectionId, itemId, 0);
+ const badTransaction = async function () {
+ await submitTransactionExpectFailAsync(zeroBalance, zeroToAlice);
+ };
+ await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees");
+ const BsponsorBalance = new BigNumber((await api.query.system.account(bob.address)).data.free.toString());
+
+ expect(BsponsorBalance.isEqualTo(AsponsorBalance)).to.be.true;
+ });
+ });
+
+ it('Remove a sponsor after it was already removed', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+ await removeCollectionSponsorExpectSuccess(collectionId);
+ await removeCollectionSponsorExpectSuccess(collectionId);
+ });
+
+ it('Remove sponsor in a collection that never had the sponsor set', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await removeCollectionSponsorExpectSuccess(collectionId);
+ });
+
+ it('Remove sponsor for a collection that had the sponsor set, but not confirmed', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await removeCollectionSponsorExpectSuccess(collectionId);
+ });
+
+});
+
+describe('(!negative test!) integration test: ext. removeCollectionSponsor():', () => {
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ alice = keyring.addFromUri(`//Alice`);
+ bob = keyring.addFromUri(`//Bob`);
+ charlie = keyring.addFromUri(`//Charlie`);
+ });
+ });
+
+ it('(!negative test!) Remove sponsor for a collection that never existed', async () => {
+ // Find the collection that never existed
+ const collectionId = 0;
+ await usingApi(async (api) => {
+ const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1;
+ });
+
+ await removeCollectionSponsorExpectFailure(collectionId);
+ });
+
+ it('(!negative test!) Remove sponsor in a destroyed collection', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await destroyCollectionExpectSuccess(collectionId);
+ await removeCollectionSponsorExpectFailure(collectionId);
+ });
+
+ it('Set - remove - confirm: fails', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await removeCollectionSponsorExpectSuccess(collectionId);
+ await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+ });
+
+ it('Set - confirm - remove - confirm: Sponsor cannot come back', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await confirmSponsorshipExpectSuccess(collectionId, '//Bob');
+ await removeCollectionSponsorExpectSuccess(collectionId);
+ await confirmSponsorshipExpectFailure(collectionId, '//Bob');
+ });
+
+});
tests/src/rpc.load.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/rpc.load.ts
@@ -0,0 +1,145 @@
+//
+// This file is subject to the terms and conditions defined in
+// file 'LICENSE', which is part of this source code package.
+//
+
+import { expect, assert } from "chai";
+import usingApi, { submitTransactionAsync, submitTransactionExpectFailAsync } from "./substrate/substrate-api";
+import { IKeyringPair } from "@polkadot/types/types";
+import { Abi, BlueprintPromise as Blueprint, CodePromise, ContractPromise as Contract } from "@polkadot/api-contract";
+import { ApiPromise, Keyring } from "@polkadot/api";
+import { ApiTypes, SubmittableExtrinsic } from "@polkadot/api/types";
+import { BigNumber } from 'bignumber.js';
+import { findUnusedAddress } from './util/helpers'
+import fs from "fs";
+import privateKey from "./substrate/privateKey";
+
+const value = 0;
+const gasLimit = 500000n * 1000000n;
+const endowment = `1000000000000000`;
+
+
+function deployBlueprint(alice: IKeyringPair, code: CodePromise): Promise<Blueprint> {
+ return new Promise<Blueprint>(async (resolve, reject) => {
+ const unsub = await code
+ .createBlueprint()
+ .signAndSend(alice, (result) => {
+ if (result.status.isInBlock || result.status.isFinalized) {
+ // here we have an additional field in the result, containing the blueprint
+ resolve(result.blueprint);
+ unsub();
+ }
+ })
+ });
+}
+
+function deployContract(alice: IKeyringPair, blueprint: Blueprint) : Promise<any> {
+ return new Promise<any>(async (resolve, reject) => {
+ const unsub = await blueprint.tx
+ .new(endowment, gasLimit)
+ .signAndSend(alice, (result) => {
+ if (result.status.isInBlock || result.status.isFinalized) {
+ unsub();
+ resolve(result);
+ }
+ });
+ });
+}
+
+async function prepareDeployer(api: ApiPromise) {
+ // Find unused address
+ const deployer = await findUnusedAddress(api);
+
+ // Transfer balance to it
+ const keyring = new Keyring({ type: 'sr25519' });
+ const alice = keyring.addFromUri(`//Alice`);
+ let amount = new BigNumber(endowment);
+ amount = amount.plus(1e15);
+ const tx = api.tx.balances.transfer(deployer.address, amount.toFixed());
+ await submitTransactionAsync(alice, tx);
+
+ return deployer;
+}
+
+async function deployLoadTester(api: ApiPromise): Promise<[Contract, IKeyringPair]> {
+ const metadata = JSON.parse(fs.readFileSync('./src/load_test_sc/metadata.json').toString('utf-8'));
+ const abi = new Abi(metadata);
+
+ const deployer = await prepareDeployer(api);
+
+ const wasm = fs.readFileSync('./src/load_test_sc/loadtester.wasm');
+
+ const code = new CodePromise(api, abi, wasm);
+
+ const blueprint = await deployBlueprint(deployer, code);
+ const contract = (await deployContract(deployer, blueprint))['contract'] as Contract;
+
+ return [contract, deployer];
+}
+
+async function getScData(contract: Contract, deployer: IKeyringPair) {
+ const result = await contract.query.get(deployer.address, value, gasLimit);
+
+ if(!result.result.isSuccess) {
+ throw `Failed to get value`;
+ }
+ return result.result.asSuccess.data;
+}
+
+
+describe('RPC Tests', () => {
+ it('Simple RPC Load Test', async () => {
+ await usingApi(async api => {
+ let count = 0;
+ let hrTime = process.hrtime();
+ let microsec1 = hrTime[0] * 1000000 + hrTime[1] / 1000;
+ let rate = 0;
+ const checkPoint = 1000;
+ while (true) {
+ await api.rpc.system.chain();
+ count++;
+ process.stdout.write(`RPC reads: ${count} times at rate ${rate} r/s \r`);
+
+ if (count % checkPoint == 0) {
+ hrTime = process.hrtime();
+ let microsec2 = hrTime[0] * 1000000 + hrTime[1] / 1000;
+ rate = 1000000*checkPoint/(microsec2 - microsec1);
+ microsec1 = microsec2;
+ }
+ }
+ });
+ });
+
+ it.only('Smart Contract RPC Load Test', async () => {
+ await usingApi(async api => {
+
+ // Deploy smart contract
+ const [contract, deployer] = await deployLoadTester(api);
+
+ // Fill smart contract up with data
+ const bob = privateKey("//Bob");
+ const tx = contract.tx.bloat(value, gasLimit, 200);
+ await submitTransactionAsync(bob, tx);
+
+ // Run load test
+ let count = 0;
+ let hrTime = process.hrtime();
+ let microsec1 = hrTime[0] * 1000000 + hrTime[1] / 1000;
+ let rate = 0;
+ const checkPoint = 10;
+ while (true) {
+ await getScData(contract, deployer);
+ count++;
+ process.stdout.write(`SC reads: ${count} times at rate ${rate} r/s \r`);
+
+ if (count % checkPoint == 0) {
+ hrTime = process.hrtime();
+ let microsec2 = hrTime[0] * 1000000 + hrTime[1] / 1000;
+ rate = 1000000*checkPoint/(microsec2 - microsec1);
+ microsec1 = microsec2;
+ }
+ }
+ });
+ });
+
+});
tests/src/setCollectionSponsor.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/setCollectionSponsor.test.ts
@@ -0,0 +1,82 @@
+//
+// This file is subject to the terms and conditions defined in
+// file 'LICENSE', which is part of this source code package.
+//
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+import { default as usingApi } from "./substrate/substrate-api";
+import { createCollectionExpectSuccess, setCollectionSponsorExpectSuccess, destroyCollectionExpectSuccess, setCollectionSponsorExpectFailure } from "./util/helpers";
+import { Keyring } from "@polkadot/api";
+import { IKeyringPair } from "@polkadot/types/types";
+import type { AccountId } from '@polkadot/types/interfaces';
+
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+let bob: IKeyringPair;
+
+describe('integration test: ext. setCollectionSponsor():', () => {
+
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ bob = keyring.addFromUri(`//Bob`);
+ });
+ });
+
+ it('Set NFT collection sponsor', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ });
+ it('Set Fungible collection sponsor', async () => {
+ const collectionId = await createCollectionExpectSuccess({ mode: 'Fungible' });
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ });
+ it('Set ReFungible collection sponsor', async () => {
+ const collectionId = await createCollectionExpectSuccess({ mode: 'ReFungible' });
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ });
+
+ it('Set the same sponsor repeatedly', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ });
+ it('Replace collection sponsor', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+
+ const keyring = new Keyring({ type: 'sr25519' });
+ const charlie = keyring.addFromUri(`//Charlie`);
+ await setCollectionSponsorExpectSuccess(collectionId, bob.address);
+ await setCollectionSponsorExpectSuccess(collectionId, charlie.address);
+ });
+});
+
+describe('(!negative test!) integration test: ext. setCollectionSponsor():', () => {
+ before(async () => {
+ await usingApi(async (api) => {
+ const keyring = new Keyring({ type: 'sr25519' });
+ bob = keyring.addFromUri(`//Bob`);
+ });
+ });
+
+ it('(!negative test!) Add sponsor with a non-owner', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await setCollectionSponsorExpectFailure(collectionId, bob.address, '//Bob');
+ });
+ it('(!negative test!) Add sponsor to a collection that never existed', async () => {
+ // Find the collection that never existed
+ const collectionId = 0;
+ await usingApi(async (api) => {
+ const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1;
+ });
+
+ await setCollectionSponsorExpectFailure(collectionId, bob.address);
+ });
+ it('(!negative test!) Add sponsor to a collection that was destroyed', async () => {
+ const collectionId = await createCollectionExpectSuccess();
+ await destroyCollectionExpectSuccess(collectionId);
+ await setCollectionSponsorExpectFailure(collectionId, bob.address);
+ });
+});
tests/src/substrate/substrate-api.tsdiffbeforeafterboth--- a/tests/src/substrate/substrate-api.ts
+++ b/tests/src/substrate/substrate-api.ts
@@ -33,21 +33,42 @@
}
}
+enum TransactionStatus {
+ Success,
+ Fail,
+ NotReady
+}
+
+function getTransactionStatus(events: EventRecord[], status: ExtrinsicStatus): TransactionStatus {
+ if (status.isReady) {
+ return TransactionStatus.NotReady;
+ }
+ if (status.isBroadcast) {
+ return TransactionStatus.NotReady;
+ }
+ if (status.isInBlock || status.isFinalized) {
+ if(events.filter(e => e.event.data.method === 'ExtrinsicFailed').length > 0) {
+ return TransactionStatus.Fail;
+ }
+ if(events.filter(e => e.event.data.method === 'ExtrinsicSuccess').length > 0) {
+ return TransactionStatus.Success;
+ }
+ }
+
+ return TransactionStatus.Fail;
+}
+
export function submitTransactionAsync(sender: IKeyringPair, transaction: SubmittableExtrinsic<ApiTypes>): Promise<EventRecord[]> {
return new Promise(async function(resolve, reject) {
try {
await transaction.signAndSend(sender, ({ events = [], status }) => {
- if (status.isReady) {
- // nothing to do
- // console.log(`Current tx status is Ready`);
- } else if (status.isBroadcast) {
- // nothing to do
- // console.log(`Current tx status is Broadcast`);
- } else if (status.isInBlock || status.isFinalized) {
+ const transactionStatus = getTransactionStatus(events, status);
+
+ if (transactionStatus == TransactionStatus.Success) {
resolve(events);
- } else {
+ } else if (transactionStatus == TransactionStatus.Fail) {
console.log(`Something went wrong with transaction. Status: ${status}`);
- reject("Transaction failed");
+ reject(events);
}
});
} catch (e) {
@@ -58,19 +79,35 @@
}
export function submitTransactionExpectFailAsync(sender: IKeyringPair, transaction: SubmittableExtrinsic<ApiTypes>): Promise<EventRecord[]> {
- return new Promise(async function(resolve, reject) {
+ const consoleError = console.error;
+ const consoleLog = console.log;
+ console.error = () => {};
+ console.log = () => {};
+
+ return new Promise<EventRecord[]>(async function(res, rej) {
+ const resolve = (rec: EventRecord[]) => {
+ setTimeout(() => {
+ res(rec);
+ console.error = consoleError;
+ console.log = consoleLog;
+
+ });
+ };
+ const reject = (errror: any) => {
+ setTimeout(() => {
+ rej(errror);
+ console.error = consoleError;
+ console.log = consoleLog;
+ });
+ };
try {
await transaction.signAndSend(sender, ({ events = [], status }) => {
- if (status.isReady) {
- // nothing to do
- // console.log(`Current tx status is Ready`);
- } else if (status.isBroadcast) {
- // nothing to do
- // console.log(`Current tx status is Broadcast`);
- } else if (status.isInBlock || status.isFinalized) {
+ const transactionStatus = getTransactionStatus(events, status);
+
+ if (transactionStatus == TransactionStatus.Success) {
resolve(events);
- } else {
- reject("Transaction failed");
+ } else if (transactionStatus == TransactionStatus.Fail) {
+ reject(events);
}
});
} catch (e) {
tests/src/transfer.test.tsdiffbeforeafterboth--- a/tests/src/transfer.test.ts
+++ b/tests/src/transfer.test.ts
@@ -33,6 +33,8 @@
// Find unused address
const pk = await findUnusedAddress(api);
+ const error = console.error;
+ const log = console.log;
console.log = function () {};
console.error = function () {};
@@ -42,8 +44,8 @@
};
await expect(badTransaction()).to.be.rejectedWith("Inability to pay some fees");
- delete console.log;
- delete console.error;
+ console.log = log;
+ console.error = error;
});
});
});
tests/src/util/helpers.tsdiffbeforeafterboth--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -7,12 +7,14 @@
import chaiAsPromised from 'chai-as-promised';
import type { AccountId, EventRecord } from '@polkadot/types/interfaces';
import { ApiPromise, Keyring } from "@polkadot/api";
-import { default as usingApi, submitTransactionAsync } from "../substrate/substrate-api";
+import { default as usingApi, submitTransactionAsync, submitTransactionExpectFailAsync } from "../substrate/substrate-api";
import privateKey from '../substrate/privateKey';
-import { alicesPublicKey } from "../accounts";
+import { alicesPublicKey, nullPublicKey } from "../accounts";
import { strToUTF16, utf16ToStr, hexToStr } from '../util/util';
import { IKeyringPair } from "@polkadot/types/types";
import { BigNumber } from 'bignumber.js';
+import { Struct, Enum } from '@polkadot/types/codec';
+import { u128 } from '@polkadot/types/primitive';
chai.use(chaiAsPromised);
const expect = chai.expect;
@@ -26,6 +28,12 @@
collectionId: number
};
+type CreateItemResult = {
+ success: boolean,
+ collectionId: number,
+ itemId: number
+};
+
export function getGenericResult(events: EventRecord[]): GenericResult {
let result: GenericResult = {
success: false
@@ -57,8 +65,46 @@
return result;
}
-export async function createCollectionExpectSuccess(name: string, description: string, tokenPrefix: string, mode: string): Promise<number> {
+function getCreateItemResult(events: EventRecord[]): CreateItemResult {
+ let success = false;
let collectionId: number = 0;
+ let itemId: number = 0;
+ events.forEach(({ phase, event: { data, method, section } }) => {
+ // console.log(` ${phase}: ${section}.${method}:: ${data}`);
+ if (method == 'ExtrinsicSuccess') {
+ success = true;
+ } else if ((section == 'nft') && (method == 'ItemCreated')) {
+ collectionId = parseInt(data[0].toString());
+ itemId = parseInt(data[1].toString());
+ }
+ });
+ let result: CreateItemResult = {
+ success,
+ collectionId,
+ itemId
+ }
+ return result;
+}
+
+export type CollectionMode = 'NFT' | 'Fungible' | 'ReFungible';
+export type CreateCollectionParams = {
+ mode: CollectionMode,
+ name: string,
+ description: string,
+ tokenPrefix: string
+};
+
+const defaultCreateCollectionParams: CreateCollectionParams = {
+ name: 'name',
+ description: 'description',
+ mode: 'NFT',
+ tokenPrefix: 'prefix'
+}
+
+export async function createCollectionExpectSuccess(params: Partial<CreateCollectionParams> = {}): Promise<number> {
+ const {name, description, mode, tokenPrefix } = {...defaultCreateCollectionParams, ...params};
+
+ let collectionId: number = 0;
await usingApi(async (api) => {
// Get number of collections before the transaction
const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString());
@@ -91,15 +137,17 @@
return collectionId;
}
-export async function createCollectionExpectFailure(name: string, description: string, tokenPrefix: string, mode: string) {
+export async function createCollectionExpectFailure(params: Partial<CreateCollectionParams> = {}) {
+ const {name, description, mode, tokenPrefix } = {...defaultCreateCollectionParams, ...params};
+
await usingApi(async (api) => {
// Get number of collections before the transaction
const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString());
// Run the CreateCollection transaction
const alicePrivateKey = privateKey('//Alice');
- const tx = api.tx.nft.createCollection(name, description, tokenPrefix, mode);
- const events = await submitTransactionAsync(alicePrivateKey, tx);
+ const tx = api.tx.nft.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), mode);
+ const events = await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;
const result = getCreateCollectionResult(events);
// Get number of collections after the transaction
@@ -123,20 +171,231 @@
return unused;
}
-export async function createItemExpectSuccess(collectionId: number, createMode: string, senderSeed: string = '//Alice') {
+function getDestroyResult(events: EventRecord[]): boolean {
+ let success: boolean = false;
+ events.forEach(({ phase, event: { data, method, section } }) => {
+ // console.log(` ${phase}: ${section}.${method}:: ${data}`);
+ if (method == 'ExtrinsicSuccess') {
+ success = true;
+ }
+ });
+ return success;
+}
+
+export async function destroyCollectionExpectFailure(collectionId: number, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+ // Run the DestroyCollection transaction
+ const alicePrivateKey = privateKey(senderSeed);
+ const tx = api.tx.nft.destroyCollection(collectionId);
+ await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;
+ });
+}
+
+export async function destroyCollectionExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+ // Run the DestroyCollection transaction
+ const alicePrivateKey = privateKey(senderSeed);
+ const tx = api.tx.nft.destroyCollection(collectionId);
+ const events = await submitTransactionAsync(alicePrivateKey, tx);
+ const result = getDestroyResult(events);
+
+ // Get the collection
+ const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
+
+ // What to expect
+ expect(result).to.be.true;
+ expect(collection).to.be.not.null;
+ expect(collection.Owner).to.be.equal(nullPublicKey);
+ });
+}
+
+export async function setCollectionSponsorExpectSuccess(collectionId: number, sponsor: string) {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const alicePrivateKey = privateKey('//Alice');
+ const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor);
+ const events = await submitTransactionAsync(alicePrivateKey, tx);
+ const result = getGenericResult(events);
+
+ // Get the collection
+ const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
+
+ // What to expect
+ expect(result.success).to.be.true;
+ expect(collection.Sponsor.toString()).to.be.equal(sponsor.toString());
+ expect(collection.SponsorConfirmed).to.be.false;
+ });
+}
+
+export async function removeCollectionSponsorExpectSuccess(collectionId: number) {
await usingApi(async (api) => {
- const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString());
+ // Run the transaction
+ const alicePrivateKey = privateKey('//Alice');
+ const tx = api.tx.nft.removeCollectionSponsor(collectionId);
+ const events = await submitTransactionAsync(alicePrivateKey, tx);
+ const result = getGenericResult(events);
+ // Get the collection
+ const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
+
+ // What to expect
+ expect(result.success).to.be.true;
+ expect(collection.Sponsor).to.be.equal(nullPublicKey);
+ expect(collection.SponsorConfirmed).to.be.false;
+ });
+}
+
+export async function removeCollectionSponsorExpectFailure(collectionId: number) {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const alicePrivateKey = privateKey('//Alice');
+ const tx = api.tx.nft.removeCollectionSponsor(collectionId);
+ await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;
+ });
+}
+
+export async function setCollectionSponsorExpectFailure(collectionId: number, sponsor: string, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const alicePrivateKey = privateKey(senderSeed);
+ const tx = api.tx.nft.setCollectionSponsor(collectionId, sponsor);
+ await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected;
+ });
+}
+
+export async function confirmSponsorshipExpectSuccess(collectionId: number, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+
+ // Run the transaction
const sender = privateKey(senderSeed);
- const tx = api.tx.nft.createItem(collectionId, sender.address, createMode);
+ const tx = api.tx.nft.confirmSponsorship(collectionId);
const events = await submitTransactionAsync(sender, tx);
const result = getGenericResult(events);
-
+
+ // Get the collection
+ const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
+
+ // What to expect
+ expect(result.success).to.be.true;
+ expect(collection.Sponsor).to.be.equal(sender.address);
+ expect(collection.SponsorConfirmed).to.be.true;
+ });
+}
+
+export async function confirmSponsorshipExpectFailure(collectionId: number, senderSeed: string = '//Alice') {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const sender = privateKey(senderSeed);
+ const tx = api.tx.nft.confirmSponsorship(collectionId);
+ await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected;
+ });
+}
+
+export interface CreateFungibleData extends Struct {
+ readonly value: u128;
+};
+
+export interface CreateReFungibleData extends Struct {};
+export interface CreateNftData extends Struct {};
+
+export interface CreateItemData extends Enum {
+ NFT: CreateNftData,
+ Fungible: CreateFungibleData,
+ ReFungible: CreateReFungibleData
+};
+
+export async function createItemExpectSuccess(sender: IKeyringPair, collectionId: number, createMode: string, owner: string = '') {
+ let newItemId: number = 0;
+ await usingApi(async (api) => {
+ const AItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString());
+ const Aitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON();
+ const AItemBalance = new BigNumber(Aitem.Value);
+
+ if (owner === '') owner = sender.address;
+
+ let tx;
+ if (createMode == 'Fungible') {
+ let createData = {fungible: {value: 10}};
+ tx = api.tx.nft.createItem(collectionId, owner, createData);
+ }
+ else {
+ tx = api.tx.nft.createItem(collectionId, owner, createMode);
+ }
+ const events = await submitTransactionAsync(sender, tx);
+ const result = getCreateItemResult(events);
+
const BItemCount = parseInt((await api.query.nft.itemListIndex(collectionId)).toString());
+ const Bitem: any = (await api.query.nft.fungibleItemList(collectionId, owner)).toJSON();
+ const BItemBalance = new BigNumber(Bitem.Value);
+
+ // What to expect
+ expect(result.success).to.be.true;
+ if (createMode == 'Fungible') {
+ expect(BItemBalance.minus(AItemBalance).toNumber()).to.be.equal(10);
+ }
+ else {
+ expect(BItemCount).to.be.equal(AItemCount+1);
+ }
+ expect(collectionId).to.be.equal(result.collectionId);
+ expect(BItemCount).to.be.equal(result.itemId);
+ newItemId = result.itemId;
+ });
+ return newItemId;
+}
+
+export async function enableWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number) {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const tx = api.tx.nft.setPublicAccessMode(collectionId, 'WhiteList');
+ const events = await submitTransactionAsync(sender, tx);
+ const result = getGenericResult(events);
+
+ // Get the collection
+ const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
+
+ // What to expect
+ expect(result.success).to.be.true;
+ expect(collection.Access).to.be.equal('WhiteList');
+ });
+}
+
+export async function enablePublicMintingExpectSuccess(sender: IKeyringPair, collectionId: number) {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const tx = api.tx.nft.setMintPermission(collectionId, true);
+ const events = await submitTransactionAsync(sender, tx);
+ const result = getGenericResult(events);
+ // Get the collection
+ const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
+
// What to expect
expect(result.success).to.be.true;
- expect(BItemCount).to.be.equal(AItemCount+1);
+ expect(collection.MintMode).to.be.equal(true);
});
}
+
+export async function addToWhiteListExpectSuccess(sender: IKeyringPair, collectionId: number, address: string) {
+ await usingApi(async (api) => {
+
+ // Run the transaction
+ const tx = api.tx.nft.addToWhiteList(collectionId, address);
+ const events = await submitTransactionAsync(sender, tx);
+ const result = getGenericResult(events);
+
+ // Get the collection
+ const collection: any = (await api.query.nft.collection(collectionId)).toJSON();
+
+ // What to expect
+ expect(result.success).to.be.true;
+ expect(collection.MintMode).to.be.equal(true);
+ });
+}
+