difftreelog
add EVM event for `destoyCollection`, refactor `Unique` pallet code, add test for events
in: master
14 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5825,7 +5825,7 @@
[[package]]
name = "pallet-common"
-version = "0.1.8"
+version = "0.1.9"
dependencies = [
"ethereum",
"evm-coder",
pallets/common/CHANGELOG.mddiffbeforeafterboth--- a/pallets/common/CHANGELOG.md
+++ b/pallets/common/CHANGELOG.md
@@ -2,29 +2,37 @@
All notable changes to this project will be documented in this file.
+## [0.1.9] - 2022-10-13
+
+## Added
+
+- EVM event for `destroy_collection`.
+
## [0.1.8] - 2022-08-24
## Added
- - Eth methods for collection
- + set_collection_sponsor_substrate
- + has_collection_pending_sponsor
- + remove_collection_sponsor
- + get_collection_sponsor
+
+- Eth methods for collection
+ - set_collection_sponsor_substrate
+ - has_collection_pending_sponsor
+ - remove_collection_sponsor
+ - get_collection_sponsor
- Add convert function from `uint256` to `CrossAccountId`.
## [0.1.7] - 2022-08-19
### Added
- - Add convert funtion from `CrossAccountId` to eth `uint256`.
+- Add convert funtion from `CrossAccountId` to eth `uint256`.
-
## [0.1.6] - 2022-08-16
### Added
-- New Ethereum API methods: changeOwner, changeOwner(Substrate) and verifyOwnerOrAdmin(Substrate).
+- New Ethereum API methods: changeOwner, changeOwner(Substrate) and verifyOwnerOrAdmin(Substrate).
+
<!-- bureaucrate goes here -->
+
## [v0.1.5] 2022-08-16
### Other changes
@@ -45,19 +53,21 @@
- build: Upgrade polkadot to v0.9.25 cdfb9bdc7b205ff1b5134f034ef9973d769e5e6b
## [0.1.3] - 2022-07-25
+
### Add
-- Some static property keys and values.
+- Some static property keys and values.
+
## [0.1.2] - 2022-07-20
### Fixed
-- Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid
- mutability modifiers, causing invalid stub/abi generation.
+- Some methods in `#[solidity_interface]` for `CollectionHandle` had invalid
+ mutability modifiers, causing invalid stub/abi generation.
## [0.1.1] - 2022-07-14
### Added
- - 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.
+- 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.
pallets/common/Cargo.tomldiffbeforeafterboth--- a/pallets/common/Cargo.toml
+++ b/pallets/common/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pallet-common"
-version = "0.1.8"
+version = "0.1.9"
license = "GPLv3"
edition = "2021"
pallets/common/src/erc.rsdiffbeforeafterboth--- a/pallets/common/src/erc.rs
+++ b/pallets/common/src/erc.rs
@@ -53,6 +53,12 @@
#[indexed]
collection_id: address,
},
+ /// The collection has been destroyed.
+ CollectionDestroyed {
+ /// Collection ID.
+ #[indexed]
+ collection_id: address,
+ },
}
/// Does not always represent a full collection, for RFT it is either
pallets/common/src/lib.rsdiffbeforeafterboth--- a/pallets/common/src/lib.rs
+++ b/pallets/common/src/lib.rs
@@ -999,6 +999,13 @@
<CollectionProperties<T>>::remove(collection.id);
<Pallet<T>>::deposit_event(Event::CollectionDestroyed(collection.id));
+
+ <PalletEvm<T>>::deposit_log(
+ erc::CollectionHelpersEvents::CollectionDestroyed {
+ collection_id: eth::collection_id_to_address(collection.id),
+ }
+ .to_log(T::ContractAddress::get()),
+ );
Ok(())
}
pallets/unique/src/eth/mod.rsdiffbeforeafterboth--- a/pallets/unique/src/eth/mod.rs
+++ b/pallets/unique/src/eth/mod.rs
@@ -19,9 +19,10 @@
use core::marker::PhantomData;
use ethereum as _;
use evm_coder::{execution::*, generate_stubgen, solidity_interface, solidity, weight, types::*};
-use frame_support::{traits::Get, storage::StorageNMap};
+use frame_support::traits::Get;
+
+use crate::Pallet;
-use crate::sp_api_hidden_includes_decl_storage::hidden_include::StorageDoubleMap;
use pallet_common::{
CollectionById,
dispatch::CollectionDispatch,
@@ -39,10 +40,7 @@
CollectionMode, PropertyValue, CollectionFlags,
};
-use crate::{
- Config, SelfWeightOf, weights::WeightInfo, NftTransferBasket, FungibleTransferBasket,
- ReFungibleTransferBasket, NftApproveBasket, FungibleApproveBasket, RefungibleApproveBasket,
-};
+use crate::{Config, SelfWeightOf, weights::WeightInfo};
use sp_std::vec::Vec;
use alloc::format;
@@ -302,30 +300,13 @@
}
#[weight(<SelfWeightOf<T>>::destroy_collection())]
- #[solidity(rename_selector = "destroyCollection")]
fn destroy_collection(&mut self, caller: caller, collection_address: address) -> Result<void> {
let caller = T::CrossAccountId::from_eth(caller);
- let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)
- .ok_or("Invalid collection address format".into())
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
- let collection = <pallet_common::CollectionHandle<T>>::try_get(collection_id)
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
- collection
- .check_is_internal()
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
- T::CollectionDispatch::destroy(caller, collection)
- .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)?;
-
- let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- Ok(())
+ let collection_id = pallet_common::eth::map_eth_to_id(&collection_address)
+ .ok_or("Invalid collection address format")?;
+ <Pallet<T>>::destroy_collection_internal(caller, collection_id)
+ .map_err(pallet_evm_coder_substrate::dispatch_to_evm::<T>)
}
/// Check if a collection exists
pallets/unique/src/eth/stubs/CollectionHelpers.rawdiffbeforeafterbothbinary blob — no preview
pallets/unique/src/eth/stubs/CollectionHelpers.soldiffbeforeafterboth--- a/pallets/unique/src/eth/stubs/CollectionHelpers.sol
+++ b/pallets/unique/src/eth/stubs/CollectionHelpers.sol
@@ -20,6 +20,7 @@
/// @dev inlined interface
contract CollectionHelpersEvents {
event CollectionCreated(address indexed owner, address indexed collectionId);
+ event CollectionDestroyed(address indexed collectionId);
}
/// @title Contract, which allows users to operate with collections
pallets/unique/src/lib.rsdiffbeforeafterboth--- a/pallets/unique/src/lib.rs
+++ b/pallets/unique/src/lib.rs
@@ -362,25 +362,8 @@
#[weight = <SelfWeightOf<T>>::destroy_collection()]
pub fn destroy_collection(origin, collection_id: CollectionId) -> DispatchResult {
let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?);
- let collection = <CollectionHandle<T>>::try_get(collection_id)?;
- collection.check_is_internal()?;
-
- // =========
-
- T::CollectionDispatch::destroy(sender, collection)?;
- // TODO: basket cleanup should be moved elsewhere
- // Maybe runtime dispatch.rs should perform it?
-
- let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
- let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
-
- Ok(())
+ Self::destroy_collection_internal(sender, collection_id)
}
/// Add an address to allow list.
@@ -1151,4 +1134,28 @@
target_collection.save()
}
+
+ #[inline(always)]
+ pub(crate) fn destroy_collection_internal(
+ sender: T::CrossAccountId,
+ collection_id: CollectionId,
+ ) -> DispatchResult {
+ let collection = <CollectionHandle<T>>::try_get(collection_id)?;
+ collection.check_is_internal()?;
+
+ T::CollectionDispatch::destroy(sender, collection)?;
+
+ // TODO: basket cleanup should be moved elsewhere
+ // Maybe runtime dispatch.rs should perform it?
+
+ let _ = <NftTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <FungibleTransferBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <ReFungibleTransferBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
+
+ let _ = <NftApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <FungibleApproveBasket<T>>::clear_prefix(collection_id, u32::MAX, None);
+ let _ = <RefungibleApproveBasket<T>>::clear_prefix((collection_id,), u32::MAX, None);
+
+ Ok(())
+ }
}
tests/.vscode/settings.jsondiffbeforeafterboth--- a/tests/.vscode/settings.json
+++ b/tests/.vscode/settings.json
@@ -1,5 +1,12 @@
{
- "mocha.enabled": true,
- "mochaExplorer.files": "**/*.test.ts",
- "mochaExplorer.require": "ts-node/register"
+ "mocha.enabled": true,
+ "mochaExplorer.files": "**/*.test.ts",
+ "mochaExplorer.require": "ts-node/register",
+ "eslint.format.enable": true,
+ "[javascript]": {
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
+ }
}
tests/src/eth/api/CollectionHelpers.soldiffbeforeafterboth--- a/tests/src/eth/api/CollectionHelpers.sol
+++ b/tests/src/eth/api/CollectionHelpers.sol
@@ -15,6 +15,7 @@
/// @dev inlined interface
interface CollectionHelpersEvents {
event CollectionCreated(address indexed owner, address indexed collectionId);
+ event CollectionDestroyed(address indexed collectionId);
}
/// @title Contract, which allows users to operate with collections
tests/src/eth/collectionHelpersAbi.jsondiffbeforeafterboth--- a/tests/src/eth/collectionHelpersAbi.json
+++ b/tests/src/eth/collectionHelpersAbi.json
@@ -19,6 +19,19 @@
"type": "event"
},
{
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "collectionId",
+ "type": "address"
+ }
+ ],
+ "name": "CollectionDestroyed",
+ "type": "event"
+ },
+ {
"inputs": [],
"name": "collectionCreationFee",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
tests/src/eth/createNFTCollection.test.tsdiffbeforeafterboth--- a/tests/src/eth/createNFTCollection.test.ts
+++ b/tests/src/eth/createNFTCollection.test.ts
@@ -22,7 +22,7 @@
describe('Create NFT collection from EVM', () => {
let donor: IKeyringPair;
- before(async function() {
+ before(async function () {
await usingEthPlaygrounds(async (_helper, privateKey) => {
donor = await privateKey({filename: __filename});
});
@@ -35,10 +35,28 @@
const description = 'Some description';
const prefix = 'token prefix';
- const {collectionId} = await helper.eth.createNFTCollection(owner, name, description, prefix);
- const data = (await helper.rft.getData(collectionId))!;
+ // todo:playgrounds this might fail when in async environment.
+ const collectionCountBefore = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
+ const {collectionId, collectionAddress, events} = await helper.eth.createNFTCollection(owner, name, description, prefix);
+
+ expect(events).to.be.deep.equal([
+ {
+ address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F',
+ event: 'CollectionCreated',
+ args: {
+ owner: owner,
+ collectionId: collectionAddress,
+ },
+ },
+ ]);
+
+ const collectionCountAfter = +(await helper.callRpc('api.rpc.unique.collectionStats')).created;
+
const collection = helper.nft.getCollectionObject(collectionId);
-
+ const data = (await collection.getData())!;
+
+ expect(collectionCountAfter - collectionCountBefore).to.be.eq(1);
+ expect(collectionId).to.be.eq(collectionCountAfter);
expect(data.name).to.be.eq(name);
expect(data.description).to.be.eq(description);
expect(data.raw.tokenPrefix).to.be.eq(prefix);
@@ -57,8 +75,19 @@
const prefix = 'token prefix';
const baseUri = 'BaseURI';
- const {collectionId} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri);
+ const {collectionId, collectionAddress, events} = await helper.eth.createERC721MetadataCompatibleNFTCollection(owner, name, description, prefix, baseUri);
+ expect(events).to.be.deep.equal([
+ {
+ address: '0x6C4E9fE1AE37a41E93CEE429e8E1881aBdcbb54F',
+ event: 'CollectionCreated',
+ args: {
+ owner: owner,
+ collectionId: collectionAddress,
+ },
+ },
+ ]);
+
const collection = helper.nft.getCollectionObject(collectionId);
const data = (await collection.getData())!;
@@ -95,12 +124,12 @@
await collectionHelpers.methods
.createNFTCollection('A', 'A', 'A')
.send({value: Number(2n * helper.balance.getOneTokenNominal())});
-
+
expect(await collectionHelpers.methods
.isCollectionExist(expectedCollectionAddress)
.call()).to.be.true;
});
-
+
itEth('Set sponsorship', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const sponsor = await helper.eth.createAccountWithBalance(donor);
@@ -147,7 +176,7 @@
await collection.methods['setCollectionLimit(string,bool)']('ownerCanTransfer', limits.ownerCanTransfer).send();
await collection.methods['setCollectionLimit(string,bool)']('ownerCanDestroy', limits.ownerCanDestroy).send();
await collection.methods['setCollectionLimit(string,bool)']('transfersEnabled', limits.transfersEnabled).send();
-
+
const data = (await helper.nft.getData(collectionId))!;
expect(data.raw.limits.accountTokenOwnershipLimit).to.be.eq(limits.accountTokenOwnershipLimit);
expect(data.raw.limits.sponsoredDataSize).to.be.eq(limits.sponsoredDataSize);
@@ -166,7 +195,7 @@
expect(await helper.ethNativeContract.collectionHelpers(collectionAddressForNonexistentCollection)
.methods.isCollectionExist(collectionAddressForNonexistentCollection).call())
.to.be.false;
-
+
const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Exister', 'absolutely anything', 'EVC');
expect(await helper.ethNativeContract.collectionHelpers(collectionAddress)
.methods.isCollectionExist(collectionAddress).call())
@@ -178,7 +207,7 @@
let donor: IKeyringPair;
let nominal: bigint;
- before(async function() {
+ before(async function () {
await usingEthPlaygrounds(async (helper, privateKey) => {
donor = await privateKey({filename: __filename});
nominal = helper.balance.getOneTokenNominal();
@@ -197,7 +226,7 @@
await expect(collectionHelper.methods
.createNFTCollection(collectionName, description, tokenPrefix)
.call({value: Number(2n * nominal)})).to.be.rejectedWith('name is too long. Max length is ' + MAX_NAME_LENGTH);
-
+
}
{
const MAX_DESCRIPTION_LENGTH = 256;
@@ -218,7 +247,7 @@
.call({value: Number(2n * nominal)})).to.be.rejectedWith('token_prefix is too long. Max length is ' + MAX_TOKEN_PREFIX_LENGTH);
}
});
-
+
itEth('(!negative test!) Create collection (no funds)', async ({helper}) => {
const owner = await helper.eth.createAccountWithBalance(donor);
const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
@@ -238,7 +267,7 @@
await expect(malfeasantCollection.methods
.setCollectionSponsor(sponsor)
.call()).to.be.rejectedWith(EXPECTED_ERROR);
-
+
const sponsorCollection = helper.ethNativeContract.collection(collectionAddress, 'nft', sponsor);
await expect(sponsorCollection.methods
.confirmCollectionSponsorship()
@@ -259,4 +288,31 @@
.setCollectionLimit('badLimit', 'true')
.call()).to.be.rejectedWith('unknown boolean limit "badLimit"');
});
-});
+
+ itEth('destroyCollection', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const {collectionAddress} = await helper.eth.createNFTCollection(owner, 'Limits', 'absolutely anything', 'OLF');
+ const collectionHelper = helper.ethNativeContract.collectionHelpers(owner);
+
+
+ const result = await collectionHelper.methods
+ .destroyCollection(collectionAddress)
+ .send({from: owner});
+
+ const events = helper.eth.normalizeEvents(result.events);
+
+ expect(events).to.be.deep.equal([
+ {
+ address: collectionHelper.options.address,
+ event: 'CollectionDestroyed',
+ args: {
+ collectionId: collectionAddress,
+ },
+ },
+ ]);
+
+ expect(await collectionHelper.methods
+ .isCollectionExist(collectionAddress)
+ .call()).to.be.false;
+ });
+});
\ No newline at end of file
tests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034/* eslint-disable function-call-argument-newline */5// eslint-disable-next-line @typescript-eslint/triple-slash-reference6/// <reference path="unique.dev.d.ts" />78import {readFile} from 'fs/promises';910import Web3 from 'web3';11import {WebsocketProvider} from 'web3-core';12import {Contract} from 'web3-eth-contract';1314import * as solc from 'solc';1516import {evmToAddress} from '@polkadot/util-crypto';17import {IKeyringPair} from '@polkadot/types/types';1819import {DevUniqueHelper} from '../../../util/playgrounds/unique.dev';2021import {ContractImports, CompiledContract, TEthCrossAccount, NormalizedEvent} from './types';2223// Native contracts ABI24import collectionHelpersAbi from '../../collectionHelpersAbi.json';25import fungibleAbi from '../../fungibleAbi.json';26import nonFungibleAbi from '../../nonFungibleAbi.json';27import refungibleAbi from '../../reFungibleAbi.json';28import refungibleTokenAbi from '../../reFungibleTokenAbi.json';29import contractHelpersAbi from './../contractHelpersAbi.json';30import {ICrossAccountId, TEthereumAccount} from '../../../util/playgrounds/types';3132class EthGroupBase {33 helper: EthUniqueHelper;3435 constructor(helper: EthUniqueHelper) {36 this.helper = helper;37 }38}394041class ContractGroup extends EthGroupBase {42 async findImports(imports?: ContractImports[]){43 if(!imports) return function(path: string) {44 return {error: `File not found: ${path}`};45 };4647 const knownImports = {} as {[key: string]: string};48 for(const imp of imports) {49 knownImports[imp.solPath] = (await readFile(imp.fsPath)).toString();50 }5152 return function(path: string) {53 if(path in knownImports) return {contents: knownImports[path]};54 return {error: `File not found: ${path}`};55 };56 }5758 async compile(name: string, src: string, imports?: ContractImports[]): Promise<CompiledContract> {59 const out = JSON.parse(solc.compile(JSON.stringify({60 language: 'Solidity',61 sources: {62 [`${name}.sol`]: {63 content: src,64 },65 },66 settings: {67 outputSelection: {68 '*': {69 '*': ['*'],70 },71 },72 },73 }), {import: await this.findImports(imports)})).contracts[`${name}.sol`][name];7475 return {76 abi: out.abi,77 object: '0x' + out.evm.bytecode.object,78 };79 }8081 async deployByCode(signer: string, name: string, src: string, imports?: ContractImports[]): Promise<Contract> {82 const compiledContract = await this.compile(name, src, imports);83 return this.deployByAbi(signer, compiledContract.abi, compiledContract.object);84 }8586 async deployByAbi(signer: string, abi: any, object: string): Promise<Contract> {87 const web3 = this.helper.getWeb3();88 const contract = new web3.eth.Contract(abi, undefined, {89 data: object,90 from: signer,91 gas: this.helper.eth.DEFAULT_GAS,92 });93 return await contract.deploy({data: object}).send({from: signer});94 }9596}9798class NativeContractGroup extends EthGroupBase {99100 contractHelpers(caller: string): Contract {101 const web3 = this.helper.getWeb3();102 return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, gas: this.helper.eth.DEFAULT_GAS});103 }104105 collectionHelpers(caller: string) {106 const web3 = this.helper.getWeb3();107 return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, gas: this.helper.eth.DEFAULT_GAS});108 }109110 collection(address: string, mode: 'nft' | 'rft' | 'ft', caller?: string): Contract {111 const abi = {112 'nft': nonFungibleAbi,113 'rft': refungibleAbi,114 'ft': fungibleAbi,115 }[mode];116 const web3 = this.helper.getWeb3();117 return new web3.eth.Contract(abi as any, address, {gas: this.helper.eth.DEFAULT_GAS, ...(caller ? {from: caller} : {})});118 }119120 collectionById(collectionId: number, mode: 'nft' | 'rft' | 'ft', caller?: string): Contract {121 return this.collection(this.helper.ethAddress.fromCollectionId(collectionId), mode, caller);122 }123124 rftToken(address: string, caller?: string): Contract {125 const web3 = this.helper.getWeb3();126 return new web3.eth.Contract(refungibleTokenAbi as any, address, {gas: this.helper.eth.DEFAULT_GAS, ...(caller ? {from: caller} : {})});127 }128129 rftTokenById(collectionId: number, tokenId: number, caller?: string): Contract {130 return this.rftToken(this.helper.ethAddress.fromTokenId(collectionId, tokenId), caller);131 }132}133134135class EthGroup extends EthGroupBase {136 DEFAULT_GAS = 2_500_000;137138 createAccount() {139 const web3 = this.helper.getWeb3();140 const account = web3.eth.accounts.create();141 web3.eth.accounts.wallet.add(account.privateKey);142 return account.address;143 }144145 async createAccountWithBalance(donor: IKeyringPair, amount=100n) {146 const account = this.createAccount();147 await this.transferBalanceFromSubstrate(donor, account, amount);148149 return account;150 }151152 async transferBalanceFromSubstrate(donor: IKeyringPair, recepient: string, amount=100n, inTokens=true) {153 return await this.helper.balance.transferToSubstrate(donor, evmToAddress(recepient), amount * (inTokens ? this.helper.balance.getOneTokenNominal() : 1n));154 }155156 async getCollectionCreationFee(signer: string) {157 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);158 return await collectionHelper.methods.collectionCreationFee().call();159 }160161 async sendEVM(signer: IKeyringPair, contractAddress: string, abi: string, value: string, gasLimit?: number) {162 if(!gasLimit) gasLimit = this.DEFAULT_GAS;163 const web3 = this.helper.getWeb3();164 const gasPrice = await web3.eth.getGasPrice();165 // TODO: check execution status166 await this.helper.executeExtrinsic(167 signer,168 'api.tx.evm.call', [this.helper.address.substrateToEth(signer.address), contractAddress, abi, value, gasLimit, gasPrice, null, null, []],169 true,170 );171 }172173 async callEVM(signer: TEthereumAccount, contractAddress: string, abi: string) {174 return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]);175 }176177 async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {178 const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();179 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);180181 const result = await collectionHelper.methods.createNFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});182183 const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);184 const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);185186 return {collectionId, collectionAddress};187 }188189 async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {190 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);191192 const {collectionId, collectionAddress} = await this.createNFTCollection(signer, name, description, tokenPrefix);193194 await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();195196 return {collectionId, collectionAddress};197 }198199 async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {200 const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();201 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);202203 const result = await collectionHelper.methods.createRFTCollection(name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});204205 const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);206 const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);207208 return {collectionId, collectionAddress};209 }210211 async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string}> {212 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);213214 const {collectionId, collectionAddress} = await this.createRFTCollection(signer, name, description, tokenPrefix);215216 await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();217218 return {collectionId, collectionAddress};219 }220221 async deployCollectorContract(signer: string): Promise<Contract> {222 return await this.helper.ethContract.deployByCode(signer, 'Collector', `223 // SPDX-License-Identifier: UNLICENSED224 pragma solidity ^0.8.6;225226 contract Collector {227 uint256 collected;228 fallback() external payable {229 giveMoney();230 }231 function giveMoney() public payable {232 collected += msg.value;233 }234 function getCollected() public view returns (uint256) {235 return collected;236 }237 function getUnaccounted() public view returns (uint256) {238 return address(this).balance - collected;239 }240241 function withdraw(address payable target) public {242 target.transfer(collected);243 collected = 0;244 }245 }246 `);247 }248249 async deployFlipper(signer: string): Promise<Contract> {250 return await this.helper.ethContract.deployByCode(signer, 'Flipper', `251 // SPDX-License-Identifier: UNLICENSED252 pragma solidity ^0.8.6;253254 contract Flipper {255 bool value = false;256 function flip() public {257 value = !value;258 }259 function getValue() public view returns (bool) {260 return value;261 }262 }263 `);264 }265266 async recordCallFee(user: string, call: () => Promise<any>): Promise<bigint> {267 const before = await this.helper.balance.getEthereum(user);268 await call();269 // In dev mode, the transaction might not finish processing in time270 await this.helper.wait.newBlocks(1);271 const after = await this.helper.balance.getEthereum(user);272273 return before - after;274 }275276 normalizeEvents(events: any): NormalizedEvent[] {277 const output = [];278 for (const key of Object.keys(events)) {279 if (key.match(/^[0-9]+$/)) {280 output.push(events[key]);281 } else if (Array.isArray(events[key])) {282 output.push(...events[key]);283 } else {284 output.push(events[key]);285 }286 }287 output.sort((a, b) => a.logIndex - b.logIndex);288 return output.map(({address, event, returnValues}) => {289 const args: { [key: string]: string } = {};290 for (const key of Object.keys(returnValues)) {291 if (!key.match(/^[0-9]+$/)) {292 args[key] = returnValues[key];293 }294 }295 return {296 address,297 event,298 args,299 };300 });301 }302303 async calculateFee(address: ICrossAccountId, code: () => Promise<any>): Promise<bigint> {304 const wrappedCode = async () => {305 await code();306 // In dev mode, the transaction might not finish processing in time307 await this.helper.wait.newBlocks(1);308 };309 return await this.helper.arrange.calculcateFee(address, wrappedCode);310 }311}312313class EthAddressGroup extends EthGroupBase {314 extractCollectionId(address: string): number {315 if (!(address.length === 42 || address.length === 40)) throw new Error('address wrong format');316 return parseInt(address.substr(address.length - 8), 16);317 }318319 fromCollectionId(collectionId: number): string {320 if (collectionId >= 0xffffffff || collectionId < 0) throw new Error('collectionId overflow');321 return Web3.utils.toChecksumAddress(`0x17c4e6453cc49aaaaeaca894e6d9683e${collectionId.toString(16).padStart(8,'0')}`);322 }323324 extractTokenId(address: string): {collectionId: number, tokenId: number} {325 if (!address.startsWith('0x'))326 throw 'address not starts with "0x"';327 if (address.length > 42)328 throw 'address length is more than 20 bytes';329 return {330 collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)),331 tokenId: Number('0x' + address.substring(address.length - 8)),332 };333 }334335 fromTokenId(collectionId: number, tokenId: number): string {336 return this.helper.util.getTokenAddress({collectionId, tokenId});337 }338339 normalizeAddress(address: string): string {340 return '0x' + address.substring(address.length - 40);341 }342}343344export type EthUniqueHelperConstructor = new (...args: any[]) => EthUniqueHelper;345346export class EthCrossAccountGroup extends EthGroupBase {347 fromAddress(address: TEthereumAccount): TEthCrossAccount {348 return {349 0: address,350 1: '0',351 field_0: address,352 field_1: '0',353 };354 }355356 fromKeyringPair(keyring: IKeyringPair): TEthCrossAccount {357 return {358 0: '0x0000000000000000000000000000000000000000',359 1: keyring.addressRaw,360 field_0: '0x0000000000000000000000000000000000000000',361 field_1: keyring.addressRaw,362 };363 }364}365366export class EthUniqueHelper extends DevUniqueHelper {367 web3: Web3 | null = null;368 web3Provider: WebsocketProvider | null = null;369370 eth: EthGroup;371 ethAddress: EthAddressGroup;372 ethNativeContract: NativeContractGroup;373 ethContract: ContractGroup;374 ethCrossAccount: EthCrossAccountGroup;375376 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {377 options.helperBase = options.helperBase ?? EthUniqueHelper;378379 super(logger, options);380 this.eth = new EthGroup(this);381 this.ethAddress = new EthAddressGroup(this);382 this.ethCrossAccount = new EthCrossAccountGroup(this);383 this.ethNativeContract = new NativeContractGroup(this);384 this.ethContract = new ContractGroup(this);385 }386387 getWeb3(): Web3 {388 if(this.web3 === null) throw Error('Web3 not connected');389 return this.web3;390 }391392 async connectWeb3(wsEndpoint: string) {393 if(this.web3 !== null) return;394 this.web3Provider = new Web3.providers.WebsocketProvider(wsEndpoint);395 this.web3 = new Web3(this.web3Provider);396 }397398 async disconnect() {399 if(this.web3 === null) return;400 this.web3Provider?.connection.close();401402 await super.disconnect();403 }404405 clearApi() {406 super.clearApi();407 this.web3 = null;408 }409410 clone(helperCls: EthUniqueHelperConstructor, options?: { [key: string]: any; }): EthUniqueHelper {411 const newHelper = super.clone(helperCls, options) as EthUniqueHelper;412 newHelper.web3 = this.web3;413 newHelper.web3Provider = this.web3Provider;414415 return newHelper;416 }417}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// SPDX-License-Identifier: Apache-2.034/* eslint-disable function-call-argument-newline */5// eslint-disable-next-line @typescript-eslint/triple-slash-reference6/// <reference path="unique.dev.d.ts" />78import {readFile} from 'fs/promises';910import Web3 from 'web3';11import {WebsocketProvider} from 'web3-core';12import {Contract} from 'web3-eth-contract';1314import * as solc from 'solc';1516import {evmToAddress} from '@polkadot/util-crypto';17import {IKeyringPair} from '@polkadot/types/types';1819import {DevUniqueHelper} from '../../../util/playgrounds/unique.dev';2021import {ContractImports, CompiledContract, TEthCrossAccount, NormalizedEvent} from './types';2223// Native contracts ABI24import collectionHelpersAbi from '../../collectionHelpersAbi.json';25import fungibleAbi from '../../fungibleAbi.json';26import nonFungibleAbi from '../../nonFungibleAbi.json';27import refungibleAbi from '../../reFungibleAbi.json';28import refungibleTokenAbi from '../../reFungibleTokenAbi.json';29import contractHelpersAbi from './../contractHelpersAbi.json';30import {ICrossAccountId, TEthereumAccount} from '../../../util/playgrounds/types';3132class EthGroupBase {33 helper: EthUniqueHelper;3435 constructor(helper: EthUniqueHelper) {36 this.helper = helper;37 }38}394041class ContractGroup extends EthGroupBase {42 async findImports(imports?: ContractImports[]){43 if(!imports) return function(path: string) {44 return {error: `File not found: ${path}`};45 };4647 const knownImports = {} as {[key: string]: string};48 for(const imp of imports) {49 knownImports[imp.solPath] = (await readFile(imp.fsPath)).toString();50 }5152 return function(path: string) {53 if(path in knownImports) return {contents: knownImports[path]};54 return {error: `File not found: ${path}`};55 };56 }5758 async compile(name: string, src: string, imports?: ContractImports[]): Promise<CompiledContract> {59 const out = JSON.parse(solc.compile(JSON.stringify({60 language: 'Solidity',61 sources: {62 [`${name}.sol`]: {63 content: src,64 },65 },66 settings: {67 outputSelection: {68 '*': {69 '*': ['*'],70 },71 },72 },73 }), {import: await this.findImports(imports)})).contracts[`${name}.sol`][name];7475 return {76 abi: out.abi,77 object: '0x' + out.evm.bytecode.object,78 };79 }8081 async deployByCode(signer: string, name: string, src: string, imports?: ContractImports[]): Promise<Contract> {82 const compiledContract = await this.compile(name, src, imports);83 return this.deployByAbi(signer, compiledContract.abi, compiledContract.object);84 }8586 async deployByAbi(signer: string, abi: any, object: string): Promise<Contract> {87 const web3 = this.helper.getWeb3();88 const contract = new web3.eth.Contract(abi, undefined, {89 data: object,90 from: signer,91 gas: this.helper.eth.DEFAULT_GAS,92 });93 return await contract.deploy({data: object}).send({from: signer});94 }9596}9798class NativeContractGroup extends EthGroupBase {99100 contractHelpers(caller: string): Contract {101 const web3 = this.helper.getWeb3();102 return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, gas: this.helper.eth.DEFAULT_GAS});103 }104105 collectionHelpers(caller: string) {106 const web3 = this.helper.getWeb3();107 return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, gas: this.helper.eth.DEFAULT_GAS});108 }109110 collection(address: string, mode: 'nft' | 'rft' | 'ft', caller?: string): Contract {111 const abi = {112 'nft': nonFungibleAbi,113 'rft': refungibleAbi,114 'ft': fungibleAbi,115 }[mode];116 const web3 = this.helper.getWeb3();117 return new web3.eth.Contract(abi as any, address, {gas: this.helper.eth.DEFAULT_GAS, ...(caller ? {from: caller} : {})});118 }119120 collectionById(collectionId: number, mode: 'nft' | 'rft' | 'ft', caller?: string): Contract {121 return this.collection(this.helper.ethAddress.fromCollectionId(collectionId), mode, caller);122 }123124 rftToken(address: string, caller?: string): Contract {125 const web3 = this.helper.getWeb3();126 return new web3.eth.Contract(refungibleTokenAbi as any, address, {gas: this.helper.eth.DEFAULT_GAS, ...(caller ? {from: caller} : {})});127 }128129 rftTokenById(collectionId: number, tokenId: number, caller?: string): Contract {130 return this.rftToken(this.helper.ethAddress.fromTokenId(collectionId, tokenId), caller);131 }132}133134135class EthGroup extends EthGroupBase {136 DEFAULT_GAS = 2_500_000;137138 createAccount() {139 const web3 = this.helper.getWeb3();140 const account = web3.eth.accounts.create();141 web3.eth.accounts.wallet.add(account.privateKey);142 return account.address;143 }144145 async createAccountWithBalance(donor: IKeyringPair, amount=100n) {146 const account = this.createAccount();147 await this.transferBalanceFromSubstrate(donor, account, amount);148149 return account;150 }151152 async transferBalanceFromSubstrate(donor: IKeyringPair, recepient: string, amount=100n, inTokens=true) {153 return await this.helper.balance.transferToSubstrate(donor, evmToAddress(recepient), amount * (inTokens ? this.helper.balance.getOneTokenNominal() : 1n));154 }155156 async getCollectionCreationFee(signer: string) {157 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);158 return await collectionHelper.methods.collectionCreationFee().call();159 }160161 async sendEVM(signer: IKeyringPair, contractAddress: string, abi: string, value: string, gasLimit?: number) {162 if(!gasLimit) gasLimit = this.DEFAULT_GAS;163 const web3 = this.helper.getWeb3();164 const gasPrice = await web3.eth.getGasPrice();165 // TODO: check execution status166 await this.helper.executeExtrinsic(167 signer,168 'api.tx.evm.call', [this.helper.address.substrateToEth(signer.address), contractAddress, abi, value, gasLimit, gasPrice, null, null, []],169 true,170 );171 }172173 async callEVM(signer: TEthereumAccount, contractAddress: string, abi: string) {174 return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]);175 }176 177 async createCollecion(functionName: string, signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {178 const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();179 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);180 181 const result = await collectionHelper.methods[functionName](name, description, tokenPrefix).send({value: Number(collectionCreationPrice)});182183 const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);184 const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);185 const events = this.helper.eth.normalizeEvents(result.events);186 187 return {collectionId, collectionAddress, events};188 }189 190 async createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {191 return this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix);192 }193194 async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {195 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);196197 const {collectionId, collectionAddress, events} = await this.createCollecion('createNFTCollection', signer, name, description, tokenPrefix);198199 await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();200201 return {collectionId, collectionAddress, events};202 }203204 async createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string}> {205 return this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix);206 }207208 async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {209 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);210211 const {collectionId, collectionAddress, events} = await this.createCollecion('createRFTCollection', signer, name, description, tokenPrefix);212213 await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();214215 return {collectionId, collectionAddress, events};216 }217218 async deployCollectorContract(signer: string): Promise<Contract> {219 return await this.helper.ethContract.deployByCode(signer, 'Collector', `220 // SPDX-License-Identifier: UNLICENSED221 pragma solidity ^0.8.6;222223 contract Collector {224 uint256 collected;225 fallback() external payable {226 giveMoney();227 }228 function giveMoney() public payable {229 collected += msg.value;230 }231 function getCollected() public view returns (uint256) {232 return collected;233 }234 function getUnaccounted() public view returns (uint256) {235 return address(this).balance - collected;236 }237238 function withdraw(address payable target) public {239 target.transfer(collected);240 collected = 0;241 }242 }243 `);244 }245246 async deployFlipper(signer: string): Promise<Contract> {247 return await this.helper.ethContract.deployByCode(signer, 'Flipper', `248 // SPDX-License-Identifier: UNLICENSED249 pragma solidity ^0.8.6;250251 contract Flipper {252 bool value = false;253 function flip() public {254 value = !value;255 }256 function getValue() public view returns (bool) {257 return value;258 }259 }260 `);261 }262263 async recordCallFee(user: string, call: () => Promise<any>): Promise<bigint> {264 const before = await this.helper.balance.getEthereum(user);265 await call();266 // In dev mode, the transaction might not finish processing in time267 await this.helper.wait.newBlocks(1);268 const after = await this.helper.balance.getEthereum(user);269270 return before - after;271 }272273 normalizeEvents(events: any): NormalizedEvent[] {274 const output = [];275 for (const key of Object.keys(events)) {276 if (key.match(/^[0-9]+$/)) {277 output.push(events[key]);278 } else if (Array.isArray(events[key])) {279 output.push(...events[key]);280 } else {281 output.push(events[key]);282 }283 }284 output.sort((a, b) => a.logIndex - b.logIndex);285 return output.map(({address, event, returnValues}) => {286 const args: { [key: string]: string } = {};287 for (const key of Object.keys(returnValues)) {288 if (!key.match(/^[0-9]+$/)) {289 args[key] = returnValues[key];290 }291 }292 return {293 address,294 event,295 args,296 };297 });298 }299300 async calculateFee(address: ICrossAccountId, code: () => Promise<any>): Promise<bigint> {301 const wrappedCode = async () => {302 await code();303 // In dev mode, the transaction might not finish processing in time304 await this.helper.wait.newBlocks(1);305 };306 return await this.helper.arrange.calculcateFee(address, wrappedCode);307 }308}309310class EthAddressGroup extends EthGroupBase {311 extractCollectionId(address: string): number {312 if (!(address.length === 42 || address.length === 40)) throw new Error('address wrong format');313 return parseInt(address.substr(address.length - 8), 16);314 }315316 fromCollectionId(collectionId: number): string {317 if (collectionId >= 0xffffffff || collectionId < 0) throw new Error('collectionId overflow');318 return Web3.utils.toChecksumAddress(`0x17c4e6453cc49aaaaeaca894e6d9683e${collectionId.toString(16).padStart(8,'0')}`);319 }320321 extractTokenId(address: string): {collectionId: number, tokenId: number} {322 if (!address.startsWith('0x'))323 throw 'address not starts with "0x"';324 if (address.length > 42)325 throw 'address length is more than 20 bytes';326 return {327 collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)),328 tokenId: Number('0x' + address.substring(address.length - 8)),329 };330 }331332 fromTokenId(collectionId: number, tokenId: number): string {333 return this.helper.util.getTokenAddress({collectionId, tokenId});334 }335336 normalizeAddress(address: string): string {337 return '0x' + address.substring(address.length - 40);338 }339}340341export type EthUniqueHelperConstructor = new (...args: any[]) => EthUniqueHelper;342343export class EthCrossAccountGroup extends EthGroupBase {344 fromAddress(address: TEthereumAccount): TEthCrossAccount {345 return {346 0: address,347 1: '0',348 field_0: address,349 field_1: '0',350 };351 }352353 fromKeyringPair(keyring: IKeyringPair): TEthCrossAccount {354 return {355 0: '0x0000000000000000000000000000000000000000',356 1: keyring.addressRaw,357 field_0: '0x0000000000000000000000000000000000000000',358 field_1: keyring.addressRaw,359 };360 }361}362363export class EthUniqueHelper extends DevUniqueHelper {364 web3: Web3 | null = null;365 web3Provider: WebsocketProvider | null = null;366367 eth: EthGroup;368 ethAddress: EthAddressGroup;369 ethNativeContract: NativeContractGroup;370 ethContract: ContractGroup;371 ethCrossAccount: EthCrossAccountGroup;372373 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {374 options.helperBase = options.helperBase ?? EthUniqueHelper;375376 super(logger, options);377 this.eth = new EthGroup(this);378 this.ethAddress = new EthAddressGroup(this);379 this.ethCrossAccount = new EthCrossAccountGroup(this);380 this.ethNativeContract = new NativeContractGroup(this);381 this.ethContract = new ContractGroup(this);382 }383384 getWeb3(): Web3 {385 if(this.web3 === null) throw Error('Web3 not connected');386 return this.web3;387 }388389 async connectWeb3(wsEndpoint: string) {390 if(this.web3 !== null) return;391 this.web3Provider = new Web3.providers.WebsocketProvider(wsEndpoint);392 this.web3 = new Web3(this.web3Provider);393 }394395 async disconnect() {396 if(this.web3 === null) return;397 this.web3Provider?.connection.close();398399 await super.disconnect();400 }401402 clearApi() {403 super.clearApi();404 this.web3 = null;405 }406407 clone(helperCls: EthUniqueHelperConstructor, options?: { [key: string]: any; }): EthUniqueHelper {408 const newHelper = super.clone(helperCls, options) as EthUniqueHelper;409 newHelper.web3 = this.web3;410 newHelper.web3Provider = this.web3Provider;411412 return newHelper;413 }414}