--- a/Cargo.lock +++ b/Cargo.lock @@ -6078,18 +6078,22 @@ name = "pallet-evm-contract-helpers" version = "0.1.0" dependencies = [ + "ethereum", "evm-coder", "fp-evm-mapping", "frame-support", "frame-system", "log", + "pallet-common", "pallet-evm", "pallet-evm-coder-substrate", + "pallet-nonfungible", "parity-scale-codec 3.1.2", "scale-info", "sp-core", "sp-runtime", "sp-std", + "up-data-structs", "up-sponsorship", ] --- a/pallets/evm-contract-helpers/Cargo.toml +++ b/pallets/evm-contract-helpers/Cargo.toml @@ -8,18 +8,28 @@ scale-info = { version = "2.0.1", default-features = false, features = [ "derive", ] } -frame-support = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.21" } -frame-system = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.21" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.21" } -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.21" } -sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.21" } -evm-coder = { default-features = false, path = '../../crates/evm-coder' } -pallet-evm-coder-substrate = { default-features = false, path = '../../pallets/evm-coder-substrate' } +ethereum = { version = "0.12.0", default-features = false } +log = { default-features = false, version = "0.4.14" } + +# Substrate +frame-support = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.21' } +frame-system = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.21' } +sp-runtime = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.21' } +sp-std = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.21' } +sp-core = { default-features = false, git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.21' } + +# Unique pallet-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.21-logs" } fp-evm-mapping = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.21-logs" } up-sponsorship = { version = "0.1.0", default-features = false, git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = "polkadot-v0.9.21" } -log = "0.4.14" +# Locals +evm-coder = { default-features = false, path = '../../crates/evm-coder' } +pallet-common = { default-features = false, path = '../../pallets/common' } +pallet-evm-coder-substrate = { default-features = false, path = '../../pallets/evm-coder-substrate' } +pallet-nonfungible = { default-features = false, path = '../../pallets/nonfungible' } +up-data-structs = { default-features = false, path = '../../primitives/data-structs' } + [dependencies.codec] default-features = false features = ['derive'] --- a/pallets/evm-contract-helpers/src/eth.rs +++ b/pallets/evm-contract-helpers/src/eth.rs @@ -15,11 +15,12 @@ // along with Unique Network. If not, see . use core::marker::PhantomData; -use evm_coder::{abi::AbiWriter, execution::Result, generate_stubgen, solidity_interface, types::*}; +use evm_coder::{abi::AbiWriter, execution::*, generate_stubgen, solidity_interface, types::*, ToLog}; +use ethereum as _; use pallet_evm_coder_substrate::{SubstrateRecorder, WithRecorder}; use pallet_evm::{ ExitRevert, OnCreate, OnMethodCall, PrecompileResult, PrecompileFailure, - account::CrossAccountId, + account::CrossAccountId, Pallet as PalletEvm }; use sp_core::H160; use crate::{ @@ -40,6 +41,16 @@ } } +#[derive(ToLog)] +pub enum ContractHelperEvent { + CollectionCreated { + #[indexed] + owner: address, + #[indexed] + collection_id: address, + }, +} + #[solidity_interface(name = "ContractHelpers")] impl ContractHelpers { fn contract_owner(&self, contract_address: address) -> Result
{ @@ -126,6 +137,25 @@ >::toggle_allowed(contract_address, user, allowed); Ok(()) } + + fn create_721_collection(&self, caller: caller) -> Result
{ + let caller = T::CrossAccountId::from_eth(caller); + let collection_id = >::init_collection( + caller.as_sub().clone(), + Default::default(), + ) + .map_err(pallet_evm_coder_substrate::dispatch_to_evm::)?; + let address = pallet_common::eth::collection_id_to_address(collection_id); + + >::deposit_log( + ContractHelperEvent::CollectionCreated { + owner: *caller.as_eth(), + collection_id: address, + } + .to_log(address), + ); + Ok(address) + } } pub struct HelpersOnMethodCall(PhantomData<*const T>); --- a/pallets/evm-contract-helpers/src/lib.rs +++ b/pallets/evm-contract-helpers/src/lib.rs @@ -31,7 +31,7 @@ #[pallet::config] pub trait Config: - frame_system::Config + pallet_evm_coder_substrate::Config + pallet_evm::account::Config + frame_system::Config + pallet_evm_coder_substrate::Config + pallet_evm::account::Config + pallet_nonfungible::Config { type ContractAddress: Get; type DefaultSponsoringRateLimit: Get; --- a/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol +++ b/pallets/evm-contract-helpers/src/stubs/ContractHelpers.sol @@ -21,7 +21,7 @@ } } -// Selector: 7b4866f9 +// Selector: e123b7a8 contract ContractHelpers is Dummy, ERC165 { // Selector: contractOwner(address) 5152b14c function contractOwner(address contractAddress) @@ -144,4 +144,11 @@ allowed; dummy = 0; } + + // Selector: create721Collection() 9a6bd151 + function create721Collection() public view returns (address) { + require(false, stub_error); + dummy; + return 0x0000000000000000000000000000000000000000; + } } --- a/tests/src/eth/api/ContractHelpers.sol +++ b/tests/src/eth/api/ContractHelpers.sol @@ -12,7 +12,7 @@ function supportsInterface(bytes4 interfaceID) external view returns (bool); } -// Selector: 7b4866f9 +// Selector: e123b7a8 interface ContractHelpers is Dummy, ERC165 { // Selector: contractOwner(address) 5152b14c function contractOwner(address contractAddress) @@ -71,4 +71,7 @@ address user, bool allowed ) external; + + // Selector: create721Collection() 9a6bd151 + function create721Collection() external view returns (address); } --- a/tests/src/eth/base.test.ts +++ b/tests/src/eth/base.test.ts @@ -14,9 +14,21 @@ // You should have received a copy of the GNU General Public License // along with Unique Network. If not, see . -import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, deployFlipper, ethBalanceViaSub, GAS_ARGS, itWeb3, recordEthFee, usingWeb3} from './util/helpers'; +import { + collectionIdFromAddress, + collectionIdToAddress, + contractHelpers, + createEthAccount, + createEthAccountWithBalance, + deployFlipper, + ethBalanceViaSub, + GAS_ARGS, + itWeb3, + recordEthFee, + usingWeb3, +} from './util/helpers'; import {expect} from 'chai'; -import {createCollectionExpectSuccess, createItemExpectSuccess, UNIQUE} from '../util/helpers'; +import {createCollectionExpectSuccess, createItemExpectSuccess, getCreatedCollectionCount, UNIQUE} from '../util/helpers'; import nonFungibleAbi from './nonFungibleAbi.json'; import privateKey from '../substrate/privateKey'; import {Contract} from 'web3-eth-contract'; @@ -112,3 +124,18 @@ expect(await contract(web3).methods.supportsInterface('0x01ffc9a7').call()).to.be.true; }); }); + +describe('Create collection from EVM', () => { + itWeb3('Create collection', async ({api, web3}) => { + const owner = await createEthAccountWithBalance(api, web3); + console.log(owner); + const helpers = contractHelpers(web3, owner); + const collectionCountBefore = await getCreatedCollectionCount(api); + const result = await helpers.methods.create721Collection().send(); + console.log(result.events[0].raw); + const collectionId = collectionIdFromAddress(result.events[0].raw.topics[2]); + const collectionCountAfter = await getCreatedCollectionCount(api); + expect(collectionCountAfter - collectionCountBefore).to.be.eq(1); + expect(collectionId).to.be.eq(collectionCountAfter); + }); +}); --- a/tests/src/eth/util/contractHelpersAbi.json +++ b/tests/src/eth/util/contractHelpersAbi.json @@ -1,216 +1,248 @@ [ - { - "inputs": [ - { - "internalType": "address", - "name": "contract", - "type": "address" - } - ], - "name": "allowlistEnabled", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "address", - "name": "caller", - "type": "address" - } - ], - "name": "allowed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - } - ], - "name": "contractOwner", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - } - ], - "name": "sponsoringEnabled", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "address", - "name": "user", - "type": "address" - }, - { - "internalType": "bool", - "name": "isAllowed", - "type": "bool" - } - ], - "name": "toggleAllowed", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "bool", - "name": "enabled", - "type": "bool" - } - ], - "name": "toggleAllowlist", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "bool", - "name": "enabled", - "type": "bool" - } - ], - "name": "toggleSponsoring", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "uint8", - "name": "mode", - "type": "uint8" - } - ], - "name": "setSponsoringMode", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - } - ], - "name": "sponsoringMode", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "target", - "type": "address" - }, - { - "internalType": "uint32", - "name": "limit", - "type": "uint32" - } - ], - "name": "setSponsoringRateLimit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "contractAddress", - "type": "address" - } - ], - "name": "getSponsoringRateLimit", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - } -] + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "allowed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "allowlistEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "contractOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "create721Collection", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "getSponsoringRateLimit", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "uint8", + "name": "mode", + "type": "uint8" + } + ], + "name": "setSponsoringMode", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "uint32", + "name": "rateLimit", + "type": "uint32" + } + ], + "name": "setSponsoringRateLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "sponsoringEnabled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + } + ], + "name": "sponsoringMode", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceID", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "bool", + "name": "allowed", + "type": "bool" + } + ], + "name": "toggleAllowed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "toggleAllowlist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "contractAddress", + "type": "address" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + } + ], + "name": "toggleSponsoring", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file --- a/tests/src/eth/util/helpers.ts +++ b/tests/src/eth/util/helpers.ts @@ -72,6 +72,9 @@ ]); return Web3.utils.toChecksumAddress('0x' + buf.toString('hex')); } +export function collectionIdFromAddress(address: string): number { + return Number('0x' + address.substring(address.length - 8)); +} export function tokenIdToAddress(collection: number, token: number): string { const buf = Buffer.from([0xf8, 0x23, 0x8c, 0xcf, 0xff, 0x8e, 0xd8, 0x87, 0x46, 0x3f, 0xd5, 0xe0, @@ -84,7 +87,6 @@ return { Ethereum: tokenIdToAddress(collection, token), }; -} export function createEthAccount(web3: Web3) { const account = web3.eth.accounts.create();