difftreelog
Merge branch 'develop' into feature/token_owners
in: master
15 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6320,13 +6320,16 @@
[[package]]
name = "pallet-refungible"
-version = "0.1.1"
+version = "0.1.2"
dependencies = [
+ "ethereum",
+ "evm-coder",
"frame-benchmarking",
"frame-support",
"frame-system",
"pallet-common",
"pallet-evm",
+ "pallet-evm-coder-substrate",
"pallet-structure",
"parity-scale-codec 3.1.5",
"scale-info",
Makefilediffbeforeafterboth--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,10 @@
NONFUNGIBLE_EVM_STUBS=./pallets/nonfungible/src/stubs
NONFUNGIBLE_EVM_ABI=./tests/src/eth/nonFungibleAbi.json
+REFUNGIBLE_EVM_STUBS=./pallets/refungible/src/stubs
+RENFUNGIBLE_EVM_ABI=./tests/src/eth/reFungibleAbi.json
+RENFUNGIBLE_TOKEN_EVM_ABI=./tests/src/eth/reFungibleTokenAbi.json
+
CONTRACT_HELPERS_STUBS=./pallets/evm-contract-helpers/src/stubs/
CONTRACT_HELPERS_ABI=./tests/src/eth/util/contractHelpersAbi.json
@@ -21,7 +25,7 @@
TESTS_API=./tests/src/eth/api/
.PHONY: regenerate_solidity
-regenerate_solidity: UniqueFungible.sol UniqueNFT.sol ContractHelpers.sol CollectionHelpers.sol
+regenerate_solidity: UniqueFungible.sol UniqueNFT.sol UniqueRefungibleToken.sol ContractHelpers.sol CollectionHelpers.sol
UniqueFungible.sol:
PACKAGE=pallet-fungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
@@ -30,6 +34,10 @@
UniqueNFT.sol:
PACKAGE=pallet-nonfungible NAME=erc::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
PACKAGE=pallet-nonfungible NAME=erc::gen_impl OUTPUT=$(NONFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
+
+UniqueRefungibleToken.sol:
+ PACKAGE=pallet-refungible NAME=erc_token::gen_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
+ PACKAGE=pallet-refungible NAME=erc_token::gen_impl OUTPUT=$(REFUNGIBLE_EVM_STUBS)/$@ ./.maintain/scripts/generate_sol.sh
ContractHelpers.sol:
PACKAGE=pallet-evm-contract-helpers NAME=eth::contract_helpers_iface OUTPUT=$(TESTS_API)/$@ ./.maintain/scripts/generate_sol.sh
@@ -47,6 +55,10 @@
INPUT=$(NONFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NONFUNGIBLE_EVM_STUBS)/UniqueNFT.raw ./.maintain/scripts/compile_stub.sh
INPUT=$(NONFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(NONFUNGIBLE_EVM_ABI) ./.maintain/scripts/generate_abi.sh
+UniqueRefungibleToken: UniqueRefungibleToken.sol
+ INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(REFUNGIBLE_EVM_STUBS)/UniqueRefungibleToken.raw ./.maintain/scripts/compile_stub.sh
+ INPUT=$(REFUNGIBLE_EVM_STUBS)/$< OUTPUT=$(RENFUNGIBLE_TOKEN_EVM_ABI) ./.maintain/scripts/generate_abi.sh
+
ContractHelpers: ContractHelpers.sol
INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_STUBS)/ContractHelpers.raw ./.maintain/scripts/compile_stub.sh
INPUT=$(CONTRACT_HELPERS_STUBS)/$< OUTPUT=$(CONTRACT_HELPERS_ABI) ./.maintain/scripts/generate_abi.sh
@@ -55,7 +67,7 @@
INPUT=$(COLLECTION_HELPER_STUBS)/$< OUTPUT=$(COLLECTION_HELPER_STUBS)/CollectionHelpers.raw ./.maintain/scripts/compile_stub.sh
INPUT=$(COLLECTION_HELPER_STUBS)/$< OUTPUT=$(COLLECTION_HELPER_ABI) ./.maintain/scripts/generate_abi.sh
-evm_stubs: UniqueFungible UniqueNFT ContractHelpers CollectionHelpers
+evm_stubs: UniqueFungible UniqueNFT UniqueRefungibleToken ContractHelpers CollectionHelpers
.PHONY: _bench
_bench:
pallets/refungible/CHANGELOG.mddiffbeforeafterboth--- a/pallets/refungible/CHANGELOG.md
+++ b/pallets/refungible/CHANGELOG.md
@@ -2,12 +2,17 @@
All notable changes to this project will be documented in this file.
-## [0.1.1] - 2022-07-14
+## [v0.1.2] - 2022-07-14
-### Added
+### Other changes
- - Implementation of RPC method `token_owners` returning 10 owners in no particular order.
- This was an internal request to improve the web interface and support fractionalization event.
+feat(refungible-pallet): add ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
+test(refungible-pallet): add tests for ERC-20 EVM API for RFT token pieces ([#413](https://github.com/UniqueNetwork/unique-chain/pull/413))
+
+## [v0.1.1] - 2022-07-14
+
+### Other changes
+
+- feat: RPC method `token_owners` returning 10 owners in no particular order.
-
-
\ No newline at end of file
+This was an internal request to improve the web interface and support fractionalization event.
pallets/refungible/Cargo.tomldiffbeforeafterboth--- a/pallets/refungible/Cargo.toml
+++ b/pallets/refungible/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pallet-refungible"
-version = "0.1.1"
+version = "0.1.2"
license = "GPLv3"
edition = "2021"
@@ -11,36 +11,43 @@
version = '3.1.2'
[dependencies]
-frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.24" }
+evm-coder = { default-features = false, path = '../../crates/evm-coder' }
+pallet-evm-coder-substrate = { default-features = false, path = '../../pallets/evm-coder-substrate' }
pallet-common = { default-features = false, path = '../common' }
pallet-structure = { default-features = false, path = '../structure' }
+struct-versioning = { path = "../../crates/struct-versioning" }
up-data-structs = { default-features = false, path = '../../primitives/data-structs' }
+
+ethereum = { version = "0.12.0", default-features = false }
+scale-info = { version = "2.0.1", default-features = false, features = ["derive",] }
+
frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
-scale-info = { version = "2.0.1", default-features = false, features = [
- "derive",
-] }
-struct-versioning = { path = "../../crates/struct-versioning" }
+frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.24" }
+sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
+sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.24" }
[features]
default = ["std"]
std = [
+ "ethereum/std",
+ "evm-coder/std",
+ 'frame-benchmarking/std',
"frame-support/std",
"frame-system/std",
+ "pallet-common/std",
+ "pallet-evm/std",
+ "pallet-evm-coder-substrate/std",
+ "pallet-structure/std",
"sp-runtime/std",
"sp-std/std",
"up-data-structs/std",
- "pallet-common/std",
- "pallet-structure/std",
- 'frame-benchmarking/std',
- "pallet-evm/std",
]
runtime-benchmarks = [
'frame-benchmarking',
'frame-support/runtime-benchmarks',
'frame-system/runtime-benchmarks',
+ 'up-data-structs/runtime-benchmarks',
]
pallets/refungible/src/erc.rsdiffbeforeafterboth--- a/pallets/refungible/src/erc.rs
+++ b/pallets/refungible/src/erc.rs
@@ -14,34 +14,35 @@
// You should have received a copy of the GNU General Public License
// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
-use up_data_structs::TokenId;
-use pallet_common::erc::CommonEvmHandler;
-use pallet_evm::PrecompileHandle;
+extern crate alloc;
+use evm_coder::{generate_stubgen, solidity_interface, types::*};
-use crate::{Config, RefungibleHandle};
+use pallet_common::{CollectionHandle, erc::CollectionCall, erc::CommonEvmHandler};
-impl<T: Config> CommonEvmHandler for RefungibleHandle<T> {
- const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");
+use pallet_evm::PrecompileHandle;
+use pallet_evm_coder_substrate::call;
- fn call(
- self,
- _handle: &mut impl PrecompileHandle,
- ) -> Option<pallet_common::erc::PrecompileResult> {
- // TODO: Implement RFT variant of ERC721
- None
- }
-}
+use crate::{Config, RefungibleHandle};
-pub struct RefungibleTokenHandle<T: Config>(pub RefungibleHandle<T>, pub TokenId);
+#[solidity_interface(
+ name = "UniqueRFT",
+ is(via("CollectionHandle<T>", common_mut, Collection),)
+)]
+impl<T: Config> RefungibleHandle<T> where T::AccountId: From<[u8; 32]> {}
-impl<T: Config> CommonEvmHandler for RefungibleTokenHandle<T> {
- const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungibleToken.raw");
+// Not a tests, but code generators
+generate_stubgen!(gen_impl, UniqueRFTCall<()>, true);
+generate_stubgen!(gen_iface, UniqueRFTCall<()>, false);
+impl<T: Config> CommonEvmHandler for RefungibleHandle<T>
+where
+ T::AccountId: From<[u8; 32]>,
+{
+ const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungible.raw");
fn call(
self,
- _handle: &mut impl PrecompileHandle,
+ handle: &mut impl PrecompileHandle,
) -> Option<pallet_common::erc::PrecompileResult> {
- // TODO: Implement RFT variant of ERC20
- None
+ call::<T, UniqueRFTCall<T>, _, _>(handle, self)
}
}
pallets/refungible/src/erc_token.rsdiffbeforeafterboth--- /dev/null
+++ b/pallets/refungible/src/erc_token.rs
@@ -0,0 +1,257 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+//! # Refungible Pallet EVM API for token pieces
+//!
+//! Provides ERC-20 standart support implementation and EVM API for unique extensions for Refungible Pallet.
+//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.
+
+extern crate alloc;
+use core::{
+ char::{REPLACEMENT_CHARACTER, decode_utf16},
+ convert::TryInto,
+ ops::Deref,
+};
+use evm_coder::{ToLog, execution::*, generate_stubgen, solidity_interface, types::*, weight};
+use pallet_common::{
+ CommonWeightInfo,
+ erc::{CommonEvmHandler, PrecompileResult},
+};
+use pallet_evm::{account::CrossAccountId, PrecompileHandle};
+use pallet_evm_coder_substrate::{call, dispatch_to_evm, WithRecorder};
+use pallet_structure::{SelfWeightOf as StructureWeight, weights::WeightInfo as _};
+use sp_std::vec::Vec;
+use up_data_structs::TokenId;
+
+use crate::{
+ Allowance, Balance, common::CommonWeights, Config, Pallet, RefungibleHandle, SelfWeightOf,
+ weights::WeightInfo, TotalSupply,
+};
+
+pub struct RefungibleTokenHandle<T: Config>(pub RefungibleHandle<T>, pub TokenId);
+
+#[derive(ToLog)]
+pub enum ERC20Events {
+ /// @dev This event is emitted when the amount of tokens (value) is sent
+ /// from the from address to the to address. In the case of minting new
+ /// tokens, the transfer is usually from the 0 address while in the case
+ /// of burning tokens the transfer is to 0.
+ Transfer {
+ #[indexed]
+ from: address,
+ #[indexed]
+ to: address,
+ value: uint256,
+ },
+ /// @dev This event is emitted when the amount of tokens (value) is approved
+ /// by the owner to be used by the spender.
+ Approval {
+ #[indexed]
+ owner: address,
+ #[indexed]
+ spender: address,
+ value: uint256,
+ },
+}
+
+/// @title Standard ERC20 token
+///
+/// @dev Implementation of the basic standard token.
+/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md
+#[solidity_interface(name = "ERC20", events(ERC20Events))]
+impl<T: Config> RefungibleTokenHandle<T> {
+ /// @return the name of the token.
+ fn name(&self) -> Result<string> {
+ Ok(decode_utf16(self.name.iter().copied())
+ .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
+ .collect::<string>())
+ }
+
+ /// @return the symbol of the token.
+ fn symbol(&self) -> Result<string> {
+ Ok(string::from_utf8_lossy(&self.token_prefix).into())
+ }
+
+ /// @dev Total number of tokens in existence
+ fn total_supply(&self) -> Result<uint256> {
+ self.consume_store_reads(1)?;
+ Ok(<TotalSupply<T>>::get((self.id, self.1)).into())
+ }
+
+ /// @dev Not supported
+ fn decimals(&self) -> Result<uint8> {
+ // Decimals aren't supported for refungible tokens
+ Ok(0)
+ }
+
+ /// @dev Gets the balance of the specified address.
+ /// @param owner The address to query the balance of.
+ /// @return An uint256 representing the amount owned by the passed address.
+ fn balance_of(&self, owner: address) -> Result<uint256> {
+ self.consume_store_reads(1)?;
+ let owner = T::CrossAccountId::from_eth(owner);
+ let balance = <Balance<T>>::get((self.id, self.1, owner));
+ Ok(balance.into())
+ }
+
+ /// @dev Transfer token for a specified address
+ /// @param to The address to transfer to.
+ /// @param amount The amount to be transferred.
+ #[weight(<CommonWeights<T>>::transfer())]
+ fn transfer(&mut self, caller: caller, to: address, amount: uint256) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let to = T::CrossAccountId::from_eth(to);
+ let amount = amount.try_into().map_err(|_| "amount overflow")?;
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+ <Pallet<T>>::transfer(self, &caller, &to, self.1, amount, &budget)
+ .map_err(|_| "transfer error")?;
+ Ok(true)
+ }
+
+ /// @dev Transfer tokens from one address to another
+ /// @param from address The address which you want to send tokens from
+ /// @param to address The address which you want to transfer to
+ /// @param amount uint256 the amount of tokens to be transferred
+ #[weight(<CommonWeights<T>>::transfer_from())]
+ fn transfer_from(
+ &mut self,
+ caller: caller,
+ from: address,
+ to: address,
+ amount: uint256,
+ ) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let from = T::CrossAccountId::from_eth(from);
+ let to = T::CrossAccountId::from_eth(to);
+ let amount = amount.try_into().map_err(|_| "amount overflow")?;
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+ <Pallet<T>>::transfer_from(self, &caller, &from, &to, self.1, amount, &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+
+ /// @dev Approve the passed address to spend the specified amount of tokens on behalf of `msg.sender`.
+ /// Beware that changing an allowance with this method brings the risk that someone may use both the old
+ /// and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
+ /// race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
+ /// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
+ /// @param spender The address which will spend the funds.
+ /// @param amount The amount of tokens to be spent.
+ #[weight(<SelfWeightOf<T>>::approve())]
+ fn approve(&mut self, caller: caller, spender: address, amount: uint256) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let spender = T::CrossAccountId::from_eth(spender);
+ let amount = amount.try_into().map_err(|_| "amount overflow")?;
+
+ <Pallet<T>>::set_allowance(self, &caller, &spender, self.1, amount)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+
+ /// @dev Function to check the amount of tokens that an owner allowed to a spender.
+ /// @param owner address The address which owns the funds.
+ /// @param spender address The address which will spend the funds.
+ /// @return A uint256 specifying the amount of tokens still available for the spender.
+ fn allowance(&self, owner: address, spender: address) -> Result<uint256> {
+ self.consume_store_reads(1)?;
+ let owner = T::CrossAccountId::from_eth(owner);
+ let spender = T::CrossAccountId::from_eth(spender);
+
+ Ok(<Allowance<T>>::get((self.id, self.1, owner, spender)).into())
+ }
+}
+
+#[solidity_interface(name = "ERC20UniqueExtensions")]
+impl<T: Config> RefungibleTokenHandle<T> {
+ /// @dev Function that burns an amount of the token of a given account,
+ /// deducting from the sender's allowance for said account.
+ /// @param from The account whose tokens will be burnt.
+ /// @param amount The amount that will be burnt.
+ #[weight(<SelfWeightOf<T>>::burn_from())]
+ fn burn_from(&mut self, caller: caller, from: address, amount: uint256) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let from = T::CrossAccountId::from_eth(from);
+ let amount = amount.try_into().map_err(|_| "amount overflow")?;
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+
+ <Pallet<T>>::burn_from(self, &caller, &from, self.1, amount, &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+
+ /// @dev Function that changes total amount of the tokens.
+ /// Throws if `msg.sender` doesn't owns all of the tokens.
+ /// @param amount New total amount of the tokens.
+ #[weight(<SelfWeightOf<T>>::repartition_item())]
+ fn repartition(&mut self, caller: caller, amount: uint256) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let amount = amount.try_into().map_err(|_| "amount overflow")?;
+
+ <Pallet<T>>::repartition(self, &caller, self.1, amount).map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+}
+
+impl<T: Config> RefungibleTokenHandle<T> {
+ pub fn into_inner(self) -> RefungibleHandle<T> {
+ self.0
+ }
+ pub fn common_mut(&mut self) -> &mut RefungibleHandle<T> {
+ &mut self.0
+ }
+}
+
+impl<T: Config> WithRecorder<T> for RefungibleTokenHandle<T> {
+ fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {
+ self.0.recorder()
+ }
+ fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {
+ self.0.into_recorder()
+ }
+}
+
+impl<T: Config> Deref for RefungibleTokenHandle<T> {
+ type Target = RefungibleHandle<T>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+#[solidity_interface(name = "UniqueRefungibleToken", is(ERC20, ERC20UniqueExtensions,))]
+impl<T: Config> RefungibleTokenHandle<T> where T::AccountId: From<[u8; 32]> {}
+
+generate_stubgen!(gen_impl, UniqueRefungibleTokenCall<()>, true);
+generate_stubgen!(gen_iface, UniqueRefungibleTokenCall<()>, false);
+
+impl<T: Config> CommonEvmHandler for RefungibleTokenHandle<T>
+where
+ T::AccountId: From<[u8; 32]>,
+{
+ const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungibleToken.raw");
+
+ fn call(self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
+ call::<T, UniqueRefungibleTokenCall<T>, _, _>(handle, self)
+ }
+}
pallets/refungible/src/lib.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//! # Refungible Pallet18//!19//! The Refungible pallet provides functionality for handling refungible collections and tokens.20//!21//! - [`Config`]22//! - [`RefungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`]25//!26//! ## Overview27//!28//! The Refungible pallet provides functions for:29//!30//! - RFT collection creation and removal31//! - Minting and burning of RFT tokens32//! - Partition and repartition of RFT tokens33//! - Retrieving number of pieces of RFT token34//! - Retrieving account balances35//! - Transfering RFT token pieces36//! - Burning RFT token pieces37//! - Setting and checking allowance for RFT tokens38//!39//! ### Terminology40//!41//! - **RFT token:** Non fungible token that was partitioned to pieces. If an account owns all42//! of the RFT token pieces than it owns the RFT token and can repartition it.43//!44//! - **RFT Collection:** A collection of RFT tokens. All RFT tokens are part of a collection.45//! Each collection has its own settings and set of permissions.46//!47//! - **RFT token piece:** A fungible part of an RFT token.48//!49//! - **Balance:** RFT token pieces owned by an account50//!51//! - **Allowance:** Maximum number of RFT token pieces that one account is allowed to52//! transfer from the balance of another account53//!54//! - **Burning:** The process of “deleting” a token from a collection or removing token pieces from55//! an account balance.56//!57//! ### Implementations58//!59//! The Refungible pallet provides implementations for the following traits. If these traits provide60//! the functionality that you need, then you can avoid coupling with the Refungible pallet.61//!62//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight63//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing64//! with collections65//! - [`RefungibleExtensions`](pallet_common::RefungibleExtensions): Functions specific for refungible66//! collection67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create RFT collection. RFT collection can be configured to allow or deny access for73//! some accounts.74//! - `destroy_collection` - Destroy exising RFT collection. There should be no tokens in the collection.75//! - `burn` - Burn some amount of RFT token pieces owned by account. Burns the RFT token if no pieces left.76//! - `transfer` - Transfer some amount of RFT token pieces. Transfers should be enabled for RFT collection.77//! Nests the RFT token if RFT token pieces are sent to another token.78//! - `create_item` - Mint RFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account to transfer balance from sender's account.80//! - `repartition` - Repartition token to selected number of pieces. Sender should own all existing pieces.81//!82//! ## Assumptions83//!84//! * Total number of pieces for one token shouldn't exceed `up_data_structs::MAX_REFUNGIBLE_PIECES`.85//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`.86//! * Sender should be in collection's allow list to perform operations on tokens.8788#![cfg_attr(not(feature = "std"), no_std)]8990use frame_support::{ensure, fail, BoundedVec, transactional, storage::with_transaction};91use up_data_structs::{92 AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId,93 CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget,94 Property, PropertyScope, TrySetProperty, PropertyKey, PropertyValue, PropertyPermission,95 PropertyKeyPermission,96};97use pallet_evm::account::CrossAccountId;98use pallet_common::{99 Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,100 CommonCollectionOperations as _,101};102use pallet_structure::Pallet as PalletStructure;103use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};104use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};105use core::ops::Deref;106use codec::{Encode, Decode, MaxEncodedLen};107use scale_info::TypeInfo;108109pub use pallet::*;110#[cfg(feature = "runtime-benchmarks")]111pub mod benchmarking;112pub mod common;113pub mod erc;114pub mod weights;115pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;116117#[struct_versioning::versioned(version = 2, upper)]118#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]119pub struct ItemData {120 pub const_data: BoundedVec<u8, CustomDataLimit>,121122 #[version(..2)]123 pub variable_data: BoundedVec<u8, CustomDataLimit>,124}125126#[frame_support::pallet]127pub mod pallet {128 use super::*;129 use frame_support::{130 Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,131 traits::StorageVersion,132 };133 use frame_system::pallet_prelude::*;134 use up_data_structs::{CollectionId, TokenId};135 use super::weights::WeightInfo;136137 #[pallet::error]138 pub enum Error<T> {139 /// Not Refungible item data used to mint in Refungible collection.140 NotRefungibleDataUsedToMintFungibleCollectionToken,141 /// Maximum refungibility exceeded142 WrongRefungiblePieces,143 /// Refungible token can't be repartitioned by user who isn't owns all pieces144 RepartitionWhileNotOwningAllPieces,145 /// Refungible token can't nest other tokens146 RefungibleDisallowsNesting,147 /// Setting item properties is not allowed148 SettingPropertiesNotAllowed,149 }150151 #[pallet::config]152 pub trait Config:153 frame_system::Config + pallet_common::Config + pallet_structure::Config154 {155 type WeightInfo: WeightInfo;156 }157158 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);159160 #[pallet::pallet]161 #[pallet::storage_version(STORAGE_VERSION)]162 #[pallet::generate_store(pub(super) trait Store)]163 pub struct Pallet<T>(_);164165 /// Amount of tokens minted for collection166 #[pallet::storage]167 pub type TokensMinted<T: Config> =168 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;169170 /// Amount of burnt tokens for collection171 #[pallet::storage]172 pub type TokensBurnt<T: Config> =173 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;174175 /// Custom data serialized to bytes for token176 #[pallet::storage]177 pub type TokenData<T: Config> = StorageNMap<178 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),179 Value = ItemData,180 QueryKind = ValueQuery,181 >;182183 #[pallet::storage]184 #[pallet::getter(fn token_properties)]185 pub type TokenProperties<T: Config> = StorageNMap<186 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),187 Value = up_data_structs::Properties,188 QueryKind = ValueQuery,189 OnEmpty = up_data_structs::TokenProperties,190 >;191192 /// Total amount of pieces for token193 #[pallet::storage]194 pub type TotalSupply<T: Config> = StorageNMap<195 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),196 Value = u128,197 QueryKind = ValueQuery,198 >;199200 /// Used to enumerate tokens owned by account201 #[pallet::storage]202 pub type Owned<T: Config> = StorageNMap<203 Key = (204 Key<Twox64Concat, CollectionId>,205 Key<Blake2_128Concat, T::CrossAccountId>,206 Key<Twox64Concat, TokenId>,207 ),208 Value = bool,209 QueryKind = ValueQuery,210 >;211212 /// Amount of tokens owned by account213 #[pallet::storage]214 pub type AccountBalance<T: Config> = StorageNMap<215 Key = (216 Key<Twox64Concat, CollectionId>,217 // Owner218 Key<Blake2_128Concat, T::CrossAccountId>,219 ),220 Value = u32,221 QueryKind = ValueQuery,222 >;223224 /// Amount of token pieces owned by account225 #[pallet::storage]226 pub type Balance<T: Config> = StorageNMap<227 Key = (228 Key<Twox64Concat, CollectionId>,229 Key<Twox64Concat, TokenId>,230 // Owner231 Key<Blake2_128Concat, T::CrossAccountId>,232 ),233 Value = u128,234 QueryKind = ValueQuery,235 >;236237 /// Allowance set by an owner for a spender for a token238 #[pallet::storage]239 pub type Allowance<T: Config> = StorageNMap<240 Key = (241 Key<Twox64Concat, CollectionId>,242 Key<Twox64Concat, TokenId>,243 // Owner244 Key<Blake2_128, T::CrossAccountId>,245 // Spender246 Key<Blake2_128Concat, T::CrossAccountId>,247 ),248 Value = u128,249 QueryKind = ValueQuery,250 >;251252 #[pallet::hooks]253 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {254 fn on_runtime_upgrade() -> Weight {255 if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {256 <TokenData<T>>::translate_values::<ItemDataVersion1, _>(|v| {257 Some(<ItemDataVersion2>::from(v))258 })259 }260261 0262 }263 }264}265266pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);267impl<T: Config> RefungibleHandle<T> {268 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {269 Self(inner)270 }271 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {272 self.0273 }274}275impl<T: Config> Deref for RefungibleHandle<T> {276 type Target = pallet_common::CollectionHandle<T>;277278 fn deref(&self) -> &Self::Target {279 &self.0280 }281}282283impl<T: Config> Pallet<T> {284 /// Get number of RFT tokens in collection285 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {286 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)287 }288289 /// Check that RFT token exists290 ///291 /// - `token`: Token ID.292 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {293 <TotalSupply<T>>::contains_key((collection.id, token))294 }295296 pub fn set_scoped_token_property(297 collection_id: CollectionId,298 token_id: TokenId,299 scope: PropertyScope,300 property: Property,301 ) -> DispatchResult {302 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {303 properties.try_scoped_set(scope, property.key, property.value)304 })305 .map_err(<CommonError<T>>::from)?;306307 Ok(())308 }309310 pub fn set_scoped_token_properties(311 collection_id: CollectionId,312 token_id: TokenId,313 scope: PropertyScope,314 properties: impl Iterator<Item = Property>,315 ) -> DispatchResult {316 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {317 stored_properties.try_scoped_set_from_iter(scope, properties)318 })319 .map_err(<CommonError<T>>::from)?;320321 Ok(())322 }323}324325// unchecked calls skips any permission checks326impl<T: Config> Pallet<T> {327 /// Create RFT collection328 ///329 /// `init_collection` will take non-refundable deposit for collection creation.330 ///331 /// - `data`: Contains settings for collection limits and permissions.332 pub fn init_collection(333 owner: T::CrossAccountId,334 data: CreateCollectionData<T::AccountId>,335 ) -> Result<CollectionId, DispatchError> {336 <PalletCommon<T>>::init_collection(owner, data, false)337 }338339 /// Destroy RFT collection340 ///341 /// `destroy_collection` will throw error if collection contains any tokens.342 /// Only owner can destroy collection.343 pub fn destroy_collection(344 collection: RefungibleHandle<T>,345 sender: &T::CrossAccountId,346 ) -> DispatchResult {347 let id = collection.id;348349 if Self::collection_has_tokens(id) {350 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());351 }352353 // =========354355 PalletCommon::destroy_collection(collection.0, sender)?;356357 <TokensMinted<T>>::remove(id);358 <TokensBurnt<T>>::remove(id);359 <TokenData<T>>::remove_prefix((id,), None);360 <TotalSupply<T>>::remove_prefix((id,), None);361 <Balance<T>>::remove_prefix((id,), None);362 <Allowance<T>>::remove_prefix((id,), None);363 <Owned<T>>::remove_prefix((id,), None);364 <AccountBalance<T>>::remove_prefix((id,), None);365 Ok(())366 }367368 fn collection_has_tokens(collection_id: CollectionId) -> bool {369 <TokenData<T>>::iter_prefix((collection_id,))370 .next()371 .is_some()372 }373374 pub fn burn_token_unchecked(375 collection: &RefungibleHandle<T>,376 token_id: TokenId,377 ) -> DispatchResult {378 let burnt = <TokensBurnt<T>>::get(collection.id)379 .checked_add(1)380 .ok_or(ArithmeticError::Overflow)?;381382 <TokensBurnt<T>>::insert(collection.id, burnt);383 <TokenData<T>>::remove((collection.id, token_id));384 <TokenProperties<T>>::remove((collection.id, token_id));385 <TotalSupply<T>>::remove((collection.id, token_id));386 <Balance<T>>::remove_prefix((collection.id, token_id), None);387 <Allowance<T>>::remove_prefix((collection.id, token_id), None);388 // TODO: ERC721 transfer event389 Ok(())390 }391392 /// Burn RFT token pieces393 ///394 /// `burn` will decrease total amount of token pieces and amount owned by sender.395 /// `burn` can be called even if there are multiple owners of the RFT token.396 /// If sender wouldn't have any pieces left after `burn` than she will stop being397 /// one of the owners of the token. If there is no account that owns any pieces of398 /// the token than token will be burned too.399 ///400 /// - `amount`: Amount of token pieces to burn.401 /// - `token`: Token who's pieces should be burned402 /// - `collection`: Collection that contains the token403 pub fn burn(404 collection: &RefungibleHandle<T>,405 owner: &T::CrossAccountId,406 token: TokenId,407 amount: u128,408 ) -> DispatchResult {409 let total_supply = <TotalSupply<T>>::get((collection.id, token))410 .checked_sub(amount)411 .ok_or(<CommonError<T>>::TokenValueTooLow)?;412413 // This was probally last owner of this token?414 if total_supply == 0 {415 // Ensure user actually owns this amount416 ensure!(417 <Balance<T>>::get((collection.id, token, owner)) == amount,418 <CommonError<T>>::TokenValueTooLow419 );420 let account_balance = <AccountBalance<T>>::get((collection.id, owner))421 .checked_sub(1)422 // Should not occur423 .ok_or(ArithmeticError::Underflow)?;424425 // =========426427 <Owned<T>>::remove((collection.id, owner, token));428 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);429 <AccountBalance<T>>::insert((collection.id, owner), account_balance);430 Self::burn_token_unchecked(collection, token)?;431 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(432 collection.id,433 token,434 owner.clone(),435 amount,436 ));437 return Ok(());438 }439440 let balance = <Balance<T>>::get((collection.id, token, owner))441 .checked_sub(amount)442 .ok_or(<CommonError<T>>::TokenValueTooLow)?;443 let account_balance = if balance == 0 {444 <AccountBalance<T>>::get((collection.id, owner))445 .checked_sub(1)446 // Should not occur447 .ok_or(ArithmeticError::Underflow)?448 } else {449 0450 };451452 // =========453454 if balance == 0 {455 <Owned<T>>::remove((collection.id, owner, token));456 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);457 <Balance<T>>::remove((collection.id, token, owner));458 <AccountBalance<T>>::insert((collection.id, owner), account_balance);459 } else {460 <Balance<T>>::insert((collection.id, token, owner), balance);461 }462 <TotalSupply<T>>::insert((collection.id, token), total_supply);463 // TODO: ERC20 transfer event464 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(465 collection.id,466 token,467 owner.clone(),468 amount,469 ));470 Ok(())471 }472473 #[transactional]474 fn modify_token_properties(475 collection: &RefungibleHandle<T>,476 sender: &T::CrossAccountId,477 token_id: TokenId,478 properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,479 is_token_create: bool,480 nesting_budget: &dyn Budget,481 ) -> DispatchResult {482 let is_collection_admin = || collection.is_owner_or_admin(sender);483 let is_token_owner = || -> Result<bool, DispatchError> {484 let balance = collection.balance(sender.clone(), token_id);485 let total_pieces: u128 =486 Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);487 if balance != total_pieces {488 return Ok(false);489 }490491 let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(492 sender.clone(),493 collection.id,494 token_id,495 None,496 nesting_budget,497 )?;498499 Ok(is_bundle_owner)500 };501502 for (key, value) in properties {503 let permission = <PalletCommon<T>>::property_permissions(collection.id)504 .get(&key)505 .cloned()506 .unwrap_or_else(PropertyPermission::none);507508 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))509 .get(&key)510 .is_some();511512 match permission {513 PropertyPermission { mutable: false, .. } if is_property_exists => {514 return Err(<CommonError<T>>::NoPermission.into());515 }516517 PropertyPermission {518 collection_admin,519 token_owner,520 ..521 } => {522 //TODO: investigate threats during public minting.523 let is_token_create =524 is_token_create && (collection_admin || token_owner) && value.is_some();525 if !(is_token_create526 || (collection_admin && is_collection_admin())527 || (token_owner && is_token_owner()?))528 {529 fail!(<CommonError<T>>::NoPermission);530 }531 }532 }533534 match value {535 Some(value) => {536 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {537 properties.try_set(key.clone(), value)538 })539 .map_err(<CommonError<T>>::from)?;540541 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(542 collection.id,543 token_id,544 key,545 ));546 }547 None => {548 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {549 properties.remove(&key)550 })551 .map_err(<CommonError<T>>::from)?;552553 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(554 collection.id,555 token_id,556 key,557 ));558 }559 }560 }561562 Ok(())563 }564565 pub fn set_token_properties(566 collection: &RefungibleHandle<T>,567 sender: &T::CrossAccountId,568 token_id: TokenId,569 properties: impl Iterator<Item = Property>,570 is_token_create: bool,571 nesting_budget: &dyn Budget,572 ) -> DispatchResult {573 Self::modify_token_properties(574 collection,575 sender,576 token_id,577 properties.map(|p| (p.key, Some(p.value))),578 is_token_create,579 nesting_budget,580 )581 }582583 pub fn set_token_property(584 collection: &RefungibleHandle<T>,585 sender: &T::CrossAccountId,586 token_id: TokenId,587 property: Property,588 nesting_budget: &dyn Budget,589 ) -> DispatchResult {590 let is_token_create = false;591592 Self::set_token_properties(593 collection,594 sender,595 token_id,596 [property].into_iter(),597 is_token_create,598 nesting_budget,599 )600 }601602 pub fn delete_token_properties(603 collection: &RefungibleHandle<T>,604 sender: &T::CrossAccountId,605 token_id: TokenId,606 property_keys: impl Iterator<Item = PropertyKey>,607 nesting_budget: &dyn Budget,608 ) -> DispatchResult {609 let is_token_create = false;610611 Self::modify_token_properties(612 collection,613 sender,614 token_id,615 property_keys.into_iter().map(|key| (key, None)),616 is_token_create,617 nesting_budget,618 )619 }620621 pub fn delete_token_property(622 collection: &RefungibleHandle<T>,623 sender: &T::CrossAccountId,624 token_id: TokenId,625 property_key: PropertyKey,626 nesting_budget: &dyn Budget,627 ) -> DispatchResult {628 Self::delete_token_properties(629 collection,630 sender,631 token_id,632 [property_key].into_iter(),633 nesting_budget,634 )635 }636637 /// Transfer RFT token pieces from one account to another.638 ///639 /// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.640 ///641 /// - `from`: Owner of token pieces to transfer.642 /// - `to`: Recepient of transfered token pieces.643 /// - `amount`: Amount of token pieces to transfer.644 /// - `token`: Token whos pieces should be transfered645 /// - `collection`: Collection that contains the token646 pub fn transfer(647 collection: &RefungibleHandle<T>,648 from: &T::CrossAccountId,649 to: &T::CrossAccountId,650 token: TokenId,651 amount: u128,652 nesting_budget: &dyn Budget,653 ) -> DispatchResult {654 ensure!(655 collection.limits.transfers_enabled(),656 <CommonError<T>>::TransferNotAllowed657 );658659 if collection.permissions.access() == AccessMode::AllowList {660 collection.check_allowlist(from)?;661 collection.check_allowlist(to)?;662 }663 <PalletCommon<T>>::ensure_correct_receiver(to)?;664665 let balance_from = <Balance<T>>::get((collection.id, token, from))666 .checked_sub(amount)667 .ok_or(<CommonError<T>>::TokenValueTooLow)?;668 let mut create_target = false;669 let from_to_differ = from != to;670 let balance_to = if from != to {671 let old_balance = <Balance<T>>::get((collection.id, token, to));672 if old_balance == 0 {673 create_target = true;674 }675 Some(676 old_balance677 .checked_add(amount)678 .ok_or(ArithmeticError::Overflow)?,679 )680 } else {681 None682 };683684 let account_balance_from = if balance_from == 0 {685 Some(686 <AccountBalance<T>>::get((collection.id, from))687 .checked_sub(1)688 // Should not occur689 .ok_or(ArithmeticError::Underflow)?,690 )691 } else {692 None693 };694 // Account data is created in token, AccountBalance should be increased695 // But only if from != to as we shouldn't check overflow in this case696 let account_balance_to = if create_target && from_to_differ {697 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))698 .checked_add(1)699 .ok_or(ArithmeticError::Overflow)?;700 ensure!(701 account_balance_to < collection.limits.account_token_ownership_limit(),702 <CommonError<T>>::AccountTokenLimitExceeded,703 );704705 Some(account_balance_to)706 } else {707 None708 };709710 // =========711712 <PalletStructure<T>>::nest_if_sent_to_token(713 from.clone(),714 to,715 collection.id,716 token,717 nesting_budget,718 )?;719720 if let Some(balance_to) = balance_to {721 // from != to722 if balance_from == 0 {723 <Balance<T>>::remove((collection.id, token, from));724 <PalletStructure<T>>::unnest_if_nested(from, collection.id, token);725 } else {726 <Balance<T>>::insert((collection.id, token, from), balance_from);727 }728 <Balance<T>>::insert((collection.id, token, to), balance_to);729 if let Some(account_balance_from) = account_balance_from {730 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);731 <Owned<T>>::remove((collection.id, from, token));732 }733 if let Some(account_balance_to) = account_balance_to {734 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);735 <Owned<T>>::insert((collection.id, to, token), true);736 }737 }738739 // TODO: ERC20 transfer event740 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(741 collection.id,742 token,743 from.clone(),744 to.clone(),745 amount,746 ));747 Ok(())748 }749750 /// Batched operation to create multiple RFT tokens.751 ///752 /// Same as `create_item` but creates multiple tokens.753 ///754 /// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.755 pub fn create_multiple_items(756 collection: &RefungibleHandle<T>,757 sender: &T::CrossAccountId,758 data: Vec<CreateRefungibleExData<T::CrossAccountId>>,759 nesting_budget: &dyn Budget,760 ) -> DispatchResult {761 if !collection.is_owner_or_admin(sender) {762 ensure!(763 collection.permissions.mint_mode(),764 <CommonError<T>>::PublicMintingNotAllowed765 );766 collection.check_allowlist(sender)?;767768 for item in data.iter() {769 for user in item.users.keys() {770 collection.check_allowlist(user)?;771 }772 }773 }774775 for item in data.iter() {776 for (owner, _) in item.users.iter() {777 <PalletCommon<T>>::ensure_correct_receiver(owner)?;778 }779 }780781 // Total pieces per tokens782 let totals = data783 .iter()784 .map(|data| {785 Ok(data786 .users787 .iter()788 .map(|u| u.1)789 .try_fold(0u128, |acc, v| acc.checked_add(*v))790 .ok_or(ArithmeticError::Overflow)?)791 })792 .collect::<Result<Vec<_>, DispatchError>>()?;793 for total in &totals {794 ensure!(795 *total <= MAX_REFUNGIBLE_PIECES,796 <Error<T>>::WrongRefungiblePieces797 );798 }799800 let first_token_id = <TokensMinted<T>>::get(collection.id);801 let tokens_minted = first_token_id802 .checked_add(data.len() as u32)803 .ok_or(ArithmeticError::Overflow)?;804 ensure!(805 tokens_minted < collection.limits.token_limit(),806 <CommonError<T>>::CollectionTokenLimitExceeded807 );808809 let mut balances = BTreeMap::new();810 for data in &data {811 for owner in data.users.keys() {812 let balance = balances813 .entry(owner)814 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));815 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;816817 ensure!(818 *balance <= collection.limits.account_token_ownership_limit(),819 <CommonError<T>>::AccountTokenLimitExceeded,820 );821 }822 }823824 for (i, token) in data.iter().enumerate() {825 let token_id = TokenId(first_token_id + i as u32 + 1);826 for (to, _) in token.users.iter() {827 <PalletStructure<T>>::check_nesting(828 sender.clone(),829 to,830 collection.id,831 token_id,832 nesting_budget,833 )?;834 }835 }836837 // =========838839 with_transaction(|| {840 for (i, data) in data.iter().enumerate() {841 let token_id = first_token_id + i as u32 + 1;842 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);843844 <TokenData<T>>::insert(845 (collection.id, token_id),846 ItemData {847 const_data: data.const_data.clone(),848 },849 );850851 for (user, amount) in data.users.iter() {852 if *amount == 0 {853 continue;854 }855 <Balance<T>>::insert((collection.id, token_id, &user), amount);856 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);857 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(858 user,859 collection.id,860 TokenId(token_id),861 );862 }863864 if let Err(e) = Self::set_token_properties(865 collection,866 sender,867 TokenId(token_id),868 data.properties.clone().into_iter(),869 true,870 nesting_budget,871 ) {872 return TransactionOutcome::Rollback(Err(e));873 }874 }875 TransactionOutcome::Commit(Ok(()))876 })?;877878 <TokensMinted<T>>::insert(collection.id, tokens_minted);879880 for (account, balance) in balances {881 <AccountBalance<T>>::insert((collection.id, account), balance);882 }883884 for (i, token) in data.into_iter().enumerate() {885 let token_id = first_token_id + i as u32 + 1;886887 for (user, amount) in token.users.into_iter() {888 if amount == 0 {889 continue;890 }891892 // TODO: ERC20 transfer event893 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(894 collection.id,895 TokenId(token_id),896 user,897 amount,898 ));899 }900 }901 Ok(())902 }903904 pub fn set_allowance_unchecked(905 collection: &RefungibleHandle<T>,906 sender: &T::CrossAccountId,907 spender: &T::CrossAccountId,908 token: TokenId,909 amount: u128,910 ) {911 if amount == 0 {912 <Allowance<T>>::remove((collection.id, token, sender, spender));913 } else {914 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);915 }916 // TODO: ERC20 approval event917 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(918 collection.id,919 token,920 sender.clone(),921 spender.clone(),922 amount,923 ))924 }925926 /// Set allowance for the spender to `transfer` or `burn` sender's token pieces.927 ///928 /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.929 pub fn set_allowance(930 collection: &RefungibleHandle<T>,931 sender: &T::CrossAccountId,932 spender: &T::CrossAccountId,933 token: TokenId,934 amount: u128,935 ) -> DispatchResult {936 if collection.permissions.access() == AccessMode::AllowList {937 collection.check_allowlist(sender)?;938 collection.check_allowlist(spender)?;939 }940941 <PalletCommon<T>>::ensure_correct_receiver(spender)?;942943 if <Balance<T>>::get((collection.id, token, sender)) < amount {944 ensure!(945 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),946 <CommonError<T>>::CantApproveMoreThanOwned947 );948 }949950 // =========951952 Self::set_allowance_unchecked(collection, sender, spender, token, amount);953 Ok(())954 }955956 /// Returns allowance, which should be set after transaction957 fn check_allowed(958 collection: &RefungibleHandle<T>,959 spender: &T::CrossAccountId,960 from: &T::CrossAccountId,961 token: TokenId,962 amount: u128,963 nesting_budget: &dyn Budget,964 ) -> Result<Option<u128>, DispatchError> {965 if spender.conv_eq(from) {966 return Ok(None);967 }968 if collection.permissions.access() == AccessMode::AllowList {969 // `from`, `to` checked in [`transfer`]970 collection.check_allowlist(spender)?;971 }972 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {973 // TODO: should collection owner be allowed to perform this transfer?974 ensure!(975 <PalletStructure<T>>::check_indirectly_owned(976 spender.clone(),977 source.0,978 source.1,979 None,980 nesting_budget981 )?,982 <CommonError<T>>::ApprovedValueTooLow,983 );984 return Ok(None);985 }986 let allowance =987 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);988 if allowance.is_none() {989 ensure!(990 collection.ignores_allowance(spender),991 <CommonError<T>>::ApprovedValueTooLow992 );993 }994 Ok(allowance)995 }996997 /// Transfer RFT token pieces from one account to another.998 ///999 /// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1000 /// The owner should set allowance for the spender to transfer pieces.1001 ///1002 /// [`transfer`]: struct.Pallet.html#method.transfer1003 pub fn transfer_from(1004 collection: &RefungibleHandle<T>,1005 spender: &T::CrossAccountId,1006 from: &T::CrossAccountId,1007 to: &T::CrossAccountId,1008 token: TokenId,1009 amount: u128,1010 nesting_budget: &dyn Budget,1011 ) -> DispatchResult {1012 let allowance =1013 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10141015 // =========10161017 Self::transfer(collection, from, to, token, amount, nesting_budget)?;1018 if let Some(allowance) = allowance {1019 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1020 }1021 Ok(())1022 }10231024 /// Burn RFT token pieces from the account.1025 ///1026 /// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1027 /// set allowance for the spender to burn pieces1028 ///1029 /// [`burn`]: struct.Pallet.html#method.burn1030 pub fn burn_from(1031 collection: &RefungibleHandle<T>,1032 spender: &T::CrossAccountId,1033 from: &T::CrossAccountId,1034 token: TokenId,1035 amount: u128,1036 nesting_budget: &dyn Budget,1037 ) -> DispatchResult {1038 let allowance =1039 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10401041 // =========10421043 Self::burn(collection, from, token, amount)?;1044 if let Some(allowance) = allowance {1045 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1046 }1047 Ok(())1048 }10491050 /// Create RFT token.1051 ///1052 /// The sender should be the owner/admin of the collection or collection should be configured1053 /// to allow public minting.1054 ///1055 /// - `data`: Contains list of users who will become the owners of the token pieces and amount1056 /// of token pieces they will receive.1057 pub fn create_item(1058 collection: &RefungibleHandle<T>,1059 sender: &T::CrossAccountId,1060 data: CreateRefungibleExData<T::CrossAccountId>,1061 nesting_budget: &dyn Budget,1062 ) -> DispatchResult {1063 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1064 }10651066 /// Repartition RFT token.1067 ///1068 /// `repartition` will set token balance of the sender and total amount of token pieces.1069 /// Sender should own all of the token pieces. `repartition' could be done even if some1070 /// token pieces were burned before.1071 ///1072 /// - `amount`: Total amount of token pieces that the token will have after `repartition`.1073 pub fn repartition(1074 collection: &RefungibleHandle<T>,1075 owner: &T::CrossAccountId,1076 token: TokenId,1077 amount: u128,1078 ) -> DispatchResult {1079 ensure!(1080 amount <= MAX_REFUNGIBLE_PIECES,1081 <Error<T>>::WrongRefungiblePieces1082 );1083 ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1084 // Ensure user owns all pieces1085 let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1086 let balance = <Balance<T>>::get((collection.id, token, owner));1087 ensure!(1088 total_pieces == balance,1089 <Error<T>>::RepartitionWhileNotOwningAllPieces1090 );10911092 <Balance<T>>::insert((collection.id, token, owner), amount);1093 <TotalSupply<T>>::insert((collection.id, token), amount);1094 Ok(())1095 }10961097 fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option<T::CrossAccountId> {1098 let mut owner = None;1099 let mut count = 0;1100 for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {1101 count += 1;1102 if count > 1 {1103 return None;1104 }1105 owner = Some(key);1106 }1107 owner1108 }11091110 fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1111 <TotalSupply<T>>::try_get((collection_id, token_id)).ok()1112 }11131114 pub fn set_collection_properties(1115 collection: &RefungibleHandle<T>,1116 sender: &T::CrossAccountId,1117 properties: Vec<Property>,1118 ) -> DispatchResult {1119 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)1120 }11211122 pub fn delete_collection_properties(1123 collection: &RefungibleHandle<T>,1124 sender: &T::CrossAccountId,1125 property_keys: Vec<PropertyKey>,1126 ) -> DispatchResult {1127 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)1128 }11291130 pub fn set_token_property_permissions(1131 collection: &RefungibleHandle<T>,1132 sender: &T::CrossAccountId,1133 property_permissions: Vec<PropertyKeyPermission>,1134 ) -> DispatchResult {1135 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1136 }11371138 /// Returns 10 token in no particular order.1139 ///1140 /// There is no direct way to get token holders in ascending order,1141 /// since `iter_prefix` returns values in no particular order.1142 /// Therefore, getting the 10 largest holders with a large value of holders1143 /// can lead to impact memory allocation + sorting with `n * log (n)`.1144 pub fn token_owners(1145 collection_id: CollectionId,1146 token: TokenId,1147 ) -> Option<Vec<T::CrossAccountId>> {1148 let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))1149 .map(|(owner, _amount)| owner)1150 .take(10)1151 .collect();11521153 if res.is_empty() {1154 None1155 } else {1156 Some(res)1157 }1158 }1159}1// 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//! # Refungible Pallet18//!19//! The Refungible pallet provides functionality for handling refungible collections and tokens.20//!21//! - [`Config`]22//! - [`RefungibleHandle`]23//! - [`Pallet`]24//! - [`CommonWeights`]25//!26//! ## Overview27//!28//! The Refungible pallet provides functions for:29//!30//! - RFT collection creation and removal31//! - Minting and burning of RFT tokens32//! - Partition and repartition of RFT tokens33//! - Retrieving number of pieces of RFT token34//! - Retrieving account balances35//! - Transfering RFT token pieces36//! - Burning RFT token pieces37//! - Setting and checking allowance for RFT tokens38//!39//! ### Terminology40//!41//! - **RFT token:** Non fungible token that was partitioned to pieces. If an account owns all42//! of the RFT token pieces than it owns the RFT token and can repartition it.43//!44//! - **RFT Collection:** A collection of RFT tokens. All RFT tokens are part of a collection.45//! Each collection has its own settings and set of permissions.46//!47//! - **RFT token piece:** A fungible part of an RFT token.48//!49//! - **Balance:** RFT token pieces owned by an account50//!51//! - **Allowance:** Maximum number of RFT token pieces that one account is allowed to52//! transfer from the balance of another account53//!54//! - **Burning:** The process of “deleting” a token from a collection or removing token pieces from55//! an account balance.56//!57//! ### Implementations58//!59//! The Refungible pallet provides implementations for the following traits. If these traits provide60//! the functionality that you need, then you can avoid coupling with the Refungible pallet.61//!62//! - [`CommonWeightInfo`](pallet_common::CommonWeightInfo): Functions for retrieval of transaction weight63//! - [`CommonCollectionOperations`](pallet_common::CommonCollectionOperations): Functions for dealing64//! with collections65//! - [`RefungibleExtensions`](pallet_common::RefungibleExtensions): Functions specific for refungible66//! collection67//!68//! ## Interface69//!70//! ### Dispatchable Functions71//!72//! - `init_collection` - Create RFT collection. RFT collection can be configured to allow or deny access for73//! some accounts.74//! - `destroy_collection` - Destroy exising RFT collection. There should be no tokens in the collection.75//! - `burn` - Burn some amount of RFT token pieces owned by account. Burns the RFT token if no pieces left.76//! - `transfer` - Transfer some amount of RFT token pieces. Transfers should be enabled for RFT collection.77//! Nests the RFT token if RFT token pieces are sent to another token.78//! - `create_item` - Mint RFT token in collection. Sender should have permission to mint tokens.79//! - `set_allowance` - Set allowance for another account to transfer balance from sender's account.80//! - `repartition` - Repartition token to selected number of pieces. Sender should own all existing pieces.81//!82//! ## Assumptions83//!84//! * Total number of pieces for one token shouldn't exceed `up_data_structs::MAX_REFUNGIBLE_PIECES`.85//! * Total number of tokens of all types shouldn't be greater than `up_data_structs::MAX_TOKEN_PREFIX_LENGTH`.86//! * Sender should be in collection's allow list to perform operations on tokens.8788#![cfg_attr(not(feature = "std"), no_std)]8990use crate::erc_token::ERC20Events;9192use codec::{Encode, Decode, MaxEncodedLen};93use core::ops::Deref;94use evm_coder::ToLog;95use frame_support::{BoundedVec, ensure, fail, storage::with_transaction, transactional};96use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm};97use pallet_evm_coder_substrate::WithRecorder;98use pallet_common::{99 CommonCollectionOperations, Error as CommonError, Event as CommonEvent, Pallet as PalletCommon,100};101use pallet_structure::Pallet as PalletStructure;102use scale_info::TypeInfo;103use sp_core::H160;104use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome};105use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap};106use up_data_structs::{107 AccessMode, budget::Budget, CollectionId, CreateCollectionData, CreateRefungibleExData,108 CustomDataLimit, mapping::TokenAddressMapping, MAX_REFUNGIBLE_PIECES, TokenId, Property,109 PropertyKey, PropertyKeyPermission, PropertyPermission, PropertyScope, PropertyValue,110 TrySetProperty,111};112113pub use pallet::*;114#[cfg(feature = "runtime-benchmarks")]115pub mod benchmarking;116pub mod common;117pub mod erc;118pub mod erc_token;119pub mod weights;120pub(crate) type SelfWeightOf<T> = <T as Config>::WeightInfo;121122#[struct_versioning::versioned(version = 2, upper)]123#[derive(Encode, Decode, Default, TypeInfo, MaxEncodedLen)]124pub struct ItemData {125 pub const_data: BoundedVec<u8, CustomDataLimit>,126127 #[version(..2)]128 pub variable_data: BoundedVec<u8, CustomDataLimit>,129}130131#[frame_support::pallet]132pub mod pallet {133 use super::*;134 use frame_support::{135 Blake2_128, Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key,136 traits::StorageVersion,137 };138 use frame_system::pallet_prelude::*;139 use up_data_structs::{CollectionId, TokenId};140 use super::weights::WeightInfo;141142 #[pallet::error]143 pub enum Error<T> {144 /// Not Refungible item data used to mint in Refungible collection.145 NotRefungibleDataUsedToMintFungibleCollectionToken,146 /// Maximum refungibility exceeded147 WrongRefungiblePieces,148 /// Refungible token can't be repartitioned by user who isn't owns all pieces149 RepartitionWhileNotOwningAllPieces,150 /// Refungible token can't nest other tokens151 RefungibleDisallowsNesting,152 /// Setting item properties is not allowed153 SettingPropertiesNotAllowed,154 }155156 #[pallet::config]157 pub trait Config:158 frame_system::Config + pallet_common::Config + pallet_structure::Config159 {160 type WeightInfo: WeightInfo;161 }162163 const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);164165 #[pallet::pallet]166 #[pallet::storage_version(STORAGE_VERSION)]167 #[pallet::generate_store(pub(super) trait Store)]168 pub struct Pallet<T>(_);169170 /// Amount of tokens minted for collection171 #[pallet::storage]172 pub type TokensMinted<T: Config> =173 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;174175 /// Amount of burnt tokens for collection176 #[pallet::storage]177 pub type TokensBurnt<T: Config> =178 StorageMap<Hasher = Twox64Concat, Key = CollectionId, Value = u32, QueryKind = ValueQuery>;179180 /// Custom data serialized to bytes for token181 #[pallet::storage]182 pub type TokenData<T: Config> = StorageNMap<183 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),184 Value = ItemData,185 QueryKind = ValueQuery,186 >;187188 #[pallet::storage]189 #[pallet::getter(fn token_properties)]190 pub type TokenProperties<T: Config> = StorageNMap<191 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),192 Value = up_data_structs::Properties,193 QueryKind = ValueQuery,194 OnEmpty = up_data_structs::TokenProperties,195 >;196197 /// Total amount of pieces for token198 #[pallet::storage]199 pub type TotalSupply<T: Config> = StorageNMap<200 Key = (Key<Twox64Concat, CollectionId>, Key<Twox64Concat, TokenId>),201 Value = u128,202 QueryKind = ValueQuery,203 >;204205 /// Used to enumerate tokens owned by account206 #[pallet::storage]207 pub type Owned<T: Config> = StorageNMap<208 Key = (209 Key<Twox64Concat, CollectionId>,210 Key<Blake2_128Concat, T::CrossAccountId>,211 Key<Twox64Concat, TokenId>,212 ),213 Value = bool,214 QueryKind = ValueQuery,215 >;216217 /// Amount of tokens owned by account218 #[pallet::storage]219 pub type AccountBalance<T: Config> = StorageNMap<220 Key = (221 Key<Twox64Concat, CollectionId>,222 // Owner223 Key<Blake2_128Concat, T::CrossAccountId>,224 ),225 Value = u32,226 QueryKind = ValueQuery,227 >;228229 /// Amount of token pieces owned by account230 #[pallet::storage]231 pub type Balance<T: Config> = StorageNMap<232 Key = (233 Key<Twox64Concat, CollectionId>,234 Key<Twox64Concat, TokenId>,235 // Owner236 Key<Blake2_128Concat, T::CrossAccountId>,237 ),238 Value = u128,239 QueryKind = ValueQuery,240 >;241242 /// Allowance set by an owner for a spender for a token243 #[pallet::storage]244 pub type Allowance<T: Config> = StorageNMap<245 Key = (246 Key<Twox64Concat, CollectionId>,247 Key<Twox64Concat, TokenId>,248 // Owner249 Key<Blake2_128, T::CrossAccountId>,250 // Spender251 Key<Blake2_128Concat, T::CrossAccountId>,252 ),253 Value = u128,254 QueryKind = ValueQuery,255 >;256257 #[pallet::hooks]258 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {259 fn on_runtime_upgrade() -> Weight {260 if StorageVersion::get::<Pallet<T>>() < StorageVersion::new(1) {261 <TokenData<T>>::translate_values::<ItemDataVersion1, _>(|v| {262 Some(<ItemDataVersion2>::from(v))263 })264 }265266 0267 }268 }269}270271pub struct RefungibleHandle<T: Config>(pallet_common::CollectionHandle<T>);272impl<T: Config> RefungibleHandle<T> {273 pub fn cast(inner: pallet_common::CollectionHandle<T>) -> Self {274 Self(inner)275 }276 pub fn into_inner(self) -> pallet_common::CollectionHandle<T> {277 self.0278 }279 pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle<T> {280 &mut self.0281 }282}283284impl<T: Config> Deref for RefungibleHandle<T> {285 type Target = pallet_common::CollectionHandle<T>;286287 fn deref(&self) -> &Self::Target {288 &self.0289 }290}291292impl<T: Config> WithRecorder<T> for RefungibleHandle<T> {293 fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder<T> {294 self.0.recorder()295 }296 fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder<T> {297 self.0.into_recorder()298 }299}300301impl<T: Config> Pallet<T> {302 /// Get number of RFT tokens in collection303 pub fn total_supply(collection: &RefungibleHandle<T>) -> u32 {304 <TokensMinted<T>>::get(collection.id) - <TokensBurnt<T>>::get(collection.id)305 }306307 /// Check that RFT token exists308 ///309 /// - `token`: Token ID.310 pub fn token_exists(collection: &RefungibleHandle<T>, token: TokenId) -> bool {311 <TotalSupply<T>>::contains_key((collection.id, token))312 }313314 pub fn set_scoped_token_property(315 collection_id: CollectionId,316 token_id: TokenId,317 scope: PropertyScope,318 property: Property,319 ) -> DispatchResult {320 TokenProperties::<T>::try_mutate((collection_id, token_id), |properties| {321 properties.try_scoped_set(scope, property.key, property.value)322 })323 .map_err(<CommonError<T>>::from)?;324325 Ok(())326 }327328 pub fn set_scoped_token_properties(329 collection_id: CollectionId,330 token_id: TokenId,331 scope: PropertyScope,332 properties: impl Iterator<Item = Property>,333 ) -> DispatchResult {334 TokenProperties::<T>::try_mutate((collection_id, token_id), |stored_properties| {335 stored_properties.try_scoped_set_from_iter(scope, properties)336 })337 .map_err(<CommonError<T>>::from)?;338339 Ok(())340 }341}342343// unchecked calls skips any permission checks344impl<T: Config> Pallet<T> {345 /// Create RFT collection346 ///347 /// `init_collection` will take non-refundable deposit for collection creation.348 ///349 /// - `data`: Contains settings for collection limits and permissions.350 pub fn init_collection(351 owner: T::CrossAccountId,352 data: CreateCollectionData<T::AccountId>,353 ) -> Result<CollectionId, DispatchError> {354 <PalletCommon<T>>::init_collection(owner, data, false)355 }356357 /// Destroy RFT collection358 ///359 /// `destroy_collection` will throw error if collection contains any tokens.360 /// Only owner can destroy collection.361 pub fn destroy_collection(362 collection: RefungibleHandle<T>,363 sender: &T::CrossAccountId,364 ) -> DispatchResult {365 let id = collection.id;366367 if Self::collection_has_tokens(id) {368 return Err(<CommonError<T>>::CantDestroyNotEmptyCollection.into());369 }370371 // =========372373 PalletCommon::destroy_collection(collection.0, sender)?;374375 <TokensMinted<T>>::remove(id);376 <TokensBurnt<T>>::remove(id);377 <TokenData<T>>::remove_prefix((id,), None);378 <TotalSupply<T>>::remove_prefix((id,), None);379 <Balance<T>>::remove_prefix((id,), None);380 <Allowance<T>>::remove_prefix((id,), None);381 <Owned<T>>::remove_prefix((id,), None);382 <AccountBalance<T>>::remove_prefix((id,), None);383 Ok(())384 }385386 fn collection_has_tokens(collection_id: CollectionId) -> bool {387 <TokenData<T>>::iter_prefix((collection_id,))388 .next()389 .is_some()390 }391392 pub fn burn_token_unchecked(393 collection: &RefungibleHandle<T>,394 token_id: TokenId,395 ) -> DispatchResult {396 let burnt = <TokensBurnt<T>>::get(collection.id)397 .checked_add(1)398 .ok_or(ArithmeticError::Overflow)?;399400 <TokensBurnt<T>>::insert(collection.id, burnt);401 <TokenData<T>>::remove((collection.id, token_id));402 <TokenProperties<T>>::remove((collection.id, token_id));403 <TotalSupply<T>>::remove((collection.id, token_id));404 <Balance<T>>::remove_prefix((collection.id, token_id), None);405 <Allowance<T>>::remove_prefix((collection.id, token_id), None);406 // TODO: ERC721 transfer event407 Ok(())408 }409410 /// Burn RFT token pieces411 ///412 /// `burn` will decrease total amount of token pieces and amount owned by sender.413 /// `burn` can be called even if there are multiple owners of the RFT token.414 /// If sender wouldn't have any pieces left after `burn` than she will stop being415 /// one of the owners of the token. If there is no account that owns any pieces of416 /// the token than token will be burned too.417 ///418 /// - `amount`: Amount of token pieces to burn.419 /// - `token`: Token who's pieces should be burned420 /// - `collection`: Collection that contains the token421 pub fn burn(422 collection: &RefungibleHandle<T>,423 owner: &T::CrossAccountId,424 token: TokenId,425 amount: u128,426 ) -> DispatchResult {427 let total_supply = <TotalSupply<T>>::get((collection.id, token))428 .checked_sub(amount)429 .ok_or(<CommonError<T>>::TokenValueTooLow)?;430431 // This was probally last owner of this token?432 if total_supply == 0 {433 // Ensure user actually owns this amount434 ensure!(435 <Balance<T>>::get((collection.id, token, owner)) == amount,436 <CommonError<T>>::TokenValueTooLow437 );438 let account_balance = <AccountBalance<T>>::get((collection.id, owner))439 .checked_sub(1)440 // Should not occur441 .ok_or(ArithmeticError::Underflow)?;442443 // =========444445 <Owned<T>>::remove((collection.id, owner, token));446 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);447 <AccountBalance<T>>::insert((collection.id, owner), account_balance);448 Self::burn_token_unchecked(collection, token)?;449 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(450 collection.id,451 token,452 owner.clone(),453 amount,454 ));455 return Ok(());456 }457458 let balance = <Balance<T>>::get((collection.id, token, owner))459 .checked_sub(amount)460 .ok_or(<CommonError<T>>::TokenValueTooLow)?;461 let account_balance = if balance == 0 {462 <AccountBalance<T>>::get((collection.id, owner))463 .checked_sub(1)464 // Should not occur465 .ok_or(ArithmeticError::Underflow)?466 } else {467 0468 };469470 // =========471472 if balance == 0 {473 <Owned<T>>::remove((collection.id, owner, token));474 <PalletStructure<T>>::unnest_if_nested(owner, collection.id, token);475 <Balance<T>>::remove((collection.id, token, owner));476 <AccountBalance<T>>::insert((collection.id, owner), account_balance);477 } else {478 <Balance<T>>::insert((collection.id, token, owner), balance);479 }480 <TotalSupply<T>>::insert((collection.id, token), total_supply);481482 <PalletEvm<T>>::deposit_log(483 ERC20Events::Transfer {484 from: *owner.as_eth(),485 to: H160::default(),486 value: amount.into(),487 }488 .to_log(T::EvmTokenAddressMapping::token_to_address(489 collection.id,490 token,491 )),492 );493 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(494 collection.id,495 token,496 owner.clone(),497 amount,498 ));499 Ok(())500 }501502 #[transactional]503 fn modify_token_properties(504 collection: &RefungibleHandle<T>,505 sender: &T::CrossAccountId,506 token_id: TokenId,507 properties: impl Iterator<Item = (PropertyKey, Option<PropertyValue>)>,508 is_token_create: bool,509 nesting_budget: &dyn Budget,510 ) -> DispatchResult {511 let is_collection_admin = || collection.is_owner_or_admin(sender);512 let is_token_owner = || -> Result<bool, DispatchError> {513 let balance = collection.balance(sender.clone(), token_id);514 let total_pieces: u128 =515 Self::total_pieces(collection.id, token_id).unwrap_or(u128::MAX);516 if balance != total_pieces {517 return Ok(false);518 }519520 let is_bundle_owner = <PalletStructure<T>>::check_indirectly_owned(521 sender.clone(),522 collection.id,523 token_id,524 None,525 nesting_budget,526 )?;527528 Ok(is_bundle_owner)529 };530531 for (key, value) in properties {532 let permission = <PalletCommon<T>>::property_permissions(collection.id)533 .get(&key)534 .cloned()535 .unwrap_or_else(PropertyPermission::none);536537 let is_property_exists = TokenProperties::<T>::get((collection.id, token_id))538 .get(&key)539 .is_some();540541 match permission {542 PropertyPermission { mutable: false, .. } if is_property_exists => {543 return Err(<CommonError<T>>::NoPermission.into());544 }545546 PropertyPermission {547 collection_admin,548 token_owner,549 ..550 } => {551 //TODO: investigate threats during public minting.552 let is_token_create =553 is_token_create && (collection_admin || token_owner) && value.is_some();554 if !(is_token_create555 || (collection_admin && is_collection_admin())556 || (token_owner && is_token_owner()?))557 {558 fail!(<CommonError<T>>::NoPermission);559 }560 }561 }562563 match value {564 Some(value) => {565 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {566 properties.try_set(key.clone(), value)567 })568 .map_err(<CommonError<T>>::from)?;569570 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertySet(571 collection.id,572 token_id,573 key,574 ));575 }576 None => {577 <TokenProperties<T>>::try_mutate((collection.id, token_id), |properties| {578 properties.remove(&key)579 })580 .map_err(<CommonError<T>>::from)?;581582 <PalletCommon<T>>::deposit_event(CommonEvent::TokenPropertyDeleted(583 collection.id,584 token_id,585 key,586 ));587 }588 }589 }590591 Ok(())592 }593594 pub fn set_token_properties(595 collection: &RefungibleHandle<T>,596 sender: &T::CrossAccountId,597 token_id: TokenId,598 properties: impl Iterator<Item = Property>,599 is_token_create: bool,600 nesting_budget: &dyn Budget,601 ) -> DispatchResult {602 Self::modify_token_properties(603 collection,604 sender,605 token_id,606 properties.map(|p| (p.key, Some(p.value))),607 is_token_create,608 nesting_budget,609 )610 }611612 pub fn set_token_property(613 collection: &RefungibleHandle<T>,614 sender: &T::CrossAccountId,615 token_id: TokenId,616 property: Property,617 nesting_budget: &dyn Budget,618 ) -> DispatchResult {619 let is_token_create = false;620621 Self::set_token_properties(622 collection,623 sender,624 token_id,625 [property].into_iter(),626 is_token_create,627 nesting_budget,628 )629 }630631 pub fn delete_token_properties(632 collection: &RefungibleHandle<T>,633 sender: &T::CrossAccountId,634 token_id: TokenId,635 property_keys: impl Iterator<Item = PropertyKey>,636 nesting_budget: &dyn Budget,637 ) -> DispatchResult {638 let is_token_create = false;639640 Self::modify_token_properties(641 collection,642 sender,643 token_id,644 property_keys.into_iter().map(|key| (key, None)),645 is_token_create,646 nesting_budget,647 )648 }649650 pub fn delete_token_property(651 collection: &RefungibleHandle<T>,652 sender: &T::CrossAccountId,653 token_id: TokenId,654 property_key: PropertyKey,655 nesting_budget: &dyn Budget,656 ) -> DispatchResult {657 Self::delete_token_properties(658 collection,659 sender,660 token_id,661 [property_key].into_iter(),662 nesting_budget,663 )664 }665666 /// Transfer RFT token pieces from one account to another.667 ///668 /// If the sender is no longer owns any pieces after the `transfer` than she stops being an owner of the token.669 ///670 /// - `from`: Owner of token pieces to transfer.671 /// - `to`: Recepient of transfered token pieces.672 /// - `amount`: Amount of token pieces to transfer.673 /// - `token`: Token whos pieces should be transfered674 /// - `collection`: Collection that contains the token675 pub fn transfer(676 collection: &RefungibleHandle<T>,677 from: &T::CrossAccountId,678 to: &T::CrossAccountId,679 token: TokenId,680 amount: u128,681 nesting_budget: &dyn Budget,682 ) -> DispatchResult {683 ensure!(684 collection.limits.transfers_enabled(),685 <CommonError<T>>::TransferNotAllowed686 );687688 if collection.permissions.access() == AccessMode::AllowList {689 collection.check_allowlist(from)?;690 collection.check_allowlist(to)?;691 }692 <PalletCommon<T>>::ensure_correct_receiver(to)?;693694 let balance_from = <Balance<T>>::get((collection.id, token, from))695 .checked_sub(amount)696 .ok_or(<CommonError<T>>::TokenValueTooLow)?;697 let mut create_target = false;698 let from_to_differ = from != to;699 let balance_to = if from != to {700 let old_balance = <Balance<T>>::get((collection.id, token, to));701 if old_balance == 0 {702 create_target = true;703 }704 Some(705 old_balance706 .checked_add(amount)707 .ok_or(ArithmeticError::Overflow)?,708 )709 } else {710 None711 };712713 let account_balance_from = if balance_from == 0 {714 Some(715 <AccountBalance<T>>::get((collection.id, from))716 .checked_sub(1)717 // Should not occur718 .ok_or(ArithmeticError::Underflow)?,719 )720 } else {721 None722 };723 // Account data is created in token, AccountBalance should be increased724 // But only if from != to as we shouldn't check overflow in this case725 let account_balance_to = if create_target && from_to_differ {726 let account_balance_to = <AccountBalance<T>>::get((collection.id, to))727 .checked_add(1)728 .ok_or(ArithmeticError::Overflow)?;729 ensure!(730 account_balance_to < collection.limits.account_token_ownership_limit(),731 <CommonError<T>>::AccountTokenLimitExceeded,732 );733734 Some(account_balance_to)735 } else {736 None737 };738739 // =========740741 <PalletStructure<T>>::nest_if_sent_to_token(742 from.clone(),743 to,744 collection.id,745 token,746 nesting_budget,747 )?;748749 if let Some(balance_to) = balance_to {750 // from != to751 if balance_from == 0 {752 <Balance<T>>::remove((collection.id, token, from));753 <PalletStructure<T>>::unnest_if_nested(from, collection.id, token);754 } else {755 <Balance<T>>::insert((collection.id, token, from), balance_from);756 }757 <Balance<T>>::insert((collection.id, token, to), balance_to);758 if let Some(account_balance_from) = account_balance_from {759 <AccountBalance<T>>::insert((collection.id, from), account_balance_from);760 <Owned<T>>::remove((collection.id, from, token));761 }762 if let Some(account_balance_to) = account_balance_to {763 <AccountBalance<T>>::insert((collection.id, to), account_balance_to);764 <Owned<T>>::insert((collection.id, to, token), true);765 }766 }767768 <PalletEvm<T>>::deposit_log(769 ERC20Events::Transfer {770 from: *from.as_eth(),771 to: *to.as_eth(),772 value: amount.into(),773 }774 .to_log(T::EvmTokenAddressMapping::token_to_address(775 collection.id,776 token,777 )),778 );779 <PalletCommon<T>>::deposit_event(CommonEvent::Transfer(780 collection.id,781 token,782 from.clone(),783 to.clone(),784 amount,785 ));786 Ok(())787 }788789 /// Batched operation to create multiple RFT tokens.790 ///791 /// Same as `create_item` but creates multiple tokens.792 ///793 /// - `data`: Same as 'data` in `create_item` but contains data for multiple tokens.794 pub fn create_multiple_items(795 collection: &RefungibleHandle<T>,796 sender: &T::CrossAccountId,797 data: Vec<CreateRefungibleExData<T::CrossAccountId>>,798 nesting_budget: &dyn Budget,799 ) -> DispatchResult {800 if !collection.is_owner_or_admin(sender) {801 ensure!(802 collection.permissions.mint_mode(),803 <CommonError<T>>::PublicMintingNotAllowed804 );805 collection.check_allowlist(sender)?;806807 for item in data.iter() {808 for user in item.users.keys() {809 collection.check_allowlist(user)?;810 }811 }812 }813814 for item in data.iter() {815 for (owner, _) in item.users.iter() {816 <PalletCommon<T>>::ensure_correct_receiver(owner)?;817 }818 }819820 // Total pieces per tokens821 let totals = data822 .iter()823 .map(|data| {824 Ok(data825 .users826 .iter()827 .map(|u| u.1)828 .try_fold(0u128, |acc, v| acc.checked_add(*v))829 .ok_or(ArithmeticError::Overflow)?)830 })831 .collect::<Result<Vec<_>, DispatchError>>()?;832 for total in &totals {833 ensure!(834 *total <= MAX_REFUNGIBLE_PIECES,835 <Error<T>>::WrongRefungiblePieces836 );837 }838839 let first_token_id = <TokensMinted<T>>::get(collection.id);840 let tokens_minted = first_token_id841 .checked_add(data.len() as u32)842 .ok_or(ArithmeticError::Overflow)?;843 ensure!(844 tokens_minted < collection.limits.token_limit(),845 <CommonError<T>>::CollectionTokenLimitExceeded846 );847848 let mut balances = BTreeMap::new();849 for data in &data {850 for owner in data.users.keys() {851 let balance = balances852 .entry(owner)853 .or_insert_with(|| <AccountBalance<T>>::get((collection.id, owner)));854 *balance = balance.checked_add(1).ok_or(ArithmeticError::Overflow)?;855856 ensure!(857 *balance <= collection.limits.account_token_ownership_limit(),858 <CommonError<T>>::AccountTokenLimitExceeded,859 );860 }861 }862863 for (i, token) in data.iter().enumerate() {864 let token_id = TokenId(first_token_id + i as u32 + 1);865 for (to, _) in token.users.iter() {866 <PalletStructure<T>>::check_nesting(867 sender.clone(),868 to,869 collection.id,870 token_id,871 nesting_budget,872 )?;873 }874 }875876 // =========877878 with_transaction(|| {879 for (i, data) in data.iter().enumerate() {880 let token_id = first_token_id + i as u32 + 1;881 <TotalSupply<T>>::insert((collection.id, token_id), totals[i]);882883 <TokenData<T>>::insert(884 (collection.id, token_id),885 ItemData {886 const_data: data.const_data.clone(),887 },888 );889890 for (user, amount) in data.users.iter() {891 if *amount == 0 {892 continue;893 }894 <Balance<T>>::insert((collection.id, token_id, &user), amount);895 <Owned<T>>::insert((collection.id, &user, TokenId(token_id)), true);896 <PalletStructure<T>>::nest_if_sent_to_token_unchecked(897 user,898 collection.id,899 TokenId(token_id),900 );901 }902903 if let Err(e) = Self::set_token_properties(904 collection,905 sender,906 TokenId(token_id),907 data.properties.clone().into_iter(),908 true,909 nesting_budget,910 ) {911 return TransactionOutcome::Rollback(Err(e));912 }913 }914 TransactionOutcome::Commit(Ok(()))915 })?;916917 <TokensMinted<T>>::insert(collection.id, tokens_minted);918919 for (account, balance) in balances {920 <AccountBalance<T>>::insert((collection.id, account), balance);921 }922923 for (i, token) in data.into_iter().enumerate() {924 let token_id = first_token_id + i as u32 + 1;925926 for (user, amount) in token.users.into_iter() {927 if amount == 0 {928 continue;929 }930931 <PalletEvm<T>>::deposit_log(932 ERC20Events::Transfer {933 from: H160::default(),934 to: *user.as_eth(),935 value: amount.into(),936 }937 .to_log(T::EvmTokenAddressMapping::token_to_address(938 collection.id,939 TokenId(token_id),940 )),941 );942 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(943 collection.id,944 TokenId(token_id),945 user,946 amount,947 ));948 }949 }950 Ok(())951 }952953 pub fn set_allowance_unchecked(954 collection: &RefungibleHandle<T>,955 sender: &T::CrossAccountId,956 spender: &T::CrossAccountId,957 token: TokenId,958 amount: u128,959 ) {960 if amount == 0 {961 <Allowance<T>>::remove((collection.id, token, sender, spender));962 } else {963 <Allowance<T>>::insert((collection.id, token, sender, spender), amount);964 }965966 <PalletEvm<T>>::deposit_log(967 ERC20Events::Approval {968 owner: *sender.as_eth(),969 spender: *spender.as_eth(),970 value: amount.into(),971 }972 .to_log(T::EvmTokenAddressMapping::token_to_address(973 collection.id,974 token,975 )),976 );977 <PalletCommon<T>>::deposit_event(CommonEvent::Approved(978 collection.id,979 token,980 sender.clone(),981 spender.clone(),982 amount,983 ))984 }985986 /// Set allowance for the spender to `transfer` or `burn` sender's token pieces.987 ///988 /// - `amount`: Amount of token pieces the spender is allowed to `transfer` or `burn.989 pub fn set_allowance(990 collection: &RefungibleHandle<T>,991 sender: &T::CrossAccountId,992 spender: &T::CrossAccountId,993 token: TokenId,994 amount: u128,995 ) -> DispatchResult {996 if collection.permissions.access() == AccessMode::AllowList {997 collection.check_allowlist(sender)?;998 collection.check_allowlist(spender)?;999 }10001001 <PalletCommon<T>>::ensure_correct_receiver(spender)?;10021003 if <Balance<T>>::get((collection.id, token, sender)) < amount {1004 ensure!(1005 collection.ignores_owned_amount(sender) && Self::token_exists(collection, token),1006 <CommonError<T>>::CantApproveMoreThanOwned1007 );1008 }10091010 // =========10111012 Self::set_allowance_unchecked(collection, sender, spender, token, amount);1013 Ok(())1014 }10151016 /// Returns allowance, which should be set after transaction1017 fn check_allowed(1018 collection: &RefungibleHandle<T>,1019 spender: &T::CrossAccountId,1020 from: &T::CrossAccountId,1021 token: TokenId,1022 amount: u128,1023 nesting_budget: &dyn Budget,1024 ) -> Result<Option<u128>, DispatchError> {1025 if spender.conv_eq(from) {1026 return Ok(None);1027 }1028 if collection.permissions.access() == AccessMode::AllowList {1029 // `from`, `to` checked in [`transfer`]1030 collection.check_allowlist(spender)?;1031 }1032 if let Some(source) = T::CrossTokenAddressMapping::address_to_token(from) {1033 // TODO: should collection owner be allowed to perform this transfer?1034 ensure!(1035 <PalletStructure<T>>::check_indirectly_owned(1036 spender.clone(),1037 source.0,1038 source.1,1039 None,1040 nesting_budget1041 )?,1042 <CommonError<T>>::ApprovedValueTooLow,1043 );1044 return Ok(None);1045 }1046 let allowance =1047 <Allowance<T>>::get((collection.id, token, from, &spender)).checked_sub(amount);1048 if allowance.is_none() {1049 ensure!(1050 collection.ignores_allowance(spender),1051 <CommonError<T>>::ApprovedValueTooLow1052 );1053 }1054 Ok(allowance)1055 }10561057 /// Transfer RFT token pieces from one account to another.1058 ///1059 /// Same as the [`transfer`] but spender doesn't needs to be an owner of the token pieces.1060 /// The owner should set allowance for the spender to transfer pieces.1061 ///1062 /// [`transfer`]: struct.Pallet.html#method.transfer1063 pub fn transfer_from(1064 collection: &RefungibleHandle<T>,1065 spender: &T::CrossAccountId,1066 from: &T::CrossAccountId,1067 to: &T::CrossAccountId,1068 token: TokenId,1069 amount: u128,1070 nesting_budget: &dyn Budget,1071 ) -> DispatchResult {1072 let allowance =1073 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;10741075 // =========10761077 Self::transfer(collection, from, to, token, amount, nesting_budget)?;1078 if let Some(allowance) = allowance {1079 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1080 }1081 Ok(())1082 }10831084 /// Burn RFT token pieces from the account.1085 ///1086 /// Same as the [`burn`] but spender doesn't need to be an owner of the token pieces. The owner should1087 /// set allowance for the spender to burn pieces1088 ///1089 /// [`burn`]: struct.Pallet.html#method.burn1090 pub fn burn_from(1091 collection: &RefungibleHandle<T>,1092 spender: &T::CrossAccountId,1093 from: &T::CrossAccountId,1094 token: TokenId,1095 amount: u128,1096 nesting_budget: &dyn Budget,1097 ) -> DispatchResult {1098 let allowance =1099 Self::check_allowed(collection, spender, from, token, amount, nesting_budget)?;11001101 // =========11021103 Self::burn(collection, from, token, amount)?;1104 if let Some(allowance) = allowance {1105 Self::set_allowance_unchecked(collection, from, spender, token, allowance);1106 }1107 Ok(())1108 }11091110 /// Create RFT token.1111 ///1112 /// The sender should be the owner/admin of the collection or collection should be configured1113 /// to allow public minting.1114 ///1115 /// - `data`: Contains list of users who will become the owners of the token pieces and amount1116 /// of token pieces they will receive.1117 pub fn create_item(1118 collection: &RefungibleHandle<T>,1119 sender: &T::CrossAccountId,1120 data: CreateRefungibleExData<T::CrossAccountId>,1121 nesting_budget: &dyn Budget,1122 ) -> DispatchResult {1123 Self::create_multiple_items(collection, sender, vec![data], nesting_budget)1124 }11251126 /// Repartition RFT token.1127 ///1128 /// `repartition` will set token balance of the sender and total amount of token pieces.1129 /// Sender should own all of the token pieces. `repartition' could be done even if some1130 /// token pieces were burned before.1131 ///1132 /// - `amount`: Total amount of token pieces that the token will have after `repartition`.1133 pub fn repartition(1134 collection: &RefungibleHandle<T>,1135 owner: &T::CrossAccountId,1136 token: TokenId,1137 amount: u128,1138 ) -> DispatchResult {1139 ensure!(1140 amount <= MAX_REFUNGIBLE_PIECES,1141 <Error<T>>::WrongRefungiblePieces1142 );1143 ensure!(amount > 0, <CommonError<T>>::TokenValueTooLow);1144 // Ensure user owns all pieces1145 let total_pieces = Self::total_pieces(collection.id, token).unwrap_or(u128::MAX);1146 let balance = <Balance<T>>::get((collection.id, token, owner));1147 ensure!(1148 total_pieces == balance,1149 <Error<T>>::RepartitionWhileNotOwningAllPieces1150 );11511152 <Balance<T>>::insert((collection.id, token, owner), amount);1153 <TotalSupply<T>>::insert((collection.id, token), amount);11541155 if amount > total_pieces {1156 let mint_amount = amount - total_pieces;1157 <PalletEvm<T>>::deposit_log(1158 ERC20Events::Transfer {1159 from: H160::default(),1160 to: *owner.as_eth(),1161 value: mint_amount.into(),1162 }1163 .to_log(T::EvmTokenAddressMapping::token_to_address(1164 collection.id,1165 token,1166 )),1167 );1168 <PalletCommon<T>>::deposit_event(CommonEvent::ItemCreated(1169 collection.id,1170 token,1171 owner.clone(),1172 mint_amount,1173 ));1174 } else if total_pieces > amount {1175 let burn_amount = total_pieces - amount;1176 <PalletEvm<T>>::deposit_log(1177 ERC20Events::Transfer {1178 from: *owner.as_eth(),1179 to: H160::default(),1180 value: burn_amount.into(),1181 }1182 .to_log(T::EvmTokenAddressMapping::token_to_address(1183 collection.id,1184 token,1185 )),1186 );1187 <PalletCommon<T>>::deposit_event(CommonEvent::ItemDestroyed(1188 collection.id,1189 token,1190 owner.clone(),1191 burn_amount,1192 ));1193 }11941195 Ok(())1196 }11971198 fn token_owner(collection_id: CollectionId, token_id: TokenId) -> Option<T::CrossAccountId> {1199 let mut owner = None;1200 let mut count = 0;1201 for key in Balance::<T>::iter_key_prefix((collection_id, token_id)) {1202 count += 1;1203 if count > 1 {1204 return None;1205 }1206 owner = Some(key);1207 }1208 owner1209 }12101211 fn total_pieces(collection_id: CollectionId, token_id: TokenId) -> Option<u128> {1212 <TotalSupply<T>>::try_get((collection_id, token_id)).ok()1213 }12141215 pub fn set_collection_properties(1216 collection: &RefungibleHandle<T>,1217 sender: &T::CrossAccountId,1218 properties: Vec<Property>,1219 ) -> DispatchResult {1220 <PalletCommon<T>>::set_collection_properties(collection, sender, properties)1221 }12221223 pub fn delete_collection_properties(1224 collection: &RefungibleHandle<T>,1225 sender: &T::CrossAccountId,1226 property_keys: Vec<PropertyKey>,1227 ) -> DispatchResult {1228 <PalletCommon<T>>::delete_collection_properties(collection, sender, property_keys)1229 }12301231 pub fn set_token_property_permissions(1232 collection: &RefungibleHandle<T>,1233 sender: &T::CrossAccountId,1234 property_permissions: Vec<PropertyKeyPermission>,1235 ) -> DispatchResult {1236 <PalletCommon<T>>::set_token_property_permissions(collection, sender, property_permissions)1237 }12381239 /// Returns 10 token in no particular order.1240 ///1241 /// There is no direct way to get token holders in ascending order,1242 /// since `iter_prefix` returns values in no particular order.1243 /// Therefore, getting the 10 largest holders with a large value of holders1244 /// can lead to impact memory allocation + sorting with `n * log (n)`.1245 pub fn token_owners(1246 collection_id: CollectionId,1247 token: TokenId,1248 ) -> Option<Vec<T::CrossAccountId>> {1249 let res: Vec<T::CrossAccountId> = <Balance<T>>::iter_prefix((collection_id, token))1250 .map(|(owner, _amount)| owner)1251 .take(10)1252 .collect();12531254 if res.is_empty() {1255 None1256 } else {1257 Some(res)1258 }1259 }1260}pallets/refungible/src/stubs/UniqueRefungibleToken.rawdiffbeforeafterbothbinary blob — no preview
pallets/refungible/src/stubs/UniqueRefungibleToken.soldiffbeforeafterboth--- /dev/null
+++ b/pallets/refungible/src/stubs/UniqueRefungibleToken.sol
@@ -0,0 +1,138 @@
+// SPDX-License-Identifier: OTHER
+// This code is automatically generated
+
+pragma solidity >=0.8.0 <0.9.0;
+
+// Common stubs holder
+contract Dummy {
+ uint8 dummy;
+ string stub_error = "this contract is implemented in native";
+}
+
+contract ERC165 is Dummy {
+ function supportsInterface(bytes4 interfaceID)
+ external
+ view
+ returns (bool)
+ {
+ require(false, stub_error);
+ interfaceID;
+ return true;
+ }
+}
+
+// Inline
+contract ERC20Events {
+ event Transfer(address indexed from, address indexed to, uint256 value);
+ event Approval(
+ address indexed owner,
+ address indexed spender,
+ uint256 value
+ );
+}
+
+// Selector: 942e8b22
+contract ERC20 is Dummy, ERC165, ERC20Events {
+ // Selector: name() 06fdde03
+ function name() public view returns (string memory) {
+ require(false, stub_error);
+ dummy;
+ return "";
+ }
+
+ // Selector: symbol() 95d89b41
+ function symbol() public view returns (string memory) {
+ require(false, stub_error);
+ dummy;
+ return "";
+ }
+
+ // Selector: totalSupply() 18160ddd
+ function totalSupply() public view returns (uint256) {
+ require(false, stub_error);
+ dummy;
+ return 0;
+ }
+
+ // Selector: decimals() 313ce567
+ function decimals() public view returns (uint8) {
+ require(false, stub_error);
+ dummy;
+ return 0;
+ }
+
+ // Selector: balanceOf(address) 70a08231
+ function balanceOf(address owner) public view returns (uint256) {
+ require(false, stub_error);
+ owner;
+ dummy;
+ return 0;
+ }
+
+ // Selector: transfer(address,uint256) a9059cbb
+ function transfer(address to, uint256 amount) public returns (bool) {
+ require(false, stub_error);
+ to;
+ amount;
+ dummy = 0;
+ return false;
+ }
+
+ // Selector: transferFrom(address,address,uint256) 23b872dd
+ function transferFrom(
+ address from,
+ address to,
+ uint256 amount
+ ) public returns (bool) {
+ require(false, stub_error);
+ from;
+ to;
+ amount;
+ dummy = 0;
+ return false;
+ }
+
+ // Selector: approve(address,uint256) 095ea7b3
+ function approve(address spender, uint256 amount) public returns (bool) {
+ require(false, stub_error);
+ spender;
+ amount;
+ dummy = 0;
+ return false;
+ }
+
+ // Selector: allowance(address,address) dd62ed3e
+ function allowance(address owner, address spender)
+ public
+ view
+ returns (uint256)
+ {
+ require(false, stub_error);
+ owner;
+ spender;
+ dummy;
+ return 0;
+ }
+}
+
+// Selector: ab8deb37
+contract ERC20UniqueExtensions is Dummy, ERC165 {
+ // Selector: burnFrom(address,uint256) 79cc6790
+ function burnFrom(address from, uint256 amount) public returns (bool) {
+ require(false, stub_error);
+ from;
+ amount;
+ dummy = 0;
+ return false;
+ }
+
+ // Selector: repartition(uint256) d2418ca7
+ function repartition(uint256 amount) public returns (bool) {
+ require(false, stub_error);
+ amount;
+ dummy = 0;
+ return false;
+ }
+}
+
+contract UniqueRefungibleToken is Dummy, ERC165, ERC20, ERC20UniqueExtensions {}
runtime/common/src/dispatch.rsdiffbeforeafterboth--- a/runtime/common/src/dispatch.rs
+++ b/runtime/common/src/dispatch.rs
@@ -25,7 +25,9 @@
pub use pallet_common::dispatch::CollectionDispatch;
use pallet_fungible::{Pallet as PalletFungible, FungibleHandle};
use pallet_nonfungible::{Pallet as PalletNonfungible, NonfungibleHandle};
-use pallet_refungible::{Pallet as PalletRefungible, RefungibleHandle, erc::RefungibleTokenHandle};
+use pallet_refungible::{
+ Pallet as PalletRefungible, RefungibleHandle, erc_token::RefungibleTokenHandle,
+};
use up_data_structs::{
CollectionMode, CreateCollectionData, MAX_DECIMAL_POINTS, mapping::TokenAddressMapping,
};
tests/src/eth/api/UniqueRefungibleToken.soldiffbeforeafterboth--- /dev/null
+++ b/tests/src/eth/api/UniqueRefungibleToken.sol
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: OTHER
+// This code is automatically generated
+
+pragma solidity >=0.8.0 <0.9.0;
+
+// Common stubs holder
+interface Dummy {
+
+}
+
+interface ERC165 is Dummy {
+ function supportsInterface(bytes4 interfaceID) external view returns (bool);
+}
+
+// Inline
+interface ERC20Events {
+ event Transfer(address indexed from, address indexed to, uint256 value);
+ event Approval(
+ address indexed owner,
+ address indexed spender,
+ uint256 value
+ );
+}
+
+// Selector: 942e8b22
+interface ERC20 is Dummy, ERC165, ERC20Events {
+ // Selector: name() 06fdde03
+ function name() external view returns (string memory);
+
+ // Selector: symbol() 95d89b41
+ function symbol() external view returns (string memory);
+
+ // Selector: totalSupply() 18160ddd
+ function totalSupply() external view returns (uint256);
+
+ // Selector: decimals() 313ce567
+ function decimals() external view returns (uint8);
+
+ // Selector: balanceOf(address) 70a08231
+ function balanceOf(address owner) external view returns (uint256);
+
+ // Selector: transfer(address,uint256) a9059cbb
+ function transfer(address to, uint256 amount) external returns (bool);
+
+ // Selector: transferFrom(address,address,uint256) 23b872dd
+ function transferFrom(
+ address from,
+ address to,
+ uint256 amount
+ ) external returns (bool);
+
+ // Selector: approve(address,uint256) 095ea7b3
+ function approve(address spender, uint256 amount) external returns (bool);
+
+ // Selector: allowance(address,address) dd62ed3e
+ function allowance(address owner, address spender)
+ external
+ view
+ returns (uint256);
+}
+
+// Selector: ab8deb37
+interface ERC20UniqueExtensions is Dummy, ERC165 {
+ // Selector: burnFrom(address,uint256) 79cc6790
+ function burnFrom(address from, uint256 amount) external returns (bool);
+
+ // Selector: repartition(uint256) d2418ca7
+ function repartition(uint256 amount) external returns (bool);
+}
+
+interface UniqueRefungibleToken is
+ Dummy,
+ ERC165,
+ ERC20,
+ ERC20UniqueExtensions
+{}
tests/src/eth/reFungibleToken.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/eth/reFungibleToken.test.ts
@@ -0,0 +1,458 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {approve, createCollection, createRefungibleToken, transfer, transferFrom, UNIQUE} from '../util/helpers';
+import {createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, tokenIdToAddress, transferBalanceToEth} from './util/helpers';
+import reFungibleTokenAbi from './reFungibleTokenAbi.json';
+
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
+chai.use(chaiAsPromised);
+const expect = chai.expect;
+
+describe('Refungible token: Information getting', () => {
+ itWeb3('totalSupply', async ({api, web3, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: caller})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+ const totalSupply = await contract.methods.totalSupply().call();
+
+ expect(totalSupply).to.equal('200');
+ });
+
+ itWeb3('balanceOf', async ({api, web3, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: caller})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+ const balance = await contract.methods.balanceOf(caller).call();
+
+ expect(balance).to.equal('200');
+ });
+
+ itWeb3('decimals', async ({api, web3, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: caller})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: caller, ...GAS_ARGS});
+ const decimals = await contract.methods.decimals().call();
+
+ expect(decimals).to.equal('0');
+ });
+});
+
+describe('Refungible: Plain calls', () => {
+ itWeb3('Can perform approve()', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+
+ const spender = createEthAccount(web3);
+
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+ {
+ const result = await contract.methods.approve(spender, 100).send({from: owner});
+ const events = normalizeEvents(result.events);
+
+ expect(events).to.be.deep.equal([
+ {
+ address,
+ event: 'Approval',
+ args: {
+ owner,
+ spender,
+ value: '100',
+ },
+ },
+ ]);
+ }
+
+ {
+ const allowance = await contract.methods.allowance(owner, spender).call();
+ expect(+allowance).to.equal(100);
+ }
+ });
+
+ itWeb3('Can perform transferFrom()', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+ const owner = createEthAccount(web3);
+ await transferBalanceToEth(api, alice, owner);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+ const spender = createEthAccount(web3);
+ await transferBalanceToEth(api, alice, spender);
+
+ const receiver = createEthAccount(web3);
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+ await contract.methods.approve(spender, 100).send();
+
+ {
+ const result = await contract.methods.transferFrom(owner, receiver, 49).send({from: spender});
+ const events = normalizeEvents(result.events);
+ expect(events).to.be.deep.equal([
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: owner,
+ to: receiver,
+ value: '49',
+ },
+ },
+ {
+ address,
+ event: 'Approval',
+ args: {
+ owner,
+ spender,
+ value: '51',
+ },
+ },
+ ]);
+ }
+
+ {
+ const balance = await contract.methods.balanceOf(receiver).call();
+ expect(+balance).to.equal(49);
+ }
+
+ {
+ const balance = await contract.methods.balanceOf(owner).call();
+ expect(+balance).to.equal(151);
+ }
+ });
+
+ itWeb3('Can perform transfer()', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+ const owner = createEthAccount(web3);
+ await transferBalanceToEth(api, alice, owner);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+ const receiver = createEthAccount(web3);
+ await transferBalanceToEth(api, alice, receiver);
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+ {
+ const result = await contract.methods.transfer(receiver, 50).send({from: owner});
+ const events = normalizeEvents(result.events);
+ expect(events).to.be.deep.equal([
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: owner,
+ to: receiver,
+ value: '50',
+ },
+ },
+ ]);
+ }
+
+ {
+ const balance = await contract.methods.balanceOf(owner).call();
+ expect(+balance).to.equal(150);
+ }
+
+ {
+ const balance = await contract.methods.balanceOf(receiver).call();
+ expect(+balance).to.equal(50);
+ }
+ });
+
+ itWeb3('Can perform repartition()', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+ const owner = createEthAccount(web3);
+ await transferBalanceToEth(api, alice, owner);
+
+ const receiver = createEthAccount(web3);
+ await transferBalanceToEth(api, alice, receiver);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n, {Ethereum: owner})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+ await contract.methods.repartition(200).send({from: owner});
+ expect(+await contract.methods.balanceOf(owner).call()).to.be.equal(200);
+ await contract.methods.transfer(receiver, 110).send({from: owner});
+ expect(+await contract.methods.balanceOf(owner).call()).to.be.equal(90);
+ expect(+await contract.methods.balanceOf(receiver).call()).to.be.equal(110);
+
+ await expect(contract.methods.repartition(80).send({from: owner})).to.eventually.be.rejected;
+
+ await contract.methods.transfer(receiver, 90).send({from: owner});
+ expect(+await contract.methods.balanceOf(owner).call()).to.be.equal(0);
+ expect(+await contract.methods.balanceOf(receiver).call()).to.be.equal(200);
+
+ await contract.methods.repartition(150).send({from: receiver});
+ await expect(contract.methods.transfer(owner, 160).send({from: receiver})).to.eventually.be.rejected;
+ expect(+await contract.methods.balanceOf(receiver).call()).to.be.equal(150);
+ });
+
+ itWeb3('Can repartition with increased amount', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+ const owner = createEthAccount(web3);
+ await transferBalanceToEth(api, alice, owner);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n, {Ethereum: owner})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+ const result = await contract.methods.repartition(200).send();
+ const events = normalizeEvents(result.events);
+
+ expect(events).to.deep.equal([
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: owner,
+ value: '100',
+ },
+ },
+ ]);
+ });
+
+ itWeb3('Can repartition with decreased amount', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {name: 'token name', mode: {type: 'ReFungible'}})).collectionId;
+
+ const owner = createEthAccount(web3);
+ await transferBalanceToEth(api, alice, owner);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n, {Ethereum: owner})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+ const result = await contract.methods.repartition(50).send();
+ const events = normalizeEvents(result.events);
+ expect(events).to.deep.equal([
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: owner,
+ to: '0x0000000000000000000000000000000000000000',
+ value: '50',
+ },
+ },
+ ]);
+ });
+});
+
+describe('Refungible: Fees', () => {
+ itWeb3('approve() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const spender = createEthAccount(web3);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+ const cost = await recordEthFee(api, owner, () => contract.methods.approve(spender, 100).send({from: owner}));
+ expect(cost < BigInt(0.2 * Number(UNIQUE)));
+ });
+
+ itWeb3('transferFrom() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const spender = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+ await contract.methods.approve(spender, 100).send({from: owner});
+
+ const cost = await recordEthFee(api, spender, () => contract.methods.transferFrom(owner, spender, 100).send({from: spender}));
+ expect(cost < BigInt(0.2 * Number(UNIQUE)));
+ });
+
+ itWeb3('transfer() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const receiver = createEthAccount(web3);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n, {Ethereum: owner})).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address, {from: owner, ...GAS_ARGS});
+
+ const cost = await recordEthFee(api, owner, () => contract.methods.transfer(receiver, 100).send({from: owner}));
+ expect(cost < BigInt(0.2 * Number(UNIQUE)));
+ });
+});
+
+describe('Refungible: Substrate calls', () => {
+ itWeb3('Events emitted for approve()', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+ const receiver = createEthAccount(web3);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n)).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address);
+
+ const events = await recordEvents(contract, async () => {
+ expect(await approve(api, collectionId, tokenId, alice, {Ethereum: receiver}, 100n)).to.be.true;
+ });
+
+ expect(events).to.be.deep.equal([
+ {
+ address,
+ event: 'Approval',
+ args: {
+ owner: subToEth(alice.address),
+ spender: receiver,
+ value: '100',
+ },
+ },
+ ]);
+ });
+
+ itWeb3('Events emitted for transferFrom()', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+ const bob = privateKeyWrapper('//Bob');
+
+ const receiver = createEthAccount(web3);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n)).itemId;
+ expect(await approve(api, collectionId, tokenId, alice, bob.address, 100n)).to.be.true;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address);
+
+ const events = await recordEvents(contract, async () => {
+ expect(await transferFrom(api, collectionId, tokenId, bob, alice, {Ethereum: receiver}, 51n)).to.be.true;
+ });
+
+ expect(events).to.be.deep.equal([
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: subToEth(alice.address),
+ to: receiver,
+ value: '51',
+ },
+ },
+ {
+ address,
+ event: 'Approval',
+ args: {
+ owner: subToEth(alice.address),
+ spender: subToEth(bob.address),
+ value: '49',
+ },
+ },
+ ]);
+ });
+
+ itWeb3('Events emitted for transfer()', async ({web3, api, privateKeyWrapper}) => {
+ const alice = privateKeyWrapper('//Alice');
+
+ const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+
+ const receiver = createEthAccount(web3);
+
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 200n)).itemId;
+
+ const address = tokenIdToAddress(collectionId, tokenId);
+ const contract = new web3.eth.Contract(reFungibleTokenAbi as any, address);
+
+ const events = await recordEvents(contract, async () => {
+ expect(await transfer(api, collectionId, tokenId, alice, {Ethereum: receiver}, 51n)).to.be.true;
+ });
+
+ expect(events).to.be.deep.equal([
+ {
+ address,
+ event: 'Transfer',
+ args: {
+ from: subToEth(alice.address),
+ to: receiver,
+ value: '51',
+ },
+ },
+ ]);
+ });
+});
tests/src/eth/reFungibleTokenAbi.jsondiffbeforeafterboth--- /dev/null
+++ b/tests/src/eth/reFungibleTokenAbi.json
@@ -0,0 +1,158 @@
+[
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint256",
+ "name": "value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "uint256",
+ "name": "value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ {
+ "inputs": [
+ { "internalType": "address", "name": "owner", "type": "address" },
+ { "internalType": "address", "name": "spender", "type": "address" }
+ ],
+ "name": "allowance",
+ "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "address", "name": "spender", "type": "address" },
+ { "internalType": "uint256", "name": "amount", "type": "uint256" }
+ ],
+ "name": "approve",
+ "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "address", "name": "owner", "type": "address" }
+ ],
+ "name": "balanceOf",
+ "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "address", "name": "from", "type": "address" },
+ { "internalType": "uint256", "name": "amount", "type": "uint256" }
+ ],
+ "name": "burnFrom",
+ "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "decimals",
+ "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "name",
+ "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "uint256", "name": "amount", "type": "uint256" }
+ ],
+ "name": "repartition",
+ "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" }
+ ],
+ "name": "supportsInterface",
+ "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "address", "name": "to", "type": "address" },
+ { "internalType": "uint256", "name": "amount", "type": "uint256" }
+ ],
+ "name": "transfer",
+ "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ { "internalType": "address", "name": "from", "type": "address" },
+ { "internalType": "address", "name": "to", "type": "address" },
+ { "internalType": "uint256", "name": "amount", "type": "uint256" }
+ ],
+ "name": "transferFrom",
+ "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ }
+]
tests/src/refungible.test.tsdiffbeforeafterboth--- a/tests/src/refungible.test.ts
+++ b/tests/src/refungible.test.ts
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
-import {default as usingApi, executeTransaction} from './substrate/substrate-api';
+import {default as usingApi, submitTransactionAsync} from './substrate/substrate-api';
import {IKeyringPair} from '@polkadot/types/types';
import {
createCollectionExpectSuccess,
@@ -34,6 +34,8 @@
getDetailedCollectionInfo,
normalizeAccountId,
CrossAccountId,
+ getCreateItemsResult,
+ getDestroyItemsResult,
} from './util/helpers';
import chai from 'chai';
@@ -226,6 +228,46 @@
await expect(transfer(api, collectionId, tokenId, bob, alice, 160n)).to.eventually.be.rejected;
});
});
+
+ it('Repartition with increased amount', async () => {
+ await usingApi(async api => {
+ const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
+
+ const tx = api.tx.unique.repartition(collectionId, tokenId, 200n);
+ const events = await submitTransactionAsync(alice, tx);
+ const substrateEvents = getCreateItemsResult(events);
+ expect(substrateEvents).to.include.deep.members([
+ {
+ success: true,
+ collectionId,
+ itemId: tokenId,
+ recipient: {Substrate: alice.address},
+ amount: 100,
+ },
+ ]);
+ });
+ });
+
+ it('Repartition with decreased amount', async () => {
+ await usingApi(async api => {
+ const collectionId = (await createCollection(api, alice, {mode: {type: 'ReFungible'}})).collectionId;
+ const tokenId = (await createRefungibleToken(api, alice, collectionId, 100n)).itemId;
+
+ const tx = api.tx.unique.repartition(collectionId, tokenId, 50n);
+ const events = await submitTransactionAsync(alice, tx);
+ const substrateEvents = getDestroyItemsResult(events);
+ expect(substrateEvents).to.include.deep.members([
+ {
+ success: true,
+ collectionId,
+ itemId: tokenId,
+ owner: {Substrate: alice.address},
+ amount: 50,
+ },
+ ]);
+ });
+ });
});
describe('Test Refungible properties:', () => {
tests/src/util/helpers.tsdiffbeforeafterboth--- a/tests/src/util/helpers.ts
+++ b/tests/src/util/helpers.ts
@@ -103,6 +103,15 @@
collectionId: number;
itemId: number;
recipient?: CrossAccountId;
+ amount?: number;
+}
+
+interface DestroyItemResult {
+ success: boolean;
+ collectionId: number;
+ itemId: number;
+ owner: CrossAccountId;
+ amount: number;
}
interface TransferResult {
@@ -220,12 +229,14 @@
const collectionId = parseInt(data[0].toString(), 10);
const itemId = parseInt(data[1].toString(), 10);
const recipient = normalizeAccountId(data[2].toJSON() as any);
+ const amount = parseInt(data[3].toString(), 10);
const itemRes: CreateItemResult = {
success: true,
collectionId,
itemId,
recipient,
+ amount,
};
results.push(itemRes);
@@ -255,6 +266,31 @@
return result;
}
+export function getDestroyItemsResult(events: EventRecord[]): DestroyItemResult[] {
+ const results: DestroyItemResult[] = [];
+
+ const genericResult = getGenericResult<DestroyItemResult[]>(events, 'common', 'ItemDestroyed', (data) => {
+ const collectionId = parseInt(data[0].toString(), 10);
+ const itemId = parseInt(data[1].toString(), 10);
+ const owner = normalizeAccountId(data[2].toJSON() as any);
+ const amount = parseInt(data[3].toString(), 10);
+
+ const itemRes: DestroyItemResult = {
+ success: true,
+ collectionId,
+ itemId,
+ owner,
+ amount,
+ };
+
+ results.push(itemRes);
+ return results;
+ });
+
+ if (!genericResult.success) return [];
+ return results;
+}
+
export function getTransferResult(api: ApiPromise, events: EventRecord[]): TransferResult {
for (const {event} of events) {
if (api.events.common.Transfer.is(event)) {