From fc406280aa2f70f1b73d5377f5903955fdf22845 Mon Sep 17 00:00:00 2001 From: Grigoriy Simonov Date: Thu, 18 Aug 2022 09:47:20 +0000 Subject: [PATCH] feature/mint-for-fungible-token --- --- a/pallets/fungible/src/erc.rs +++ b/pallets/fungible/src/erc.rs @@ -129,7 +129,23 @@ } } -#[solidity_interface(name = ERC20UniqueExtensions)] +#[solidity_interface(name = "ERC20Mintable")] +impl FungibleHandle { + #[weight(>::create_item())] + fn mint(&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()); + >::create_item(&self, &caller, (to, amount), &budget) + .map_err(dispatch_to_evm::)?; + Ok(true) + } +} + +#[solidity_interface(name = "ERC20UniqueExtensions")] impl FungibleHandle { #[weight(>::burn_from())] fn burn_from(&mut self, caller: caller, from: address, amount: uint256) -> Result { @@ -144,12 +160,29 @@ .map_err(dispatch_to_evm::)?; Ok(true) } + + #[weight(>::create_multiple_items_ex(amounts.len() as u32))] + fn mint_bulk(&mut self, caller: caller, amounts: Vec<(address, uint256)>) -> Result { + let caller = T::CrossAccountId::from_eth(caller); + let budget = self + .recorder + .weight_calls_budget(>::find_parent()); + let amounts = amounts + .into_iter() + .map(|(to, amount)| Ok((T::CrossAccountId::from_eth(to), amount.try_into().map_err(|_| "amount overflow")?))) + .collect::>()?; + + >::create_multiple_items(&self, &caller, amounts, &budget) + .map_err(dispatch_to_evm::)?; + Ok(true) + } } #[solidity_interface( name = UniqueFungible, is( ERC20, + ERC20Mintable, ERC20UniqueExtensions, Collection(common_mut, CollectionHandle), ) --- a/pallets/fungible/src/stubs/UniqueFungible.sol +++ b/pallets/fungible/src/stubs/UniqueFungible.sol @@ -3,7 +3,13 @@ pragma solidity >=0.8.0 <0.9.0; -/// @dev common stubs holder +// Anonymous struct +struct Tuple0 { + address field_0; + uint256 field_1; +} + +// Common stubs holder contract Dummy { uint8 dummy; string stub_error = "this contract is implemented in native"; @@ -21,8 +27,49 @@ } } -/// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0xe54be640 +// Inline +contract ERC20Events { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} + +// Selector: 40c10f19 +contract ERC20Mintable is Dummy, ERC165 { + // Selector: mint(address,uint256) 40c10f19 + function mint(address to, uint256 amount) public returns (bool) { + require(false, stub_error); + to; + amount; + dummy = 0; + return false; + } +} + +// Selector: 63034ac5 +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: mintBulk((address,uint256)[]) 1acf2d55 + function mintBulk(Tuple0[] memory amounts) public returns (bool) { + require(false, stub_error); + amounts; + dummy = 0; + return false; + } +} + +// Selector: 6cf113cd contract Collection is Dummy, ERC165 { /// Set collection property. /// @@ -476,6 +523,7 @@ Dummy, ERC165, ERC20, + ERC20Mintable, ERC20UniqueExtensions, Collection {} --- a/tests/src/eth/api/UniqueFungible.sol +++ b/tests/src/eth/api/UniqueFungible.sol @@ -3,7 +3,13 @@ pragma solidity >=0.8.0 <0.9.0; -/// @dev common stubs holder +// Anonymous struct +struct Tuple0 { + address field_0; + uint256 field_1; +} + +// Common stubs holder interface Dummy { } @@ -12,8 +18,32 @@ function supportsInterface(bytes4 interfaceID) external view returns (bool); } -/// @title A contract that allows you to work with collections. -/// @dev the ERC-165 identifier for this interface is 0xe54be640 +// Inline +interface ERC20Events { + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} + +// Selector: 40c10f19 +interface ERC20Mintable is Dummy, ERC165 { + // Selector: mint(address,uint256) 40c10f19 + function mint(address to, uint256 amount) external returns (bool); +} + +// Selector: 63034ac5 +interface ERC20UniqueExtensions is Dummy, ERC165 { + // Selector: burnFrom(address,uint256) 79cc6790 + function burnFrom(address from, uint256 amount) external returns (bool); + + // Selector: mintBulk((address,uint256)[]) 1acf2d55 + function mintBulk(Tuple0[] memory amounts) external returns (bool); +} + +// Selector: 6cf113cd interface Collection is Dummy, ERC165 { /// Set collection property. /// @@ -207,30 +237,8 @@ /// @dev EVM selector for this function is: 0xd34b55b8, /// or in textual repr: uniqueCollectionType() function uniqueCollectionType() external returns (string memory); - - /// Changes collection owner to another account - /// - /// @dev Owner can be changed only by current owner - /// @param newOwner new owner account - /// @dev EVM selector for this function is: 0x13af4035, - /// or in textual repr: setOwner(address) - function setOwner(address newOwner) external; - - /// Changes collection owner to another substrate account - /// - /// @dev Owner can be changed only by current owner - /// @param newOwner new owner substrate account - /// @dev EVM selector for this function is: 0xb212138f, - /// or in textual repr: setOwnerSubstrate(uint256) - function setOwnerSubstrate(uint256 newOwner) external; } -/// @dev anonymous struct -struct Tuple6 { - address field_0; - uint256 field_1; -} - /// @dev the ERC-165 identifier for this interface is 0x79cc6790 interface ERC20UniqueExtensions is Dummy, ERC165 { /// @dev EVM selector for this function is: 0x79cc6790, @@ -298,6 +306,7 @@ Dummy, ERC165, ERC20, + ERC20Mintable, ERC20UniqueExtensions, Collection {} --- a/tests/src/eth/fungible.test.ts +++ b/tests/src/eth/fungible.test.ts @@ -14,10 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {approveExpectSuccess, createCollectionExpectSuccess, createFungibleItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE} from '../util/helpers'; -import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, transferBalanceToEth} from './util/helpers'; +import {approveExpectSuccess, createCollection, createCollectionExpectSuccess, createFungibleItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE} from '../util/helpers'; +import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, transferBalanceToEth} from './util/helpers'; import fungibleAbi from './fungibleAbi.json'; import {expect} from 'chai'; +import {submitTransactionAsync} from '../substrate/substrate-api'; describe('Fungible: Information getting', () => { itWeb3('totalSupply', async ({api, web3, privateKeyWrapper}) => { @@ -58,6 +59,129 @@ }); describe('Fungible: Plain calls', () => { + itWeb3('Can perform mint()', async ({web3, api, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + const collection = await createCollection(api, alice, { + name: 'token name', + mode: {type: 'Fungible', decimalPoints: 0}, + }); + + const receiver = createEthAccount(web3); + + const collectionIdAddress = collectionIdToAddress(collection.collectionId); + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const changeAdminTx = api.tx.unique.addCollectionAdmin(collection.collectionId, {Ethereum: owner}); + await submitTransactionAsync(alice, changeAdminTx); + + const collectionContract = evmCollection(web3, owner, collectionIdAddress, {type: 'Fungible', decimalPoints: 0}); + const result = await collectionContract.methods.mint(receiver, 100).send(); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver, + value: '100', + }, + }, + ]); + }); + + itWeb3('Can perform mintBulk()', async ({web3, api, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + const collection = await createCollection(api, alice, { + name: 'token name', + mode: {type: 'Fungible', decimalPoints: 0}, + }); + + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const receiver1 = createEthAccount(web3); + const receiver2 = createEthAccount(web3); + const receiver3 = createEthAccount(web3); + + const collectionIdAddress = collectionIdToAddress(collection.collectionId); + const changeAdminTx = api.tx.unique.addCollectionAdmin(collection.collectionId, {Ethereum: owner}); + await submitTransactionAsync(alice, changeAdminTx); + + const collectionContract = evmCollection(web3, owner, collectionIdAddress, {type: 'Fungible', decimalPoints: 0}); + const result = await collectionContract.methods.mintBulk([ + [receiver1, 10], + [receiver2, 20], + [receiver3, 30], + ]).call(); + console.log(result); + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address:collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver1, + value: '10', + }, + }, + { + address:collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver2, + value: '20', + }, + }, + { + address:collectionIdAddress, + event: 'Transfer', + args: { + from: '0x0000000000000000000000000000000000000000', + to: receiver3, + value: '30', + }, + }, + ]); + }); + + itWeb3('Can perform burn()', async ({web3, api, privateKeyWrapper}) => { + const alice = privateKeyWrapper('//Alice'); + const collection = await createCollection(api, alice, { + name: 'token name', + mode: {type: 'Fungible', decimalPoints: 0}, + }); + + const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + const changeAdminTx = api.tx.unique.addCollectionAdmin(collection.collectionId, {Ethereum: owner}); + await submitTransactionAsync(alice, changeAdminTx); + const receiver = await createEthAccountWithBalance(api, web3, privateKeyWrapper); + + const collectionIdAddress = collectionIdToAddress(collection.collectionId); + const collectionContract = evmCollection(web3, owner, collectionIdAddress, {type: 'Fungible', decimalPoints: 0}); + await collectionContract.methods.mint(receiver, 100).send(); + + const result = await collectionContract.methods.burnFrom(receiver, 49).send({from: receiver}); + + const events = normalizeEvents(result.events); + + expect(events).to.be.deep.equal([ + { + address: collectionIdAddress, + event: 'Transfer', + args: { + from: receiver, + to: '0x0000000000000000000000000000000000000000', + value: '49', + }, + }, + ]); + + const balance = await collectionContract.methods.balanceOf(receiver).call(); + expect(balance).to.equal('51'); + }); + itWeb3('Can perform approve()', async ({web3, api, privateKeyWrapper}) => { const collection = await createCollectionExpectSuccess({ name: 'token name', --- a/tests/src/eth/fungibleAbi.json +++ b/tests/src/eth/fungibleAbi.json @@ -151,6 +151,33 @@ "type": "function" }, { + "inputs": [ + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "mint", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "field_0", "type": "address" }, + { "internalType": "uint256", "name": "field_1", "type": "uint256" } + ], + "internalType": "struct Tuple0[]", + "name": "amounts", + "type": "tuple[]" + } + ], + "name": "mintBulk", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { "inputs": [], "name": "getCollectionSponsor", "outputs": [ -- gitstuff