difftreelog
feat add event TokenChanged
in: master
8 files changed
pallets/common/src/erc.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! This module contains the implementation of pallet methods for evm.1819pub use pallet_evm::{PrecompileOutput, PrecompileResult, PrecompileHandle, account::CrossAccountId};20use evm_coder::{21 abi::AbiType,22 solidity_interface, solidity, ToLog,23 types::*,24 types::Property as PropertyStruct,25 execution::{Result, Error},26 weight,27};28use pallet_evm_coder_substrate::dispatch_to_evm;29use sp_std::vec::Vec;30use up_data_structs::{31 AccessMode, CollectionMode, CollectionPermissions, OwnerRestrictedSet, Property,32 SponsoringRateLimit, SponsorshipState,33};34use alloc::format;3536use crate::{37 Pallet, CollectionHandle, Config, CollectionProperties, SelfWeightOf,38 eth::{EthCrossAccount, convert_cross_account_to_uint256},39 weights::WeightInfo,40};4142/// Events for ethereum collection helper.43#[derive(ToLog)]44pub enum CollectionHelpersEvents {45 /// The collection has been created.46 CollectionCreated {47 /// Collection owner.48 #[indexed]49 owner: address,5051 /// Collection ID.52 #[indexed]53 collection_id: address,54 },55 /// The collection has been destroyed.56 CollectionDestroyed {57 /// Collection ID.58 #[indexed]59 collection_id: address,60 },61 /// The collection has been changed.62 CollectionChanged {63 /// Collection ID.64 #[indexed]65 collection_id: address,66 },67}6869/// Does not always represent a full collection, for RFT it is either70/// collection (Implementing ERC721), or specific collection token (Implementing ERC20).71pub trait CommonEvmHandler {72 /// Raw compiled binary code of the contract stub73 const CODE: &'static [u8];7475 /// Call precompiled handle.76 fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult>;77}7879/// @title A contract that allows you to work with collections.80#[solidity_interface(name = Collection)]81impl<T: Config> CollectionHandle<T>82where83 T::AccountId: From<[u8; 32]> + AsRef<[u8; 32]>,84{85 /// Set collection property.86 ///87 /// @param key Property key.88 /// @param value Propery value.89 #[solidity(hide)]90 #[weight(<SelfWeightOf<T>>::set_collection_properties(1))]91 fn set_collection_property(92 &mut self,93 caller: caller,94 key: string,95 value: bytes,96 ) -> Result<void> {97 let caller = T::CrossAccountId::from_eth(caller);98 let key = <Vec<u8>>::from(key)99 .try_into()100 .map_err(|_| "key too large")?;101 let value = value.0.try_into().map_err(|_| "value too large")?;102103 <Pallet<T>>::set_collection_property(self, &caller, Property { key, value })104 .map_err(dispatch_to_evm::<T>)105 }106107 /// Set collection properties.108 ///109 /// @param properties Vector of properties key/value pair.110 #[weight(<SelfWeightOf<T>>::set_collection_properties(properties.len() as u32))]111 fn set_collection_properties(112 &mut self,113 caller: caller,114 properties: Vec<PropertyStruct>,115 ) -> Result<void> {116 let caller = T::CrossAccountId::from_eth(caller);117118 let properties = properties119 .into_iter()120 .map(|PropertyStruct { key, value }| {121 let key = <Vec<u8>>::from(key)122 .try_into()123 .map_err(|_| "key too large")?;124125 let value = value.0.try_into().map_err(|_| "value too large")?;126127 Ok(Property { key, value })128 })129 .collect::<Result<Vec<_>>>()?;130131 <Pallet<T>>::set_collection_properties(self, &caller, properties)132 .map_err(dispatch_to_evm::<T>)133 }134135 /// Delete collection property.136 ///137 /// @param key Property key.138 #[solidity(hide)]139 #[weight(<SelfWeightOf<T>>::delete_collection_properties(1))]140 fn delete_collection_property(&mut self, caller: caller, key: string) -> Result<()> {141 let caller = T::CrossAccountId::from_eth(caller);142 let key = <Vec<u8>>::from(key)143 .try_into()144 .map_err(|_| "key too large")?;145146 <Pallet<T>>::delete_collection_property(self, &caller, key).map_err(dispatch_to_evm::<T>)147 }148149 /// Delete collection properties.150 ///151 /// @param keys Properties keys.152 #[weight(<SelfWeightOf<T>>::delete_collection_properties(keys.len() as u32))]153 fn delete_collection_properties(&mut self, caller: caller, keys: Vec<string>) -> Result<()> {154 let caller = T::CrossAccountId::from_eth(caller);155 let keys = keys156 .into_iter()157 .map(|key| {158 <Vec<u8>>::from(key)159 .try_into()160 .map_err(|_| Error::Revert("key too large".into()))161 })162 .collect::<Result<Vec<_>>>()?;163164 <Pallet<T>>::delete_collection_properties(self, &caller, keys).map_err(dispatch_to_evm::<T>)165 }166167 /// Get collection property.168 ///169 /// @dev Throws error if key not found.170 ///171 /// @param key Property key.172 /// @return bytes The property corresponding to the key.173 fn collection_property(&self, key: string) -> Result<bytes> {174 let key = <Vec<u8>>::from(key)175 .try_into()176 .map_err(|_| "key too large")?;177178 let props = CollectionProperties::<T>::get(self.id);179 let prop = props.get(&key).ok_or("key not found")?;180181 Ok(bytes(prop.to_vec()))182 }183184 /// Get collection properties.185 ///186 /// @param keys Properties keys. Empty keys for all propertyes.187 /// @return Vector of properties key/value pairs.188 fn collection_properties(&self, keys: Vec<string>) -> Result<Vec<PropertyStruct>> {189 let keys = keys190 .into_iter()191 .map(|key| {192 <Vec<u8>>::from(key)193 .try_into()194 .map_err(|_| Error::Revert("key too large".into()))195 })196 .collect::<Result<Vec<_>>>()?;197198 let properties = Pallet::<T>::filter_collection_properties(199 self.id,200 if keys.is_empty() { None } else { Some(keys) },201 )202 .map_err(dispatch_to_evm::<T>)?;203204 let properties = properties205 .into_iter()206 .map(|p| {207 let key =208 string::from_utf8(p.key.into()).map_err(|e| Error::Revert(format!("{}", e)))?;209 let value = bytes(p.value.to_vec());210 Ok(PropertyStruct { key, value })211 })212 .collect::<Result<Vec<_>>>()?;213 Ok(properties)214 }215216 /// Set the sponsor of the collection.217 ///218 /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.219 ///220 /// @param sponsor Address of the sponsor from whose account funds will be debited for operations with the contract.221 #[solidity(hide)]222 fn set_collection_sponsor(&mut self, caller: caller, sponsor: address) -> Result<void> {223 self.consume_store_reads_and_writes(1, 1)?;224225 let caller = T::CrossAccountId::from_eth(caller);226227 let sponsor = T::CrossAccountId::from_eth(sponsor);228 self.set_sponsor(&caller, sponsor.as_sub().clone())229 .map_err(dispatch_to_evm::<T>)230 }231232 /// Set the sponsor of the collection.233 ///234 /// @dev In order for sponsorship to work, it must be confirmed on behalf of the sponsor.235 ///236 /// @param sponsor Cross account address of the sponsor from whose account funds will be debited for operations with the contract.237 fn set_collection_sponsor_cross(238 &mut self,239 caller: caller,240 sponsor: EthCrossAccount,241 ) -> Result<void> {242 self.consume_store_reads_and_writes(1, 1)?;243244 let caller = T::CrossAccountId::from_eth(caller);245246 let sponsor = sponsor.into_sub_cross_account::<T>()?;247 self.set_sponsor(&caller, sponsor.as_sub().clone())248 .map_err(dispatch_to_evm::<T>)249 }250251 /// Whether there is a pending sponsor.252 fn has_collection_pending_sponsor(&self) -> Result<bool> {253 Ok(matches!(254 self.collection.sponsorship,255 SponsorshipState::Unconfirmed(_)256 ))257 }258259 /// Collection sponsorship confirmation.260 ///261 /// @dev After setting the sponsor for the collection, it must be confirmed with this function.262 fn confirm_collection_sponsorship(&mut self, caller: caller) -> Result<void> {263 self.consume_store_writes(1)?;264265 let caller = T::CrossAccountId::from_eth(caller);266 self.confirm_sponsorship(caller.as_sub())267 .map_err(dispatch_to_evm::<T>)268 }269270 /// Remove collection sponsor.271 fn remove_collection_sponsor(&mut self, caller: caller) -> Result<void> {272 self.consume_store_reads_and_writes(1, 1)?;273 let caller = T::CrossAccountId::from_eth(caller);274 self.remove_sponsor(&caller).map_err(dispatch_to_evm::<T>)275 }276277 /// Get current sponsor.278 ///279 /// @return Tuble with sponsor address and his substrate mirror. If there is no confirmed sponsor error "Contract has no sponsor" throw.280 fn collection_sponsor(&self) -> Result<(address, uint256)> {281 let sponsor = match self.collection.sponsorship.sponsor() {282 Some(sponsor) => sponsor,283 None => return Ok(Default::default()),284 };285 let sponsor = T::CrossAccountId::from_sub(sponsor.clone());286 let result: (address, uint256) = if sponsor.is_canonical_substrate() {287 let sponsor = convert_cross_account_to_uint256::<T>(&sponsor);288 (Default::default(), sponsor)289 } else {290 let sponsor = *sponsor.as_eth();291 (sponsor, Default::default())292 };293 Ok(result)294 }295296 /// Set limits for the collection.297 /// @dev Throws error if limit not found.298 /// @param limit Name of the limit. Valid names:299 /// "accountTokenOwnershipLimit",300 /// "sponsoredDataSize",301 /// "sponsoredDataRateLimit",302 /// "tokenLimit",303 /// "sponsorTransferTimeout",304 /// "sponsorApproveTimeout"305 /// "ownerCanTransfer",306 /// "ownerCanDestroy",307 /// "transfersEnabled"308 /// @param value Value of the limit.309 #[solidity(rename_selector = "setCollectionLimit")]310 fn set_int_limit(&mut self, caller: caller, limit: string, value: uint256) -> Result<void> {311 self.consume_store_reads_and_writes(1, 1)?;312313 let value = value314 .try_into()315 .map_err(|_| Error::Revert(format!("can't convert value to u32 \"{}\"", value)))?;316317 let convert_value_to_bool = || match value {318 0 => Ok(false),319 1 => Ok(true),320 _ => {321 return Err(Error::Revert(format!(322 "can't convert value to boolean \"{}\"",323 value324 )))325 }326 };327328 let mut limits = self.limits.clone();329330 match limit.as_str() {331 "accountTokenOwnershipLimit" => {332 limits.account_token_ownership_limit = Some(value);333 }334 "sponsoredDataSize" => {335 limits.sponsored_data_size = Some(value);336 }337 "sponsoredDataRateLimit" => {338 limits.sponsored_data_rate_limit = Some(SponsoringRateLimit::Blocks(value));339 }340 "tokenLimit" => {341 limits.token_limit = Some(value);342 }343 "sponsorTransferTimeout" => {344 limits.sponsor_transfer_timeout = Some(value);345 }346 "sponsorApproveTimeout" => {347 limits.sponsor_approve_timeout = Some(value);348 }349 "ownerCanTransfer" => {350 limits.owner_can_transfer = Some(convert_value_to_bool()?);351 }352 "ownerCanDestroy" => {353 limits.owner_can_destroy = Some(convert_value_to_bool()?);354 }355 "transfersEnabled" => {356 limits.transfers_enabled = Some(convert_value_to_bool()?);357 }358 _ => return Err(Error::Revert(format!("unknown limit \"{}\"", limit))),359 }360361 let caller = T::CrossAccountId::from_eth(caller);362 <Pallet<T>>::update_limits(&caller, self, limits).map_err(dispatch_to_evm::<T>)363 }364365 /// Get contract address.366 fn contract_address(&self) -> Result<address> {367 Ok(crate::eth::collection_id_to_address(self.id))368 }369370 /// Add collection admin.371 /// @param newAdmin Cross account administrator address.372 fn add_collection_admin_cross(373 &mut self,374 caller: caller,375 new_admin: EthCrossAccount,376 ) -> Result<void> {377 self.consume_store_reads_and_writes(2, 2)?;378379 let caller = T::CrossAccountId::from_eth(caller);380 let new_admin = new_admin.into_sub_cross_account::<T>()?;381 <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;382 Ok(())383 }384385 /// Remove collection admin.386 /// @param admin Cross account administrator address.387 fn remove_collection_admin_cross(388 &mut self,389 caller: caller,390 admin: EthCrossAccount,391 ) -> Result<void> {392 self.consume_store_reads_and_writes(2, 2)?;393394 let caller = T::CrossAccountId::from_eth(caller);395 let admin = admin.into_sub_cross_account::<T>()?;396 <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;397 Ok(())398 }399400 /// Add collection admin.401 /// @param newAdmin Address of the added administrator.402 #[solidity(hide)]403 fn add_collection_admin(&mut self, caller: caller, new_admin: address) -> Result<void> {404 self.consume_store_reads_and_writes(2, 2)?;405406 let caller = T::CrossAccountId::from_eth(caller);407 let new_admin = T::CrossAccountId::from_eth(new_admin);408 <Pallet<T>>::toggle_admin(self, &caller, &new_admin, true).map_err(dispatch_to_evm::<T>)?;409 Ok(())410 }411412 /// Remove collection admin.413 ///414 /// @param admin Address of the removed administrator.415 #[solidity(hide)]416 fn remove_collection_admin(&mut self, caller: caller, admin: address) -> Result<void> {417 self.consume_store_reads_and_writes(2, 2)?;418419 let caller = T::CrossAccountId::from_eth(caller);420 let admin = T::CrossAccountId::from_eth(admin);421 <Pallet<T>>::toggle_admin(self, &caller, &admin, false).map_err(dispatch_to_evm::<T>)?;422 Ok(())423 }424425 /// Toggle accessibility of collection nesting.426 ///427 /// @param enable If "true" degenerates to nesting: 'Owner' else to nesting: 'Disabled'428 #[solidity(rename_selector = "setCollectionNesting")]429 fn set_nesting_bool(&mut self, caller: caller, enable: bool) -> Result<void> {430 self.consume_store_reads_and_writes(1, 1)?;431432 let caller = T::CrossAccountId::from_eth(caller);433434 let mut permissions = self.collection.permissions.clone();435 let mut nesting = permissions.nesting().clone();436 nesting.token_owner = enable;437 nesting.restricted = None;438 permissions.nesting = Some(nesting);439440 <Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)441 }442443 /// Toggle accessibility of collection nesting.444 ///445 /// @param enable If "true" degenerates to nesting: {OwnerRestricted: [1, 2, 3]} else to nesting: 'Disabled'446 /// @param collections Addresses of collections that will be available for nesting.447 #[solidity(rename_selector = "setCollectionNesting")]448 fn set_nesting(449 &mut self,450 caller: caller,451 enable: bool,452 collections: Vec<address>,453 ) -> Result<void> {454 self.consume_store_reads_and_writes(1, 1)?;455456 if collections.is_empty() {457 return Err("no addresses provided".into());458 }459 let caller = T::CrossAccountId::from_eth(caller);460461 let mut permissions = self.collection.permissions.clone();462 match enable {463 false => {464 let mut nesting = permissions.nesting().clone();465 nesting.token_owner = false;466 nesting.restricted = None;467 permissions.nesting = Some(nesting);468 }469 true => {470 let mut bv = OwnerRestrictedSet::new();471 for i in collections {472 bv.try_insert(crate::eth::map_eth_to_id(&i).ok_or_else(|| {473 Error::Revert("Can't convert address into collection id".into())474 })?)475 .map_err(|_| "too many collections")?;476 }477 let mut nesting = permissions.nesting().clone();478 nesting.token_owner = true;479 nesting.restricted = Some(bv);480 permissions.nesting = Some(nesting);481 }482 };483484 <Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)485 }486487 /// Set the collection access method.488 /// @param mode Access mode489 /// 0 for Normal490 /// 1 for AllowList491 fn set_collection_access(&mut self, caller: caller, mode: uint8) -> Result<void> {492 self.consume_store_reads_and_writes(1, 1)?;493494 let caller = T::CrossAccountId::from_eth(caller);495 let permissions = CollectionPermissions {496 access: Some(match mode {497 0 => AccessMode::Normal,498 1 => AccessMode::AllowList,499 _ => return Err("not supported access mode".into()),500 }),501 ..Default::default()502 };503 <Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)504 }505506 /// Checks that user allowed to operate with collection.507 ///508 /// @param user User address to check.509 fn allowlisted_cross(&self, user: EthCrossAccount) -> Result<bool> {510 let user = user.into_sub_cross_account::<T>()?;511 Ok(Pallet::<T>::allowed(self.id, user))512 }513514 /// Add the user to the allowed list.515 ///516 /// @param user Address of a trusted user.517 #[solidity(hide)]518 fn add_to_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {519 self.consume_store_writes(1)?;520521 let caller = T::CrossAccountId::from_eth(caller);522 let user = T::CrossAccountId::from_eth(user);523 <Pallet<T>>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;524 Ok(())525 }526527 /// Add user to allowed list.528 ///529 /// @param user User cross account address.530 fn add_to_collection_allow_list_cross(531 &mut self,532 caller: caller,533 user: EthCrossAccount,534 ) -> Result<void> {535 self.consume_store_writes(1)?;536537 let caller = T::CrossAccountId::from_eth(caller);538 let user = user.into_sub_cross_account::<T>()?;539 Pallet::<T>::toggle_allowlist(self, &caller, &user, true).map_err(dispatch_to_evm::<T>)?;540 Ok(())541 }542543 /// Remove the user from the allowed list.544 ///545 /// @param user Address of a removed user.546 #[solidity(hide)]547 fn remove_from_collection_allow_list(&mut self, caller: caller, user: address) -> Result<void> {548 self.consume_store_writes(1)?;549550 let caller = T::CrossAccountId::from_eth(caller);551 let user = T::CrossAccountId::from_eth(user);552 <Pallet<T>>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;553 Ok(())554 }555556 /// Remove user from allowed list.557 ///558 /// @param user User cross account address.559 fn remove_from_collection_allow_list_cross(560 &mut self,561 caller: caller,562 user: EthCrossAccount,563 ) -> Result<void> {564 self.consume_store_writes(1)?;565566 let caller = T::CrossAccountId::from_eth(caller);567 let user = user.into_sub_cross_account::<T>()?;568 Pallet::<T>::toggle_allowlist(self, &caller, &user, false).map_err(dispatch_to_evm::<T>)?;569 Ok(())570 }571572 /// Switch permission for minting.573 ///574 /// @param mode Enable if "true".575 fn set_collection_mint_mode(&mut self, caller: caller, mode: bool) -> Result<void> {576 self.consume_store_reads_and_writes(1, 1)?;577578 let caller = T::CrossAccountId::from_eth(caller);579 let permissions = CollectionPermissions {580 mint_mode: Some(mode),581 ..Default::default()582 };583 <Pallet<T>>::update_permissions(&caller, self, permissions).map_err(dispatch_to_evm::<T>)584 }585586 /// Check that account is the owner or admin of the collection587 ///588 /// @param user account to verify589 /// @return "true" if account is the owner or admin590 #[solidity(hide, rename_selector = "isOwnerOrAdmin")]591 fn is_owner_or_admin_eth(&self, user: address) -> Result<bool> {592 let user = T::CrossAccountId::from_eth(user);593 Ok(self.is_owner_or_admin(&user))594 }595596 /// Check that account is the owner or admin of the collection597 ///598 /// @param user User cross account to verify599 /// @return "true" if account is the owner or admin600 fn is_owner_or_admin_cross(&self, user: EthCrossAccount) -> Result<bool> {601 let user = user.into_sub_cross_account::<T>()?;602 Ok(self.is_owner_or_admin(&user))603 }604605 /// Returns collection type606 ///607 /// @return `Fungible` or `NFT` or `ReFungible`608 fn unique_collection_type(&self) -> Result<string> {609 let mode = match self.collection.mode {610 CollectionMode::Fungible(_) => "Fungible",611 CollectionMode::NFT => "NFT",612 CollectionMode::ReFungible => "ReFungible",613 };614 Ok(mode.into())615 }616617 /// Get collection owner.618 ///619 /// @return Tuble with sponsor address and his substrate mirror.620 /// If address is canonical then substrate mirror is zero and vice versa.621 fn collection_owner(&self) -> Result<EthCrossAccount> {622 Ok(EthCrossAccount::from_sub_cross_account::<T>(623 &T::CrossAccountId::from_sub(self.owner.clone()),624 ))625 }626627 /// Changes collection owner to another account628 ///629 /// @dev Owner can be changed only by current owner630 /// @param newOwner new owner account631 #[solidity(hide, rename_selector = "changeCollectionOwner")]632 fn set_owner(&mut self, caller: caller, new_owner: address) -> Result<void> {633 self.consume_store_writes(1)?;634635 let caller = T::CrossAccountId::from_eth(caller);636 let new_owner = T::CrossAccountId::from_eth(new_owner);637 self.change_owner(caller, new_owner)638 .map_err(dispatch_to_evm::<T>)639 }640641 /// Get collection administrators642 ///643 /// @return Vector of tuples with admins address and his substrate mirror.644 /// If address is canonical then substrate mirror is zero and vice versa.645 fn collection_admins(&self) -> Result<Vec<EthCrossAccount>> {646 let result = crate::IsAdmin::<T>::iter_prefix((self.id,))647 .map(|(admin, _)| EthCrossAccount::from_sub_cross_account::<T>(&admin))648 .collect();649 Ok(result)650 }651652 /// Changes collection owner to another account653 ///654 /// @dev Owner can be changed only by current owner655 /// @param newOwner new owner cross account656 fn change_collection_owner_cross(657 &mut self,658 caller: caller,659 new_owner: EthCrossAccount,660 ) -> Result<void> {661 self.consume_store_writes(1)?;662663 let caller = T::CrossAccountId::from_eth(caller);664 let new_owner = new_owner.into_sub_cross_account::<T>()?;665 self.change_owner(caller, new_owner)666 .map_err(dispatch_to_evm::<T>)667 }668}669670/// ### Note671/// Do not forget to add: `self.consume_store_reads(1)?;`672fn check_is_owner_or_admin<T: Config>(673 caller: caller,674 collection: &CollectionHandle<T>,675) -> Result<T::CrossAccountId> {676 let caller = T::CrossAccountId::from_eth(caller);677 collection678 .check_is_owner_or_admin(&caller)679 .map_err(dispatch_to_evm::<T>)?;680 Ok(caller)681}682683/// ### Note684/// Do not forget to add: `self.consume_store_writes(1)?;`685fn save<T: Config>(collection: &CollectionHandle<T>) -> Result<void> {686 collection687 .check_is_internal()688 .map_err(dispatch_to_evm::<T>)?;689 collection.save().map_err(dispatch_to_evm::<T>)?;690 Ok(())691}692693/// Contains static property keys and values.694pub mod static_property {695 use evm_coder::{696 execution::{Result, Error},697 };698 use alloc::format;699700 const EXPECT_CONVERT_ERROR: &str = "length < limit";701702 /// Keys.703 pub mod key {704 use super::*;705706 /// Key "baseURI".707 pub fn base_uri() -> up_data_structs::PropertyKey {708 property_key_from_bytes(b"baseURI").expect(EXPECT_CONVERT_ERROR)709 }710711 /// Key "url".712 pub fn url() -> up_data_structs::PropertyKey {713 property_key_from_bytes(b"URI").expect(EXPECT_CONVERT_ERROR)714 }715716 /// Key "suffix".717 pub fn suffix() -> up_data_structs::PropertyKey {718 property_key_from_bytes(b"URISuffix").expect(EXPECT_CONVERT_ERROR)719 }720721 /// Key "parentNft".722 pub fn parent_nft() -> up_data_structs::PropertyKey {723 property_key_from_bytes(b"parentNft").expect(EXPECT_CONVERT_ERROR)724 }725 }726727 /// Convert `byte` to [`PropertyKey`].728 pub fn property_key_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyKey> {729 bytes.to_vec().try_into().map_err(|_| {730 Error::Revert(format!(731 "Property key is too long. Max length is {}.",732 up_data_structs::PropertyKey::bound()733 ))734 })735 }736737 /// Convert `bytes` to [`PropertyValue`].738 pub fn property_value_from_bytes(bytes: &[u8]) -> Result<up_data_structs::PropertyValue> {739 bytes.to_vec().try_into().map_err(|_| {740 Error::Revert(format!(741 "Property key is too long. Max length is {}.",742 up_data_structs::PropertyKey::bound()743 ))744 })745 }746}pallets/nonfungible/src/lib.rsdiffbeforeafterboth--- a/pallets/nonfungible/src/lib.rs
+++ b/pallets/nonfungible/src/lib.rs
@@ -108,11 +108,11 @@
use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};
use pallet_common::{
Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, CollectionHandle,
- eth::collection_id_to_address,
+ eth::collection_id_to_address, erc::CollectionHelpersEvents,
};
use pallet_structure::{Pallet as PalletStructure, Error as StructureError};
use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder};
-use sp_core::H160;
+use sp_core::{H160, Get};
use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};
use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};
use core::ops::Deref;
@@ -673,6 +673,14 @@
));
}
}
+
+ <PalletEvm<T>>::deposit_log(
+ CollectionHelpersEvents::TokenChanged {
+ collection_id: collection_id_to_address(collection.id),
+ token_id: token_id.into(),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
}
Ok(())
pallets/refungible/src/lib.rsdiffbeforeafterboth--- a/pallets/refungible/src/lib.rs
+++ b/pallets/refungible/src/lib.rs
@@ -102,11 +102,11 @@
use pallet_evm_coder_substrate::WithRecorder;
use pallet_common::{
CommonCollectionOperations, Error as CommonError, eth::collection_id_to_address,
- Event as CommonEvent, Pallet as PalletCommon,
+ Event as CommonEvent, Pallet as PalletCommon, erc::CollectionHelpersEvents,
};
use pallet_structure::Pallet as PalletStructure;
use scale_info::TypeInfo;
-use sp_core::H160;
+use sp_core::{Get, H160};
use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};
use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};
use up_data_structs::{
@@ -649,6 +649,14 @@
));
}
}
+
+ <PalletEvm<T>>::deposit_log(
+ CollectionHelpersEvents::TokenChanged {
+ collection_id: collection_id_to_address(collection.id),
+ token_id: token_id.into(),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
}
Ok(())
pallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterbothbinary blob — no preview
pallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -22,6 +22,7 @@
event CollectionCreated(address indexed owner, address indexed collectionId);
event CollectionDestroyed(address indexed collectionId);
event CollectionChanged(address indexed collectionId);
+ event TokenChanged(address indexed collectionId, uint256 tokenId);
}
/// @title Contract, which allows users to operate with collections
tests/src/eth/abi/collectionHelpers.jsondiffbeforeafterboth--- a/tests/src/eth/abi/collectionHelpers.json
+++ b/tests/src/eth/abi/collectionHelpers.json
@@ -45,6 +45,25 @@
"type": "event"
},
{
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "collectionId",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "TokenChanged",
+ "type": "event"
+ },
+ {
"inputs": [
{ "internalType": "uint32", "name": "collectionId", "type": "uint32" }
],
tests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -17,6 +17,7 @@
event CollectionCreated(address indexed owner, address indexed collectionId);
event CollectionDestroyed(address indexed collectionId);
event CollectionChanged(address indexed collectionId);
+ event TokenChanged(address indexed collectionId, uint256 tokenId);
}
/// @title Contract, which allows users to operate with collections
tests/src/eth/events.test.tsdiffbeforeafterboth--- a/tests/src/eth/events.test.ts
+++ b/tests/src/eth/events.test.ts
@@ -354,5 +354,48 @@
}
unsubscribe();
});
-
+
+ itEth('CollectionChanged event for TokenPropertySet, TokenPropertyDeleted', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const {collectionAddress} = await helper.eth.createCollecion('createNFTCollection', owner, 'A', 'B', 'C');
+ const collection = await helper.ethNativeContract.collection(collectionAddress, 'nft', owner);
+ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+ const result = await collection.methods.mint(owner).send({from: owner});
+ const tokenId = result.events.Transfer.returnValues.tokenId;
+ await collection.methods.setTokenPropertyPermission('A', true, true, true).send({from: owner});
+
+
+ const ethEvents: any = [];
+ collectionHelper.events.allEvents((_: any, event: any) => {
+ ethEvents.push(event);
+ });
+ const {unsubscribe, collectedEvents: subEvents} = await helper.subscribeEvents([{section: 'common', names: ['TokenPropertySet', 'TokenPropertyDeleted']}]);
+ {
+ await collection.methods.setProperties(tokenId, [{key: 'A', value: [1,2,3]}]).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'TokenChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'TokenPropertySet'}]);
+ ethEvents.pop();
+ subEvents.pop();
+ }
+ {
+ await collection.methods.deleteProperties(tokenId, ['A']).send({from: owner});
+ expect(ethEvents).to.be.like([
+ {
+ event: 'TokenChanged',
+ returnValues: {
+ collectionId: collectionAddress
+ }
+ }
+ ]);
+ expect(subEvents).to.be.like([{method: 'TokenPropertyDeleted'}]);
+ }
+ unsubscribe();
+ });
});
\ No newline at end of file