git.delta.rocks / unique-network / refs/commits / 905f9b3923e7

difftreelog

refactor fix complex value type support in evm-coder

Yaroslav Bolyukin2023-08-29parent: #1a4aa2d.patch.diff
in: master

14 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2653,8 +2653,9 @@
 
 [[package]]
 name = "evm-coder"
-version = "0.3.6"
-source = "git+https://github.com/uniquenetwork/evm-coder?tag=v0.3.6#be59ae41a5d2ec9389bae22d54f81122b6ba55a4"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b88ae5a449e7e9dfef59c0dd2df396bc56d81a6f4e297490c0c64aa73f7f7ad4"
 dependencies = [
  "ethereum",
  "evm-coder-procedural",
@@ -2665,8 +2666,9 @@
 
 [[package]]
 name = "evm-coder-procedural"
-version = "0.3.6"
-source = "git+https://github.com/uniquenetwork/evm-coder?tag=v0.3.6#be59ae41a5d2ec9389bae22d54f81122b6ba55a4"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6223c1063c1f53380b4b9aaf2e4d82eba4808c661c61265619b804b09b7a6b1a"
 dependencies = [
  "Inflector",
  "hex",
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,7 +27,7 @@
 [workspace.dependencies]
 # Unique
 app-promotion-rpc = { path = "primitives/app_promotion_rpc", default-features = false }
-evm-coder = { git = "https://github.com/uniquenetwork/evm-coder", tag = "v0.3.6", default-features = false, features = [
+evm-coder = { version = "0.4.2", default-features = false, features = [
 	'bondrewd',
 ] }
 pallet-app-promotion = { path = "pallets/app-promotion", default-features = false }
modifiedpallets/common/Cargo.tomldiffbeforeafterboth
--- a/pallets/common/Cargo.toml
+++ b/pallets/common/Cargo.toml
@@ -35,7 +35,8 @@
 	"sp-std/std",
 	"up-data-structs/std",
 	"up-pov-estimate-rpc/std",
+	"evm-coder/std",
 ]
-stubgen = ["evm-coder/stubgen"]
+stubgen = ["evm-coder/stubgen", "up-data-structs/stubgen"]
 tests = []
 try-runtime = ["frame-support/try-runtime"]
modifiedpallets/common/src/eth.rsdiffbeforeafterboth
--- a/pallets/common/src/eth.rs
+++ b/pallets/common/src/eth.rs
@@ -549,8 +549,6 @@
 /// Collection properties
 #[derive(Debug, Default, AbiCoder)]
 pub struct CreateCollectionData {
-	/// Collection sponsor
-	pub pending_sponsor: CrossAddress,
 	/// Collection name
 	pub name: String,
 	/// Collection description
@@ -571,6 +569,8 @@
 	pub nesting_settings: CollectionNestingAndPermission,
 	/// Collection limits
 	pub limits: Vec<CollectionLimitValue>,
+	/// Collection sponsor
+	pub pending_sponsor: CrossAddress,
 	/// Extra collection flags
 	pub flags: CollectionFlags,
 }
modifiedpallets/evm-coder-substrate/src/lib.rsdiffbeforeafterboth
--- a/pallets/evm-coder-substrate/src/lib.rs
+++ b/pallets/evm-coder-substrate/src/lib.rs
@@ -26,7 +26,7 @@
 use frame_support::dispatch::Weight;
 
 use core::marker::PhantomData;
-use sp_std::cell::RefCell;
+use sp_std::{cell::RefCell, vec::Vec};
 
 use codec::Decode;
 use frame_support::pallet_prelude::DispatchError;
@@ -46,8 +46,8 @@
 pub use spez::spez;
 
 use evm_coder::{
-	abi::{AbiReader, AbiWrite, AbiWriter},
 	types::{Msg, Value},
+	AbiEncode,
 };
 
 pub use pallet::*;
@@ -168,7 +168,7 @@
 	pub fn evm_to_precompile_output(
 		self,
 		handle: &mut impl PrecompileHandle,
-		result: execution::Result<Option<AbiWriter>>,
+		result: execution::Result<Option<Vec<u8>>>,
 	) -> Option<PrecompileResult> {
 		use execution::Error;
 		// We ignore error here, as it should not occur, as we have our own bookkeeping of gas
@@ -176,18 +176,13 @@
 		Some(match result {
 			Ok(Some(v)) => Ok(PrecompileOutput {
 				exit_status: ExitSucceed::Returned,
-				output: v.finish(),
+				output: v,
 			}),
 			Ok(None) => return None,
-			Err(Error::Revert(e)) => {
-				let mut writer = AbiWriter::new_call(evm_coder::fn_selector!(Error(string)));
-				(&e as &str).abi_write(&mut writer);
-
-				Err(PrecompileFailure::Revert {
-					exit_status: ExitRevert::Reverted,
-					output: writer.finish(),
-				})
-			}
+			Err(Error::Revert(e)) => Err(PrecompileFailure::Revert {
+				exit_status: ExitRevert::Reverted,
+				output: (&e as &str,).abi_encode_call(evm_coder::fn_selector!(Error(string))),
+			}),
 			Err(Error::Fatal(f)) => Err(PrecompileFailure::Fatal { exit_status: f }),
 			Err(Error::Error(e)) => Err(e.into()),
 		})
@@ -266,7 +261,7 @@
 	C: evm_coder::Call + PreDispatch,
 	E: evm_coder::Callable<C> + WithRecorder<T>,
 	H: PrecompileHandle,
-	execution::ResultWithPostInfo<AbiWriter>: From<ResultWithPostInfoOf<E, AbiWriter>>,
+	execution::ResultWithPostInfo<Vec<u8>>: From<ResultWithPostInfoOf<E, Vec<u8>>>,
 {
 	let result = call_internal(
 		handle.context().caller,
@@ -282,18 +277,16 @@
 	e: &mut E,
 	value: Value,
 	input: &[u8],
-) -> execution::Result<Option<AbiWriter>>
+) -> execution::Result<Option<Vec<u8>>>
 where
 	T: Config,
 	C: evm_coder::Call + PreDispatch,
 	E: Contract + evm_coder::Callable<C> + WithRecorder<T>,
-	execution::ResultWithPostInfo<AbiWriter>: From<ResultWithPostInfoOf<E, AbiWriter>>,
+	execution::ResultWithPostInfo<Vec<u8>>: From<ResultWithPostInfoOf<E, Vec<u8>>>,
 {
-	let (selector, mut reader) = AbiReader::new_call(input)?;
-	let call = C::parse(selector, &mut reader)?;
+	let call = C::parse_full(input)?;
 	if call.is_none() {
-		let selector = u32::from_be_bytes(selector);
-		return Err(format!("unrecognized selector: 0x{selector:0<8x}").into());
+		return Err("unrecognized selector".into());
 	}
 	let call = call.unwrap();
 
modifiedpallets/evm-contract-helpers/src/eth.rsdiffbeforeafterboth
--- a/pallets/evm-contract-helpers/src/eth.rs
+++ b/pallets/evm-contract-helpers/src/eth.rs
@@ -19,7 +19,7 @@
 extern crate alloc;
 use core::marker::PhantomData;
 use evm_coder::{
-	abi::{AbiWriter, AbiType},
+	abi::{AbiType, AbiEncode},
 	generate_stubgen, solidity_interface,
 	types::*,
 	ToLog,
@@ -370,11 +370,8 @@
 		{
 			return Some(Err(PrecompileFailure::Revert {
 				exit_status: ExitRevert::Reverted,
-				output: {
-					let mut writer = AbiWriter::new_call(evm_coder::fn_selector!(Error(string)));
-					writer.string("Target contract is allowlisted");
-					writer.finish()
-				},
+				output: ("target contract is allowlisted",)
+					.abi_encode_call(evm_coder::fn_selector!(Error(string))),
 			}));
 		}
 
modifiedpallets/gov-origins/src/lib.rsdiffbeforeafterboth
--- a/pallets/gov-origins/src/lib.rs
+++ b/pallets/gov-origins/src/lib.rs
@@ -31,6 +31,7 @@
 
 	#[derive(PartialEq, Eq, Clone, MaxEncodedLen, Encode, Decode, TypeInfo, RuntimeDebug)]
 	#[pallet::origin]
+	#[non_exhaustive]
 	pub enum Origin {
 		/// Origin able to send proposal from fellowship collective to democracy pallet.
 		FellowshipProposition,
modifiedpallets/unique/src/eth/mod.rsdiffbeforeafterboth
--- a/pallets/unique/src/eth/mod.rs
+++ b/pallets/unique/src/eth/mod.rs
@@ -139,116 +139,114 @@
 	T: Config + pallet_common::Config + pallet_nonfungible::Config + pallet_refungible::Config,
 	T::AccountId: From<[u8; 32]>,
 {
-	/*
-		/// Create a collection
-		/// @return address Address of the newly created collection
-		#[weight(<SelfWeightOf<T>>::create_collection())]
-		#[solidity(rename_selector = "createCollection")]
-		fn create_collection(
-			&mut self,
-			caller: Caller,
-			value: Value,
-			data: eth::CreateCollectionData,
-		) -> Result<Address> {
-			let (caller, name, description, token_prefix) =
-				convert_data::<T>(caller, data.name, data.description, data.token_prefix)?;
-			if data.mode != eth::CollectionMode::Fungible && data.decimals != 0 {
-				return Err("decimals are only supported for NFT and RFT collections".into());
-			}
-			let mode = match data.mode {
-				eth::CollectionMode::Fungible => CollectionMode::Fungible(data.decimals),
-				eth::CollectionMode::Nonfungible => CollectionMode::NFT,
-				eth::CollectionMode::Refungible => CollectionMode::ReFungible,
-			};
+	/// Create a collection
+	/// @return address Address of the newly created collection
+	#[weight(<SelfWeightOf<T>>::create_collection())]
+	#[solidity(rename_selector = "createCollection")]
+	fn create_collection(
+		&mut self,
+		caller: Caller,
+		value: Value,
+		data: eth::CreateCollectionData,
+	) -> Result<Address> {
+		let (caller, name, description, token_prefix) =
+			convert_data::<T>(caller, data.name, data.description, data.token_prefix)?;
+		if data.mode != eth::CollectionMode::Fungible && data.decimals != 0 {
+			return Err("decimals are only supported for NFT and RFT collections".into());
+		}
+		let mode = match data.mode {
+			eth::CollectionMode::Fungible => CollectionMode::Fungible(data.decimals),
+			eth::CollectionMode::Nonfungible => CollectionMode::NFT,
+			eth::CollectionMode::Refungible => CollectionMode::ReFungible,
+		};
 
-			let properties: BoundedVec<_, _> = data
-				.properties
-				.into_iter()
-				.map(eth::Property::try_into)
-				.collect::<Result<Vec<_>>>()?
-				.try_into()
-				.map_err(|_| "too many properties")?;
+		let properties: BoundedVec<_, _> = data
+			.properties
+			.into_iter()
+			.map(eth::Property::try_into)
+			.collect::<Result<Vec<_>>>()?
+			.try_into()
+			.map_err(|_| "too many properties")?;
 
-			let token_property_permissions =
-				eth::TokenPropertyPermission::into_property_key_permissions(
-					data.token_property_permissions,
-				)?
-				.try_into()
-				.map_err(|_| "too many property permissions")?;
+		let token_property_permissions =
+			eth::TokenPropertyPermission::into_property_key_permissions(
+				data.token_property_permissions,
+			)?
+			.try_into()
+			.map_err(|_| "too many property permissions")?;
 
-			let limits = if !data.limits.is_empty() {
-				Some(
-					data.limits
-						.into_iter()
-						.collect::<Result<up_data_structs::CollectionLimits>>()?,
-				)
-			} else {
-				None
-			};
+		let limits = if !data.limits.is_empty() {
+			Some(
+				data.limits
+					.into_iter()
+					.collect::<Result<up_data_structs::CollectionLimits>>()?,
+			)
+		} else {
+			None
+		};
 
-			let pending_sponsor = data.pending_sponsor.into_option_sub_cross_account::<T>()?;
+		let pending_sponsor = data.pending_sponsor.into_option_sub_cross_account::<T>()?;
 
-			let restricted = if !data.nesting_settings.restricted.is_empty() {
-				Some(
-					data.nesting_settings
-						.restricted
-						.iter()
-						.map(map_eth_to_id)
-						.collect::<Option<BTreeSet<_>>>()
-						.ok_or("can't convert address into collection id")?
-						.try_into()
-						.map_err(|_| "too many collections")?,
-				)
-			} else {
-				None
-			};
+		let restricted = if !data.nesting_settings.restricted.is_empty() {
+			Some(
+				data.nesting_settings
+					.restricted
+					.iter()
+					.map(map_eth_to_id)
+					.collect::<Option<BTreeSet<_>>>()
+					.ok_or("can't convert address into collection id")?
+					.try_into()
+					.map_err(|_| "too many collections")?,
+			)
+		} else {
+			None
+		};
 
-			let admin_list = data
-				.admin_list
-				.into_iter()
-				.map(|admin| admin.into_sub_cross_account::<T>())
-				.collect::<Result<Vec<_>>>()?;
+		let admin_list = data
+			.admin_list
+			.into_iter()
+			.map(|admin| admin.into_sub_cross_account::<T>())
+			.collect::<Result<Vec<_>>>()?;
 
-			let flags = data.flags;
-			if !flags.is_allowed_for_user() {
-				return Err("internal flags were used".into());
-			}
+		let flags = data.flags;
+		if !flags.is_allowed_for_user() {
+			return Err("internal flags were used".into());
+		}
 
-			let data = CreateCollectionData {
-				name,
-				mode,
-				description,
-				token_prefix,
-				properties,
-				token_property_permissions,
-				limits,
-				pending_sponsor,
+		let data = CreateCollectionData {
+			name,
+			mode,
+			description,
+			token_prefix,
+			properties,
+			token_property_permissions,
+			limits,
+			pending_sponsor,
+			access: None,
+			permissions: Some(CollectionPermissions {
 				access: None,
-				permissions: Some(CollectionPermissions {
-					access: None,
-					mint_mode: None,
-					nesting: Some(NestingPermissions {
-						token_owner: data.nesting_settings.token_owner,
-						collection_admin: data.nesting_settings.collection_admin,
-						restricted,
-						#[cfg(feature = "runtime-benchmarks")]
-						permissive: true,
-					}),
+				mint_mode: None,
+				nesting: Some(NestingPermissions {
+					token_owner: data.nesting_settings.token_owner,
+					collection_admin: data.nesting_settings.collection_admin,
+					restricted,
+					#[cfg(feature = "runtime-benchmarks")]
+					permissive: true,
 				}),
-				admin_list,
-				flags,
-			};
-			check_sent_amount_equals_collection_creation_price::<T>(value)?;
-			let collection_helpers_address =
-				T::CrossAccountId::from_eth(<T as pallet_common::Config>::ContractAddress::get());
+			}),
+			admin_list,
+			flags,
+		};
+		check_sent_amount_equals_collection_creation_price::<T>(value)?;
+		let collection_helpers_address =
+			T::CrossAccountId::from_eth(<T as pallet_common::Config>::ContractAddress::get());
 
-			let collection_id = T::CollectionDispatch::create(caller, collection_helpers_address, data)
-				.map_err(dispatch_to_evm::<T>)?;
+		let collection_id = T::CollectionDispatch::create(caller, collection_helpers_address, data)
+			.map_err(dispatch_to_evm::<T>)?;
 
-			let address = pallet_common::eth::collection_id_to_address(collection_id);
-			Ok(address)
-		}
-	*/
+		let address = pallet_common::eth::collection_id_to_address(collection_id);
+		Ok(address)
+	}
 
 	/// Create an NFT collection
 	/// @param name Name of the collection
modifiedprimitives/data-structs/Cargo.tomldiffbeforeafterboth
--- a/primitives/data-structs/Cargo.toml
+++ b/primitives/data-structs/Cargo.toml
@@ -39,3 +39,4 @@
 	"sp-runtime/std",
 	"sp-std/std",
 ]
+stubgen = ["evm-coder/stubgen"]
modifiedruntime/common/config/pallets/mod.rsdiffbeforeafterboth
--- a/runtime/common/config/pallets/mod.rs
+++ b/runtime/common/config/pallets/mod.rs
@@ -24,7 +24,7 @@
 		weights::CommonWeights,
 		RelayChainBlockNumberProvider,
 	},
-	Runtime, RuntimeEvent, RuntimeCall, RuntimeOrigin, RUNTIME_NAME, TOKEN_SYMBOL, DECIMALS,
+	Runtime, RuntimeEvent, RuntimeCall, RUNTIME_NAME, TOKEN_SYMBOL, DECIMALS,
 	Balances,
 };
 use frame_support::traits::{ConstU32, ConstU64, Currency};
modifiedruntime/common/ethereum/sponsoring.rsdiffbeforeafterboth
--- a/runtime/common/ethereum/sponsoring.rs
+++ b/runtime/common/ethereum/sponsoring.rs
@@ -17,7 +17,7 @@
 //! Implements EVM sponsoring logic via TransactionValidityHack
 
 use core::{convert::TryInto, marker::PhantomData};
-use evm_coder::{Call, abi::AbiReader};
+use evm_coder::{Call};
 use pallet_common::{CollectionHandle, eth::map_eth_to_id};
 use pallet_evm::account::CrossAccountId;
 use pallet_evm_transaction_payment::CallContext;
@@ -66,11 +66,11 @@
 		if let Some(collection_id) = map_eth_to_id(&call_context.contract_address) {
 			let collection = <CollectionHandle<T>>::new(collection_id)?;
 			let sponsor = collection.sponsorship.sponsor()?.clone();
-			let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?;
+			// let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?;
 			Some(T::CrossAccountId::from_sub(match &collection.mode {
 				CollectionMode::NFT => {
 					let collection = NonfungibleHandle::cast(collection);
-					let call = <UniqueNFTCall<T>>::parse(method_id, &mut reader).ok()??;
+					let call = <UniqueNFTCall<T>>::parse_full(&call_context.input).ok()??;
 					match call {
 						UniqueNFTCall::TokenProperties(call) => match call {
 							TokenPropertiesCall::SetProperty {
@@ -161,11 +161,11 @@
 					}
 				}
 				CollectionMode::ReFungible => {
-					let call = <UniqueRefungibleCall<T>>::parse(method_id, &mut reader).ok()??;
+					let call = <UniqueRefungibleCall<T>>::parse_full(&call_context.input).ok()??;
 					refungible::call_sponsor(call, collection, who).map(|()| sponsor)
 				}
 				CollectionMode::Fungible(_) => {
-					let call = <UniqueFungibleCall<T>>::parse(method_id, &mut reader).ok()??;
+					let call = <UniqueFungibleCall<T>>::parse_full(&call_context.input).ok()??;
 					match call {
 						UniqueFungibleCall::ERC20(ERC20Call::Transfer { .. }) => {
 							withdraw_transfer::<T>(&collection, who, &TokenId::default())
@@ -196,8 +196,7 @@
 			// Token existance isn't checked at this point and should be checked in `withdraw` method.
 			let token = RefungibleTokenHandle(rft_collection, token_id);
 
-			let (method_id, mut reader) = AbiReader::new_call(&call_context.input).ok()?;
-			let call = <UniqueRefungibleTokenCall<T>>::parse(method_id, &mut reader).ok()??;
+			let call = <UniqueRefungibleTokenCall<T>>::parse_full(&call_context.input).ok()??;
 			Some(T::CrossAccountId::from_sub(
 				refungible::token_call_sponsor(call, token, who).map(|()| sponsor)?,
 			))
modifiedtests/src/eth/abi/collectionHelpers.jsondiffbeforeafterboth
--- a/tests/src/eth/abi/collectionHelpers.json
+++ b/tests/src/eth/abi/collectionHelpers.json
@@ -77,15 +77,6 @@
     "inputs": [
       {
         "components": [
-          {
-            "components": [
-              { "internalType": "address", "name": "eth", "type": "address" },
-              { "internalType": "uint256", "name": "sub", "type": "uint256" }
-            ],
-            "internalType": "struct CrossAddress",
-            "name": "pending_sponsor",
-            "type": "tuple"
-          },
           { "internalType": "string", "name": "name", "type": "string" },
           { "internalType": "string", "name": "description", "type": "string" },
           {
@@ -170,6 +161,15 @@
             "type": "tuple[]"
           },
           {
+            "components": [
+              { "internalType": "address", "name": "eth", "type": "address" },
+              { "internalType": "uint256", "name": "sub", "type": "uint256" }
+            ],
+            "internalType": "struct CrossAddress",
+            "name": "pending_sponsor",
+            "type": "tuple"
+          },
+          {
             "internalType": "CollectionFlags",
             "name": "flags",
             "type": "uint8"
modifiedtests/src/eth/util/playgrounds/types.tsdiffbeforeafterboth
--- a/tests/src/eth/util/playgrounds/types.ts
+++ b/tests/src/eth/util/playgrounds/types.ts
@@ -59,8 +59,8 @@
 }
 
 export enum CollectionMode {
+	Nonfungible,
 	Fungible,
-	Nonfungible,
 	Refungible,
 }
 
@@ -129,4 +129,4 @@
     else
       this.decimals = 0;
   }
-}
\ No newline at end of file
+}
modifiedtests/src/eth/util/playgrounds/unique.dev.tsdiffbeforeafterboth
before · tests/src/eth/util/playgrounds/unique.dev.ts
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 solc from 'solc';1516import {evmToAddress} from '@polkadot/util-crypto';17import {IKeyringPair} from '@polkadot/types/types';1819import {ArrangeGroup, DevUniqueHelper} from '../../../util/playgrounds/unique.dev';2021import {ContractImports, CompiledContract, CrossAddress, NormalizedEvent, EthProperty, CollectionMode, CreateCollectionData} from './types';2223// Native contracts ABI24import collectionHelpersAbi from '../../abi/collectionHelpers.json' assert {type: 'json'};25import nativeFungibleAbi from '../../abi/nativeFungible.json' assert {type: 'json'};26import fungibleAbi from '../../abi/fungible.json' assert {type: 'json'};27import fungibleDeprecatedAbi from '../../abi/fungibleDeprecated.json' assert {type: 'json'};28import nonFungibleAbi from '../../abi/nonFungible.json' assert {type: 'json'};29import nonFungibleDeprecatedAbi from '../../abi/nonFungibleDeprecated.json' assert {type: 'json'};30import refungibleAbi from '../../abi/reFungible.json' assert {type: 'json'};31import refungibleDeprecatedAbi from '../../abi/reFungibleDeprecated.json' assert {type: 'json'};32import refungibleTokenAbi from '../../abi/reFungibleToken.json' assert {type: 'json'};33import refungibleTokenDeprecatedAbi from '../../abi/reFungibleTokenDeprecated.json' assert {type: 'json'};34import contractHelpersAbi from '../../abi/contractHelpers.json' assert {type: 'json'};35import {ICrossAccountId, TEthereumAccount} from '../../../util/playgrounds/types';36import {TCollectionMode} from '../../../util/playgrounds/types';3738class EthGroupBase {39  helper: EthUniqueHelper;40  gasPrice?: string;4142  constructor(helper: EthUniqueHelper) {43    this.helper = helper;44  }45  async getGasPrice() {46    if(this.gasPrice)47      return this.gasPrice;48    this.gasPrice = await this.helper.getWeb3().eth.getGasPrice();49    return this.gasPrice;50  }51}5253class ContractGroup extends EthGroupBase {54  async findImports(imports?: ContractImports[]) {55    if(!imports) return function(path: string) {56      return {error: `File not found: ${path}`};57    };5859    const knownImports = {} as { [key: string]: string };60    for(const imp of imports) {61      knownImports[imp.solPath] = (await readFile(imp.fsPath)).toString();62    }6364    return function(path: string) {65      if(path in knownImports) return {contents: knownImports[path]};66      return {error: `File not found: ${path}`};67    };68  }6970  async compile(name: string, src: string, imports?: ContractImports[]): Promise<CompiledContract> {71    const compiled = JSON.parse(solc.compile(JSON.stringify({72      language: 'Solidity',73      sources: {74        [`${name}.sol`]: {75          content: src,76        },77      },78      settings: {79        outputSelection: {80          '*': {81            '*': ['*'],82          },83        },84      },85    }), {import: await this.findImports(imports)}));8687    const hasErrors = compiled['errors']88      && compiled['errors'].length > 089      && compiled.errors.some(function(err: any) {90        return err.severity == 'error';91      });9293    if(hasErrors) {94      throw compiled.errors;95    }96    const out = compiled.contracts[`${name}.sol`][name];9798    return {99      abi: out.abi,100      object: '0x' + out.evm.bytecode.object,101    };102  }103104  async deployByCode(signer: string, name: string, src: string, imports?: ContractImports[], gas?: number, args?: any[]): Promise<Contract> {105    const compiledContract = await this.compile(name, src, imports);106    return this.deployByAbi(signer, compiledContract.abi, compiledContract.object, gas, args);107  }108109  async deployByAbi(signer: string, abi: any, object: string, gas?: number, args?: any[]): Promise<Contract> {110    const web3 = this.helper.getWeb3();111    const contract = new web3.eth.Contract(abi, undefined, {112      data: object,113      from: signer,114      gas: gas ?? this.helper.eth.DEFAULT_GAS,115    });116    return await contract.deploy({data: object, arguments: args}).send({from: signer});117  }118119}120121class NativeContractGroup extends EthGroupBase {122123  contractHelpers(caller: string) {124    const web3 = this.helper.getWeb3();125    return new web3.eth.Contract(contractHelpersAbi as any, this.helper.getApi().consts.evmContractHelpers.contractAddress.toString(), {126      from: caller,127      gas: this.helper.eth.DEFAULT_GAS,128    });129  }130131  collectionHelpers(caller: string) {132    const web3 = this.helper.getWeb3();133    return new web3.eth.Contract(collectionHelpersAbi as any, this.helper.getApi().consts.common.contractAddress.toString(), {134      from: caller,135      gas: this.helper.eth.DEFAULT_GAS,136    });137  }138139  collection(address: string, mode: TCollectionMode, caller?: string, mergeDeprecated = false) {140    let abi;141    if(address === this.helper.ethAddress.fromCollectionId(0)) {142      abi = nativeFungibleAbi;143    } else {144      abi ={145        'nft': nonFungibleAbi,146        'rft': refungibleAbi,147        'ft': fungibleAbi,148      }[mode];149    }150    if(mergeDeprecated) {151      const deprecated = {152        'nft': nonFungibleDeprecatedAbi,153        'rft': refungibleDeprecatedAbi,154        'ft': fungibleDeprecatedAbi,155      }[mode];156      abi = [...abi, ...deprecated];157    }158    const web3 = this.helper.getWeb3();159    return new web3.eth.Contract(abi as any, address, {160      gas: this.helper.eth.DEFAULT_GAS,161      ...(caller ? {from: caller} : {}),162    });163  }164165  collectionById(collectionId: number, mode: 'nft' | 'rft' | 'ft', caller?: string, mergeDeprecated = false) {166    return this.collection(this.helper.ethAddress.fromCollectionId(collectionId), mode, caller, mergeDeprecated);167  }168169  rftToken(address: string, caller?: string, mergeDeprecated = false) {170    const web3 = this.helper.getWeb3();171    const abi = mergeDeprecated ? [...refungibleTokenAbi, ...refungibleTokenDeprecatedAbi] : refungibleTokenAbi;172    return new web3.eth.Contract(abi as any, address, {173      gas: this.helper.eth.DEFAULT_GAS,174      ...(caller ? {from: caller} : {}),175    });176  }177178  rftTokenById(collectionId: number, tokenId: number, caller?: string, mergeDeprecated = false) {179    return this.rftToken(this.helper.ethAddress.fromTokenId(collectionId, tokenId), caller, mergeDeprecated);180  }181}182183class CreateCollectionTransaction {184  signer: string;185  data: CreateCollectionData;186  mergeDeprecated: boolean;187  helper: EthUniqueHelper;188189  constructor(helper: EthUniqueHelper, signer: string, data: CreateCollectionData, mergeDeprecated = false) {190    this.helper = helper;191    this.signer = signer;192193    let flags = 0;194    // convert CollectionFlags to number and join them in one number195    if(!data.flags || typeof data.flags == 'number') {196      flags = data.flags ?? 0;197    } else {198      for(let i = 0; i < data.flags.length; i++){199        const flag = data.flags[i];200        flags = flags | flag;201      }202    }203    data.flags = flags;204205    this.data = data;206    this.mergeDeprecated = mergeDeprecated;207  }208209  private async createTransaction() {210    const collectionHelper = await this.helper.ethNativeContract.collectionHelpers(this.signer);211    let collectionMode;212    switch (this.data.collectionMode) {213      case 'nft': collectionMode = CollectionMode.Nonfungible; break;214      case 'rft': collectionMode = CollectionMode.Refungible; break;215      case 'ft': collectionMode = CollectionMode.Fungible; break;216    }217218    const tx = collectionHelper.methods.createCollection([219      this.data.pendingSponsor,220      this.data.name,221      this.data.description,222      this.data.tokenPrefix,223      collectionMode,224      this.data.decimals,225      this.data.properties,226      this.data.tokenPropertyPermissions,227      this.data.adminList,228      this.data.nestingSettings,229      this.data.limits,230      this.data.flags,231    ]);232    return tx;233  }234235  async send(options?: any): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[], collection: Contract }> {236    const collectionCreationPrice = {237      value: Number(this.helper.balance.getCollectionCreationPrice()),238    };239    const tx = await this.createTransaction();240    const result = await tx.send({...options, ...collectionCreationPrice});241242    const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);243    const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);244    const events = this.helper.eth.normalizeEvents(result.events);245    const collection = await this.helper.ethNativeContract.collectionById(collectionId, this.data.collectionMode, this.signer, this.mergeDeprecated);246247    return {collectionId, collectionAddress, events, collection};248  }249250  async call(options?: any) {251    const collectionCreationPrice = {252      value: Number(this.helper.balance.getCollectionCreationPrice()),253    };254    const tx = await this.createTransaction();255256    return await tx.call({...options, ...collectionCreationPrice});257  }258}259260261class EthGroup extends EthGroupBase {262  DEFAULT_GAS = 2_500_000;263264  createAccount() {265    const web3 = this.helper.getWeb3();266    const account = web3.eth.accounts.create();267    web3.eth.accounts.wallet.add(account.privateKey);268    return account.address;269  }270271  async createAccountWithBalance(donor: IKeyringPair, amount = 600n) {272    const account = this.createAccount();273    await this.transferBalanceFromSubstrate(donor, account, amount);274275    return account;276  }277278  async transferBalanceFromSubstrate(donor: IKeyringPair, recepient: string, amount = 100n, inTokens = true) {279    return await this.helper.balance.transferToSubstrate(donor, evmToAddress(recepient), amount * (inTokens ? this.helper.balance.getOneTokenNominal() : 1n));280  }281282  async getCollectionCreationFee(signer: string) {283    const collectionHelper = await this.helper.ethNativeContract.collectionHelpers(signer);284    return await collectionHelper.methods.collectionCreationFee().call();285  }286287  async sendEVM(signer: IKeyringPair, contractAddress: string, abi: string, value: string, gasLimit?: number) {288    if(!gasLimit) gasLimit = this.DEFAULT_GAS;289    const web3 = this.helper.getWeb3();290    // FIXME: can't send legacy transaction using tx.evm.call291    const gasPrice = await web3.eth.getGasPrice();292    // TODO: check execution status293    await this.helper.executeExtrinsic(294      signer,295      'api.tx.evm.call', [this.helper.address.substrateToEth(signer.address), contractAddress, abi, value, gasLimit, gasPrice, null, null, []],296      true,297    );298  }299300  async callEVM(signer: TEthereumAccount, contractAddress: string, abi: string) {301    return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]);302  }303304  createCollectionMethodName(mode: TCollectionMode) {305    switch (mode) {306      case 'ft':307        return 'createFTCollection';308      case 'nft':309        return 'createNFTCollection';310      case 'rft':311        return 'createRFTCollection';312    }313  }314315  createCollection(signer: string, data: CreateCollectionData, mergeDeprecated = false): CreateCollectionTransaction {316    return new CreateCollectionTransaction(this.helper, signer, data, mergeDeprecated);317  }318319  createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {320    return this.createCollection(signer, new CreateCollectionData(name, description, tokenPrefix, 'nft')).send();321  }322323  async createERC721MetadataCompatibleCollection(signer: string, data: CreateCollectionData, baseUri: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {324    const collectionHelper = await this.helper.ethNativeContract.collectionHelpers(signer);325326    const {collectionId, collectionAddress, events} = await this.createCollection(signer, data).send();327328    await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();329330    return {collectionId, collectionAddress, events};331  }332333  async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {334    const collectionHelper = await this.helper.ethNativeContract.collectionHelpers(signer);335336    const {collectionId, collectionAddress, events} = await this.createCollection(signer, new CreateCollectionData(name, description, tokenPrefix, 'nft')).send();337338    await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();339340    return {collectionId, collectionAddress, events};341  }342343  createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {344    return this.createCollection(signer, new CreateCollectionData(name, description, tokenPrefix, 'rft')).send();345  }346347  createFungibleCollection(signer: string, name: string, decimals: number, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {348    return this.createCollection(signer, new CreateCollectionData(name, description, tokenPrefix, 'ft')).send();349  }350351  async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {352    const collectionHelper = await this.helper.ethNativeContract.collectionHelpers(signer);353354    const {collectionId, collectionAddress, events} = await this.createCollection(signer, new CreateCollectionData(name, description, tokenPrefix, 'rft')).send();355356    await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();357358    return {collectionId, collectionAddress, events};359  }360361  async deployCollectorContract(signer: string): Promise<Contract> {362    return await this.helper.ethContract.deployByCode(signer, 'Collector', `363    // SPDX-License-Identifier: UNLICENSED364    pragma solidity ^0.8.6;365366    contract Collector {367      uint256 collected;368      fallback() external payable {369        giveMoney();370      }371      function giveMoney() public payable {372        collected += msg.value;373      }374      function getCollected() public view returns (uint256) {375        return collected;376      }377      function getUnaccounted() public view returns (uint256) {378        return address(this).balance - collected;379      }380381      function withdraw(address payable target) public {382        target.transfer(collected);383        collected = 0;384      }385    }386  `);387  }388389  async deployFlipper(signer: string): Promise<Contract> {390    return await this.helper.ethContract.deployByCode(signer, 'Flipper', `391    // SPDX-License-Identifier: UNLICENSED392    pragma solidity ^0.8.6;393394    contract Flipper {395      bool value = false;396      function flip() public {397        value = !value;398      }399      function getValue() public view returns (bool) {400        return value;401      }402    }403  `);404  }405406  async recordCallFee(user: string, call: () => Promise<any>): Promise<bigint> {407    const before = await this.helper.balance.getEthereum(user);408    await call();409    // In dev mode, the transaction might not finish processing in time410    await this.helper.wait.newBlocks(1);411    const after = await this.helper.balance.getEthereum(user);412413    return before - after;414  }415416  normalizeEvents(events: any): NormalizedEvent[] {417    const output = [];418    for(const key of Object.keys(events)) {419      if(key.match(/^[0-9]+$/)) {420        output.push(events[key]);421      } else if(Array.isArray(events[key])) {422        output.push(...events[key]);423      } else {424        output.push(events[key]);425      }426    }427    output.sort((a, b) => a.logIndex - b.logIndex);428    return output.map(({address, event, returnValues}) => {429      const args: { [key: string]: string } = {};430      for(const key of Object.keys(returnValues)) {431        if(!key.match(/^[0-9]+$/)) {432          args[key] = returnValues[key];433        }434      }435      return {436        address,437        event,438        args,439      };440    });441  }442443  async calculateFee(address: ICrossAccountId, code: () => Promise<any>): Promise<bigint> {444    const wrappedCode = async () => {445      await code();446      // In dev mode, the transaction might not finish processing in time447      await this.helper.wait.newBlocks(1);448    };449    return await this.helper.arrange.calculcateFee(address, wrappedCode);450  }451}452453class EthAddressGroup extends EthGroupBase {454  extractCollectionId(address: string): number {455    if(!(address.length === 42 || address.length === 40)) throw new Error('address wrong format');456    return parseInt(address.slice(address.length - 8), 16);457  }458459  fromCollectionId(collectionId: number): string {460    if(collectionId >= 0xffffffff || collectionId < 0) throw new Error('collectionId overflow');461    return Web3.utils.toChecksumAddress(`0x17c4e6453cc49aaaaeaca894e6d9683e${collectionId.toString(16).padStart(8, '0')}`);462  }463464  extractTokenId(address: string): { collectionId: number, tokenId: number } {465    if(!address.startsWith('0x'))466      throw 'address not starts with "0x"';467    if(address.length > 42)468      throw 'address length is more than 20 bytes';469    return {470      collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)),471      tokenId: Number('0x' + address.substring(address.length - 8)),472    };473  }474475  fromTokenId(collectionId: number, tokenId: number): string {476    return this.helper.util.getTokenAddress({collectionId, tokenId});477  }478479  normalizeAddress(address: string): string {480    return '0x' + address.substring(address.length - 40);481  }482}483export class EthPropertyGroup extends EthGroupBase {484  property(key: string, value: string): EthProperty {485    return [486      key,487      '0x' + Buffer.from(value).toString('hex'),488    ];489  }490}491export type EthUniqueHelperConstructor = new (...args: any[]) => EthUniqueHelper;492493export class EthCrossAccountGroup extends EthGroupBase {494  createAccount(): CrossAddress {495    return this.fromAddress(this.helper.eth.createAccount());496  }497498  async createAccountWithBalance(donor: IKeyringPair, amount = 100n) {499    return this.fromAddress(await this.helper.eth.createAccountWithBalance(donor, amount));500  }501502  fromAddress(address: TEthereumAccount): CrossAddress {503    return {504      eth: address,505      sub: '0',506    };507  }508509  fromAddr(address: TEthereumAccount): [string, string] {510    return [511      address,512      '0',513    ];514  }515516  fromKeyringPair(keyring: IKeyringPair): CrossAddress {517    return {518      eth: '0x0000000000000000000000000000000000000000',519      sub: keyring.addressRaw,520    };521  }522}523524export class FeeGas {525  fee: number | bigint = 0n;526527  gas: number | bigint = 0n;528529  public static async build(helper: EthUniqueHelper, fee: bigint): Promise<FeeGas> {530    const instance = new FeeGas();531    instance.fee = instance.convertToTokens(fee);532    instance.gas = await instance.convertToGas(fee, helper);533    return instance;534  }535536  private async convertToGas(fee: bigint, helper: EthUniqueHelper): Promise<bigint> {537    const gasPrice = BigInt(await helper.getWeb3().eth.getGasPrice());538    return fee / gasPrice;539  }540541  private convertToTokens(value: bigint, nominal = 1_000_000_000_000_000_000n): number {542    return Number((value * 1000n) / nominal) / 1000;543  }544}545546class EthArrangeGroup extends ArrangeGroup {547  helper: EthUniqueHelper;548549  constructor(helper: EthUniqueHelper) {550    super(helper);551    this.helper = helper;552  }553554  async calculcateFeeGas(payer: ICrossAccountId, promise: () => Promise<any>): Promise<FeeGas> {555    const fee = await this.calculcateFee(payer, promise);556    return await FeeGas.build(this.helper, fee);557  }558}559export class EthUniqueHelper extends DevUniqueHelper {560  web3: Web3 | null = null;561  web3Provider: WebsocketProvider | null = null;562563  eth: EthGroup;564  ethAddress: EthAddressGroup;565  ethCrossAccount: EthCrossAccountGroup;566  ethNativeContract: NativeContractGroup;567  ethContract: ContractGroup;568  ethProperty: EthPropertyGroup;569  arrange: EthArrangeGroup;570  constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: { [key: string]: any } = {}) {571    options.helperBase = options.helperBase ?? EthUniqueHelper;572573    super(logger, options);574    this.eth = new EthGroup(this);575    this.ethAddress = new EthAddressGroup(this);576    this.ethCrossAccount = new EthCrossAccountGroup(this);577    this.ethNativeContract = new NativeContractGroup(this);578    this.ethContract = new ContractGroup(this);579    this.ethProperty = new EthPropertyGroup(this);580    this.arrange = new EthArrangeGroup(this);581    super.arrange = this.arrange;582  }583584  getWeb3(): Web3 {585    if(this.web3 === null) throw Error('Web3 not connected');586    return this.web3;587  }588589  connectWeb3(wsEndpoint: string) {590    if(this.web3 !== null) return;591    this.web3Provider = new Web3.providers.WebsocketProvider(wsEndpoint);592    this.web3 = new Web3(this.web3Provider);593  }594595  async disconnect() {596    if(this.web3 === null) return;597    this.web3Provider?.connection.close();598599    await super.disconnect();600  }601602  clearApi() {603    super.clearApi();604    this.web3 = null;605  }606607  clone(helperCls: EthUniqueHelperConstructor, options?: { [key: string]: any; }): EthUniqueHelper {608    const newHelper = super.clone(helperCls, options) as EthUniqueHelper;609    newHelper.web3 = this.web3;610    newHelper.web3Provider = this.web3Provider;611612    return newHelper;613  }614}