difftreelog
feature/mint-for-fungible-token
in: master
5 files changed
pallets/fungible/src/erc.rsdiffbeforeafterboth--- 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<T: Config> FungibleHandle<T> {
+ #[weight(<SelfWeightOf<T>>::create_item())]
+ fn mint(&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>>::create_item(&self, &caller, (to, amount), &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
+}
+
+#[solidity_interface(name = "ERC20UniqueExtensions")]
impl<T: Config> FungibleHandle<T> {
#[weight(<SelfWeightOf<T>>::burn_from())]
fn burn_from(&mut self, caller: caller, from: address, amount: uint256) -> Result<bool> {
@@ -144,12 +160,29 @@
.map_err(dispatch_to_evm::<T>)?;
Ok(true)
}
+
+ #[weight(<SelfWeightOf<T>>::create_multiple_items_ex(amounts.len() as u32))]
+ fn mint_bulk(&mut self, caller: caller, amounts: Vec<(address, uint256)>) -> Result<bool> {
+ let caller = T::CrossAccountId::from_eth(caller);
+ let budget = self
+ .recorder
+ .weight_calls_budget(<StructureWeight<T>>::find_parent());
+ let amounts = amounts
+ .into_iter()
+ .map(|(to, amount)| Ok((T::CrossAccountId::from_eth(to), amount.try_into().map_err(|_| "amount overflow")?)))
+ .collect::<Result<_>>()?;
+
+ <Pallet<T>>::create_multiple_items(&self, &caller, amounts, &budget)
+ .map_err(dispatch_to_evm::<T>)?;
+ Ok(true)
+ }
}
#[solidity_interface(
name = UniqueFungible,
is(
ERC20,
+ ERC20Mintable,
ERC20UniqueExtensions,
Collection(common_mut, CollectionHandle<T>),
)
pallets/fungible/src/stubs/UniqueFungible.soldiffbeforeafterboth--- 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
{}
tests/src/eth/api/UniqueFungible.soldiffbeforeafterboth--- 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
{}
tests/src/eth/fungible.test.tsdiffbeforeafterboth1// 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/>.1617import {approveExpectSuccess, createCollectionExpectSuccess, createFungibleItemExpectSuccess, transferExpectSuccess, transferFromExpectSuccess, UNIQUE} from '../util/helpers';18import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, GAS_ARGS, itWeb3, normalizeEvents, recordEthFee, recordEvents, subToEth, transferBalanceToEth} from './util/helpers';19import fungibleAbi from './fungibleAbi.json';20import {expect} from 'chai';2122describe('Fungible: Information getting', () => {23 itWeb3('totalSupply', async ({api, web3, privateKeyWrapper}) => {24 const collection = await createCollectionExpectSuccess({25 name: 'token name',26 mode: {type: 'Fungible', decimalPoints: 0},27 });28 const alice = privateKeyWrapper('//Alice');2930 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);3132 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Substrate: alice.address});3334 const address = collectionIdToAddress(collection);35 const contract = new web3.eth.Contract(fungibleAbi as any, address, {from: caller, ...GAS_ARGS});36 const totalSupply = await contract.methods.totalSupply().call();3738 expect(totalSupply).to.equal('200');39 });4041 itWeb3('balanceOf', async ({api, web3, privateKeyWrapper}) => {42 const collection = await createCollectionExpectSuccess({43 name: 'token name',44 mode: {type: 'Fungible', decimalPoints: 0},45 });46 const alice = privateKeyWrapper('//Alice');4748 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);4950 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Ethereum: caller});5152 const address = collectionIdToAddress(collection);53 const contract = new web3.eth.Contract(fungibleAbi as any, address, {from: caller, ...GAS_ARGS});54 const balance = await contract.methods.balanceOf(caller).call();5556 expect(balance).to.equal('200');57 });58});5960describe('Fungible: Plain calls', () => {61 itWeb3('Can perform approve()', async ({web3, api, privateKeyWrapper}) => {62 const collection = await createCollectionExpectSuccess({63 name: 'token name',64 mode: {type: 'Fungible', decimalPoints: 0},65 });66 const alice = privateKeyWrapper('//Alice');6768 const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);6970 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Ethereum: owner});7172 const spender = createEthAccount(web3);7374 const address = collectionIdToAddress(collection);75 const contract = new web3.eth.Contract(fungibleAbi as any, address, {from: owner, ...GAS_ARGS});7677 {78 const result = await contract.methods.approve(spender, 100).send({from: owner});79 const events = normalizeEvents(result.events);8081 expect(events).to.be.deep.equal([82 {83 address,84 event: 'Approval',85 args: {86 owner,87 spender,88 value: '100',89 },90 },91 ]);92 }9394 {95 const allowance = await contract.methods.allowance(owner, spender).call();96 expect(+allowance).to.equal(100);97 }98 });99100 itWeb3('Can perform transferFrom()', async ({web3, api, privateKeyWrapper}) => {101 const collection = await createCollectionExpectSuccess({102 name: 'token name',103 mode: {type: 'Fungible', decimalPoints: 0},104 });105 const alice = privateKeyWrapper('//Alice');106107 const owner = createEthAccount(web3);108 await transferBalanceToEth(api, alice, owner);109110 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Ethereum: owner});111112 const spender = createEthAccount(web3);113 await transferBalanceToEth(api, alice, spender);114115 const receiver = createEthAccount(web3);116117 const address = collectionIdToAddress(collection);118 const contract = new web3.eth.Contract(fungibleAbi as any, address, {from: owner, ...GAS_ARGS});119120 await contract.methods.approve(spender, 100).send();121122 {123 const result = await contract.methods.transferFrom(owner, receiver, 49).send({from: spender});124 const events = normalizeEvents(result.events);125 expect(events).to.be.deep.equal([126 {127 address,128 event: 'Transfer',129 args: {130 from: owner,131 to: receiver,132 value: '49',133 },134 },135 {136 address,137 event: 'Approval',138 args: {139 owner,140 spender,141 value: '51',142 },143 },144 ]);145 }146147 {148 const balance = await contract.methods.balanceOf(receiver).call();149 expect(+balance).to.equal(49);150 }151152 {153 const balance = await contract.methods.balanceOf(owner).call();154 expect(+balance).to.equal(151);155 }156 });157158 itWeb3('Can perform transfer()', async ({web3, api, privateKeyWrapper}) => {159 const collection = await createCollectionExpectSuccess({160 name: 'token name',161 mode: {type: 'Fungible', decimalPoints: 0},162 });163 const alice = privateKeyWrapper('//Alice');164165 const owner = createEthAccount(web3);166 await transferBalanceToEth(api, alice, owner);167168 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Ethereum: owner});169170 const receiver = createEthAccount(web3);171 await transferBalanceToEth(api, alice, receiver);172173 const address = collectionIdToAddress(collection);174 const contract = new web3.eth.Contract(fungibleAbi as any, address, {from: owner, ...GAS_ARGS});175176 {177 const result = await contract.methods.transfer(receiver, 50).send({from: owner});178 const events = normalizeEvents(result.events);179 expect(events).to.be.deep.equal([180 {181 address,182 event: 'Transfer',183 args: {184 from: owner,185 to: receiver,186 value: '50',187 },188 },189 ]);190 }191192 {193 const balance = await contract.methods.balanceOf(owner).call();194 expect(+balance).to.equal(150);195 }196197 {198 const balance = await contract.methods.balanceOf(receiver).call();199 expect(+balance).to.equal(50);200 }201 });202});203204describe('Fungible: Fees', () => {205 itWeb3('approve() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {206 const collection = await createCollectionExpectSuccess({207 mode: {type: 'Fungible', decimalPoints: 0},208 });209 const alice = privateKeyWrapper('//Alice');210211 const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);212 const spender = createEthAccount(web3);213214 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Ethereum: owner});215216 const address = collectionIdToAddress(collection);217 const contract = new web3.eth.Contract(fungibleAbi as any, address, {from: owner, ...GAS_ARGS});218219 const cost = await recordEthFee(api, owner, () => contract.methods.approve(spender, 100).send({from: owner}));220 expect(cost < BigInt(0.2 * Number(UNIQUE)));221 });222223 itWeb3('transferFrom() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {224 const collection = await createCollectionExpectSuccess({225 mode: {type: 'Fungible', decimalPoints: 0},226 });227 const alice = privateKeyWrapper('//Alice');228229 const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);230 const spender = await createEthAccountWithBalance(api, web3, privateKeyWrapper);231232 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Ethereum: owner});233234 const address = collectionIdToAddress(collection);235 const contract = new web3.eth.Contract(fungibleAbi as any, address, {from: owner, ...GAS_ARGS});236237 await contract.methods.approve(spender, 100).send({from: owner});238239 const cost = await recordEthFee(api, spender, () => contract.methods.transferFrom(owner, spender, 100).send({from: spender}));240 expect(cost < BigInt(0.2 * Number(UNIQUE)));241 });242243 itWeb3('transfer() call fee is less than 0.2UNQ', async ({web3, api, privateKeyWrapper}) => {244 const collection = await createCollectionExpectSuccess({245 mode: {type: 'Fungible', decimalPoints: 0},246 });247 const alice = privateKeyWrapper('//Alice');248249 const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);250 const receiver = createEthAccount(web3);251252 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n}, {Ethereum: owner});253254 const address = collectionIdToAddress(collection);255 const contract = new web3.eth.Contract(fungibleAbi as any, address, {from: owner, ...GAS_ARGS});256257 const cost = await recordEthFee(api, owner, () => contract.methods.transfer(receiver, 100).send({from: owner}));258 expect(cost < BigInt(0.2 * Number(UNIQUE)));259 });260});261262describe('Fungible: Substrate calls', () => {263 itWeb3('Events emitted for approve()', async ({web3, privateKeyWrapper}) => {264 const collection = await createCollectionExpectSuccess({265 mode: {type: 'Fungible', decimalPoints: 0},266 });267 const alice = privateKeyWrapper('//Alice');268269 const receiver = createEthAccount(web3);270271 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n});272273 const address = collectionIdToAddress(collection);274 const contract = new web3.eth.Contract(fungibleAbi as any, address);275276 const events = await recordEvents(contract, async () => {277 await approveExpectSuccess(collection, 0, alice, {Ethereum: receiver}, 100);278 });279280 expect(events).to.be.deep.equal([281 {282 address,283 event: 'Approval',284 args: {285 owner: subToEth(alice.address),286 spender: receiver,287 value: '100',288 },289 },290 ]);291 });292293 itWeb3('Events emitted for transferFrom()', async ({web3, privateKeyWrapper}) => {294 const collection = await createCollectionExpectSuccess({295 mode: {type: 'Fungible', decimalPoints: 0},296 });297 const alice = privateKeyWrapper('//Alice');298 const bob = privateKeyWrapper('//Bob');299300 const receiver = createEthAccount(web3);301302 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n});303 await approveExpectSuccess(collection, 0, alice, bob.address, 100);304305 const address = collectionIdToAddress(collection);306 const contract = new web3.eth.Contract(fungibleAbi as any, address);307308 const events = await recordEvents(contract, async () => {309 await transferFromExpectSuccess(collection, 0, bob, alice, {Ethereum: receiver}, 51, 'Fungible');310 });311312 expect(events).to.be.deep.equal([313 {314 address,315 event: 'Transfer',316 args: {317 from: subToEth(alice.address),318 to: receiver,319 value: '51',320 },321 },322 {323 address,324 event: 'Approval',325 args: {326 owner: subToEth(alice.address),327 spender: subToEth(bob.address),328 value: '49',329 },330 },331 ]);332 });333334 itWeb3('Events emitted for transfer()', async ({web3, privateKeyWrapper}) => {335 const collection = await createCollectionExpectSuccess({336 mode: {type: 'Fungible', decimalPoints: 0},337 });338 const alice = privateKeyWrapper('//Alice');339340 const receiver = createEthAccount(web3);341342 await createFungibleItemExpectSuccess(alice, collection, {Value: 200n});343344 const address = collectionIdToAddress(collection);345 const contract = new web3.eth.Contract(fungibleAbi as any, address);346347 const events = await recordEvents(contract, async () => {348 await transferExpectSuccess(collection, 0, alice, {Ethereum:receiver}, 51, 'Fungible');349 });350351 expect(events).to.be.deep.equal([352 {353 address,354 event: 'Transfer',355 args: {356 from: subToEth(alice.address),357 to: receiver,358 value: '51',359 },360 },361 ]);362 });363});tests/src/eth/fungibleAbi.jsondiffbeforeafterboth--- 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": [