difftreelog
Add `ecrecovery` and `sr25519` precompiles
in: master
18 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2274,7 +2274,7 @@
"hash256-std-hasher",
"rlp",
"rlp-derive",
- "sha3",
+ "sha3 0.10.6",
"triehash",
]
@@ -2292,7 +2292,7 @@
"rlp",
"scale-info",
"serde",
- "sha3",
+ "sha3 0.10.6",
"triehash",
]
@@ -2350,7 +2350,7 @@
"rlp",
"scale-info",
"serde",
- "sha3",
+ "sha3 0.10.6",
]
[[package]]
@@ -2380,7 +2380,7 @@
"hex",
"proc-macro2",
"quote",
- "sha3",
+ "sha3 0.10.6",
"syn",
]
@@ -2415,7 +2415,7 @@
"environmental",
"evm-core",
"primitive-types 0.12.1",
- "sha3",
+ "sha3 0.10.6",
]
[[package]]
@@ -4958,7 +4958,7 @@
"digest 0.10.6",
"multihash-derive",
"sha2 0.10.6",
- "sha3",
+ "sha3 0.10.6",
"unsigned-varint",
]
@@ -5280,6 +5280,7 @@
"cumulus-primitives-utility",
"derivative",
"evm-coder",
+ "fp-evm",
"fp-evm-mapping",
"fp-rpc",
"fp-self-contained",
@@ -5294,6 +5295,7 @@
"impl-trait-for-tuples",
"log",
"logtest",
+ "num_enum",
"orml-tokens",
"orml-traits",
"orml-vesting",
@@ -5309,6 +5311,7 @@
"pallet-evm-coder-substrate",
"pallet-evm-contract-helpers",
"pallet-evm-migration",
+ "pallet-evm-precompile-simple",
"pallet-evm-transaction-payment",
"pallet-foreign-assets",
"pallet-fungible",
@@ -5333,6 +5336,7 @@
"parachain-info",
"parity-scale-codec 3.2.1",
"polkadot-parachain",
+ "precompile-utils-macro",
"rmrk-rpc",
"scale-info",
"serde",
@@ -6005,6 +6009,16 @@
]
[[package]]
+name = "pallet-evm-precompile-simple"
+version = "2.0.0-dev"
+source = "git+https://github.com/uniquenetwork/frontier?branch=unique-polkadot-v0.9.33#c37ced433a29baad45899d6ab364fe437f97570e"
+dependencies = [
+ "fp-evm",
+ "ripemd",
+ "sp-io",
+]
+
+[[package]]
name = "pallet-evm-transaction-payment"
version = "0.1.1"
dependencies = [
@@ -8595,6 +8609,17 @@
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
+name = "precompile-utils-macro"
+version = "0.1.0"
+dependencies = [
+ "num_enum",
+ "proc-macro2",
+ "quote",
+ "sha3 0.8.2",
+ "syn",
+]
+
+[[package]]
name = "predicates"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8850,6 +8875,7 @@
"cumulus-primitives-utility",
"derivative",
"evm-coder",
+ "fp-evm",
"fp-evm-mapping",
"fp-rpc",
"fp-self-contained",
@@ -8864,6 +8890,7 @@
"impl-trait-for-tuples",
"log",
"logtest",
+ "num_enum",
"orml-tokens",
"orml-traits",
"orml-vesting",
@@ -8879,6 +8906,7 @@
"pallet-evm-coder-substrate",
"pallet-evm-contract-helpers",
"pallet-evm-migration",
+ "pallet-evm-precompile-simple",
"pallet-evm-transaction-payment",
"pallet-foreign-assets",
"pallet-fungible",
@@ -8901,6 +8929,7 @@
"parachain-info",
"parity-scale-codec 3.2.1",
"polkadot-parachain",
+ "precompile-utils-macro",
"rmrk-rpc",
"scale-info",
"serde",
@@ -9249,6 +9278,15 @@
]
[[package]]
+name = "ripemd"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
+dependencies = [
+ "digest 0.10.6",
+]
+
+[[package]]
name = "rlp"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -10961,6 +10999,19 @@
[[package]]
name = "sha3"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf"
+dependencies = [
+ "block-buffer 0.7.3",
+ "byte-tools",
+ "digest 0.8.1",
+ "keccak",
+ "opaque-debug 0.2.3",
+]
+
+[[package]]
+name = "sha3"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9"
@@ -11396,7 +11447,7 @@
"byteorder",
"digest 0.10.6",
"sha2 0.10.6",
- "sha3",
+ "sha3 0.10.6",
"sp-std",
"twox-hash",
]
@@ -13026,6 +13077,7 @@
"cumulus-primitives-utility",
"derivative",
"evm-coder",
+ "fp-evm",
"fp-evm-mapping",
"fp-rpc",
"fp-self-contained",
@@ -13040,6 +13092,7 @@
"impl-trait-for-tuples",
"log",
"logtest",
+ "num_enum",
"orml-tokens",
"orml-traits",
"orml-vesting",
@@ -13055,6 +13108,7 @@
"pallet-evm-coder-substrate",
"pallet-evm-contract-helpers",
"pallet-evm-migration",
+ "pallet-evm-precompile-simple",
"pallet-evm-transaction-payment",
"pallet-foreign-assets",
"pallet-fungible",
@@ -13077,6 +13131,7 @@
"parachain-info",
"parity-scale-codec 3.2.1",
"polkadot-parachain",
+ "precompile-utils-macro",
"rmrk-rpc",
"scale-info",
"serde",
pallets/refungible/src/erc_token.rsdiffbeforeafterboth--- a/pallets/refungible/src/erc_token.rs
+++ b/pallets/refungible/src/erc_token.rs
@@ -19,11 +19,6 @@
//! Provides ERC-20 standart support implementation and EVM API for unique extensions for Refungible Pallet.
//! Method implementations are mostly doing parameter conversion and calling Nonfungible Pallet methods.
-extern crate alloc;
-
-#[cfg(not(feature = "std"))]
-use alloc::format;
-
use core::{
char::{REPLACEMENT_CHARACTER, decode_utf16},
convert::TryInto,
runtime/common/config/ethereum.rsdiffbeforeafterboth--- a/runtime/common/config/ethereum.rs
+++ b/runtime/common/config/ethereum.rs
@@ -7,8 +7,10 @@
use sp_runtime::{RuntimeAppPublic, Perbill};
use crate::{
runtime_common::{
- dispatch::CollectionDispatchT, ethereum::sponsoring::EvmSponsorshipHandler,
- config::sponsoring::DefaultSponsoringRateLimit, DealWithFees,
+ config::sponsoring::DefaultSponsoringRateLimit,
+ DealWithFees,
+ dispatch::CollectionDispatchT,
+ ethereum::{precompiles::UniquePrecompiles, sponsoring::EvmSponsorshipHandler},
},
Runtime, Aura, Balances, RuntimeEvent, ChainId,
};
@@ -34,6 +36,7 @@
const EVM_DISPATCH_RATIO: Perbill = Perbill::from_percent(50);
parameter_types! {
pub BlockGasLimit: U256 = U256::from((NORMAL_DISPATCH_RATIO * EVM_DISPATCH_RATIO * MAXIMUM_BLOCK_WEIGHT / WeightTimePerGas::get()).ref_time());
+ pub PrecompilesValue: UniquePrecompiles<Runtime> = UniquePrecompiles::<_>::new();
}
pub struct EthereumFindAuthor<F>(core::marker::PhantomData<F>);
@@ -62,8 +65,8 @@
type CallOrigin = EnsureAddressTruncated<Self>;
type WithdrawOrigin = EnsureAddressTruncated<Self>;
type AddressMapping = HashedAddressMapping<Self::Hashing>;
- type PrecompilesType = ();
- type PrecompilesValue = ();
+ type PrecompilesType = UniquePrecompiles<Self>;
+ type PrecompilesValue = PrecompilesValue;
type Currency = Balances;
type RuntimeEvent = RuntimeEvent;
type OnMethodCall = (
runtime/common/ethereum/mod.rsdiffbeforeafterboth--- a/runtime/common/ethereum/mod.rs
+++ b/runtime/common/ethereum/mod.rs
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+pub mod precompiles;
pub mod self_contained_call;
pub mod sponsoring;
pub mod transaction_converter;
runtime/common/ethereum/precompiles/mod.rsdiffbeforeafterboth--- /dev/null
+++ b/runtime/common/ethereum/precompiles/mod.rs
@@ -0,0 +1,44 @@
+use pallet_evm::{Precompile, PrecompileHandle, PrecompileResult, PrecompileSet};
+use sp_core::H160;
+use sp_std::marker::PhantomData;
+
+use pallet_evm_precompile_simple::{ECRecover};
+use sr25519::Sr25519Precompile;
+
+mod sr25519;
+mod utils;
+
+pub struct UniquePrecompiles<R>(PhantomData<R>);
+
+impl<R> UniquePrecompiles<R>
+where
+ R: pallet_evm::Config,
+{
+ pub fn new() -> Self {
+ Self(Default::default())
+ }
+ pub fn used_addresses() -> [H160; 2] {
+ [hash(1), hash(20482)]
+ }
+}
+impl<R> PrecompileSet for UniquePrecompiles<R>
+where
+ R: pallet_evm::Config,
+{
+ fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
+ match handle.code_address() {
+ a if a == hash(1) => Some(ECRecover::execute(handle)),
+ // Sr25519 0x5002
+ a if a == hash(20482) => Some(Sr25519Precompile::<R>::execute(handle)),
+ _ => None,
+ }
+ }
+
+ fn is_precompile(&self, address: H160) -> bool {
+ Self::used_addresses().contains(&address)
+ }
+}
+
+fn hash(a: u64) -> H160 {
+ H160::from_low_u64_be(a)
+}
runtime/common/ethereum/precompiles/sr25519.rsdiffbeforeafterboth--- /dev/null
+++ b/runtime/common/ethereum/precompiles/sr25519.rs
@@ -0,0 +1,87 @@
+use fp_evm::{Context, ExitSucceed, PrecompileHandle, PrecompileOutput};
+use pallet_evm::Precompile;
+use sp_core::{crypto::UncheckedFrom, sr25519, H256};
+use sp_std::marker::PhantomData;
+use sp_std::prelude::*;
+
+use super::utils::{Bytes, EvmDataReader, EvmDataWriter, EvmResult, FunctionModifier, Gasometer};
+
+#[precompile_utils_macro::generate_function_selector]
+#[derive(Debug, PartialEq)]
+pub enum Action {
+ Verify = "verify(bytes32,bytes,bytes)",
+}
+
+/// A precompile to wrap substrate sr25519 functions.
+pub struct Sr25519Precompile<Runtime>(PhantomData<Runtime>);
+
+impl<Runtime: pallet_evm::Config> Precompile for Sr25519Precompile<Runtime> {
+ fn execute(handle: &mut impl PrecompileHandle) -> EvmResult<PrecompileOutput> {
+ log::trace!(target: "sr25519-precompile", "In sr25519 precompile");
+
+ let gasometer = Gasometer::new();
+
+ let (mut input, selector) = EvmDataReader::new_with_selector(&gasometer, handle.input())?;
+ let input = &mut input;
+
+ gasometer.check_function_modifier(
+ handle.context(),
+ handle.is_static(),
+ FunctionModifier::View,
+ )?;
+
+ match selector {
+ // Dispatchables
+ Action::Verify => Self::verify(input, &gasometer, handle.context()),
+ }
+ }
+}
+
+impl<Runtime: pallet_evm::Config> Sr25519Precompile<Runtime> {
+ fn verify(
+ input: &mut EvmDataReader,
+ gasometer: &Gasometer,
+ _: &Context,
+ ) -> EvmResult<PrecompileOutput> {
+ // Bound check
+ input.expect_arguments(gasometer, 3)?;
+
+ // Parse arguments
+ let public: sr25519::Public =
+ sr25519::Public::unchecked_from(input.read::<H256>(gasometer)?).into();
+ let signature_bytes: Vec<u8> = input.read::<Bytes>(gasometer)?.into();
+ let message: Vec<u8> = input.read::<Bytes>(gasometer)?.into();
+
+ // Parse signature
+ let signature_opt = sr25519::Signature::from_slice(&signature_bytes[..]);
+
+ let signature = if let Some(sig) = signature_opt {
+ sig
+ } else {
+ // Return `false` if signature length is wrong
+ return Ok(PrecompileOutput {
+ exit_status: ExitSucceed::Returned,
+ output: EvmDataWriter::new().write(false).build(),
+ });
+ };
+
+ log::trace!(
+ target: "sr25519-precompile",
+ "Verify signature {:?} for public {:?} and message {:?}",
+ signature, public, message,
+ );
+
+ let is_confirmed = sp_io::crypto::sr25519_verify(&signature, &message[..], &public);
+
+ log::trace!(
+ target: "sr25519-precompile",
+ "Verified signature {:?} is {:?}",
+ signature, is_confirmed,
+ );
+
+ Ok(PrecompileOutput {
+ exit_status: ExitSucceed::Returned,
+ output: EvmDataWriter::new().write(is_confirmed).build(),
+ })
+ }
+}
runtime/common/ethereum/precompiles/utils/data.rsdiffbeforeafterboth--- /dev/null
+++ b/runtime/common/ethereum/precompiles/utils/data.rs
@@ -0,0 +1,486 @@
+// Copyright 2019-2022 PureStake Inc.
+// Copyright 2022 Stake Technologies
+// This file is part of Utils package, originally developed by Purestake Inc.
+// Utils package used in Astar Network in terms of GPLv3.
+//
+// Utils is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Utils is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Utils. If not, see <http://www.gnu.org/licenses/>.
+
+use super::{EvmResult, Gasometer};
+
+use sp_std::borrow::ToOwned;
+use core::{any::type_name, ops::Range};
+use sp_core::{H160, H256, U256};
+use sp_std::{convert::TryInto, vec, vec::Vec};
+
+/// The `address` type of Solidity.
+/// H160 could represent 2 types of data (bytes20 and address) that are not encoded the same way.
+/// To avoid issues writing H160 is thus not supported.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct Address(pub H160);
+
+impl From<H160> for Address {
+ fn from(a: H160) -> Address {
+ Address(a)
+ }
+}
+
+impl From<Address> for H160 {
+ fn from(a: Address) -> H160 {
+ a.0
+ }
+}
+
+/// The `bytes`/`string` type of Solidity.
+/// It is different from `Vec<u8>` which will be serialized with padding for each `u8` element
+/// of the array, while `Bytes` is tightly packed.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Bytes(pub Vec<u8>);
+
+impl From<&[u8]> for Bytes {
+ fn from(a: &[u8]) -> Self {
+ Self(a.to_owned())
+ }
+}
+
+impl From<&str> for Bytes {
+ fn from(a: &str) -> Self {
+ a.as_bytes().into()
+ }
+}
+
+impl Into<Vec<u8>> for Bytes {
+ fn into(self: Self) -> Vec<u8> {
+ self.0
+ }
+}
+
+/// Wrapper around an EVM input slice, helping to parse it.
+/// Provide functions to parse common types.
+#[derive(Clone, Copy, Debug)]
+pub struct EvmDataReader<'a> {
+ input: &'a [u8],
+ cursor: usize,
+}
+
+impl<'a> EvmDataReader<'a> {
+ /// Create a new input parser.
+ pub fn new(input: &'a [u8]) -> Self {
+ Self { input, cursor: 0 }
+ }
+
+ /// Create a new input parser from a selector-initial input.
+ pub fn new_with_selector<T>(gasometer: &Gasometer, input: &'a [u8]) -> EvmResult<(Self, T)>
+ where
+ T: num_enum::TryFromPrimitive<Primitive = u32>,
+ {
+ if input.len() < 4 {
+ return Err(gasometer.revert("tried to parse selector out of bounds"));
+ }
+
+ let mut buffer = [0u8; 4];
+ buffer.copy_from_slice(&input[0..4]);
+ let selector = T::try_from_primitive(u32::from_be_bytes(buffer)).map_err(|_| {
+ log::trace!(
+ target: "precompile-utils",
+ "Failed to match function selector for {}",
+ type_name::<T>()
+ );
+ gasometer.revert("unknown selector")
+ })?;
+
+ Ok((Self::new(&input[4..]), selector))
+ }
+
+ /// Check the input has at least the correct amount of arguments before the end (32 bytes values).
+ pub fn expect_arguments(&self, gasometer: &Gasometer, args: usize) -> EvmResult {
+ if self.input.len() >= self.cursor + args * 32 {
+ Ok(())
+ } else {
+ Err(gasometer.revert("input doesn't match expected length"))
+ }
+ }
+
+ /// Read data from the input.
+ /// Must be provided a gasometer to generate correct Revert errors.
+ /// TODO : Benchmark and add cost of parsing to gasometer ?
+ pub fn read<T: EvmData>(&mut self, gasometer: &Gasometer) -> EvmResult<T> {
+ T::read(self, gasometer)
+ }
+
+ /// Reads a pointer, returning a reader targetting the pointed location.
+ pub fn read_pointer(&mut self, gasometer: &Gasometer) -> EvmResult<Self> {
+ let offset: usize = self
+ .read::<U256>(gasometer)
+ .map_err(|_| gasometer.revert("tried to parse array offset out of bounds"))?
+ .try_into()
+ .map_err(|_| gasometer.revert("array offset is too large"))?;
+
+ if offset >= self.input.len() {
+ return Err(gasometer.revert("pointer points out of bounds"));
+ }
+
+ Ok(Self {
+ input: &self.input[offset..],
+ cursor: 0,
+ })
+ }
+
+ /// Move the reading cursor with provided length, and return a range from the previous cursor
+ /// location to the new one.
+ /// Checks cursor overflows.
+ fn move_cursor(&mut self, gasometer: &Gasometer, len: usize) -> EvmResult<Range<usize>> {
+ let start = self.cursor;
+ let end = self
+ .cursor
+ .checked_add(len)
+ .ok_or_else(|| gasometer.revert("data reading cursor overflow"))?;
+
+ self.cursor = end;
+
+ Ok(start..end)
+ }
+}
+
+/// Help build an EVM input/output data.
+///
+/// Functions takes `self` to allow chaining all calls like
+/// `EvmDataWriter::new().write(...).write(...).build()`.
+/// While it could be more ergonomic to take &mut self, this would
+/// prevent to have a `build` function that don't clone the output.
+#[derive(Clone, Debug)]
+pub struct EvmDataWriter {
+ pub(crate) data: Vec<u8>,
+ offset_data: Vec<OffsetDatum>,
+ selector: Option<u32>,
+}
+
+#[derive(Clone, Debug)]
+struct OffsetDatum {
+ // Offset location in the container data.
+ offset_position: usize,
+ // Data pointed by the offset that must be inserted at the end of container data.
+ data: Vec<u8>,
+ // Inside of arrays, the offset is not from the start of array data (length), but from the start
+ // of the item. This shift allow to correct this.
+ offset_shift: usize,
+}
+
+impl EvmDataWriter {
+ /// Creates a new empty output builder (without selector).
+ pub fn new() -> Self {
+ Self {
+ data: vec![],
+ offset_data: vec![],
+ selector: None,
+ }
+ }
+
+ /// Return the built data.
+ pub fn build(mut self) -> Vec<u8> {
+ Self::bake_offsets(&mut self.data, self.offset_data);
+
+ if let Some(selector) = self.selector {
+ let mut output = selector.to_be_bytes().to_vec();
+ output.append(&mut self.data);
+ output
+ } else {
+ self.data
+ }
+ }
+
+ /// Add offseted data at the end of this writer's data, updating the offsets.
+ fn bake_offsets(output: &mut Vec<u8>, offsets: Vec<OffsetDatum>) {
+ for mut offset_datum in offsets {
+ let offset_position = offset_datum.offset_position;
+ let offset_position_end = offset_position + 32;
+
+ // The offset is the distance between the start of the data and the
+ // start of the pointed data (start of a struct, length of an array).
+ // Offsets in inner data are relative to the start of their respective "container".
+ // However in arrays the "container" is actually the item itself instead of the whole
+ // array, which is corrected by `offset_shift`.
+ let free_space_offset = output.len() - offset_datum.offset_shift;
+
+ // Override dummy offset to the offset it will be in the final output.
+ U256::from(free_space_offset)
+ .to_big_endian(&mut output[offset_position..offset_position_end]);
+
+ // Append this data at the end of the current output.
+ output.append(&mut offset_datum.data);
+ }
+ }
+
+ /// Write arbitrary bytes.
+ /// Doesn't handle any alignement checks, prefer using `write` instead if possible.
+ fn write_raw_bytes(mut self, value: &[u8]) -> Self {
+ self.data.extend_from_slice(value);
+ self
+ }
+
+ /// Write data of requested type.
+ pub fn write<T: EvmData>(mut self, value: T) -> Self {
+ T::write(&mut self, value);
+ self
+ }
+
+ /// Writes a pointer to given data.
+ /// The data will be appended when calling `build`.
+ /// Initially write a dummy value as offset in this writer's data, which will be replaced by
+ /// the correct offset once the pointed data is appended.
+ ///
+ /// Takes `&mut self` since its goal is to be used inside `EvmData` impl and not in chains.
+ pub fn write_pointer(&mut self, data: Vec<u8>) {
+ let offset_position = self.data.len();
+ H256::write(self, H256::repeat_byte(0xff));
+
+ self.offset_data.push(OffsetDatum {
+ offset_position,
+ data,
+ offset_shift: 0,
+ });
+ }
+}
+
+impl Default for EvmDataWriter {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+/// Data that can be converted from and to EVM data types.
+pub trait EvmData: Sized {
+ fn read(reader: &mut EvmDataReader, gasometer: &Gasometer) -> EvmResult<Self>;
+ fn write(writer: &mut EvmDataWriter, value: Self);
+}
+
+impl EvmData for H256 {
+ fn read(reader: &mut EvmDataReader, gasometer: &Gasometer) -> EvmResult<Self> {
+ let range = reader.move_cursor(gasometer, 32)?;
+
+ let data = reader
+ .input
+ .get(range)
+ .ok_or_else(|| gasometer.revert("tried to parse H256 out of bounds"))?;
+
+ Ok(H256::from_slice(data))
+ }
+
+ fn write(writer: &mut EvmDataWriter, value: Self) {
+ writer.data.extend_from_slice(value.as_bytes());
+ }
+}
+
+impl EvmData for Address {
+ fn read(reader: &mut EvmDataReader, gasometer: &Gasometer) -> EvmResult<Self> {
+ let range = reader.move_cursor(gasometer, 32)?;
+
+ let data = reader
+ .input
+ .get(range)
+ .ok_or_else(|| gasometer.revert("tried to parse H160 out of bounds"))?;
+
+ Ok(H160::from_slice(&data[12..32]).into())
+ }
+
+ fn write(writer: &mut EvmDataWriter, value: Self) {
+ H256::write(writer, value.0.into());
+ }
+}
+
+impl EvmData for U256 {
+ fn read(reader: &mut EvmDataReader, gasometer: &Gasometer) -> EvmResult<Self> {
+ let range = reader.move_cursor(gasometer, 32)?;
+
+ let data = reader
+ .input
+ .get(range)
+ .ok_or_else(|| gasometer.revert("tried to parse U256 out of bounds"))?;
+
+ Ok(U256::from_big_endian(data))
+ }
+
+ fn write(writer: &mut EvmDataWriter, value: Self) {
+ let mut buffer = [0u8; 32];
+ value.to_big_endian(&mut buffer);
+ writer.data.extend_from_slice(&buffer);
+ }
+}
+
+macro_rules! impl_evmdata_for_uints {
+ ($($uint:ty, )*) => {
+ $(
+ impl EvmData for $uint {
+ fn read(reader: &mut EvmDataReader, gasometer: &Gasometer) -> EvmResult<Self> {
+ let range = reader.move_cursor(gasometer, 32)?;
+
+ let data = reader
+ .input
+ .get(range)
+ .ok_or_else(|| gasometer.revert(alloc::format!(
+ "tried to parse {} out of bounds", core::any::type_name::<Self>()
+ )))?;
+
+ let mut buffer = [0u8; core::mem::size_of::<Self>()];
+ buffer.copy_from_slice(&data[32 - core::mem::size_of::<Self>()..]);
+ Ok(Self::from_be_bytes(buffer))
+ }
+
+ fn write(writer: &mut EvmDataWriter, value: Self) {
+ let mut buffer = [0u8; 32];
+ buffer[32 - core::mem::size_of::<Self>()..].copy_from_slice(&value.to_be_bytes());
+ writer.data.extend_from_slice(&buffer);
+ }
+ }
+ )*
+ };
+}
+
+impl_evmdata_for_uints!(u16, u32, u64, u128,);
+
+// The implementation for u8 is specific, for performance reasons.
+impl EvmData for u8 {
+ fn read(reader: &mut EvmDataReader, gasometer: &Gasometer) -> EvmResult<Self> {
+ let range = reader.move_cursor(gasometer, 32)?;
+
+ let data = reader
+ .input
+ .get(range)
+ .ok_or_else(|| gasometer.revert("tried to parse u64 out of bounds"))?;
+
+ Ok(data[31])
+ }
+
+ fn write(writer: &mut EvmDataWriter, value: Self) {
+ let mut buffer = [0u8; 32];
+ buffer[31] = value;
+
+ writer.data.extend_from_slice(&buffer);
+ }
+}
+
+impl EvmData for bool {
+ fn read(reader: &mut EvmDataReader, gasometer: &Gasometer) -> EvmResult<Self> {
+ let h256 = H256::read(reader, gasometer)
+ .map_err(|_| gasometer.revert("tried to parse bool out of bounds"))?;
+
+ Ok(!h256.is_zero())
+ }
+
+ fn write(writer: &mut EvmDataWriter, value: Self) {
+ let mut buffer = [0u8; 32];
+ if value {
+ buffer[31] = 1;
+ }
+
+ writer.data.extend_from_slice(&buffer);
+ }
+}
+
+impl<T: EvmData> EvmData for Vec<T> {
+ fn read(reader: &mut EvmDataReader, gasometer: &Gasometer) -> EvmResult<Self> {
+ let mut inner_reader = reader.read_pointer(gasometer)?;
+
+ let array_size: usize = inner_reader
+ .read::<U256>(gasometer)
+ .map_err(|_| gasometer.revert("tried to parse array length out of bounds"))?
+ .try_into()
+ .map_err(|_| gasometer.revert("array length is too large"))?;
+
+ let mut array = vec![];
+
+ let mut item_reader = EvmDataReader {
+ input: inner_reader
+ .input
+ .get(32..)
+ .ok_or_else(|| gasometer.revert("try to read array items out of bound"))?,
+ cursor: 0,
+ };
+
+ for _ in 0..array_size {
+ array.push(item_reader.read(gasometer)?);
+ }
+
+ Ok(array)
+ }
+
+ fn write(writer: &mut EvmDataWriter, value: Self) {
+ let mut inner_writer = EvmDataWriter::new().write(U256::from(value.len()));
+
+ for inner in value {
+ // Any offset in items are relative to the start of the item instead of the
+ // start of the array. However if there is offseted data it must but appended after
+ // all items (offsets) are written. We thus need to rely on `compute_offsets` to do
+ // that, and must store a "shift" to correct the offsets.
+ let shift = inner_writer.data.len();
+ let item_writer = EvmDataWriter::new().write(inner);
+
+ inner_writer = inner_writer.write_raw_bytes(&item_writer.data);
+ for mut offset_datum in item_writer.offset_data {
+ offset_datum.offset_shift += 32;
+ offset_datum.offset_position += shift;
+ inner_writer.offset_data.push(offset_datum);
+ }
+ }
+
+ writer.write_pointer(inner_writer.build());
+ }
+}
+
+impl EvmData for Bytes {
+ fn read(reader: &mut EvmDataReader, gasometer: &Gasometer) -> EvmResult<Self> {
+ let mut inner_reader = reader.read_pointer(gasometer)?;
+
+ // Read bytes/string size.
+ let array_size: usize = inner_reader
+ .read::<U256>(gasometer)
+ .map_err(|_| gasometer.revert("tried to parse bytes/string length out of bounds"))?
+ .try_into()
+ .map_err(|_| gasometer.revert("bytes/string length is too large"))?;
+
+ // Get valid range over the bytes data.
+ let range = inner_reader.move_cursor(gasometer, array_size)?;
+
+ let data = inner_reader
+ .input
+ .get(range)
+ .ok_or_else(|| gasometer.revert("tried to parse bytes/string out of bounds"))?;
+
+ let bytes = Self(data.to_owned());
+
+ Ok(bytes)
+ }
+
+ fn write(writer: &mut EvmDataWriter, value: Self) {
+ let length = value.0.len();
+
+ // Pad the data.
+ // Leave it as is if a multiple of 32, otherwise pad to next
+ // multiple or 32.
+ let chunks = length / 32;
+ let padded_size = match length % 32 {
+ 0 => chunks * 32,
+ _ => (chunks + 1) * 32,
+ };
+
+ let mut value = value.0.to_vec();
+ value.resize(padded_size, 0);
+
+ writer.write_pointer(
+ EvmDataWriter::new()
+ .write(U256::from(length))
+ .write_raw_bytes(&value)
+ .build(),
+ );
+ }
+}
runtime/common/ethereum/precompiles/utils/macro/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/runtime/common/ethereum/precompiles/utils/macro/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "precompile-utils-macro"
+authors = [ "StakeTechnologies", "PureStake" ]
+description = ""
+edition = "2018"
+version = "0.1.0"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+num_enum = { version = "0.5.3", default-features = false }
+proc-macro2 = "1.0"
+quote = "1.0"
+sha3 = "0.8"
+syn = { version = "1.0", features = [ "extra-traits", "fold", "full", "visit" ] }
runtime/common/ethereum/precompiles/utils/macro/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/runtime/common/ethereum/precompiles/utils/macro/src/lib.rs
@@ -0,0 +1,115 @@
+// Copyright 2019-2022 PureStake Inc.
+// Copyright 2022 Stake Technologies
+// This file is part of Utils package, originally developed by Purestake Inc.
+// Utils package used in Astar Network in terms of GPLv3.
+//
+// Utils is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Utils is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Utils. If not, see <http://www.gnu.org/licenses/>.
+
+#![crate_type = "proc-macro"]
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use proc_macro2::Literal;
+use quote::{quote, quote_spanned};
+use sha3::{Digest, Keccak256};
+use std::convert::TryInto;
+use syn::{parse_macro_input, spanned::Spanned, Expr, ExprLit, Ident, ItemEnum, Lit};
+
+/// This macro allows to associate to each variant of an enumeration a discriminant (of type u32
+/// whose value corresponds to the first 4 bytes of the Hash Keccak256 of the character string
+///indicated by the user of this macro.
+///
+/// Usage:
+///
+/// ```ignore
+/// #[generate_function_selector]
+/// enum Action {
+/// Toto = "toto()",
+/// Tata = "tata()",
+/// }
+/// ```
+///
+/// Extanded to:
+///
+/// ```rust
+/// #[repr(u32)]
+/// enum Action {
+/// Toto = 119097542u32,
+/// Tata = 1414311903u32,
+/// }
+/// ```
+///
+#[proc_macro_attribute]
+pub fn generate_function_selector(_: TokenStream, input: TokenStream) -> TokenStream {
+ let item = parse_macro_input!(input as ItemEnum);
+
+ let ItemEnum {
+ attrs,
+ vis,
+ enum_token,
+ ident,
+ variants,
+ ..
+ } = item;
+
+ let mut ident_expressions: Vec<Ident> = vec![];
+ let mut variant_expressions: Vec<Expr> = vec![];
+ for variant in variants {
+ match variant.discriminant {
+ Some((_, Expr::Lit(ExprLit { lit, .. }))) => {
+ if let Lit::Str(lit_str) = lit {
+ let selector = u32::from_be_bytes(
+ Keccak256::digest(lit_str.value().as_ref())[..4]
+ .try_into()
+ .unwrap(),
+ );
+ ident_expressions.push(variant.ident);
+ variant_expressions.push(Expr::Lit(ExprLit {
+ lit: Lit::Verbatim(Literal::u32_suffixed(selector)),
+ attrs: Default::default(),
+ }));
+ } else {
+ return quote_spanned! {
+ lit.span() => compile_error("Expected literal string");
+ }
+ .into();
+ }
+ }
+ Some((_eg, expr)) => {
+ return quote_spanned! {
+ expr.span() => compile_error("Expected literal");
+ }
+ .into()
+ }
+ None => {
+ return quote_spanned! {
+ variant.span() => compile_error("Each variant must have a discriminant");
+ }
+ .into()
+ }
+ }
+ }
+
+ (quote! {
+ #(#attrs)*
+ #[derive(num_enum::TryFromPrimitive, num_enum::IntoPrimitive)]
+ #[repr(u32)]
+ #vis #enum_token #ident {
+ #(
+ #ident_expressions = #variant_expressions,
+ )*
+ }
+ })
+ .into()
+}
runtime/common/ethereum/precompiles/utils/mod.rsdiffbeforeafterboth--- /dev/null
+++ b/runtime/common/ethereum/precompiles/utils/mod.rs
@@ -0,0 +1,95 @@
+// Copyright 2019-2022 PureStake Inc.
+// Copyright 2022 Stake Technologies
+// This file is part of Utils package, originally developed by Purestake Inc.
+// Utils package used in Astar Network in terms of GPLv3.
+//
+// Utils is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Utils is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Utils. If not, see <http://www.gnu.org/licenses/>.
+
+use sp_std::borrow::ToOwned;
+use fp_evm::{Context, ExitRevert, PrecompileFailure};
+use sp_core::U256;
+use sp_std::marker::PhantomData;
+
+mod data;
+
+pub use data::{Bytes, EvmData, EvmDataReader, EvmDataWriter};
+
+/// Alias for Result returning an EVM precompile error.
+pub type EvmResult<T = ()> = Result<T, PrecompileFailure>;
+
+/// Helper functions requiring a Runtime.
+/// This runtime must of course implement `pallet_evm::Config`.
+#[derive(Clone, Copy, Debug)]
+pub struct RuntimeHelper<Runtime>(PhantomData<Runtime>);
+
+/// Represents modifiers a Solidity function can be annotated with.
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum FunctionModifier {
+ /// Function that doesn't modify the state.
+ View,
+ /// Function that modifies the state and accept funds.
+ Payable,
+}
+
+/// Custom Gasometer to record costs in precompiles.
+/// It is advised to record known costs as early as possible to
+/// avoid unecessary computations if there is an Out of Gas.
+///
+/// Provides functions related to reverts, as reverts takes the recorded amount
+/// of gas into account.
+#[derive(Clone, Copy, Debug)]
+pub struct Gasometer();
+
+impl Gasometer {
+ /// Create a new Gasometer with provided gas limit.
+ /// None is no limit.
+ pub fn new() -> Self {
+ Self()
+ }
+
+ /// Revert the execution, making the user pay for the the currently
+ /// recorded cost. It is better to **revert** instead of **error** as
+ /// erroring consumes the entire gas limit, and **revert** returns an error
+ /// message to the calling contract.
+ ///
+ /// TODO : Record cost of the input based on its size and handle Out of Gas ?
+ /// This might be required if we format revert messages using user data.
+ #[must_use]
+ pub fn revert(&self, output: impl AsRef<[u8]>) -> PrecompileFailure {
+ PrecompileFailure::Revert {
+ exit_status: ExitRevert::Reverted,
+ output: output.as_ref().to_owned(),
+ }
+ }
+
+ #[must_use]
+ /// Check that a function call is compatible with the context it is
+ /// called into.
+ pub fn check_function_modifier(
+ &self,
+ context: &Context,
+ is_static: bool,
+ modifier: FunctionModifier,
+ ) -> EvmResult {
+ if is_static && modifier != FunctionModifier::View {
+ return Err(self.revert("can't call non-static function in static context"));
+ }
+
+ if modifier != FunctionModifier::Payable && context.apparent_value > U256::zero() {
+ return Err(self.revert("function is not payable"));
+ }
+
+ Ok(())
+ }
+}
runtime/opal/Cargo.tomldiffbeforeafterboth--- a/runtime/opal/Cargo.toml
+++ b/runtime/opal/Cargo.toml
@@ -471,6 +471,7 @@
up-rpc = { path = "../../primitives/rpc", default-features = false }
app-promotion-rpc = { path = "../../primitives/app_promotion_rpc", default-features = false }
rmrk-rpc = { path = "../../primitives/rmrk-rpc", default-features = false }
+fp-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
fp-evm-mapping = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
pallet-inflation = { path = '../../pallets/inflation', default-features = false }
pallet-app-promotion = { path = '../../pallets/app-promotion', default-features = false }
@@ -494,10 +495,13 @@
pallet-base-fee = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
fp-rpc = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
fp-self-contained = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
+pallet-evm-precompile-simple = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
evm-coder = { default-features = false, path = '../../crates/evm-coder' }
up-sponsorship = { default-features = false, git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = 'polkadot-v0.9.33' }
pallet-foreign-assets = { default-features = false, path = "../../pallets/foreign-assets" }
pallet-maintenance = { default-features = false, path = "../../pallets/maintenance" }
+precompile-utils-macro = { path = "../common/ethereum/precompiles/utils/macro" }
+num_enum = { version = "0.5.3", default-features = false }
################################################################################
# Test dependencies
runtime/opal/src/lib.rsdiffbeforeafterboth--- a/runtime/opal/src/lib.rs
+++ b/runtime/opal/src/lib.rs
@@ -25,6 +25,8 @@
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
+extern crate alloc;
+
use frame_support::parameter_types;
use sp_version::RuntimeVersion;
runtime/quartz/Cargo.tomldiffbeforeafterboth--- a/runtime/quartz/Cargo.toml
+++ b/runtime/quartz/Cargo.toml
@@ -463,6 +463,7 @@
pallet-unique = { path = '../../pallets/unique', default-features = false }
up-rpc = { path = "../../primitives/rpc", default-features = false }
app-promotion-rpc = { path = "../../primitives/app_promotion_rpc", default-features = false }
+fp-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
fp-evm-mapping = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
pallet-inflation = { path = '../../pallets/inflation', default-features = false }
pallet-app-promotion = { path = '../../pallets/app-promotion', default-features = false }
@@ -486,10 +487,13 @@
pallet-base-fee = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
fp-rpc = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
fp-self-contained = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
+pallet-evm-precompile-simple = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
evm-coder = { default-features = false, path = '../../crates/evm-coder' }
up-sponsorship = { default-features = false, git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = 'polkadot-v0.9.33' }
pallet-foreign-assets = { default-features = false, path = "../../pallets/foreign-assets" }
pallet-maintenance = { default-features = false, path = "../../pallets/maintenance" }
+precompile-utils-macro = { path = "../common/ethereum/precompiles/utils/macro" }
+num_enum = { version = "0.5.3", default-features = false }
################################################################################
# Other Dependencies
runtime/quartz/src/lib.rsdiffbeforeafterboth--- a/runtime/quartz/src/lib.rs
+++ b/runtime/quartz/src/lib.rs
@@ -25,6 +25,8 @@
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
+extern crate alloc;
+
use frame_support::parameter_types;
use sp_version::RuntimeVersion;
runtime/unique/Cargo.tomldiffbeforeafterboth--- a/runtime/unique/Cargo.toml
+++ b/runtime/unique/Cargo.toml
@@ -479,11 +479,15 @@
pallet-base-fee = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
fp-rpc = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
fp-self-contained = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
+fp-evm = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
fp-evm-mapping = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
+pallet-evm-precompile-simple = { default-features = false, git = "https://github.com/uniquenetwork/frontier", branch = "unique-polkadot-v0.9.33" }
evm-coder = { default-features = false, path = '../../crates/evm-coder' }
up-sponsorship = { default-features = false, git = "https://github.com/uniquenetwork/pallet-sponsoring", branch = 'polkadot-v0.9.33' }
pallet-foreign-assets = { default-features = false, path = "../../pallets/foreign-assets" }
pallet-maintenance = { default-features = false, path = "../../pallets/maintenance" }
+precompile-utils-macro = { path = "../common/ethereum/precompiles/utils/macro" }
+num_enum = { version = "0.5.3", default-features = false }
################################################################################
# Other Dependencies
runtime/unique/src/lib.rsdiffbeforeafterboth--- a/runtime/unique/src/lib.rs
+++ b/runtime/unique/src/lib.rs
@@ -25,6 +25,8 @@
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
+extern crate alloc;
+
use frame_support::parameter_types;
use sp_version::RuntimeVersion;
tests/src/eth/precompile.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/eth/precompile.test.ts
@@ -0,0 +1,112 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {IKeyringPair} from '@polkadot/types/types';
+
+import {expect, itEth, usingEthPlaygrounds} from './util';
+
+describe('Precompiles', () => {
+ let donor: IKeyringPair;
+
+ before(async function() {
+ await usingEthPlaygrounds(async (_, privateKey) => {
+ donor = await privateKey({filename: __filename});
+ });
+ });
+
+ itEth('ecrecover is supported', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const ecrecoverCompiledСontract = await helper.ethContract.compile(
+ 'ECRECOVER',
+ `
+ // SPDX-License-Identifier: MIT
+ pragma solidity ^0.8.17;
+
+ contract ECRECOVER{
+ address addressTest = 0x12Cb274aAD8251C875c0bf6872b67d9983E53fDd;
+ bytes32 msgHash1 = 0xc51dac836bc7841a01c4b631fa620904fc8724d7f9f1d3c420f0e02adf229d50;
+ bytes32 msgHash2 = 0xc51dac836bc7841a01c4b631fa620904fc8724d7f9f1d3c420f0e02adf229d51;
+ uint8 v = 0x1b;
+ bytes32 r = 0x44287513919034a471a7dc2b2ed121f95984ae23b20f9637ba8dff471b6719ef;
+ bytes32 s = 0x7d7dc30309a3baffbfd9342b97d0e804092c0aeb5821319aa732bc09146eafb4;
+
+
+ function verifyValid() public view returns(bool) {
+ // Use ECRECOVER to verify address
+ return ecrecover(msgHash1, v, r, s) == (addressTest);
+ }
+
+ function verifyInvalid() public view returns(bool) {
+ // Use ECRECOVER to verify address
+ return ecrecover(msgHash2, v, r, s) == (addressTest);
+ }
+ }
+ `,
+ );
+
+ const ecrecoverСontract = await helper.ethContract.deployByAbi(owner, ecrecoverCompiledСontract.abi, ecrecoverCompiledСontract.object);
+ expect(await ecrecoverСontract.methods.verifyValid().call({from: owner})).to.be.true;
+ expect(await ecrecoverСontract.methods.verifyInvalid().call({from: owner})).to.be.false;
+ });
+
+ itEth('sr25519 is supported', async ({helper}) => {
+ const owner = await helper.eth.createAccountWithBalance(donor);
+ const sr25519CompiledСontract = await helper.ethContract.compile(
+ 'SR25519Contract',
+ `
+ // SPDX-License-Identifier: MIT
+ pragma solidity ^0.8.17;
+
+ /**
+ * @title SR25519 signature interface.
+ */
+ interface SR25519 {
+ /**
+ * @dev Verify signed message using SR25519 crypto.
+ * @return A boolean confirming whether the public key is signer for the message.
+ */
+ function verify(
+ bytes32 public_key,
+ bytes calldata signature,
+ bytes calldata message
+ ) external view returns (bool);
+ }
+
+ contract SR25519Contract{
+ SR25519 public constant sr25519 = SR25519(0x0000000000000000000000000000000000005002);
+
+ bytes32 public_key = 0x96b2f9237ed0890fbeed891ebb81b91ac0d5d5b6e3afcdbc95df1b68d9f14036;
+ bytes signature = hex"4a5d733d7c568f2e88abf0467fd497f87c1be3e940ed897efdf9da72eaad394ef9918cb574ee99c54485775b17a0deaf46ff7a1f10346cea39fff0e4ede97689";
+ bytes message1 = hex"7372323535313920697320737570706f72746564";
+ bytes message2 = hex"7372323535313920697320737570706f7274656401";
+
+
+ function verifyValid() public view returns(bool) {
+ return sr25519.verify(public_key, signature, message1);
+ }
+
+ function verifyInvalid() public view returns(bool) {
+ return sr25519.verify(public_key, signature, message2);
+ }
+ }
+ `,
+ );
+
+ const sr25519Сontract = await helper.ethContract.deployByAbi(owner, sr25519CompiledСontract.abi, sr25519CompiledСontract.object);
+ expect(await sr25519Сontract.methods.verifyValid().call({from: owner})).to.be.true;
+ expect(await sr25519Сontract.methods.verifyInvalid().call({from: owner})).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, EthProperty} from './types';2223// Native contracts ABI24import collectionHelpersAbi from '../../abi/collectionHelpers.json';25import fungibleAbi from '../../abi/fungible.json';26import fungibleDeprecatedAbi from '../../abi/fungibleDeprecated.json';27import nonFungibleAbi from '../../abi/nonFungible.json';28import nonFungibleDeprecatedAbi from '../../abi/nonFungibleDeprecated.json';29import refungibleAbi from '../../abi/reFungible.json';30import refungibleDeprecatedAbi from '../../abi/reFungibleDeprecated.json';31import refungibleTokenAbi from '../../abi/reFungibleToken.json';32import contractHelpersAbi from '../../abi/contractHelpers.json';33import {ICrossAccountId, TEthereumAccount} from '../../../util/playgrounds/types';34import {TCollectionMode} from '../../../util/playgrounds/types';3536class EthGroupBase {37 helper: EthUniqueHelper;3839 constructor(helper: EthUniqueHelper) {40 this.helper = helper;41 }42}434445class ContractGroup extends EthGroupBase {46 async findImports(imports?: ContractImports[]){47 if(!imports) return function(path: string) {48 return {error: `File not found: ${path}`};49 };5051 const knownImports = {} as {[key: string]: string};52 for(const imp of imports) {53 knownImports[imp.solPath] = (await readFile(imp.fsPath)).toString();54 }5556 return function(path: string) {57 if(path in knownImports) return {contents: knownImports[path]};58 return {error: `File not found: ${path}`};59 };60 }6162 async compile(name: string, src: string, imports?: ContractImports[]): Promise<CompiledContract> {63 const out = JSON.parse(solc.compile(JSON.stringify({64 language: 'Solidity',65 sources: {66 [`${name}.sol`]: {67 content: src,68 },69 },70 settings: {71 outputSelection: {72 '*': {73 '*': ['*'],74 },75 },76 },77 }), {import: await this.findImports(imports)})).contracts[`${name}.sol`][name];7879 return {80 abi: out.abi,81 object: '0x' + out.evm.bytecode.object,82 };83 }8485 async deployByCode(signer: string, name: string, src: string, imports?: ContractImports[], gas?: number): Promise<Contract> {86 const compiledContract = await this.compile(name, src, imports);87 return this.deployByAbi(signer, compiledContract.abi, compiledContract.object, gas);88 }8990 async deployByAbi(signer: string, abi: any, object: string, gas?: number): Promise<Contract> {91 const web3 = this.helper.getWeb3();92 const contract = new web3.eth.Contract(abi, undefined, {93 data: object,94 from: signer,95 gas: gas ?? this.helper.eth.DEFAULT_GAS,96 });97 return await contract.deploy({data: object}).send({from: signer});98 }99100}101102class NativeContractGroup extends EthGroupBase {103104 contractHelpers(caller: string): Contract {105 const web3 = this.helper.getWeb3();106 return new web3.eth.Contract(contractHelpersAbi as any, this.helper.getApi().consts.evmContractHelpers.contractAddress.toString(), {from: caller, gas: this.helper.eth.DEFAULT_GAS});107 }108109 collectionHelpers(caller: string) {110 const web3 = this.helper.getWeb3();111 return new web3.eth.Contract(collectionHelpersAbi as any, this.helper.getApi().consts.common.contractAddress.toString(), {from: caller, gas: this.helper.eth.DEFAULT_GAS});112 }113114 collection(address: string, mode: TCollectionMode, caller?: string, mergeDeprecated = false): Contract {115 let abi = {116 'nft': nonFungibleAbi,117 'rft': refungibleAbi,118 'ft': fungibleAbi,119 }[mode];120 if (mergeDeprecated) {121 const deprecated = {122 'nft': nonFungibleDeprecatedAbi,123 'rft': refungibleDeprecatedAbi,124 'ft': fungibleDeprecatedAbi,125 }[mode];126 abi = [...abi,...deprecated];127 }128 const web3 = this.helper.getWeb3();129 return new web3.eth.Contract(abi as any, address, {gas: this.helper.eth.DEFAULT_GAS, ...(caller ? {from: caller} : {})});130 }131132 collectionById(collectionId: number, mode: 'nft' | 'rft' | 'ft', caller?: string, mergeDeprecated = false): Contract {133 return this.collection(this.helper.ethAddress.fromCollectionId(collectionId), mode, caller, mergeDeprecated);134 }135136 rftToken(address: string, caller?: string): Contract {137 const web3 = this.helper.getWeb3();138 return new web3.eth.Contract(refungibleTokenAbi as any, address, {gas: this.helper.eth.DEFAULT_GAS, ...(caller ? {from: caller} : {})});139 }140141 rftTokenById(collectionId: number, tokenId: number, caller?: string): Contract {142 return this.rftToken(this.helper.ethAddress.fromTokenId(collectionId, tokenId), caller);143 }144}145146147class EthGroup extends EthGroupBase {148 DEFAULT_GAS = 2_500_000;149150 createAccount() {151 const web3 = this.helper.getWeb3();152 const account = web3.eth.accounts.create();153 web3.eth.accounts.wallet.add(account.privateKey);154 return account.address;155 }156157 async createAccountWithBalance(donor: IKeyringPair, amount=100n) {158 const account = this.createAccount();159 await this.transferBalanceFromSubstrate(donor, account, amount);160161 return account;162 }163164 async transferBalanceFromSubstrate(donor: IKeyringPair, recepient: string, amount=100n, inTokens=true) {165 return await this.helper.balance.transferToSubstrate(donor, evmToAddress(recepient), amount * (inTokens ? this.helper.balance.getOneTokenNominal() : 1n));166 }167168 async getCollectionCreationFee(signer: string) {169 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);170 return await collectionHelper.methods.collectionCreationFee().call();171 }172173 async sendEVM(signer: IKeyringPair, contractAddress: string, abi: string, value: string, gasLimit?: number) {174 if(!gasLimit) gasLimit = this.DEFAULT_GAS;175 const web3 = this.helper.getWeb3();176 const gasPrice = await web3.eth.getGasPrice();177 // TODO: check execution status178 await this.helper.executeExtrinsic(179 signer,180 'api.tx.evm.call', [this.helper.address.substrateToEth(signer.address), contractAddress, abi, value, gasLimit, gasPrice, null, null, []],181 true,182 );183 }184185 async callEVM(signer: TEthereumAccount, contractAddress: string, abi: string) {186 return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]);187 }188189 createCollectionMethodName(mode: TCollectionMode) {190 switch (mode) {191 case 'ft':192 return 'createFTCollection';193 case 'nft':194 return 'createNFTCollection';195 case 'rft':196 return 'createRFTCollection';197 }198 }199200 async createCollection(mode: TCollectionMode, signer: string, name: string, description: string, tokenPrefix: string, decimals = 18): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {201 const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();202 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);203 const functionName: string = this.createCollectionMethodName(mode);204205 const functionParams = mode === 'ft' ? [name, decimals, description, tokenPrefix] : [name, description, tokenPrefix];206 const result = await collectionHelper.methods[functionName](...functionParams).send({value: Number(collectionCreationPrice)});207208 const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);209 const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);210 const events = this.helper.eth.normalizeEvents(result.events);211212 return {collectionId, collectionAddress, events};213 }214215 createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {216 return this.createCollection('nft', signer, name, description, tokenPrefix);217 }218219 async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {220 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);221222 const {collectionId, collectionAddress, events} = await this.createCollection('nft', signer, name, description, tokenPrefix);223224 await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();225226 return {collectionId, collectionAddress, events};227 }228229 createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[]}> {230 return this.createCollection('rft', signer, name, description, tokenPrefix);231 }232233 createFungibleCollection(signer: string, name: string, decimals: number, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[]}> {234 return this.createCollection('ft', signer, name, description, tokenPrefix, decimals);235 }236237 async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {238 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);239240 const {collectionId, collectionAddress, events} = await this.createCollection('rft', signer, name, description, tokenPrefix);241242 await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();243244 return {collectionId, collectionAddress, events};245 }246247 async deployCollectorContract(signer: string): Promise<Contract> {248 return await this.helper.ethContract.deployByCode(signer, 'Collector', `249 // SPDX-License-Identifier: UNLICENSED250 pragma solidity ^0.8.6;251252 contract Collector {253 uint256 collected;254 fallback() external payable {255 giveMoney();256 }257 function giveMoney() public payable {258 collected += msg.value;259 }260 function getCollected() public view returns (uint256) {261 return collected;262 }263 function getUnaccounted() public view returns (uint256) {264 return address(this).balance - collected;265 }266267 function withdraw(address payable target) public {268 target.transfer(collected);269 collected = 0;270 }271 }272 `);273 }274275 async deployFlipper(signer: string): Promise<Contract> {276 return await this.helper.ethContract.deployByCode(signer, 'Flipper', `277 // SPDX-License-Identifier: UNLICENSED278 pragma solidity ^0.8.6;279280 contract Flipper {281 bool value = false;282 function flip() public {283 value = !value;284 }285 function getValue() public view returns (bool) {286 return value;287 }288 }289 `);290 }291292 async recordCallFee(user: string, call: () => Promise<any>): Promise<bigint> {293 const before = await this.helper.balance.getEthereum(user);294 await call();295 // In dev mode, the transaction might not finish processing in time296 await this.helper.wait.newBlocks(1);297 const after = await this.helper.balance.getEthereum(user);298299 return before - after;300 }301302 normalizeEvents(events: any): NormalizedEvent[] {303 const output = [];304 for (const key of Object.keys(events)) {305 if (key.match(/^[0-9]+$/)) {306 output.push(events[key]);307 } else if (Array.isArray(events[key])) {308 output.push(...events[key]);309 } else {310 output.push(events[key]);311 }312 }313 output.sort((a, b) => a.logIndex - b.logIndex);314 return output.map(({address, event, returnValues}) => {315 const args: { [key: string]: string } = {};316 for (const key of Object.keys(returnValues)) {317 if (!key.match(/^[0-9]+$/)) {318 args[key] = returnValues[key];319 }320 }321 return {322 address,323 event,324 args,325 };326 });327 }328329 async calculateFee(address: ICrossAccountId, code: () => Promise<any>): Promise<bigint> {330 const wrappedCode = async () => {331 await code();332 // In dev mode, the transaction might not finish processing in time333 await this.helper.wait.newBlocks(1);334 };335 return await this.helper.arrange.calculcateFee(address, wrappedCode);336 }337}338339class EthAddressGroup extends EthGroupBase {340 extractCollectionId(address: string): number {341 if (!(address.length === 42 || address.length === 40)) throw new Error('address wrong format');342 return parseInt(address.substr(address.length - 8), 16);343 }344345 fromCollectionId(collectionId: number): string {346 if (collectionId >= 0xffffffff || collectionId < 0) throw new Error('collectionId overflow');347 return Web3.utils.toChecksumAddress(`0x17c4e6453cc49aaaaeaca894e6d9683e${collectionId.toString(16).padStart(8,'0')}`);348 }349350 extractTokenId(address: string): {collectionId: number, tokenId: number} {351 if (!address.startsWith('0x'))352 throw 'address not starts with "0x"';353 if (address.length > 42)354 throw 'address length is more than 20 bytes';355 return {356 collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)),357 tokenId: Number('0x' + address.substring(address.length - 8)),358 };359 }360361 fromTokenId(collectionId: number, tokenId: number): string {362 return this.helper.util.getTokenAddress({collectionId, tokenId});363 }364365 normalizeAddress(address: string): string {366 return '0x' + address.substring(address.length - 40);367 }368}369export class EthPropertyGroup extends EthGroupBase {370 property(key: string, value: string): EthProperty {371 return [372 key,373 '0x'+Buffer.from(value).toString('hex'),374 ];375 }376}377export type EthUniqueHelperConstructor = new (...args: any[]) => EthUniqueHelper;378379export class EthCrossAccountGroup extends EthGroupBase {380 createAccount(): TEthCrossAccount {381 return this.fromAddress(this.helper.eth.createAccount());382 }383384 async createAccountWithBalance(donor: IKeyringPair, amount=100n) {385 return this.fromAddress(await this.helper.eth.createAccountWithBalance(donor, amount));386 }387388 fromAddress(address: TEthereumAccount): TEthCrossAccount {389 return {390 eth: address,391 sub: '0',392 };393 }394395 fromKeyringPair(keyring: IKeyringPair): TEthCrossAccount {396 return {397 eth: '0x0000000000000000000000000000000000000000',398 sub: keyring.addressRaw,399 };400 }401}402403export class EthUniqueHelper extends DevUniqueHelper {404 web3: Web3 | null = null;405 web3Provider: WebsocketProvider | null = null;406407 eth: EthGroup;408 ethAddress: EthAddressGroup;409 ethCrossAccount: EthCrossAccountGroup;410 ethNativeContract: NativeContractGroup;411 ethContract: ContractGroup;412 ethProperty: EthPropertyGroup;413414 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {415 options.helperBase = options.helperBase ?? EthUniqueHelper;416417 super(logger, options);418 this.eth = new EthGroup(this);419 this.ethAddress = new EthAddressGroup(this);420 this.ethCrossAccount = new EthCrossAccountGroup(this);421 this.ethNativeContract = new NativeContractGroup(this);422 this.ethContract = new ContractGroup(this);423 this.ethProperty = new EthPropertyGroup(this);424 }425426 getWeb3(): Web3 {427 if(this.web3 === null) throw Error('Web3 not connected');428 return this.web3;429 }430431 connectWeb3(wsEndpoint: string) {432 if(this.web3 !== null) return;433 this.web3Provider = new Web3.providers.WebsocketProvider(wsEndpoint);434 this.web3 = new Web3(this.web3Provider);435 }436437 async disconnect() {438 if(this.web3 === null) return;439 this.web3Provider?.connection.close();440441 await super.disconnect();442 }443444 clearApi() {445 super.clearApi();446 this.web3 = null;447 }448449 clone(helperCls: EthUniqueHelperConstructor, options?: { [key: string]: any; }): EthUniqueHelper {450 const newHelper = super.clone(helperCls, options) as EthUniqueHelper;451 newHelper.web3 = this.web3;452 newHelper.web3Provider = this.web3Provider;453454 return newHelper;455 }456}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, EthProperty} from './types';2223// Native contracts ABI24import collectionHelpersAbi from '../../abi/collectionHelpers.json';25import fungibleAbi from '../../abi/fungible.json';26import fungibleDeprecatedAbi from '../../abi/fungibleDeprecated.json';27import nonFungibleAbi from '../../abi/nonFungible.json';28import nonFungibleDeprecatedAbi from '../../abi/nonFungibleDeprecated.json';29import refungibleAbi from '../../abi/reFungible.json';30import refungibleDeprecatedAbi from '../../abi/reFungibleDeprecated.json';31import refungibleTokenAbi from '../../abi/reFungibleToken.json';32import contractHelpersAbi from '../../abi/contractHelpers.json';33import {ICrossAccountId, TEthereumAccount} from '../../../util/playgrounds/types';34import {TCollectionMode} from '../../../util/playgrounds/types';3536class EthGroupBase {37 helper: EthUniqueHelper;3839 constructor(helper: EthUniqueHelper) {40 this.helper = helper;41 }42}434445class ContractGroup extends EthGroupBase {46 async findImports(imports?: ContractImports[]){47 if(!imports) return function(path: string) {48 return {error: `File not found: ${path}`};49 };5051 const knownImports = {} as {[key: string]: string};52 for(const imp of imports) {53 knownImports[imp.solPath] = (await readFile(imp.fsPath)).toString();54 }5556 return function(path: string) {57 if(path in knownImports) return {contents: knownImports[path]};58 return {error: `File not found: ${path}`};59 };60 }6162 async compile(name: string, src: string, imports?: ContractImports[]): Promise<CompiledContract> {63 const compiled = JSON.parse(solc.compile(JSON.stringify({64 language: 'Solidity',65 sources: {66 [`${name}.sol`]: {67 content: src,68 },69 },70 settings: {71 outputSelection: {72 '*': {73 '*': ['*'],74 },75 },76 },77 }), {import: await this.findImports(imports)}));7879 const hasErrors = compiled['errors']80 && compiled['errors'].length > 081 && compiled.errors.some(function(err: any) {82 return err.severity == 'error';83 });84 85 if (hasErrors) {86 throw compiled.errors;87 }88 const out = compiled.contracts[`${name}.sol`][name];8990 return {91 abi: out.abi,92 object: '0x' + out.evm.bytecode.object,93 };94 }9596 async deployByCode(signer: string, name: string, src: string, imports?: ContractImports[], gas?: number): Promise<Contract> {97 const compiledContract = await this.compile(name, src, imports);98 return this.deployByAbi(signer, compiledContract.abi, compiledContract.object, gas);99 }100101 async deployByAbi(signer: string, abi: any, object: string, gas?: number): Promise<Contract> {102 const web3 = this.helper.getWeb3();103 const contract = new web3.eth.Contract(abi, undefined, {104 data: object,105 from: signer,106 gas: gas ?? this.helper.eth.DEFAULT_GAS,107 });108 return await contract.deploy({data: object}).send({from: signer});109 }110111}112113class NativeContractGroup extends EthGroupBase {114115 contractHelpers(caller: string): Contract {116 const web3 = this.helper.getWeb3();117 return new web3.eth.Contract(contractHelpersAbi as any, this.helper.getApi().consts.evmContractHelpers.contractAddress.toString(), {from: caller, gas: this.helper.eth.DEFAULT_GAS});118 }119120 collectionHelpers(caller: string) {121 const web3 = this.helper.getWeb3();122 return new web3.eth.Contract(collectionHelpersAbi as any, this.helper.getApi().consts.common.contractAddress.toString(), {from: caller, gas: this.helper.eth.DEFAULT_GAS});123 }124125 collection(address: string, mode: TCollectionMode, caller?: string, mergeDeprecated = false): Contract {126 let abi = {127 'nft': nonFungibleAbi,128 'rft': refungibleAbi,129 'ft': fungibleAbi,130 }[mode];131 if (mergeDeprecated) {132 const deprecated = {133 'nft': nonFungibleDeprecatedAbi,134 'rft': refungibleDeprecatedAbi,135 'ft': fungibleDeprecatedAbi,136 }[mode];137 abi = [...abi,...deprecated];138 }139 const web3 = this.helper.getWeb3();140 return new web3.eth.Contract(abi as any, address, {gas: this.helper.eth.DEFAULT_GAS, ...(caller ? {from: caller} : {})});141 }142143 collectionById(collectionId: number, mode: 'nft' | 'rft' | 'ft', caller?: string, mergeDeprecated = false): Contract {144 return this.collection(this.helper.ethAddress.fromCollectionId(collectionId), mode, caller, mergeDeprecated);145 }146147 rftToken(address: string, caller?: string): Contract {148 const web3 = this.helper.getWeb3();149 return new web3.eth.Contract(refungibleTokenAbi as any, address, {gas: this.helper.eth.DEFAULT_GAS, ...(caller ? {from: caller} : {})});150 }151152 rftTokenById(collectionId: number, tokenId: number, caller?: string): Contract {153 return this.rftToken(this.helper.ethAddress.fromTokenId(collectionId, tokenId), caller);154 }155}156157158class EthGroup extends EthGroupBase {159 DEFAULT_GAS = 2_500_000;160161 createAccount() {162 const web3 = this.helper.getWeb3();163 const account = web3.eth.accounts.create();164 web3.eth.accounts.wallet.add(account.privateKey);165 return account.address;166 }167168 async createAccountWithBalance(donor: IKeyringPair, amount=100n) {169 const account = this.createAccount();170 await this.transferBalanceFromSubstrate(donor, account, amount);171172 return account;173 }174175 async transferBalanceFromSubstrate(donor: IKeyringPair, recepient: string, amount=100n, inTokens=true) {176 return await this.helper.balance.transferToSubstrate(donor, evmToAddress(recepient), amount * (inTokens ? this.helper.balance.getOneTokenNominal() : 1n));177 }178179 async getCollectionCreationFee(signer: string) {180 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);181 return await collectionHelper.methods.collectionCreationFee().call();182 }183184 async sendEVM(signer: IKeyringPair, contractAddress: string, abi: string, value: string, gasLimit?: number) {185 if(!gasLimit) gasLimit = this.DEFAULT_GAS;186 const web3 = this.helper.getWeb3();187 const gasPrice = await web3.eth.getGasPrice();188 // TODO: check execution status189 await this.helper.executeExtrinsic(190 signer,191 'api.tx.evm.call', [this.helper.address.substrateToEth(signer.address), contractAddress, abi, value, gasLimit, gasPrice, null, null, []],192 true,193 );194 }195196 async callEVM(signer: TEthereumAccount, contractAddress: string, abi: string) {197 return await this.helper.callRpc('api.rpc.eth.call', [{from: signer, to: contractAddress, data: abi}]);198 }199200 createCollectionMethodName(mode: TCollectionMode) {201 switch (mode) {202 case 'ft':203 return 'createFTCollection';204 case 'nft':205 return 'createNFTCollection';206 case 'rft':207 return 'createRFTCollection';208 }209 }210211 async createCollection(mode: TCollectionMode, signer: string, name: string, description: string, tokenPrefix: string, decimals = 18): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {212 const collectionCreationPrice = this.helper.balance.getCollectionCreationPrice();213 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);214 const functionName: string = this.createCollectionMethodName(mode);215216 const functionParams = mode === 'ft' ? [name, decimals, description, tokenPrefix] : [name, description, tokenPrefix];217 const result = await collectionHelper.methods[functionName](...functionParams).send({value: Number(collectionCreationPrice)});218219 const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);220 const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);221 const events = this.helper.eth.normalizeEvents(result.events);222223 return {collectionId, collectionAddress, events};224 }225226 createNFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {227 return this.createCollection('nft', signer, name, description, tokenPrefix);228 }229230 async createERC721MetadataCompatibleNFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {231 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);232233 const {collectionId, collectionAddress, events} = await this.createCollection('nft', signer, name, description, tokenPrefix);234235 await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();236237 return {collectionId, collectionAddress, events};238 }239240 createRFTCollection(signer: string, name: string, description: string, tokenPrefix: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[]}> {241 return this.createCollection('rft', signer, name, description, tokenPrefix);242 }243244 createFungibleCollection(signer: string, name: string, decimals: number, description: string, tokenPrefix: string): Promise<{ collectionId: number, collectionAddress: string, events: NormalizedEvent[]}> {245 return this.createCollection('ft', signer, name, description, tokenPrefix, decimals);246 }247248 async createERC721MetadataCompatibleRFTCollection(signer: string, name: string, description: string, tokenPrefix: string, baseUri: string): Promise<{collectionId: number, collectionAddress: string, events: NormalizedEvent[] }> {249 const collectionHelper = this.helper.ethNativeContract.collectionHelpers(signer);250251 const {collectionId, collectionAddress, events} = await this.createCollection('rft', signer, name, description, tokenPrefix);252253 await collectionHelper.methods.makeCollectionERC721MetadataCompatible(collectionAddress, baseUri).send();254255 return {collectionId, collectionAddress, events};256 }257258 async deployCollectorContract(signer: string): Promise<Contract> {259 return await this.helper.ethContract.deployByCode(signer, 'Collector', `260 // SPDX-License-Identifier: UNLICENSED261 pragma solidity ^0.8.6;262263 contract Collector {264 uint256 collected;265 fallback() external payable {266 giveMoney();267 }268 function giveMoney() public payable {269 collected += msg.value;270 }271 function getCollected() public view returns (uint256) {272 return collected;273 }274 function getUnaccounted() public view returns (uint256) {275 return address(this).balance - collected;276 }277278 function withdraw(address payable target) public {279 target.transfer(collected);280 collected = 0;281 }282 }283 `);284 }285286 async deployFlipper(signer: string): Promise<Contract> {287 return await this.helper.ethContract.deployByCode(signer, 'Flipper', `288 // SPDX-License-Identifier: UNLICENSED289 pragma solidity ^0.8.6;290291 contract Flipper {292 bool value = false;293 function flip() public {294 value = !value;295 }296 function getValue() public view returns (bool) {297 return value;298 }299 }300 `);301 }302303 async recordCallFee(user: string, call: () => Promise<any>): Promise<bigint> {304 const before = await this.helper.balance.getEthereum(user);305 await call();306 // In dev mode, the transaction might not finish processing in time307 await this.helper.wait.newBlocks(1);308 const after = await this.helper.balance.getEthereum(user);309310 return before - after;311 }312313 normalizeEvents(events: any): NormalizedEvent[] {314 const output = [];315 for (const key of Object.keys(events)) {316 if (key.match(/^[0-9]+$/)) {317 output.push(events[key]);318 } else if (Array.isArray(events[key])) {319 output.push(...events[key]);320 } else {321 output.push(events[key]);322 }323 }324 output.sort((a, b) => a.logIndex - b.logIndex);325 return output.map(({address, event, returnValues}) => {326 const args: { [key: string]: string } = {};327 for (const key of Object.keys(returnValues)) {328 if (!key.match(/^[0-9]+$/)) {329 args[key] = returnValues[key];330 }331 }332 return {333 address,334 event,335 args,336 };337 });338 }339340 async calculateFee(address: ICrossAccountId, code: () => Promise<any>): Promise<bigint> {341 const wrappedCode = async () => {342 await code();343 // In dev mode, the transaction might not finish processing in time344 await this.helper.wait.newBlocks(1);345 };346 return await this.helper.arrange.calculcateFee(address, wrappedCode);347 }348}349350class EthAddressGroup extends EthGroupBase {351 extractCollectionId(address: string): number {352 if (!(address.length === 42 || address.length === 40)) throw new Error('address wrong format');353 return parseInt(address.substr(address.length - 8), 16);354 }355356 fromCollectionId(collectionId: number): string {357 if (collectionId >= 0xffffffff || collectionId < 0) throw new Error('collectionId overflow');358 return Web3.utils.toChecksumAddress(`0x17c4e6453cc49aaaaeaca894e6d9683e${collectionId.toString(16).padStart(8,'0')}`);359 }360361 extractTokenId(address: string): {collectionId: number, tokenId: number} {362 if (!address.startsWith('0x'))363 throw 'address not starts with "0x"';364 if (address.length > 42)365 throw 'address length is more than 20 bytes';366 return {367 collectionId: Number('0x' + address.substring(address.length - 16, address.length - 8)),368 tokenId: Number('0x' + address.substring(address.length - 8)),369 };370 }371372 fromTokenId(collectionId: number, tokenId: number): string {373 return this.helper.util.getTokenAddress({collectionId, tokenId});374 }375376 normalizeAddress(address: string): string {377 return '0x' + address.substring(address.length - 40);378 }379}380export class EthPropertyGroup extends EthGroupBase {381 property(key: string, value: string): EthProperty {382 return [383 key,384 '0x'+Buffer.from(value).toString('hex'),385 ];386 }387}388export type EthUniqueHelperConstructor = new (...args: any[]) => EthUniqueHelper;389390export class EthCrossAccountGroup extends EthGroupBase {391 createAccount(): TEthCrossAccount {392 return this.fromAddress(this.helper.eth.createAccount());393 }394395 async createAccountWithBalance(donor: IKeyringPair, amount=100n) {396 return this.fromAddress(await this.helper.eth.createAccountWithBalance(donor, amount));397 }398399 fromAddress(address: TEthereumAccount): TEthCrossAccount {400 return {401 eth: address,402 sub: '0',403 };404 }405406 fromKeyringPair(keyring: IKeyringPair): TEthCrossAccount {407 return {408 eth: '0x0000000000000000000000000000000000000000',409 sub: keyring.addressRaw,410 };411 }412}413414export class EthUniqueHelper extends DevUniqueHelper {415 web3: Web3 | null = null;416 web3Provider: WebsocketProvider | null = null;417418 eth: EthGroup;419 ethAddress: EthAddressGroup;420 ethCrossAccount: EthCrossAccountGroup;421 ethNativeContract: NativeContractGroup;422 ethContract: ContractGroup;423 ethProperty: EthPropertyGroup;424425 constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) {426 options.helperBase = options.helperBase ?? EthUniqueHelper;427428 super(logger, options);429 this.eth = new EthGroup(this);430 this.ethAddress = new EthAddressGroup(this);431 this.ethCrossAccount = new EthCrossAccountGroup(this);432 this.ethNativeContract = new NativeContractGroup(this);433 this.ethContract = new ContractGroup(this);434 this.ethProperty = new EthPropertyGroup(this);435 }436437 getWeb3(): Web3 {438 if(this.web3 === null) throw Error('Web3 not connected');439 return this.web3;440 }441442 connectWeb3(wsEndpoint: string) {443 if(this.web3 !== null) return;444 this.web3Provider = new Web3.providers.WebsocketProvider(wsEndpoint);445 this.web3 = new Web3(this.web3Provider);446 }447448 async disconnect() {449 if(this.web3 === null) return;450 this.web3Provider?.connection.close();451452 await super.disconnect();453 }454455 clearApi() {456 super.clearApi();457 this.web3 = null;458 }459460 clone(helperCls: EthUniqueHelperConstructor, options?: { [key: string]: any; }): EthUniqueHelper {461 const newHelper = super.clone(helperCls, options) as EthUniqueHelper;462 newHelper.web3 = this.web3;463 newHelper.web3Provider = this.web3Provider;464465 return newHelper;466 }467}