diff --git a/.gitignore b/.gitignore index b51ac4ae02..c3eec5cbc2 100644 --- a/.gitignore +++ b/.gitignore @@ -347,4 +347,5 @@ Pipfile.lock # ignore packages/fetchai, but useful to have them locally # so to be able to run tests packages/fetchai -!packages/valory/contracts/gnosis_safe/build \ No newline at end of file +!packages/valory/contracts/gnosis_safe/build +!packages/valory/contracts/gnosis_safe_proxy_factory/build \ No newline at end of file diff --git a/packages/hashes.csv b/packages/hashes.csv index fa98f8dffb..4f14e16a40 100644 --- a/packages/hashes.csv +++ b/packages/hashes.csv @@ -1,11 +1,12 @@ valory/agents/counter,QmbEhbjyHS1W3vZnWgpKoqMr2ugKLXp5d84H8cSNBDmzEc valory/agents/counter_client,QmNzaAny4auEo4cDXq6pM7kdyY8HZ35wgCiXcinqs3e39e -valory/agents/price_estimation,QmSH2mV2D3Qakyzeozd2sAYScf56S4E6ZQkxKFAhuBHXqh +valory/agents/price_estimation,QmZcmxo49NGEca2Hjp7eT4kECAiiDBGjeFrjaAXULgeVdH valory/connections/abci,QmRYgXTxs4sqnBMF6GufZtczTiKv8LLk8ASjU3UefJArwh -valory/contracts/gnosis_safe,QmZ2J3sUtTKaXriLepUAzuV1WscRn1ykAcUChSWidETQwk +valory/contracts/gnosis_safe,QmUajWwLd1pv6VTW1AmiaShk8sm6fwPYE7meBuc2Q1mB5F +valory/contracts/gnosis_safe_proxy_factory,QmWfgDNYxYx6PGDi9Cbjt5nnoT2YFMgPFAUhSAUZAurqhT valory/protocols/abci,QmY9LyAMRBPSUiWnC821ZMczFLpw7nXsQKVqvLK14gj2dv valory/skills/abstract_abci,QmQeyVZThnkfxmXkUjBa1T55Kixssz3Lv82upCH5wHtayb valory/skills/abstract_round_abci,QmVSzSFKHQUQTkKjubyN4MicW5BK2MjpR2D9cNr5dNUAmm valory/skills/counter,QmSmt61iahp11PaYT6oNEzijMU8n15b1A4fty6HXmxi2gW valory/skills/counter_client,QmYcfMC1nzgeMMys5NHUXJTB3KKxwJFBM4tryBMpNggEHt -valory/skills/price_estimation_abci,QmexxkTzbG1ADjZdT6yQ5YM5fv9Mj2cmSi5cAfePV2DmGt +valory/skills/price_estimation_abci,QmQK8oyGnnrwhYxBAwpiehoaoWRPSM6P4xP6EwKmLdgfRW diff --git a/packages/valory/agents/price_estimation/aea-config.yaml b/packages/valory/agents/price_estimation/aea-config.yaml index d43e383769..b7839c328d 100644 --- a/packages/valory/agents/price_estimation/aea-config.yaml +++ b/packages/valory/agents/price_estimation/aea-config.yaml @@ -10,7 +10,9 @@ connections: - fetchai/http_client:0.22.0 - fetchai/ledger:0.18.0 - valory/abci:0.1.0 -contracts: [] +contracts: +- valory/gnosis_safe:0.1.0 +- valory/gnosis_safe_proxy_factory:0.1.0 protocols: - fetchai/contract_api:1.0.0 - fetchai/default:1.0.0 diff --git a/packages/valory/contracts/gnosis_safe/__init__.py b/packages/valory/contracts/gnosis_safe/__init__.py index 6ec6a87e98..ab1ba24692 100644 --- a/packages/valory/contracts/gnosis_safe/__init__.py +++ b/packages/valory/contracts/gnosis_safe/__init__.py @@ -17,4 +17,4 @@ # # ------------------------------------------------------------------------------ -"""This module contains the support resources for the Fetch oracle contract.""" +"""This module contains the support resources for the gnosis_safe (GnosisSafeL2) contract.""" diff --git a/packages/valory/contracts/gnosis_safe/contract.py b/packages/valory/contracts/gnosis_safe/contract.py index 1d934d0065..ef7bdbb22a 100644 --- a/packages/valory/contracts/gnosis_safe/contract.py +++ b/packages/valory/contracts/gnosis_safe/contract.py @@ -22,19 +22,18 @@ import logging import secrets from enum import Enum -from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, cast from aea.common import JSONLike from aea.configurations.base import PublicId -from aea.contracts.base import Contract, contract_registry +from aea.contracts.base import Contract from aea.crypto.base import LedgerApi from aea_ledger_ethereum import EthereumApi from eth_typing import ChecksumAddress, HexAddress, HexStr from hexbytes import HexBytes from packaging.version import Version from py_eth_sig_utils.eip712 import encode_typed_data -from web3.types import Nonce, TxParams, Wei +from web3.types import TxParams, Wei PUBLIC_ID = PublicId.from_str("valory/gnosis_safe:0.1.0") @@ -113,6 +112,8 @@ def get_deploy_transaction( owners = kwargs.pop("owners") threshold = kwargs.pop("threshold") salt_nonce = kwargs.pop("salt_nonce", None) + gas = kwargs.pop("gas", None) + gas_price = kwargs.pop("gas_price", None) ledger_api = cast(EthereumApi, ledger_api) tx_params, contract_address = cls._get_deploy_transaction( ledger_api, @@ -120,6 +121,8 @@ def get_deploy_transaction( owners=owners, threshold=threshold, salt_nonce=salt_nonce, + gas=gas, + gas_price=gas_price, ) result = dict(cast(Dict, tx_params)) # piggyback the contract address @@ -134,6 +137,8 @@ def _get_deploy_transaction( # pylint: disable=too-many-locals,too-many-argumen owners: List[str], threshold: int, salt_nonce: Optional[int] = None, + gas: Optional[int] = None, + gas_price: Optional[int] = None, ) -> Tuple[TxParams, str]: """ Get the deployment transaction of the new Safe. @@ -146,6 +151,8 @@ def _get_deploy_transaction( # pylint: disable=too-many-locals,too-many-argumen :param owners: a list of public keys :param threshold: the signature threshold :param salt_nonce: Use a custom nonce for the deployment. Defaults to random nonce. + :param gas: gas cost + :param gas_price: Gas price that should be used for the payment calculation :return: transaction params and contract address """ @@ -194,9 +201,7 @@ def _get_deploy_transaction( # pylint: disable=too-many-locals,too-many-argumen fallback_handler, salt_nonce, ) - safe_contract = cls.get_safe_contract_instance( - ledger_api, safe_contract_address - ) + safe_contract = cls.get_instance(ledger_api, safe_contract_address) safe_creation_tx_data = HexBytes( safe_contract.functions.setup( owners, @@ -221,7 +226,15 @@ def _get_deploy_transaction( # pylint: disable=too-many-locals,too-many-argumen ) if nonce is None: raise ValueError("No nonce returned.") - tx_params, contract_address = cls._build_tx_deploy_proxy_contract_with_nonce( + # TOFIX: lazy import until contract dependencies supported in AEA + from packages.valory.contracts.gnosis_safe_proxy_factory.contract import ( # pylint: disable=import-outside-toplevel + GnosisSafeProxyFactoryContract, + ) + + ( + tx_params, + contract_address, + ) = GnosisSafeProxyFactoryContract.build_tx_deploy_proxy_contract_with_nonce( ledger_api, proxy_factory_address, safe_contract_address, @@ -229,81 +242,11 @@ def _get_deploy_transaction( # pylint: disable=too-many-locals,too-many-argumen safe_creation_tx_data, salt_nonce, nonce=nonce, + gas=gas, + gas_price=gas_price, ) return tx_params, contract_address - @classmethod - def get_safe_contract_instance( - cls, ledger_api: LedgerApi, safe_contract_address: str - ) -> Any: - """ - Get an instance of the safe contract. - - :param ledger_api: ledger API object - :param safe_contract_address: the address of the safe contract - :return: an instance of the safe contract - """ - # hack as we currently cannot register multiple contracts :/ - contract = contract_registry.make(str(cls.contract_id)) - full_path = Path( - str(contract.configuration.directory), "build/GnosisSafe_V1_3_0.json" - ) - safe_contract_interface = ledger_api.load_contract_interface(full_path) - safe_contract = ledger_api.get_contract_instance( - safe_contract_interface, safe_contract_address - ) - return safe_contract - - @classmethod - def _build_tx_deploy_proxy_contract_with_nonce( # pylint: disable=too-many-arguments - cls, - ledger_api: LedgerApi, - proxy_factory_address: str, - master_copy: str, - address: str, - initializer: bytes, - salt_nonce: int, - gas: Optional[int] = None, - gas_price: Optional[int] = None, - nonce: Optional[int] = None, - ) -> Tuple[TxParams, str]: - """ - Deploy proxy contract via Proxy Factory using `createProxyWithNonce` (create2) - - :param ledger_api: ledger API object - :param proxy_factory_address: the address of the proxy factory - :param address: Ethereum address - :param master_copy: Address the proxy will point at - :param initializer: Data for safe creation - :param salt_nonce: Uint256 for `create2` salt - :param gas: Gas - :param gas_price: Gas Price - :param nonce: Nonce - :return: Tuple(tx-hash, tx, deployed contract address) - """ - proxy_factory_contract = cls.get_instance(ledger_api, proxy_factory_address) - - create_proxy_fn = proxy_factory_contract.functions.createProxyWithNonce( - master_copy, initializer, salt_nonce - ) - - tx_parameters = TxParams({"from": address}) - contract_address = create_proxy_fn.call(tx_parameters) - - if gas_price is not None: - tx_parameters["gasPrice"] = Wei(gas_price) - - if gas is not None: - tx_parameters["gas"] = Wei(gas) - - if nonce is not None: - tx_parameters["nonce"] = Nonce(nonce) - - transaction_dict = create_proxy_fn.buildTransaction(tx_parameters) - # Auto estimation of gas does not work. We use a little more gas just in case - transaction_dict["gas"] = Wei(transaction_dict["gas"] + 50000) - return transaction_dict, contract_address - @classmethod def get_raw_safe_transaction_hash( # pylint: disable=too-many-arguments,too-many-locals cls, @@ -344,7 +287,7 @@ def get_raw_safe_transaction_hash( # pylint: disable=too-many-arguments,too-man :param chain_id: Ethereum network chain_id is used in hash calculation for Safes >= 1.3.0. If not provided, it will be retrieved from the provided ethereum_client :return: the hash of the raw Safe transaction """ - safe_contract = cls.get_safe_contract_instance(ledger_api, contract_address) + safe_contract = cls.get_instance(ledger_api, contract_address) if safe_nonce is None: safe_nonce = safe_contract.functions.nonce().call(block_identifier="latest") if safe_version is None: @@ -459,7 +402,7 @@ def get_raw_safe_transaction( # pylint: disable=too-many-arguments,too-many-loc signatures += signature_bytes # Packed signature data ({bytes32 r}{bytes32 s}{uint8 v}) - safe_contract = cls.get_safe_contract_instance(ledger_api, contract_address) + safe_contract = cls.get_instance(ledger_api, contract_address) if safe_nonce is None: safe_nonce = safe_contract.functions.nonce().call(block_identifier="latest") diff --git a/packages/valory/contracts/gnosis_safe/contract.yaml b/packages/valory/contracts/gnosis_safe/contract.yaml index 64361c1453..ada704e5b5 100644 --- a/packages/valory/contracts/gnosis_safe/contract.yaml +++ b/packages/valory/contracts/gnosis_safe/contract.yaml @@ -2,18 +2,17 @@ name: gnosis_safe author: valory version: 0.1.0 type: contract -description: Gnosis Safe contract +description: Gnosis Safe (GnosisSafeL2) contract license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: Qmd5NcJnij2d19rhtNJgsTSBU7ErTdYVH2c621j6TKN7Qz - __init__.py: QmS2MEY7dgz18icnFs975zxCxMmYQzVkeFfe2TjnTDeDiL + __init__.py: QmWLx43KXUA8iq4uRo1VDhFPqd6dFF7xfdiMpQLAoBBMoD build/GnosisSafe_V1_3_0.json: QmafMmPcVqiTLykozgjGwNL2S8b1g5bmgMP3z6EdecgMYh - build/ProxyFactory_V1_3_0.json: QmRKHfF1hYrrSZNZmec2AS3xSPWdB29uyEWAYdAS5cKHdL - contract.py: QmXXR3N6Uu5mqqwDhoNaVfdo6xsz5P69V9vZC28LEWoHJg + contract.py: QmWTLNL8a7Vqsyw7wwCzyiJ9gHsoYQBc8TnJzJ4Qecpybp fingerprint_ignore_patterns: [] class_name: GnosisSafeContract contract_interface_paths: - ethereum: build/ProxyFactory_V1_3_0.json + ethereum: build/GnosisSafe_V1_3_0.json dependencies: py-eth-sig-utils: {} diff --git a/packages/valory/contracts/gnosis_safe_proxy_factory/README.md b/packages/valory/contracts/gnosis_safe_proxy_factory/README.md new file mode 100644 index 0000000000..0fe891711a --- /dev/null +++ b/packages/valory/contracts/gnosis_safe_proxy_factory/README.md @@ -0,0 +1,6 @@ +# Gnosis Safe Proxy Factory contract + +## Description + +## Functions + diff --git a/packages/valory/contracts/gnosis_safe_proxy_factory/__init__.py b/packages/valory/contracts/gnosis_safe_proxy_factory/__init__.py new file mode 100644 index 0000000000..f85fe8e0ba --- /dev/null +++ b/packages/valory/contracts/gnosis_safe_proxy_factory/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the support resources for the gnosis_safe_proxy_contract (GnosisSafeProxyFactory) contract.""" diff --git a/packages/valory/contracts/gnosis_safe/build/ProxyFactory_V1_3_0.json b/packages/valory/contracts/gnosis_safe_proxy_factory/build/ProxyFactory_V1_3_0.json similarity index 100% rename from packages/valory/contracts/gnosis_safe/build/ProxyFactory_V1_3_0.json rename to packages/valory/contracts/gnosis_safe_proxy_factory/build/ProxyFactory_V1_3_0.json diff --git a/packages/valory/contracts/gnosis_safe_proxy_factory/contract.py b/packages/valory/contracts/gnosis_safe_proxy_factory/contract.py new file mode 100644 index 0000000000..31a81a0c02 --- /dev/null +++ b/packages/valory/contracts/gnosis_safe_proxy_factory/contract.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains the class to connect to an Gnosis Safe Proxy Factory contract.""" +import logging +from typing import Any, Optional, Tuple + +from aea.common import JSONLike +from aea.configurations.base import PublicId +from aea.contracts.base import Contract +from aea.crypto.base import LedgerApi +from web3.types import Nonce, TxParams, Wei + + +PUBLIC_ID = PublicId.from_str("valory/gnosis_safe_proxy_factory:0.1.0") + +_logger = logging.getLogger( + f"aea.packages.{PUBLIC_ID.author}.contracts.{PUBLIC_ID.name}.contract" +) + +PROXY_FACTORY_CONTRACT = "0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2" + + +class GnosisSafeProxyFactoryContract(Contract): + """The Gnosis Safe Proxy Factory contract.""" + + contract_id = PUBLIC_ID + + @classmethod + def get_raw_transaction( + cls, ledger_api: LedgerApi, contract_address: str, **kwargs: Any + ) -> Optional[JSONLike]: + """Get the raw transaction.""" + raise NotImplementedError # pragma: nocover + + @classmethod + def get_raw_message( + cls, ledger_api: LedgerApi, contract_address: str, **kwargs: Any + ) -> Optional[bytes]: + """Get raw message.""" + raise NotImplementedError # pragma: nocover + + @classmethod + def get_state( + cls, ledger_api: LedgerApi, contract_address: str, **kwargs: Any + ) -> Optional[JSONLike]: + """Get state.""" + raise NotImplementedError # pragma: nocover + + @classmethod + def get_deploy_transaction( + cls, ledger_api: LedgerApi, deployer_address: str, **kwargs: Any + ) -> Optional[JSONLike]: + """ + Get deploy transaction. + + :param ledger_api: ledger API object. + :param deployer_address: the deployer address. + :param kwargs: the keyword arguments. + :return: an optional JSON-like object. + """ + return super().get_deploy_transaction(ledger_api, deployer_address, **kwargs) + + @classmethod + def build_tx_deploy_proxy_contract_with_nonce( # pylint: disable=too-many-arguments + cls, + ledger_api: LedgerApi, + proxy_factory_address: str, + master_copy: str, + address: str, + initializer: bytes, + salt_nonce: int, + gas: Optional[int] = None, + gas_price: Optional[int] = None, + nonce: Optional[int] = None, + ) -> Tuple[TxParams, str]: + """ + Deploy proxy contract via Proxy Factory using `createProxyWithNonce` (create2) + + :param ledger_api: ledger API object + :param proxy_factory_address: the address of the proxy factory + :param address: Ethereum address + :param master_copy: Address the proxy will point at + :param initializer: Data for safe creation + :param salt_nonce: Uint256 for `create2` salt + :param gas: Gas + :param gas_price: Gas Price + :param nonce: Nonce + :return: Tuple(tx-hash, tx, deployed contract address) + """ + proxy_factory_contract = cls.get_instance(ledger_api, proxy_factory_address) + + create_proxy_fn = proxy_factory_contract.functions.createProxyWithNonce( + master_copy, initializer, salt_nonce + ) + + tx_parameters = TxParams({"from": address}) + contract_address = create_proxy_fn.call(tx_parameters) + + if gas_price is not None: + tx_parameters["gasPrice"] = Wei(gas_price) + + if gas is not None: + tx_parameters["gas"] = Wei(gas) + + if nonce is not None: + tx_parameters["nonce"] = Nonce(nonce) + + transaction_dict = create_proxy_fn.buildTransaction(tx_parameters) + # Auto estimation of gas does not work. We use a little more gas just in case + transaction_dict["gas"] = Wei(transaction_dict["gas"] + 50000) + return transaction_dict, contract_address diff --git a/packages/valory/contracts/gnosis_safe_proxy_factory/contract.yaml b/packages/valory/contracts/gnosis_safe_proxy_factory/contract.yaml new file mode 100644 index 0000000000..a71d37b637 --- /dev/null +++ b/packages/valory/contracts/gnosis_safe_proxy_factory/contract.yaml @@ -0,0 +1,18 @@ +name: gnosis_safe_proxy_factory +author: valory +version: 0.1.0 +type: contract +description: Gnosis Safe proxy factory (GnosisSafeProxyFactory) contract +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: QmRSN363rGLMwfAmBQzNKPsx952w93E9fSyu7obaDfho5a + __init__.py: Qmdd5tuA2NPcHxhYpSW3ceNh8akivnxQC83pvTT91YyupV + build/ProxyFactory_V1_3_0.json: QmRKHfF1hYrrSZNZmec2AS3xSPWdB29uyEWAYdAS5cKHdL + contract.py: Qma2K1v9gazbGQEfexMn77fKiHVAzX7Ra2vuK3ooU9KArW +fingerprint_ignore_patterns: [] +class_name: GnosisSafeProxyFactoryContract +contract_interface_paths: + ethereum: build/ProxyFactory_V1_3_0.json +dependencies: + py-eth-sig-utils: {} diff --git a/packages/valory/skills/price_estimation_abci/skill.yaml b/packages/valory/skills/price_estimation_abci/skill.yaml index 460850b8e0..5513286466 100644 --- a/packages/valory/skills/price_estimation_abci/skill.yaml +++ b/packages/valory/skills/price_estimation_abci/skill.yaml @@ -25,6 +25,7 @@ connections: - valory/abci:0.1.0 contracts: - valory/gnosis_safe:0.1.0 +- valory/gnosis_safe_proxy_factory:0.1.0 protocols: - fetchai/contract_api:1.0.0 - fetchai/http:1.0.0 diff --git a/tests/conftest.py b/tests/conftest.py index b980ac0af4..a541fdbdaa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,6 +44,11 @@ ) +def get_key(key_path: Path) -> str: + """Returns key value from file.""" "" + return key_path.read_bytes().strip().decode() + + ROOT_DIR = _ROOT_DIR DATA_PATH = _ROOT_DIR / "tests" / "data" DEFAULT_AMOUNT = 1000000000000000000000 @@ -53,11 +58,15 @@ ETHEREUM_KEY_PATH_2 = DATA_PATH / "ethereum_key_2.txt" ETHEREUM_KEY_PATH_3 = DATA_PATH / "ethereum_key_3.txt" ETHEREUM_KEY_PATH_4 = DATA_PATH / "ethereum_key_4.txt" - - -def get_key(key_path: Path) -> str: - """Returns key value from file.""" "" - return key_path.read_bytes().strip().decode() +GANACHE_CONFIGURATION = dict( + accounts_balances=[ + (get_key(ETHEREUM_KEY_DEPLOYER), DEFAULT_AMOUNT), + (get_key(ETHEREUM_KEY_PATH_1), DEFAULT_AMOUNT), + (get_key(ETHEREUM_KEY_PATH_2), DEFAULT_AMOUNT), + (get_key(ETHEREUM_KEY_PATH_3), DEFAULT_AMOUNT), + (get_key(ETHEREUM_KEY_PATH_4), DEFAULT_AMOUNT), + ], +) @pytest.fixture() @@ -124,15 +133,7 @@ def ganache_port() -> int: @pytest.fixture(scope="session") def ganache_configuration(): """Get the Ganache configuration for testing purposes.""" - return dict( - accounts_balances=[ - (get_key(ETHEREUM_KEY_DEPLOYER), DEFAULT_AMOUNT), - (get_key(ETHEREUM_KEY_PATH_1), DEFAULT_AMOUNT), - (get_key(ETHEREUM_KEY_PATH_2), DEFAULT_AMOUNT), - (get_key(ETHEREUM_KEY_PATH_3), DEFAULT_AMOUNT), - (get_key(ETHEREUM_KEY_PATH_4), DEFAULT_AMOUNT), - ], - ) + return GANACHE_CONFIGURATION @pytest.mark.integration diff --git a/tests/fixture_helpers.py b/tests/fixture_helpers.py index 785319192b..b51f72442d 100644 --- a/tests/fixture_helpers.py +++ b/tests/fixture_helpers.py @@ -18,15 +18,26 @@ # ------------------------------------------------------------------------------ """This module contains helper classes/functions for fixtures.""" +import logging import secrets -from typing import List, Optional, Tuple, cast +from typing import Dict, List, Optional, Tuple, cast +import docker import pytest from eth_account import Account from web3 import Web3 +from tests.conftest import GANACHE_CONFIGURATION from tests.helpers.constants import KEY_PAIRS -from tests.helpers.docker.ganache import DEFAULT_GANACHE_PORT +from tests.helpers.docker.base import DockerBaseTest, DockerImage +from tests.helpers.docker.ganache import ( + DEFAULT_GANACHE_ADDR, + DEFAULT_GANACHE_PORT, + GanacheDockerImage, +) + + +logger = logging.getLogger(__name__) @pytest.mark.integration @@ -141,3 +152,19 @@ def get_nonce(self) -> int: if self.SALT_NONCE is not None: return self.SALT_NONCE return secrets.SystemRandom().randint(0, 2 ** 256 - 1) + + +class GanacheBaseTest(DockerBaseTest): + """Base pytest class for Ganache.""" + + ganache_addr: str = DEFAULT_GANACHE_ADDR + ganache_port: int = DEFAULT_GANACHE_PORT + ganache_configuration: Dict = GANACHE_CONFIGURATION + + @classmethod + def _build_image(cls) -> DockerImage: + """Build the image.""" + client = docker.from_env() + return GanacheDockerImage( + client, cls.ganache_addr, cls.ganache_port, config=cls.ganache_configuration + ) diff --git a/tests/helpers/docker/base.py b/tests/helpers/docker/base.py index 92beae344d..de30d6860b 100644 --- a/tests/helpers/docker/base.py +++ b/tests/helpers/docker/base.py @@ -133,3 +133,47 @@ def launch_image( container.stop() logger.info("Logs from container:\n%s", container.logs().decode()) container.remove() + + +class DockerBaseTest(ABC): + """Base pytest class for setting up Docker images.""" + + timeout: float = 2.0 + max_attempts: int = 10 + + _image: DockerImage + _container: Container + + @classmethod + def setup_class(cls): + """Setup up the test class.""" + cls._image = cls._build_image() + cls._image.check_skip() + cls._image.stop_if_already_running() + cls._container = cls._image.create() + cls._container.start() + logger.info(f"Setting up image {cls._image.tag}...") + success = cls._image.wait(cls.max_attempts, cls.timeout) + if not success: + cls._container.stop() + logger.info( + "Error logs from container:\n%s", cls._container.logs().decode() + ) + cls._container.remove() + pytest.fail(f"{cls._image.tag} doesn't work. Exiting...") + else: + logger.info("Done!") + time.sleep(cls.timeout) + + @classmethod + def teardown_class(cls): + """Tear down the test.""" + logger.info(f"Stopping the image {cls._image.tag}...") + cls._container.stop() + logger.info("Logs from container:\n%s", cls._container.logs().decode()) + cls._container.remove() + + @classmethod + @abstractmethod + def _build_image(cls) -> DockerImage: + """Instantiate the Docker image.""" diff --git a/tests/test_contracts/base.py b/tests/test_contracts/base.py new file mode 100644 index 0000000000..78c440d37c --- /dev/null +++ b/tests/test_contracts/base.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Base test classes.""" + +from pathlib import Path +from typing import Any, Dict, Optional, cast + +from aea.contracts.base import Contract +from aea.crypto.base import Crypto, LedgerApi +from aea.crypto.registries import crypto_registry, ledger_apis_registry +from aea_ledger_ethereum import EthereumCrypto + +from tests.conftest import ETHEREUM_KEY_DEPLOYER +from tests.fixture_helpers import GanacheBaseTest +from tests.helpers.contracts import get_register_contract +from tests.helpers.docker.ganache import DEFAULT_GANACHE_PORT + + +class BaseGanacheContractTest(GanacheBaseTest): + """Base test case for testing contracts on Ganache.""" + + directory: Path + contract: Contract + ledger_api: LedgerApi + deployer_crypto: Crypto + contract_address: Optional[str] = None + deployment_kwargs: Dict[str, Any] = {} + + @classmethod + def setup_class( + cls, + ): + """Setup test.""" + super().setup_class() + cls.contract = get_register_contract(cls.directory) + cls.ledger_api = ledger_apis_registry.make( + EthereumCrypto.identifier, + address=f"http://localhost:{DEFAULT_GANACHE_PORT}", + ) + cls.deployer_crypto = crypto_registry.make( + EthereumCrypto.identifier, private_key_path=ETHEREUM_KEY_DEPLOYER + ) + cls.deploy(**cls.deployment_kwargs) + assert cls.contract_address is not None, "Contract not deployed." + + @classmethod + def deploy(cls, **kwargs: Any): + """Deploy the contract.""" + tx = cls.contract.get_deploy_transaction( + ledger_api=cls.ledger_api, + deployer_address=str(cls.deployer_crypto.address), + **kwargs, + ) + if tx is None: + return None + tx_signed = cls.deployer_crypto.sign_transaction(tx) + tx_hash = cls.ledger_api.send_signed_transaction(tx_signed) + if tx_hash is None: + return None + tx_receipt = cls.ledger_api.get_transaction_receipt(tx_hash) + if tx_receipt is None: + return None + contract_address = cast(Dict, tx_receipt)["contractAddress"] + cls.contract_address = contract_address diff --git a/tests/test_contracts/test_gnosis_safe/__init__.py b/tests/test_contracts/test_gnosis_safe/__init__.py index d4815466b1..f764a3b559 100644 --- a/tests/test_contracts/test_gnosis_safe/__init__.py +++ b/tests/test_contracts/test_gnosis_safe/__init__.py @@ -17,4 +17,4 @@ # # ------------------------------------------------------------------------------ -"""Tests package for valory/gnosis_safe contracts.""" +"""Tests package for valory/gnosis_safe contract.""" diff --git a/tests/test_contracts/test_gnosis_safe/test_contract.py b/tests/test_contracts/test_gnosis_safe/test_contract.py index 5eae951d84..06db0020b2 100644 --- a/tests/test_contracts/test_gnosis_safe/test_contract.py +++ b/tests/test_contracts/test_gnosis_safe/test_contract.py @@ -54,6 +54,10 @@ def setup( self, ): """Setup test.""" + directory = Path( + ROOT_DIR, "packages", "valory", "contracts", "gnosis_safe_proxy_factory" + ) + _ = get_register_contract(directory) directory = Path(ROOT_DIR, "packages", "valory", "contracts", "gnosis_safe") self.contract = get_register_contract(directory) self.ledger_api = ledger_apis_registry.make( @@ -83,6 +87,10 @@ def setup( self, ): """Setup test.""" + directory = Path( + ROOT_DIR, "packages", "valory", "contracts", "gnosis_safe_proxy_factory" + ) + _ = get_register_contract(directory) directory = Path(ROOT_DIR, "packages", "valory", "contracts", "gnosis_safe") self.contract = get_register_contract(directory) self.ledger_api = ledger_apis_registry.make( diff --git a/tests/test_contracts/test_gnosis_safe_proxy_factory/__init__.py b/tests/test_contracts/test_gnosis_safe_proxy_factory/__init__.py new file mode 100644 index 0000000000..f22bafb953 --- /dev/null +++ b/tests/test_contracts/test_gnosis_safe_proxy_factory/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests package for valory/gnosis_safe_proxy_factory contract.""" diff --git a/tests/test_contracts/test_gnosis_safe_proxy_factory/test_contract.py b/tests/test_contracts/test_gnosis_safe_proxy_factory/test_contract.py new file mode 100644 index 0000000000..48629087c8 --- /dev/null +++ b/tests/test_contracts/test_gnosis_safe_proxy_factory/test_contract.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2021 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for valory/gnosis contract.""" + +from pathlib import Path + +from packages.valory.contracts.gnosis_safe.contract import SAFE_CONTRACT +from packages.valory.contracts.gnosis_safe_proxy_factory.contract import ( + PROXY_FACTORY_CONTRACT, +) + +from tests.conftest import ROOT_DIR +from tests.test_contracts.base import BaseGanacheContractTest + + +class TestGnosisSafeProxyFactory(BaseGanacheContractTest): + """Test deployment of the proxy to Ganache.""" + + directory = Path( + ROOT_DIR, "packages", "valory", "contracts", "gnosis_safe_proxy_factory" + ) + deployment_kwargs = dict(gas=10000000, gasPrice=10000000) + + def test_deploy(self): + """Test deployment results.""" + assert ( + self.contract_address != PROXY_FACTORY_CONTRACT + ), "Contract addresses should differ as we don't use deterministic deployment here." + + def test_build_tx_deploy_proxy_contract_with_nonce(self): + """Test build_tx_deploy_proxy_contract_with_nonce method.""" + result = self.contract.build_tx_deploy_proxy_contract_with_nonce( + self.ledger_api, + self.contract_address, + SAFE_CONTRACT, + self.deployer_crypto.address, + b"", + 1, + gas=1000, + gas_price=1000, + nonce=1, + ) + assert len(result) == 2 + assert len(result[0]) == 8 + assert all( + [ + key + in [ + "value", + "gas", + "gasPrice", + "chainId", + "from", + "to", + "data", + "nonce", + ] + for key in result[0].keys() + ] + )