--- 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", --- 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: --- 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. --- 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', ] --- 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 . -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 CommonEvmHandler for RefungibleHandle { - 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 { - // TODO: Implement RFT variant of ERC721 - None - } -} +use crate::{Config, RefungibleHandle}; -pub struct RefungibleTokenHandle(pub RefungibleHandle, pub TokenId); +#[solidity_interface( + name = "UniqueRFT", + is(via("CollectionHandle", common_mut, Collection),) +)] +impl RefungibleHandle where T::AccountId: From<[u8; 32]> {} -impl CommonEvmHandler for RefungibleTokenHandle { - 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 CommonEvmHandler for RefungibleHandle +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 { - // TODO: Implement RFT variant of ERC20 - None + call::, _, _>(handle, self) } } --- /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 . + +//! # 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(pub RefungibleHandle, 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 RefungibleTokenHandle { + /// @return the name of the token. + fn name(&self) -> Result { + Ok(decode_utf16(self.name.iter().copied()) + .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER)) + .collect::()) + } + + /// @return the symbol of the token. + fn symbol(&self) -> Result { + Ok(string::from_utf8_lossy(&self.token_prefix).into()) + } + + /// @dev Total number of tokens in existence + fn total_supply(&self) -> Result { + self.consume_store_reads(1)?; + Ok(>::get((self.id, self.1)).into()) + } + + /// @dev Not supported + fn decimals(&self) -> Result { + // 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 { + self.consume_store_reads(1)?; + let owner = T::CrossAccountId::from_eth(owner); + let balance = >::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(>::transfer())] + fn transfer(&mut self, caller: caller, to: address, amount: uint256) -> Result { + 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(>::find_parent()); + + >::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(>::transfer_from())] + fn transfer_from( + &mut self, + caller: caller, + from: address, + to: address, + amount: uint256, + ) -> Result { + 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(>::find_parent()); + + >::transfer_from(self, &caller, &from, &to, self.1, amount, &budget) + .map_err(dispatch_to_evm::)?; + 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(>::approve())] + fn approve(&mut self, caller: caller, spender: address, amount: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let spender = T::CrossAccountId::from_eth(spender); + let amount = amount.try_into().map_err(|_| "amount overflow")?; + + >::set_allowance(self, &caller, &spender, self.1, amount) + .map_err(dispatch_to_evm::)?; + 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 { + self.consume_store_reads(1)?; + let owner = T::CrossAccountId::from_eth(owner); + let spender = T::CrossAccountId::from_eth(spender); + + Ok(>::get((self.id, self.1, owner, spender)).into()) + } +} + +#[solidity_interface(name = "ERC20UniqueExtensions")] +impl RefungibleTokenHandle { + /// @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(>::burn_from())] + fn burn_from(&mut self, caller: caller, from: address, amount: uint256) -> Result { + 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(>::find_parent()); + + >::burn_from(self, &caller, &from, self.1, amount, &budget) + .map_err(dispatch_to_evm::)?; + 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(>::repartition_item())] + fn repartition(&mut self, caller: caller, amount: uint256) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let amount = amount.try_into().map_err(|_| "amount overflow")?; + + >::repartition(self, &caller, self.1, amount).map_err(dispatch_to_evm::)?; + Ok(true) + } +} + +impl RefungibleTokenHandle { + pub fn into_inner(self) -> RefungibleHandle { + self.0 + } + pub fn common_mut(&mut self) -> &mut RefungibleHandle { + &mut self.0 + } +} + +impl WithRecorder for RefungibleTokenHandle { + fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder { + self.0.recorder() + } + fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder { + self.0.into_recorder() + } +} + +impl Deref for RefungibleTokenHandle { + type Target = RefungibleHandle; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[solidity_interface(name = "UniqueRefungibleToken", is(ERC20, ERC20UniqueExtensions,))] +impl RefungibleTokenHandle where T::AccountId: From<[u8; 32]> {} + +generate_stubgen!(gen_impl, UniqueRefungibleTokenCall<()>, true); +generate_stubgen!(gen_iface, UniqueRefungibleTokenCall<()>, false); + +impl CommonEvmHandler for RefungibleTokenHandle +where + T::AccountId: From<[u8; 32]>, +{ + const CODE: &'static [u8] = include_bytes!("./stubs/UniqueRefungibleToken.raw"); + + fn call(self, handle: &mut impl PrecompileHandle) -> Option { + call::, _, _>(handle, self) + } +} --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -87,30 +87,35 @@ #![cfg_attr(not(feature = "std"), no_std)] -use frame_support::{ensure, fail, BoundedVec, transactional, storage::with_transaction}; -use up_data_structs::{ - AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId, - CreateCollectionData, CreateRefungibleExData, mapping::TokenAddressMapping, budget::Budget, - Property, PropertyScope, TrySetProperty, PropertyKey, PropertyValue, PropertyPermission, - PropertyKeyPermission, -}; -use pallet_evm::account::CrossAccountId; +use crate::erc_token::ERC20Events; + +use codec::{Encode, Decode, MaxEncodedLen}; +use core::ops::Deref; +use evm_coder::ToLog; +use frame_support::{BoundedVec, ensure, fail, storage::with_transaction, transactional}; +use pallet_evm::{account::CrossAccountId, Pallet as PalletEvm}; +use pallet_evm_coder_substrate::WithRecorder; use pallet_common::{ - Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, - CommonCollectionOperations as _, + CommonCollectionOperations, Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, }; use pallet_structure::Pallet as PalletStructure; +use scale_info::TypeInfo; +use sp_core::H160; use sp_runtime::{ArithmeticError, DispatchError, DispatchResult, TransactionOutcome}; use sp_std::{vec::Vec, vec, collections::btree_map::BTreeMap}; -use core::ops::Deref; -use codec::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; +use up_data_structs::{ + AccessMode, budget::Budget, CollectionId, CreateCollectionData, CreateRefungibleExData, + CustomDataLimit, mapping::TokenAddressMapping, MAX_REFUNGIBLE_PIECES, TokenId, Property, + PropertyKey, PropertyKeyPermission, PropertyPermission, PropertyScope, PropertyValue, + TrySetProperty, +}; pub use pallet::*; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; pub mod common; pub mod erc; +pub mod erc_token; pub mod weights; pub(crate) type SelfWeightOf = ::WeightInfo; @@ -271,7 +276,11 @@ pub fn into_inner(self) -> pallet_common::CollectionHandle { self.0 } + pub fn common_mut(&mut self) -> &mut pallet_common::CollectionHandle { + &mut self.0 + } } + impl Deref for RefungibleHandle { type Target = pallet_common::CollectionHandle; @@ -280,6 +289,15 @@ } } +impl WithRecorder for RefungibleHandle { + fn recorder(&self) -> &pallet_evm_coder_substrate::SubstrateRecorder { + self.0.recorder() + } + fn into_recorder(self) -> pallet_evm_coder_substrate::SubstrateRecorder { + self.0.into_recorder() + } +} + impl Pallet { /// Get number of RFT tokens in collection pub fn total_supply(collection: &RefungibleHandle) -> u32 { @@ -460,7 +478,18 @@ >::insert((collection.id, token, owner), balance); } >::insert((collection.id, token), total_supply); - // TODO: ERC20 transfer event + + >::deposit_log( + ERC20Events::Transfer { + from: *owner.as_eth(), + to: H160::default(), + value: amount.into(), + } + .to_log(T::EvmTokenAddressMapping::token_to_address( + collection.id, + token, + )), + ); >::deposit_event(CommonEvent::ItemDestroyed( collection.id, token, @@ -736,7 +765,17 @@ } } - // TODO: ERC20 transfer event + >::deposit_log( + ERC20Events::Transfer { + from: *from.as_eth(), + to: *to.as_eth(), + value: amount.into(), + } + .to_log(T::EvmTokenAddressMapping::token_to_address( + collection.id, + token, + )), + ); >::deposit_event(CommonEvent::Transfer( collection.id, token, @@ -889,7 +928,17 @@ continue; } - // TODO: ERC20 transfer event + >::deposit_log( + ERC20Events::Transfer { + from: H160::default(), + to: *user.as_eth(), + value: amount.into(), + } + .to_log(T::EvmTokenAddressMapping::token_to_address( + collection.id, + TokenId(token_id), + )), + ); >::deposit_event(CommonEvent::ItemCreated( collection.id, TokenId(token_id), @@ -913,7 +962,18 @@ } else { >::insert((collection.id, token, sender, spender), amount); } - // TODO: ERC20 approval event + + >::deposit_log( + ERC20Events::Approval { + owner: *sender.as_eth(), + spender: *spender.as_eth(), + value: amount.into(), + } + .to_log(T::EvmTokenAddressMapping::token_to_address( + collection.id, + token, + )), + ); >::deposit_event(CommonEvent::Approved( collection.id, token, @@ -1091,6 +1151,47 @@ >::insert((collection.id, token, owner), amount); >::insert((collection.id, token), amount); + + if amount > total_pieces { + let mint_amount = amount - total_pieces; + >::deposit_log( + ERC20Events::Transfer { + from: H160::default(), + to: *owner.as_eth(), + value: mint_amount.into(), + } + .to_log(T::EvmTokenAddressMapping::token_to_address( + collection.id, + token, + )), + ); + >::deposit_event(CommonEvent::ItemCreated( + collection.id, + token, + owner.clone(), + mint_amount, + )); + } else if total_pieces > amount { + let burn_amount = total_pieces - amount; + >::deposit_log( + ERC20Events::Transfer { + from: *owner.as_eth(), + to: H160::default(), + value: burn_amount.into(), + } + .to_log(T::EvmTokenAddressMapping::token_to_address( + collection.id, + token, + )), + ); + >::deposit_event(CommonEvent::ItemDestroyed( + collection.id, + token, + owner.clone(), + burn_amount, + )); + } + Ok(()) } --- /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 {} --- 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, }; --- /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 +{} --- /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 . + +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', + }, + }, + ]); + }); +}); --- /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" + } +] --- 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 . -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:', () => { --- 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(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)) {